Skip to content

Commit efa135b

Browse files
committed
feat(toolkit-lib): structured log monitoring events
1 parent 84d0b54 commit efa135b

File tree

7 files changed

+151
-61
lines changed

7 files changed

+151
-61
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './progress';
1212
export * from './watch';
1313
export * from './stack-details';
1414
export * from './diff';
15+
export * from './logs-monitor';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
/**
3+
* Payload when stack monitoring is starting or stopping for a given stack deployment.
4+
*/
5+
export interface CloudWatchLogMonitorControlEvent {
6+
/**
7+
* A unique identifier for a monitor
8+
*
9+
* Use this value to attribute events received for concurrent log monitoring.
10+
*/
11+
readonly monitor: string;
12+
13+
/**
14+
* The names of monitored log groups
15+
*/
16+
readonly logGroupNames: string[];
17+
}
18+
19+
/**
20+
* Represents a CloudWatch Log Event that will be
21+
* printed to the terminal
22+
*/
23+
export interface CloudWatchLogEvent {
24+
/**
25+
* The log event message
26+
*/
27+
readonly message: string;
28+
29+
/**
30+
* The name of the log group
31+
*/
32+
readonly logGroupName: string;
33+
34+
/**
35+
* The time at which the event occurred
36+
*/
37+
readonly timestamp: Date;
38+
}

packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { MissingContext, UpdatedContext } from '../payloads/context';
66
import type { BuildAsset, DeployConfirmationRequest, PublishAsset, StackDeployProgress, SuccessfulDeployStackResult } from '../payloads/deploy';
77
import type { StackDestroy, StackDestroyProgress } from '../payloads/destroy';
88
import type { StackDetailsPayload } from '../payloads/list';
9+
import type { CloudWatchLogEvent, CloudWatchLogMonitorControlEvent } from '../payloads/logs-monitor';
910
import type { StackRollbackProgress } from '../payloads/rollback';
1011
import type { SdkTrace } from '../payloads/sdk-trace';
1112
import type { StackActivity, StackMonitoringControlEvent } from '../payloads/stack-activity';
@@ -98,6 +99,26 @@ export const IO = {
9899
code: 'CDK_TOOLKIT_I5031',
99100
description: 'Informs about any log groups that are traced as part of the deployment',
100101
}),
102+
CDK_TOOLKIT_I5032: make.debug<CloudWatchLogMonitorControlEvent>({
103+
code: 'CDK_TOOLKIT_I5032',
104+
description: 'Start monitoring log groups',
105+
interface: 'CloudWatchLogMonitorControlEvent',
106+
}),
107+
CDK_TOOLKIT_I5033: make.info<CloudWatchLogEvent>({
108+
code: 'CDK_TOOLKIT_I5033',
109+
description: 'A log event received from Cloud Watch',
110+
interface: 'CloudWatchLogEvent',
111+
}),
112+
CDK_TOOLKIT_I5034: make.debug<CloudWatchLogMonitorControlEvent>({
113+
code: 'CDK_TOOLKIT_I5034',
114+
description: 'Stop monitoring log groups',
115+
interface: 'CloudWatchLogMonitorControlEvent',
116+
}),
117+
CDK_TOOLKIT_E5035: make.error<ErrorPayload>({
118+
code: 'CDK_TOOLKIT_E5035',
119+
description: 'A log monitoring error',
120+
interface: 'ErrorPayload',
121+
}),
101122
CDK_TOOLKIT_I5050: make.confirm<ConfirmationRequest>({
102123
code: 'CDK_TOOLKIT_I5050',
103124
description: 'Confirm rollback during deployment',

packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
519519
} finally {
520520
if (options.traceLogs) {
521521
// deploy calls that originate from watch will come with their own cloudWatchLogMonitor
522-
const cloudWatchLogMonitor = options.cloudWatchLogMonitor ?? new CloudWatchLogEventMonitor();
522+
const cloudWatchLogMonitor = options.cloudWatchLogMonitor ?? new CloudWatchLogEventMonitor({ ioHelper });
523523
const foundLogGroupsResult = await findCloudWatchLogGroups(await this.sdkProvider('deploy'), ioHelper, stack);
524524
cloudWatchLogMonitor.addLogGroups(
525525
foundLogGroupsResult.env,
@@ -637,10 +637,10 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
637637
type LatchState = 'pre-ready' | 'open' | 'deploying' | 'queued';
638638
let latch: LatchState = 'pre-ready';
639639

640-
const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor() : undefined;
640+
const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor({ ioHelper }) : undefined;
641641
const deployAndWatch = async () => {
642642
latch = 'deploying' as LatchState;
643-
cloudWatchLogMonitor?.deactivate();
643+
await cloudWatchLogMonitor?.deactivate();
644644

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

@@ -654,7 +654,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
654654
await this.invokeDeployFromWatch(assembly, options, cloudWatchLogMonitor);
655655
}
656656
latch = 'open';
657-
cloudWatchLogMonitor?.activate();
657+
await cloudWatchLogMonitor?.activate();
658658
};
659659

660660
chokidar

packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CloudFormationStackArtifact, Environment } from '@aws-cdk/cx-api';
22
import type { StackResourceSummary } from '@aws-sdk/client-cloudformation';
3+
import { IO } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/';
34
import { IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
4-
import { debug } from '../../logging';
55
import { formatErrorMessage } from '../../util';
66
import type { SDK, SdkProvider } from '../aws-auth';
77
import { EnvironmentAccess } from '../environment';
@@ -47,7 +47,7 @@ export async function findCloudWatchLogGroups(
4747
try {
4848
sdk = (await new EnvironmentAccess(sdkProvider, DEFAULT_TOOLKIT_STACK_NAME, ioHelper).accessStackForLookup(stackArtifact)).sdk;
4949
} catch (e: any) {
50-
debug(`Failed to access SDK environment: ${formatErrorMessage(e)}`);
50+
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Failed to access SDK environment: ${formatErrorMessage(e)}`));
5151
sdk = (await sdkProvider.forEnvironment(resolvedEnv, Mode.ForReading)).sdk;
5252
}
5353

packages/aws-cdk/lib/api/logs/logs-monitor.ts

Lines changed: 79 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,13 @@
11
import * as util from 'util';
22
import * as cxapi from '@aws-cdk/cx-api';
33
import * as chalk from 'chalk';
4-
import { info, error } from '../../logging';
4+
import * as uuid from 'uuid';
5+
import { IO } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io';
6+
import type { CloudWatchLogEvent } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io';
7+
import { IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
58
import { flatten } from '../../util';
69
import type { SDK } from '../aws-auth';
710

8-
/**
9-
* After reading events from all CloudWatch log groups
10-
* how long should we wait to read more events.
11-
*
12-
* If there is some error with reading events (i.e. Throttle)
13-
* then this is also how long we wait until we try again
14-
*/
15-
const SLEEP = 2_000;
16-
17-
/**
18-
* Represents a CloudWatch Log Event that will be
19-
* printed to the terminal
20-
*/
21-
interface CloudWatchLogEvent {
22-
/**
23-
* The log event message
24-
*/
25-
readonly message: string;
26-
27-
/**
28-
* The name of the log group
29-
*/
30-
readonly logGroupName: string;
31-
32-
/**
33-
* The time at which the event occurred
34-
*/
35-
readonly timestamp: Date;
36-
}
37-
3811
/**
3912
* Configuration tracking information on the log groups that are
4013
* being monitored
@@ -54,6 +27,20 @@ interface LogGroupsAccessSettings {
5427
readonly logGroupsStartTimes: { [logGroupName: string]: number };
5528
}
5629

30+
export interface CloudWatchLogEventMonitorProps {
31+
/**
32+
* The IoHost used for messaging
33+
*/
34+
readonly ioHelper: IoHelper;
35+
36+
/**
37+
* The time from which we start reading log messages
38+
*
39+
* @default - now
40+
*/
41+
readonly startTime?: Date;
42+
}
43+
5744
export class CloudWatchLogEventMonitor {
5845
/**
5946
* Determines which events not to display
@@ -65,18 +52,36 @@ export class CloudWatchLogEventMonitor {
6552
*/
6653
private readonly envsLogGroupsAccessSettings = new Map<string, LogGroupsAccessSettings>();
6754

68-
private active = false;
55+
/**
56+
* After reading events from all CloudWatch log groups
57+
* how long should we wait to read more events.
58+
*
59+
* If there is some error with reading events (i.e. Throttle)
60+
* then this is also how long we wait until we try again
61+
*/
62+
private readonly pollingInterval: number = 2_000;
63+
64+
public monitorId?: string;
65+
private readonly ioHelper: IoHelper;
6966

70-
constructor(startTime?: Date) {
71-
this.startTime = startTime?.getTime() ?? Date.now();
67+
constructor(props: CloudWatchLogEventMonitorProps) {
68+
this.startTime = props.startTime?.getTime() ?? Date.now();
69+
this.ioHelper = props.ioHelper;
7270
}
7371

7472
/**
7573
* resume reading/printing events
7674
*/
77-
public activate(): void {
78-
this.active = true;
79-
this.scheduleNextTick(0);
75+
public async activate(): Promise<void> {
76+
this.monitorId = uuid.v4();
77+
78+
await this.ioHelper.notify(IO.CDK_TOOLKIT_I5032.msg('Start monitoring log groups', {
79+
monitor: this.monitorId,
80+
logGroupNames: this.logGroupNames(),
81+
}));
82+
83+
await this.tick();
84+
this.scheduleNextTick();
8085
}
8186

8287
/**
@@ -88,9 +93,16 @@ export class CloudWatchLogEventMonitor {
8893
* Also resets the start time to be when the new deployment was triggered
8994
* and clears the list of tracked log groups
9095
*/
91-
public deactivate(): void {
92-
this.active = false;
96+
public async deactivate(): Promise<void> {
97+
const oldMonitorId = this.monitorId!;
98+
this.monitorId = undefined;
9399
this.startTime = Date.now();
100+
101+
await this.ioHelper.notify(IO.CDK_TOOLKIT_I5034.msg('Stopped monitoring log groups', {
102+
monitor: oldMonitorId,
103+
logGroupNames: this.logGroupNames(),
104+
}));
105+
94106
this.envsLogGroupsAccessSettings.clear();
95107
}
96108

@@ -119,27 +131,41 @@ export class CloudWatchLogEventMonitor {
119131
});
120132
}
121133

122-
private scheduleNextTick(sleep: number): void {
123-
setTimeout(() => void this.tick(), sleep);
134+
private logGroupNames(): string[] {
135+
return Array.from(this.envsLogGroupsAccessSettings.values()).flatMap((settings) => Object.keys(settings.logGroupsStartTimes));
136+
}
137+
138+
private scheduleNextTick(): void {
139+
if (!this.monitorId) {
140+
return;
141+
}
142+
143+
setTimeout(() => void this.tick(), this.pollingInterval);
124144
}
125145

126146
private async tick(): Promise<void> {
127147
// excluding from codecoverage because this
128148
// doesn't always run (depends on timing)
129-
/* istanbul ignore next */
130-
if (!this.active) {
149+
/* c8 ignore next */
150+
if (!this.monitorId) {
131151
return;
132152
}
153+
133154
try {
134155
const events = flatten(await this.readNewEvents());
135-
events.forEach((event) => {
136-
this.print(event);
137-
});
138-
} catch (e) {
139-
error('Error occurred while monitoring logs: %s', e);
156+
for (const event of events) {
157+
await this.print(event);
158+
}
159+
160+
// We might have been stop()ped while the network call was in progress.
161+
if (!this.monitorId) {
162+
return;
163+
}
164+
} catch (e: any) {
165+
await this.ioHelper.notify(IO.CDK_TOOLKIT_E5035.msg('Error occurred while monitoring logs: %s', { error: e }));
140166
}
141167

142-
this.scheduleNextTick(SLEEP);
168+
this.scheduleNextTick();
143169
}
144170

145171
/**
@@ -161,15 +187,16 @@ export class CloudWatchLogEventMonitor {
161187
/**
162188
* Print out a cloudwatch event
163189
*/
164-
private print(event: CloudWatchLogEvent): void {
165-
info(
190+
private async print(event: CloudWatchLogEvent): Promise<void> {
191+
await this.ioHelper.notify(IO.CDK_TOOLKIT_I5033.msg(
166192
util.format(
167193
'[%s] %s %s',
168194
chalk.blue(event.logGroupName),
169195
chalk.yellow(event.timestamp.toLocaleTimeString()),
170196
event.message.trim(),
171197
),
172-
);
198+
event,
199+
));
173200
}
174201

175202
/**

packages/aws-cdk/lib/cli/cdk-toolkit.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -697,10 +697,13 @@ export class CdkToolkit {
697697
// -------------- -------- 'cdk deploy' done -------------- 'cdk deploy' done --------------
698698
let latch: 'pre-ready' | 'open' | 'deploying' | 'queued' = 'pre-ready';
699699

700-
const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor() : undefined;
700+
const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor({
701+
action: 'watch',
702+
ioHost: this.ioHost,
703+
}) : undefined;
701704
const deployAndWatch = async () => {
702705
latch = 'deploying';
703-
cloudWatchLogMonitor?.deactivate();
706+
await cloudWatchLogMonitor?.deactivate();
704707

705708
await this.invokeDeployFromWatch(options, cloudWatchLogMonitor);
706709

@@ -714,7 +717,7 @@ export class CdkToolkit {
714717
await this.invokeDeployFromWatch(options, cloudWatchLogMonitor);
715718
}
716719
latch = 'open';
717-
cloudWatchLogMonitor?.activate();
720+
await cloudWatchLogMonitor?.activate();
718721
};
719722

720723
chokidar

0 commit comments

Comments
 (0)