Skip to content
16 changes: 8 additions & 8 deletions packages/@aws-cdk/toolkit/lib/actions/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ export interface BaseDeployOptions {
* @default 1
*/
readonly concurrency?: number;

/**
* Whether to show logs from all CloudWatch log groups in the template
* locally in the users terminal
Copy link
Contributor

@mrgrain mrgrain Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what, but the Toolkit doesn't really have a concept of a user terminal. It works with the IoHost instead (which or might not print).

I realize we copied this description from elsewhere. But hey, let's make it better now!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call out

*
* @default - false
*/
readonly traceLogs?: boolean;
}

export interface DeployOptions extends BaseDeployOptions {
Expand Down Expand Up @@ -205,14 +213,6 @@ export interface DeployOptions extends BaseDeployOptions {
*/
readonly outputsFile?: string;

/**
* Whether to show logs from all CloudWatch log groups in the template
* locally in the users terminal
*
* @default - false
*/
readonly traceLogs?: boolean;

/**
* Build/publish assets for a single stack in parallel
*
Expand Down
9 changes: 9 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/deploy/private/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DeployOptions } from '..';
import { CloudWatchLogEventMonitor } from '../../../api/aws-cdk';

export * from './helpers';

Expand All @@ -14,4 +15,12 @@ export interface ExtendedDeployOptions extends DeployOptions {
* @default - nothing extra is appended to the User-Agent header
*/
readonly extraUserAgent?: string;

/**
* Allows adding CloudWatch log groups to the log monitor via
* cloudWatchLogMonitor.setLogGroups();
*
* @default - not monitoring CloudWatch logs
*/
readonly cloudWatchLogMonitor?: CloudWatchLogEventMonitor;
}
8 changes: 0 additions & 8 deletions packages/@aws-cdk/toolkit/lib/actions/watch/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import type { BaseDeployOptions, HotswapMode } from '../deploy';

export interface WatchOptions extends BaseDeployOptions {
/**
* Whether to show CloudWatch logs for hotswapped resources
* locally in the users terminal
*
* @default - false
*/
readonly traceLogs?: boolean;

/**
* The extra string to append to the User-Agent header when performing AWS SDK calls.
*
Expand Down
5 changes: 5 additions & 0 deletions packages/@aws-cdk/toolkit/lib/api/aws-cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export { WorkGraph } from '../../../../aws-cdk/lib/util/work-graph';
export type { Concurrency } from '../../../../aws-cdk/lib/util/work-graph';
export { WorkGraphBuilder } from '../../../../aws-cdk/lib/util/work-graph-builder';
export type { AssetBuildNode, AssetPublishNode, StackNode } from '../../../../aws-cdk/lib/util/work-graph-types';
export { CloudWatchLogEventMonitor } from '../../../../aws-cdk/lib/api/logs/logs-monitor';
export { findCloudWatchLogGroups } from '../../../../aws-cdk/lib/api/logs/find-cloudwatch-logs';

// Test APIs
export { MockSdk } from '../../../../aws-cdk/test/util/mock-sdk';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay that import needs to go elsewhere. Apologies, I didn't explain the purpose of this file well enough.

Basically we are using it as bundling entrypoint. Everything in here will be bundled for distribution. We don't need to MockSdk bundled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your linter rule pointed me here :). but i'll make an exception for testing imports in that rule


// @todo Cloud Assembly and Executable - this is a messy API right now
export { CloudAssembly, sanitizePatterns, StackCollection, ExtendedStackSelection } from '../../../../aws-cdk/lib/api/cxapp/cloud-assembly';
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit/lib/api/io/private/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const CODES = {
// Toolkit Info codes
CDK_TOOLKIT_I0001: 'Display stack data',
CDK_TOOLKIT_I0002: 'Successfully deployed stacks',
CDK_TOOLKIT_I3001: 'Log groups added',
CDK_TOOLKIT_I5001: 'Display synthesis times',
CDK_TOOLKIT_I5050: 'Confirm rollback',
CDK_TOOLKIT_I5060: 'Confirm deploy security sensitive changes',
Expand Down
36 changes: 19 additions & 17 deletions packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { type RollbackOptions } from '../actions/rollback';
import { type SynthOptions } from '../actions/synth';
import { patternsArrayForWatch, WatchOptions } from '../actions/watch';
import { type SdkOptions } from '../api/aws-auth';
import { DEFAULT_TOOLKIT_STACK_NAME, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, StackActivityProgress, ResourceMigrator, obscureTemplate, serializeStructure, tagsForStack, CliIoHost, validateSnsTopicArn, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, formatErrorMessage } from '../api/aws-cdk';
import { DEFAULT_TOOLKIT_STACK_NAME, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, StackActivityProgress, ResourceMigrator, obscureTemplate, serializeStructure, tagsForStack, CliIoHost, validateSnsTopicArn, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, formatErrorMessage, CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/aws-cdk';
import { CachedCloudAssemblySource, IdentityCloudAssemblySource, StackAssembly, ICloudAssemblySource, StackSelectionStrategy } from '../api/cloud-assembly';
import { ALL_STACKS, CloudAssemblySourceBuilder } from '../api/cloud-assembly/private';
import { ToolkitError } from '../api/errors';
Expand Down Expand Up @@ -456,15 +456,17 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
[`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), e.message].join(' '),
);
} finally {
// @todo
// if (options.cloudWatchLogMonitor) {
// const foundLogGroupsResult = await findCloudWatchLogGroups(await this.sdkProvider('deploy'), stack);
// options.cloudWatchLogMonitor.addLogGroups(
// foundLogGroupsResult.env,
// foundLogGroupsResult.sdk,
// foundLogGroupsResult.logGroupNames,
// );
// }
if (options.traceLogs) {
// deploy calls that originate from watch will come with their own cloudWatchLogMonitor
const cloudWatchLogMonitor = options.cloudWatchLogMonitor ?? new CloudWatchLogEventMonitor();
const foundLogGroupsResult = await findCloudWatchLogGroups(await this.sdkProvider('deploy'), stack);
cloudWatchLogMonitor.addLogGroups(
foundLogGroupsResult.env,
foundLogGroupsResult.sdk,
foundLogGroupsResult.logGroupNames,
);
await ioHost.notify(info(`The following log groups are added: ${foundLogGroupsResult.logGroupNames}`, 'CDK_TOOLKIT_I3001'));
}

// If an outputs file has been specified, create the file path and write stack outputs to it once.
// Outputs are written after all stacks have been deployed. If a stack deployment fails,
Expand Down Expand Up @@ -568,13 +570,12 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
// -------------- -------- 'cdk deploy' done -------------- 'cdk deploy' done --------------
let latch: 'pre-ready' | 'open' | 'deploying' | 'queued' = 'pre-ready';

// @todo
// const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor() : undefined;
const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor() : undefined;
const deployAndWatch = async () => {
latch = 'deploying';
// cloudWatchLogMonitor?.deactivate();
cloudWatchLogMonitor?.deactivate();

await this.invokeDeployFromWatch(assembly, options);
await this.invokeDeployFromWatch(assembly, options, cloudWatchLogMonitor);

// If latch is still 'deploying' after the 'await', that's fine,
// but if it's 'queued', that means we need to deploy again
Expand All @@ -583,10 +584,10 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
// and thinks the above 'while' condition is always 'false' without the cast
latch = 'deploying';
await ioHost.notify(info("Detected file changes during deployment. Invoking 'cdk deploy' again"));
await this.invokeDeployFromWatch(assembly, options);
await this.invokeDeployFromWatch(assembly, options, cloudWatchLogMonitor);
}
latch = 'open';
// cloudWatchLogMonitor?.activate();
cloudWatchLogMonitor?.activate();
};

chokidar
Expand Down Expand Up @@ -769,13 +770,14 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
private async invokeDeployFromWatch(
assembly: StackAssembly,
options: WatchOptions,
cloudWatchLogMonitor?: CloudWatchLogEventMonitor,
): Promise<void> {
// watch defaults hotswap to enabled
const hotswap = options.hotswap ?? HotswapMode.HOTSWAP_ONLY;
const deployOptions: ExtendedDeployOptions = {
...options,
requireApproval: RequireApproval.NEVER,
// cloudWatchLogMonitor,
cloudWatchLogMonitor,
hotswap,
extraUserAgent: `cdk-watch/hotswap-${hotswap === HotswapMode.FULL_DEPLOYMENT ? 'off' : 'on'}`,
};
Expand Down
26 changes: 26 additions & 0 deletions packages/@aws-cdk/toolkit/test/actions/deploy.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
let mockFindCloudWatchLogGroups = jest.fn();

import { RequireApproval, StackParameters } from '../../lib';
import { MockSdk } from '../../lib/api/aws-cdk';
import { Toolkit } from '../../lib/toolkit';
import { builderFixture, TestIoHost } from '../_helpers';

const sdk = new MockSdk();
const ioHost = new TestIoHost();
const toolkit = new Toolkit({ ioHost });
const rollbackSpy = jest.spyOn(toolkit as any, '_rollback').mockResolvedValue({});
Expand All @@ -22,13 +26,19 @@ jest.mock('../../lib/api/aws-cdk', () => {
isSingleAssetPublished: jest.fn().mockResolvedValue(true),
readCurrentTemplate: jest.fn().mockResolvedValue({ Resources: {} }),
})),
findCloudWatchLogGroups: mockFindCloudWatchLogGroups,
};
});

beforeEach(() => {
ioHost.notifySpy.mockClear();
ioHost.requestSpy.mockClear();
jest.clearAllMocks();
mockFindCloudWatchLogGroups.mockReturnValue({
env: { name: 'Z', account: 'X', region: 'Y' },
sdk,
logGroupNames: ['/aws/lambda/lambda-function-name'],
});
});

describe('deploy', () => {
Expand Down Expand Up @@ -127,6 +137,22 @@ describe('deploy', () => {
successfulDeployment();
});

test('can trace logs', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
await toolkit.deploy(cx, {
traceLogs: true,
});

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
action: 'deploy',
level: 'info',
code: 'CDK_TOOLKIT_I3001',
message: expect.stringContaining('The following log groups are added: /aws/lambda/lambda-function-name'),
}));
});

test('non sns notification arn results in error', async () => {
// WHEN
const arn = 'arn:aws:sqs:us-east-1:1111111111:resource';
Expand Down
21 changes: 20 additions & 1 deletion packages/@aws-cdk/toolkit/test/actions/watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,39 @@ describe('watch', () => {
}));
});

test('can trace logs', async () => {
// GIVEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'debug';
await toolkit.watch(cx, {
include: [],
traceLogs: true,
});

// WHEN
await fakeChokidarWatcherOn.readyCallback();

// THEN
expect(deploySpy).toHaveBeenCalledWith(expect.anything(), 'watch', expect.objectContaining({
cloudWatchLogMonitor: expect.anything(), // Not undefined
}));
});

describe.each([
[HotswapMode.FALL_BACK, 'on'],
[HotswapMode.HOTSWAP_ONLY, 'on'],
[HotswapMode.FULL_DEPLOYMENT, 'off'],
])('%p mode', (hotswapMode, userAgent) => {
test('passes through the correct hotswap mode to deployStack()', async () => {
// WHEN
// GIVEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'warn';
await toolkit.watch(cx, {
include: [],
hotswap: hotswapMode,
});

// WHEN
await fakeChokidarWatcherOn.readyCallback();

// THEN
Expand Down