Skip to content

Commit

Permalink
refactor(cli): CliIoHost is more self contained (#32993)
Browse files Browse the repository at this point in the history
### Description of changes

We currently have to maintain a global singleton `CliIoHost` until we
have passed the ioHost through all the layers for logging. Previously
the global settings for this `IoHost` were all over the place using
setter functions and global variables. This refactor unifies all these
APIs on the `CliIoHost`, through the global instance.

We also need the ability to register a _different_ `IoHost` that must be
used for reporting. This is the case when a Toolkit integrator provides
a custom implemenation.

### Describe any new or updated permissions being added

no

### Description of how you validated changes

Existing and updated test cases.

### Checklist
- [x] My code adheres to the [CONTRIBUTING
GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and
[DESIGN
GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache-2.0 license*
  • Loading branch information
mrgrain authored Jan 17, 2025
1 parent 6c5accd commit 72e089b
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 185 deletions.
17 changes: 14 additions & 3 deletions packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ export type ToolkitAction =
| 'deploy'
| 'rollback'
| 'watch'
| 'destroy';
| 'destroy'
| 'doctor'
| 'gc'
| 'import'
| 'metadata'
| 'init'
| 'migrate';

export interface ToolkitOptions {
/**
Expand Down Expand Up @@ -78,9 +84,14 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab

public constructor(private readonly props: ToolkitOptions = {}) {
super();

this.ioHost = props.ioHost ?? CliIoHost.getIoHost();
this.toolkitStackName = props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME;

// Hacky way to re-use the global IoHost until we have fully removed the need for it
const globalIoHost = CliIoHost.instance();
if (props.ioHost) {
globalIoHost.registerIoHost(props.ioHost as any);
}
this.ioHost = globalIoHost as IIoHost;
}

public async dispose(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ArtifactMetadataEntryType, type MetadataEntry } from '@aws-cdk/cloud-as
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import { ResourceEvent, StackEventPoller } from './stack-event-poller';
import { error, setIoMessageThreshold, info } from '../../../logging';
import { IoMessageLevel } from '../../../toolkit/cli-io-host';
import { error, info } from '../../../logging';
import { CliIoHost, IoMessageLevel } from '../../../toolkit/cli-io-host';
import type { ICloudFormationClient } from '../../aws-auth';
import { RewritableBlock } from '../display';

Expand Down Expand Up @@ -623,12 +623,13 @@ export class CurrentActivityPrinter extends ActivityPrinterBase {
*/
public readonly updateSleep: number = 2_000;

private oldLogThreshold: IoMessageLevel = 'info';
private oldLogThreshold: IoMessageLevel;
private readonly stream: NodeJS.WriteStream;
private block: RewritableBlock;

constructor(props: PrinterProps) {
super(props);
this.oldLogThreshold = CliIoHost.instance().logLevel;
this.stream = props.stream;
this.block = new RewritableBlock(this.stream);
}
Expand Down Expand Up @@ -674,11 +675,12 @@ export class CurrentActivityPrinter extends ActivityPrinterBase {
public start() {
// Need to prevent the waiter from printing 'stack not stable' every 5 seconds, it messes
// with the output calculations.
setIoMessageThreshold('info');
this.oldLogThreshold = CliIoHost.instance().logLevel;
CliIoHost.instance().logLevel = 'info';
}

public stop() {
setIoMessageThreshold(this.oldLogThreshold);
CliIoHost.instance().logLevel = this.oldLogThreshold;

// Print failures at the end
const lines = new Array<string>();
Expand Down
45 changes: 31 additions & 14 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { docs } from '../lib/commands/docs';
import { doctor } from '../lib/commands/doctor';
import { getMigrateScanType } from '../lib/commands/migrate';
import { cliInit, printAvailableTemplates } from '../lib/init';
import { data, debug, error, info, setCI, setIoMessageThreshold } from '../lib/logging';
import { data, debug, error, info } from '../lib/logging';
import { Notices } from '../lib/notices';
import { Command, Configuration, Settings } from '../lib/settings';
import * as version from '../lib/version';
Expand All @@ -40,11 +40,12 @@ if (!process.stdout.isTTY) {

export async function exec(args: string[], synthesizer?: Synthesizer): Promise<number | void> {
const argv = await parseCommandLineArguments(args);
const cmd = argv._[0];

// if one -v, log at a DEBUG level
// if 2 -v, log at a TRACE level
let ioMessageLevel: IoMessageLevel = 'info';
if (argv.verbose) {
let ioMessageLevel: IoMessageLevel;
switch (argv.verbose) {
case 1:
ioMessageLevel = 'debug';
Expand All @@ -54,18 +55,20 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
ioMessageLevel = 'trace';
break;
}
setIoMessageThreshold(ioMessageLevel);
}

const ioHost = CliIoHost.instance({
logLevel: ioMessageLevel,
isTTY: process.stdout.isTTY,
isCI: Boolean(argv.ci),
currentAction: cmd,
}, true);

// Debug should always imply tracing
if (argv.debug || argv.verbose > 2) {
enableTracing(true);
}

if (argv.ci) {
setCI(true);
}

try {
await checkForPlatformWarnings();
} catch (e) {
Expand All @@ -83,8 +86,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});
await configuration.load();

const cmd = argv._[0];

const notices = Notices.create({
context: configuration.context,
output: configuration.settings.get(['outdir']),
Expand Down Expand Up @@ -175,7 +176,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
}

async function main(command: string, args: any): Promise<number | void> {
CliIoHost.currentAction = command as any;
ioHost.currentAction = command as any;
const toolkitStackName: string = ToolkitInfo.determineName(configuration.settings.get(['toolkitStackName']));
debug(`Toolkit stack: ${chalk.bold(toolkitStackName)}`);

Expand Down Expand Up @@ -205,6 +206,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n

switch (command) {
case 'context':
ioHost.currentAction = 'context';
return context({
context: configuration.context,
clear: argv.clear,
Expand All @@ -214,21 +216,25 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'docs':
case 'doc':
ioHost.currentAction = 'docs';
return docs({ browser: configuration.settings.get(['browser']) });

case 'doctor':
ioHost.currentAction = 'doctor';
return doctor();

case 'ls':
case 'list':
CliIoHost.currentAction = 'list';
ioHost.currentAction = 'list';
return cli.list(args.STACKS, {
long: args.long,
json: argv.json,
showDeps: args.showDependencies,
});

case 'diff':
ioHost.currentAction = 'diff';
const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL_CONTEXT);
return cli.diff({
stackNames: args.STACKS,
Expand All @@ -246,6 +252,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'bootstrap':
ioHost.currentAction = 'bootstrap';
const source: BootstrapSource = determineBootstrapVersion(args);

if (args.showTemplate) {
Expand Down Expand Up @@ -277,7 +284,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'deploy':
CliIoHost.currentAction = 'deploy';
ioHost.currentAction = 'deploy';
const parameterMap: { [name: string]: string | undefined } = {};
for (const parameter of args.parameters) {
if (typeof parameter === 'string') {
Expand Down Expand Up @@ -356,6 +363,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'rollback':
ioHost.currentAction = 'rollback';
return cli.rollback({
selector,
toolkitStackName,
Expand All @@ -366,6 +374,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'import':
ioHost.currentAction = 'import';
return cli.import({
selector,
toolkitStackName,
Expand All @@ -383,6 +392,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'watch':
ioHost.currentAction = 'watch';
return cli.watch({
selector,
exclusively: args.exclusively,
Expand All @@ -402,7 +412,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'destroy':
CliIoHost.currentAction = 'destroy';
ioHost.currentAction = 'destroy';
return cli.destroy({
selector,
exclusively: args.exclusively,
Expand All @@ -412,6 +422,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});

case 'gc':
ioHost.currentAction = 'gc';
if (!configuration.settings.get(['unstable']).includes('gc')) {
throw new ToolkitError('Unstable feature use: \'gc\' is unstable. It must be opted in via \'--unstable\', e.g. \'cdk gc --unstable=gc\'');
}
Expand All @@ -426,7 +437,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n

case 'synthesize':
case 'synth':
CliIoHost.currentAction = 'synth';
ioHost.currentAction = 'synth';
const quiet = configuration.settings.get(['quiet']) ?? args.quiet;
if (args.exclusively) {
return cli.synth(args.STACKS, args.exclusively, quiet, args.validation, argv.json);
Expand All @@ -435,17 +446,21 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
}

case 'notices':
ioHost.currentAction = 'notices';
// This is a valid command, but we're postponing its execution
return;

case 'metadata':
ioHost.currentAction = 'metadata';
return cli.metadata(args.STACK, argv.json);

case 'acknowledge':
case 'ack':
ioHost.currentAction = 'notices';
return cli.acknowledge(args.ID);

case 'init':
ioHost.currentAction = 'init';
const language = configuration.settings.get(['language']);
if (args.list) {
return printAvailableTemplates(language);
Expand All @@ -458,6 +473,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});
}
case 'migrate':
ioHost.currentAction = 'migrate';
return cli.migrate({
stackName: args['stack-name'],
fromPath: args['from-path'],
Expand All @@ -471,6 +487,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
compress: args.compress,
});
case 'version':
ioHost.currentAction = 'version';
return data(version.DISPLAY_VERSION);

default:
Expand Down
56 changes: 14 additions & 42 deletions packages/aws-cdk/lib/logging.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,11 @@
import * as util from 'util';
import * as chalk from 'chalk';
import { IoMessageLevel, IoMessage, CliIoHost, IoMessageSpecificCode, IoMessageCode, IoMessageCodeCategory, IoCodeLevel } from './toolkit/cli-io-host';
import { IoMessageLevel, IoMessage, CliIoHost, IoMessageSpecificCode, IoMessageCode, IoMessageCodeCategory, IoCodeLevel, levelPriority } from './toolkit/cli-io-host';

// Corking mechanism
let CORK_COUNTER = 0;
const logBuffer: IoMessage<any>[] = [];

const levelPriority: Record<IoMessageLevel, number> = {
error: 0,
warn: 1,
info: 2,
debug: 3,
trace: 4,
};

let currentIoMessageThreshold: IoMessageLevel = 'info';

/**
* Sets the current threshold. Messages with a lower priority level will be ignored.
* @param level The new log level threshold
*/
export function setIoMessageThreshold(level: IoMessageLevel) {
currentIoMessageThreshold = level;
}

/**
* Sets whether the logger is running in CI mode.
* In CI mode, all non-error output goes to stdout instead of stderr.
* @param newCI - Whether CI mode should be enabled
*/
export function setCI(newCI: boolean) {
CliIoHost.ci = newCI;
}

/**
* Executes a block of code with corked logging. All log messages during execution
* are buffered and only written when all nested cork blocks complete (when CORK_COUNTER reaches 0).
Expand All @@ -48,14 +21,14 @@ export async function withCorkedLogging<T>(block: () => Promise<T>): Promise<T>
if (CORK_COUNTER === 0) {
// Process each buffered message through notify
for (const ioMessage of logBuffer) {
void CliIoHost.getIoHost().notify(ioMessage);
void CliIoHost.instance().notify(ioMessage);
}
logBuffer.splice(0);
}
}
}

interface LogOptions {
interface LogMessage {
/**
* The log level to use
*/
Expand All @@ -79,28 +52,27 @@ interface LogOptions {

/**
* Internal core logging function that writes messages through the CLI IO host.
* @param options Configuration options for the log message. See {@link LogOptions}
* @param msg Configuration options for the log message. See {@link LogMessage}
*/
function log(options: LogOptions) {
if (levelPriority[options.level] > levelPriority[currentIoMessageThreshold]) {
return;
}

function log(msg: LogMessage) {
const ioMessage: IoMessage<undefined> = {
level: options.level,
message: options.message,
forceStdout: options.forceStdout,
level: msg.level,
message: msg.message,
forceStdout: msg.forceStdout,
time: new Date(),
action: CliIoHost.currentAction,
code: options.code,
action: CliIoHost.instance().currentAction,
code: msg.code,
};

if (CORK_COUNTER > 0) {
if (levelPriority[msg.level] > levelPriority[CliIoHost.instance().logLevel]) {
return;
}
logBuffer.push(ioMessage);
return;
}

void CliIoHost.getIoHost().notify(ioMessage);
void CliIoHost.instance().notify(ioMessage);
}

/**
Expand Down
Loading

0 comments on commit 72e089b

Please sign in to comment.