packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts (107 lines of code) (raw):

import * as util from 'node:util'; import * as uuid from 'uuid'; import type { ActionLessMessage, IoHelper } from './io-helper'; import type { IoMessageMaker } from './message-maker'; import type { Duration } from '../../../payloads/types'; import { formatTime } from '../../../util'; export interface SpanEnd { readonly duration: number; } /** * Describes a specific span * * A span definition is a pair of `IoMessageMaker`s to create a start and end message of the span respectively. * It also has a display name, that is used for auto-generated message text when they are not provided. */ export interface SpanDefinition<S extends object, E extends SpanEnd> { readonly name: string; readonly start: IoMessageMaker<S>; readonly end: IoMessageMaker<E>; } /** * Used in conditional types to check if a type (e.g. after omitting fields) is an empty object * This is needed because counter-intuitive neither `object` nor `{}` represent that. */ type EmptyObject = { [index: string | number | symbol]: never; } /** * Helper type to force a parameter to be not present of the computed type is an empty object */ type VoidWhenEmpty<T> = T extends EmptyObject ? void : T /** * Helper type to force a parameter to be an empty object if the computed type is an empty object * This is weird, but some computed types (e.g. using `Omit`) don't end up enforcing this. */ type ForceEmpty<T> = T extends EmptyObject ? EmptyObject : T /** * Make some properties optional */ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; /** * Ending the span returns the observed duration */ interface ElapsedTime { readonly asMs: number; readonly asSec: number; } /** * A message span that can be ended and read times from */ export interface IMessageSpan<E extends SpanEnd> { /** * Get the time elapsed since the start */ elapsedTime(): Promise<ElapsedTime>; /** * Sends a simple, generic message with the current timing * For more complex intermediate messages, get the `elapsedTime` and use `notify` */ timing(maker: IoMessageMaker<Duration>, message?: string): Promise<ElapsedTime>; /** * Sends an arbitrary intermediate message as part of the span */ notify(message: ActionLessMessage<unknown>): Promise<void>; /** * End the span with a payload */ end(payload: VoidWhenEmpty<Omit<E, keyof SpanEnd>>): Promise<ElapsedTime>; /** * End the span with a payload, overwriting */ end(payload: VoidWhenEmpty<Optional<E, keyof SpanEnd>>): Promise<ElapsedTime>; /** * End the span with a message and payload */ end(message: string, payload: ForceEmpty<Optional<E, keyof SpanEnd>>): Promise<ElapsedTime>; } /** * Helper class to make spans around blocks of work * * Blocks are enclosed by a start and end message. * All messages of the span share a unique id. * The end message contains the time passed between start and end. */ export class SpanMaker<S extends object, E extends SpanEnd> { private readonly definition: SpanDefinition<S, E>; private readonly ioHelper: IoHelper; public constructor(ioHelper: IoHelper, definition: SpanDefinition<S, E>) { this.definition = definition; this.ioHelper = ioHelper; } /** * Starts the span and initially notifies the IoHost * @returns a message span */ public async begin(payload: VoidWhenEmpty<S>): Promise<IMessageSpan<E>>; public async begin(message: string, payload: S): Promise<IMessageSpan<E>>; public async begin(a: any, b?: S): Promise<IMessageSpan<E>> { const spanId = uuid.v4(); const startTime = new Date().getTime(); const notify = (msg: ActionLessMessage<unknown>): Promise<void> => { return this.ioHelper.notify(withSpanId(spanId, msg)); }; const startInput = parseArgs<S>(a, b); const startMsg = startInput.message ?? `Starting ${this.definition.name} ...`; const startPayload = startInput.payload; await notify(this.definition.start.msg( startMsg, startPayload, )); const timingMsgTemplate = '\n✨ %s time: %ds\n'; const time = () => { const elapsedTime = new Date().getTime() - startTime; return { asMs: elapsedTime, asSec: formatTime(elapsedTime), }; }; return { elapsedTime: async (): Promise<ElapsedTime> => { return time(); }, notify: async(msg: ActionLessMessage<unknown>): Promise<void> => { await notify(msg); }, timing: async(maker: IoMessageMaker<Duration>, message?: string): Promise<ElapsedTime> => { const duration = time(); const timingMsg = message ? message : util.format(timingMsgTemplate, this.definition.name, duration.asSec); await notify(maker.msg(timingMsg, { duration: duration.asMs, })); return duration; }, end: async (x: any, y?: ForceEmpty<Optional<E, keyof SpanEnd>>): Promise<ElapsedTime> => { const duration = time(); const endInput = parseArgs<ForceEmpty<Optional<E, keyof SpanEnd>>>(x, y); const endMsg = endInput.message ?? util.format(timingMsgTemplate, this.definition.name, duration.asSec); const endPayload = endInput.payload; await notify(this.definition.end.msg( endMsg, { duration: duration.asMs, ...endPayload, } as E)); return duration; }, }; } } function parseArgs<S extends object>(first: any, second?: S): { message: string | undefined; payload: S } { const firstIsMessage = typeof first === 'string'; // When the first argument is a string or we have a second argument, then the first arg is the message const message = (firstIsMessage || second) ? first : undefined; // When the first argument is a string or we have a second argument, // then the second arg is the payload, otherwise the first arg is the payload const payload = (firstIsMessage || second) ? second : first; return { message, payload, }; } function withSpanId(span: string, message: ActionLessMessage<unknown>): ActionLessMessage<unknown> { return { ...message, span, }; }