Skip to content

Commit f227c9b

Browse files
authored
feat(toolkit-lib): emit drift in span (#550)
When we merged #442 we forgot to add message spans to the drift command. This PR rectifies this. Also contains a refactor of `MessageSpan` to be a separate class and to represent itself as `IoHelper`. With that we can now pass a `Span` down into subroutines and have those messages also emitted as part of the span. Start using that in some places. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 566e2ab commit f227c9b

File tree

7 files changed

+139
-104
lines changed

7 files changed

+139
-104
lines changed

packages/@aws-cdk/toolkit-lib/docs/message-registry.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ Please let us know by [opening an issue](https://github.com/aws/aws-cdk-cli/issu
7373
| `CDK_TOOLKIT_I4000` | Diff stacks is starting | `trace` | {@link StackSelectionDetails} |
7474
| `CDK_TOOLKIT_I4001` | Output of the diff command | `result` | {@link DiffResult} |
7575
| `CDK_TOOLKIT_I4002` | The diff for a single stack | `result` | {@link StackDiff} |
76-
| `CDK_TOOLKIT_I4590` | Results of the drift command | `result` | {@link DriftResultPayload} |
77-
| `CDK_TOOLKIT_I4591` | Missing drift result fort a stack. | `warn` | {@link SingleStack} |
76+
| `CDK_TOOLKIT_I4500` | Drift detection is starting | `trace` | {@link StackSelectionDetails} |
77+
| `CDK_TOOLKIT_I4592` | Results of the drift | `result` | {@link Duration} |
78+
| `CDK_TOOLKIT_I4590` | Results of a stack drift | `result` | {@link DriftResultPayload} |
79+
| `CDK_TOOLKIT_W4591` | Missing drift result fort a stack. | `warn` | {@link SingleStack} |
7880
| `CDK_TOOLKIT_I5000` | Provides deployment times | `info` | {@link Duration} |
7981
| `CDK_TOOLKIT_I5001` | Provides total time in deploy action, including synth and rollback | `info` | {@link Duration} |
8082
| `CDK_TOOLKIT_I5002` | Provides time for resource migration | `info` | {@link Duration} |

packages/@aws-cdk/toolkit-lib/lib/api/hotswap/hotswap-deployments.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ async function applyAllHotswapOperations(sdk: SDK, ioSpan: IMessageSpan<any>, ho
499499
return Promise.resolve([]);
500500
}
501501

502-
await ioSpan.notifyDefault('info', `\n${ICON} hotswapping resources:`);
502+
await ioSpan.defaults.info(`\n${ICON} hotswapping resources:`);
503503
const limit = pLimit(10);
504504
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
505505
return Promise.all(hotswappableChanges.map(hotswapOperation => limit(() => {
@@ -594,7 +594,7 @@ async function logRejectedChanges(
594594
}
595595
messages.push(''); // newline
596596

597-
await ioSpan.notifyDefault('info', messages.join('\n'));
597+
await ioSpan.defaults.info(messages.join('\n'));
598598
}
599599

600600
/**

packages/@aws-cdk/toolkit-lib/lib/api/io/private/io-helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class IoHelper implements IIoHost, IActionAwareIoHost {
5959
* Create a new marker from a given registry entry
6060
*/
6161
public span<S extends object, E extends SpanEnd>(definition: SpanDefinition<S, E>) {
62-
return new SpanMaker(this, definition);
62+
return new SpanMaker(this, definition, (ioHost) => IoHelper.fromActionAwareIoHost(ioHost));
6363
}
6464
}
6565

packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,23 @@ export const IO = {
9797
}),
9898

9999
// 4: Drift (45xx - 49xx)
100+
CDK_TOOLKIT_I4500: make.trace<StackSelectionDetails>({
101+
code: 'CDK_TOOLKIT_I4500',
102+
description: 'Drift detection is starting',
103+
interface: 'StackSelectionDetails',
104+
}),
105+
CDK_TOOLKIT_I4509: make.result<Duration>({
106+
code: 'CDK_TOOLKIT_I4592',
107+
description: 'Results of the drift',
108+
interface: 'Duration',
109+
}),
100110
CDK_TOOLKIT_I4590: make.result<DriftResultPayload>({
101111
code: 'CDK_TOOLKIT_I4590',
102-
description: 'Results of the drift command',
112+
description: 'Results of a stack drift',
103113
interface: 'DriftResultPayload',
104114
}),
105-
CDK_TOOLKIT_I4591: make.warn<SingleStack>({
106-
code: 'CDK_TOOLKIT_I4591',
115+
CDK_TOOLKIT_W4591: make.warn<SingleStack>({
116+
code: 'CDK_TOOLKIT_W4591',
107117
description: 'Missing drift result fort a stack.',
108118
interface: 'SingleStack',
109119
}),
@@ -536,6 +546,11 @@ export const SPAN = {
536546
start: IO.CDK_TOOLKIT_I4000,
537547
end: IO.CDK_TOOLKIT_I4001,
538548
},
549+
DRIFT_APP: {
550+
name: 'Drift',
551+
start: IO.CDK_TOOLKIT_I4000,
552+
end: IO.CDK_TOOLKIT_I4509,
553+
},
539554
DESTROY_STACK: {
540555
name: 'Destroy',
541556
start: IO.CDK_TOOLKIT_I7100,

packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts

Lines changed: 81 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as util from 'node:util';
22
import * as uuid from 'uuid';
3-
import type { ActionLessMessage, IoHelper } from './io-helper';
3+
import type { ActionLessMessage, ActionLessRequest, IoHelper } from './io-helper';
44
import type * as make from './message-maker';
55
import type { Duration } from '../../../payloads/types';
66
import { formatTime } from '../../../util';
7-
import type { IoMessageLevel } from '../io-message';
7+
import type { IActionAwareIoHost } from '../io-host';
8+
import type { IoDefaultMessages } from './io-default-messages';
89

910
export interface SpanEnd {
1011
readonly duration: number;
@@ -57,7 +58,15 @@ interface ElapsedTime {
5758
/**
5859
* A message span that can be ended and read times from
5960
*/
60-
export interface IMessageSpan<E extends SpanEnd> {
61+
export interface IMessageSpan<E extends SpanEnd> extends IActionAwareIoHost {
62+
/**
63+
* An IoHelper wrapped around the span.
64+
*/
65+
readonly asHelper: IoHelper;
66+
/**
67+
* An IoDefaultMessages wrapped around the span.
68+
*/
69+
readonly defaults: IoDefaultMessages;
6170
/**
6271
* Get the time elapsed since the start
6372
*/
@@ -67,14 +76,6 @@ export interface IMessageSpan<E extends SpanEnd> {
6776
* For more complex intermediate messages, get the `elapsedTime` and use `notify`
6877
*/
6978
timing(maker: make.IoMessageMaker<Duration>, message?: string): Promise<ElapsedTime>;
70-
/**
71-
* Sends an arbitrary intermediate message as part of the span
72-
*/
73-
notify(message: ActionLessMessage<unknown>): Promise<void>;
74-
/**
75-
* Sends an arbitrary intermediate default message as part of the span
76-
*/
77-
notifyDefault(level: IoMessageLevel, message: string): Promise<void>;
7879
/**
7980
* End the span with a payload
8081
*/
@@ -99,10 +100,12 @@ export interface IMessageSpan<E extends SpanEnd> {
99100
export class SpanMaker<S extends object, E extends SpanEnd> {
100101
private readonly definition: SpanDefinition<S, E>;
101102
private readonly ioHelper: IoHelper;
103+
private makeHelper: (ioHost: IActionAwareIoHost) => IoHelper;
102104

103-
public constructor(ioHelper: IoHelper, definition: SpanDefinition<S, E>) {
105+
public constructor(ioHelper: IoHelper, definition: SpanDefinition<S, E>, makeHelper: (ioHost: IActionAwareIoHost) => IoHelper) {
104106
this.definition = definition;
105107
this.ioHelper = ioHelper;
108+
this.makeHelper = makeHelper;
106109
}
107110

108111
/**
@@ -112,68 +115,77 @@ export class SpanMaker<S extends object, E extends SpanEnd> {
112115
public async begin(payload: VoidWhenEmpty<S>): Promise<IMessageSpan<E>>;
113116
public async begin(message: string, payload: S): Promise<IMessageSpan<E>>;
114117
public async begin(a: any, b?: S): Promise<IMessageSpan<E>> {
115-
const spanId = uuid.v4();
116-
const startTime = new Date().getTime();
117-
118-
const notify = (msg: ActionLessMessage<unknown>): Promise<void> => {
119-
return this.ioHelper.notify(withSpanId(spanId, msg));
120-
};
121-
118+
const span = new MessageSpan(this.ioHelper, this.definition, this.makeHelper);
122119
const startInput = parseArgs<S>(a, b);
123120
const startMsg = startInput.message ?? `Starting ${this.definition.name} ...`;
124121
const startPayload = startInput.payload;
122+
await span.notify(this.definition.start.msg(startMsg, startPayload));
125123

126-
await notify(this.definition.start.msg(
127-
startMsg,
128-
startPayload,
129-
));
130-
131-
const timingMsgTemplate = '\n✨ %s time: %ds\n';
132-
const time = () => {
133-
const elapsedTime = new Date().getTime() - startTime;
134-
return {
135-
asMs: elapsedTime,
136-
asSec: formatTime(elapsedTime),
137-
};
138-
};
124+
return span;
125+
}
126+
}
127+
128+
class MessageSpan<S extends object, E extends SpanEnd> implements IMessageSpan<E> {
129+
public readonly asHelper: IoHelper;
130+
131+
private readonly definition: SpanDefinition<S, E>;
132+
private readonly ioHelper: IoHelper;
133+
private readonly spanId: string;
134+
private readonly startTime: number;
135+
private readonly timingMsgTemplate: string;
136+
137+
public constructor(ioHelper: IoHelper, definition: SpanDefinition<S, E>, makeHelper: (ioHost: IActionAwareIoHost) => IoHelper) {
138+
this.definition = definition;
139+
this.ioHelper = ioHelper;
140+
this.spanId = uuid.v4();
141+
this.startTime = new Date().getTime();
142+
this.timingMsgTemplate = '\n✨ %s time: %ds\n';
143+
this.asHelper = makeHelper(this);
144+
}
145+
146+
public get defaults(): IoDefaultMessages {
147+
return this.asHelper.defaults;
148+
}
149+
150+
public async elapsedTime(): Promise<ElapsedTime> {
151+
return this.time();
152+
}
153+
public async timing(maker: make.IoMessageMaker<Duration>, message?: string): Promise<ElapsedTime> {
154+
const duration = this.time();
155+
const timingMsg = message ? message : util.format(this.timingMsgTemplate, this.definition.name, duration.asSec);
156+
await this.notify(maker.msg(timingMsg, {
157+
duration: duration.asMs,
158+
}));
159+
return duration;
160+
}
161+
public async notify(msg: ActionLessMessage<unknown>): Promise<void> {
162+
return this.ioHelper.notify(withSpanId(this.spanId, msg));
163+
}
164+
public async end(x: any, y?: ForceEmpty<Optional<E, keyof SpanEnd>>): Promise<ElapsedTime> {
165+
const duration = this.time();
166+
167+
const endInput = parseArgs<ForceEmpty<Optional<E, keyof SpanEnd>>>(x, y);
168+
const endMsg = endInput.message ?? util.format(this.timingMsgTemplate, this.definition.name, duration.asSec);
169+
const endPayload = endInput.payload;
170+
171+
await this.notify(this.definition.end.msg(
172+
endMsg, {
173+
duration: duration.asMs,
174+
...endPayload,
175+
} as E));
176+
177+
return duration;
178+
}
179+
180+
public async requestResponse<T>(msg: ActionLessRequest<unknown, T>): Promise<T> {
181+
return this.ioHelper.requestResponse(withSpanId(this.spanId, msg));
182+
}
139183

184+
private time() {
185+
const elapsedTime = new Date().getTime() - this.startTime;
140186
return {
141-
elapsedTime: async (): Promise<ElapsedTime> => {
142-
return time();
143-
},
144-
145-
notify: async(msg: ActionLessMessage<unknown>): Promise<void> => {
146-
await notify(msg);
147-
},
148-
149-
notifyDefault: async(level: IoMessageLevel, msg: string): Promise<void> => {
150-
await notify(this.ioHelper.defaults.msg(level, msg));
151-
},
152-
153-
timing: async(maker: make.IoMessageMaker<Duration>, message?: string): Promise<ElapsedTime> => {
154-
const duration = time();
155-
const timingMsg = message ? message : util.format(timingMsgTemplate, this.definition.name, duration.asSec);
156-
await notify(maker.msg(timingMsg, {
157-
duration: duration.asMs,
158-
}));
159-
return duration;
160-
},
161-
162-
end: async (x: any, y?: ForceEmpty<Optional<E, keyof SpanEnd>>): Promise<ElapsedTime> => {
163-
const duration = time();
164-
165-
const endInput = parseArgs<ForceEmpty<Optional<E, keyof SpanEnd>>>(x, y);
166-
const endMsg = endInput.message ?? util.format(timingMsgTemplate, this.definition.name, duration.asSec);
167-
const endPayload = endInput.payload;
168-
169-
await notify(this.definition.end.msg(
170-
endMsg, {
171-
duration: duration.asMs,
172-
...endPayload,
173-
} as E));
174-
175-
return duration;
176-
},
187+
asMs: elapsedTime,
188+
asSec: formatTime(elapsedTime),
177189
};
178190
}
179191
}
@@ -194,7 +206,7 @@ function parseArgs<S extends object>(first: any, second?: S): { message: string
194206
};
195207
}
196208

197-
function withSpanId(span: string, message: ActionLessMessage<unknown>): ActionLessMessage<unknown> {
209+
function withSpanId<T extends object>(span: string, message: T): T & { span: string } {
198210
return {
199211
...message,
200212
span,

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

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,11 @@ export class Toolkit extends CloudAssemblySourceBuilder {
283283
const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks });
284284

285285
// NOTE: NOT 'await using' because we return ownership to the caller
286-
const assembly = await assemblyFromSource(ioHelper, cx);
286+
const assembly = await assemblyFromSource(synthSpan.asHelper, cx);
287287

288288
const stacks = await assembly.selectStacksV2(selectStacks);
289289
const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : [];
290-
await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHelper);
290+
await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), synthSpan.asHelper);
291291
await synthSpan.end();
292292

293293
// if we have a single stack, print it to STDOUT
@@ -328,7 +328,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
328328
const ioHelper = asIoHelper(this.ioHost, 'diff');
329329
const selectStacks = options.stacks ?? ALL_STACKS;
330330
const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks });
331-
await using assembly = await assemblyFromSource(ioHelper, cx);
331+
await using assembly = await assemblyFromSource(synthSpan.asHelper, cx);
332332
const stacks = await assembly.selectStacksV2(selectStacks);
333333
await synthSpan.end();
334334

@@ -340,7 +340,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
340340

341341
let diffs = 0;
342342

343-
const templateInfos = await prepareDiff(ioHelper, stacks, deployments, await this.sdkProvider('diff'), options);
343+
const templateInfos = await prepareDiff(diffSpan.asHelper, stacks, deployments, await this.sdkProvider('diff'), options);
344344
const templateDiffs: { [name: string]: TemplateDiff } = {};
345345
for (const templateInfo of templateInfos) {
346346
const formatter = new DiffFormatter({ templateInfo });
@@ -352,8 +352,8 @@ export class Toolkit extends CloudAssemblySourceBuilder {
352352
// We only warn about BROADENING changes
353353
if (securityDiff.permissionChangeType == PermissionChangeType.BROADENING) {
354354
const warningMessage = 'This deployment will make potentially sensitive changes according to your current security approval level.\nPlease confirm you intend to make the following modifications:\n';
355-
await diffSpan.notifyDefault('warn', warningMessage);
356-
await diffSpan.notifyDefault('info', securityDiff.formattedDiff);
355+
await diffSpan.defaults.warn(warningMessage);
356+
await diffSpan.defaults.info(securityDiff.formattedDiff);
357357
}
358358

359359
// Stack Diff
@@ -384,22 +384,25 @@ export class Toolkit extends CloudAssemblySourceBuilder {
384384
*/
385385
public async drift(cx: ICloudAssemblySource, options: DriftOptions): Promise<{ [name: string]: DriftResult }> {
386386
const ioHelper = asIoHelper(this.ioHost, 'drift');
387-
const sdkProvider = await this.sdkProvider('drift');
388387
const selectStacks = options.stacks ?? ALL_STACKS;
389-
await using assembly = await assemblyFromSource(ioHelper, cx);
388+
const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks });
389+
await using assembly = await assemblyFromSource(synthSpan.asHelper, cx);
390390
const stacks = await assembly.selectStacksV2(selectStacks);
391+
await synthSpan.end();
391392

393+
const driftSpan = await ioHelper.span(SPAN.DRIFT_APP).begin({ stacks: selectStacks });
392394
const allDriftResults: { [name: string]: DriftResult } = {};
393395
const unavailableDrifts = [];
396+
const sdkProvider = await this.sdkProvider('drift');
394397

395398
for (const stack of stacks.stackArtifacts) {
396399
const cfn = (await sdkProvider.forEnvironment(stack.environment, Mode.ForReading)).sdk.cloudFormation();
397-
const driftResults = await detectStackDrift(cfn, ioHelper, stack.stackName);
400+
const driftResults = await detectStackDrift(cfn, driftSpan.asHelper, stack.stackName);
398401

399402
if (!driftResults.StackResourceDrifts) {
400403
const stackName = stack.displayName ?? stack.stackName;
401404
unavailableDrifts.push(stackName);
402-
await ioHelper.notify(IO.CDK_TOOLKIT_I4591.msg(`${stackName}: No drift results available`, { stack }));
405+
await driftSpan.notify(IO.CDK_TOOLKIT_W4591.msg(`${stackName}: No drift results available`, { stack }));
403406
continue;
404407
}
405408

@@ -418,24 +421,24 @@ export class Toolkit extends CloudAssemblySourceBuilder {
418421
allDriftResults[formatter.stackName] = stackDrift;
419422

420423
// header
421-
await ioHelper.defaults.info(driftOutput.stackHeader);
424+
await driftSpan.defaults.info(driftOutput.stackHeader);
422425

423426
// print the different sections at different levels
424427
if (driftOutput.unchanged) {
425-
await ioHelper.defaults.debug(driftOutput.unchanged);
428+
await driftSpan.defaults.debug(driftOutput.unchanged);
426429
}
427430
if (driftOutput.unchecked) {
428-
await ioHelper.defaults.debug(driftOutput.unchecked);
431+
await driftSpan.defaults.debug(driftOutput.unchecked);
429432
}
430433
if (driftOutput.modified) {
431-
await ioHelper.defaults.info(driftOutput.modified);
434+
await driftSpan.defaults.info(driftOutput.modified);
432435
}
433436
if (driftOutput.deleted) {
434-
await ioHelper.defaults.info(driftOutput.deleted);
437+
await driftSpan.defaults.info(driftOutput.deleted);
435438
}
436439

437440
// main stack result
438-
await ioHelper.notify(IO.CDK_TOOLKIT_I4590.msg(driftOutput.summary, {
441+
await driftSpan.notify(IO.CDK_TOOLKIT_I4590.msg(driftOutput.summary, {
439442
stack,
440443
drift: stackDrift,
441444
}));
@@ -444,9 +447,9 @@ export class Toolkit extends CloudAssemblySourceBuilder {
444447
// print summary
445448
const totalDrifts = Object.values(allDriftResults).reduce((total, current) => total + (current.numResourcesWithDrift ?? 0), 0);
446449
const totalUnchecked = Object.values(allDriftResults).reduce((total, current) => total + (current.numResourcesUnchecked ?? 0), 0);
447-
await ioHelper.defaults.result(`\n✨ Number of resources with drift: ${totalDrifts}${totalUnchecked ? ` (${totalUnchecked} unchecked)` : ''}`);
450+
await driftSpan.end(`\n✨ Number of resources with drift: ${totalDrifts}${totalUnchecked ? ` (${totalUnchecked} unchecked)` : ''}`);
448451
if (unavailableDrifts.length) {
449-
await ioHelper.defaults.warn(`\n⚠️ Failed to check drift for ${unavailableDrifts.length} stack(s). Check log for more details.`);
452+
await driftSpan.defaults.warn(`\n⚠️ Failed to check drift for ${unavailableDrifts.length} stack(s). Check log for more details.`);
450453
}
451454

452455
return allDriftResults;

0 commit comments

Comments
 (0)