Skip to content

Commit

Permalink
chore: unconnected CliIoHost logger-only implementation (#32503)
Browse files Browse the repository at this point in the history
### Issue #32345

Closes #32345

### Reason for this change

Setting the ground work for our [Programmatic Toolkit](aws/aws-cdk-rfcs#654)

### Description of changes

Created an unconnected CLIIoHost with a singular initial action available `notify`. In this implementation of the soon to be defined IoHost we are only writing logs to stdout and stderr.

### Description of how you validated changes

Verified via unit testing as this is currently unconnected to the greater AWS CDK CLI

### 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
HBobertz authored Dec 27, 2024
1 parent 9c69648 commit 1c9103e
Show file tree
Hide file tree
Showing 2 changed files with 493 additions and 0 deletions.
142 changes: 142 additions & 0 deletions packages/aws-cdk/lib/toolkit/cli-io-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as chalk from 'chalk';

/**
* Basic message structure for toolkit notifications.
* Messages are emitted by the toolkit and handled by the IoHost.
*/
interface IoMessage {
/**
* The time the message was emitted.
*/
readonly time: Date;

/**
* The log level of the message.
*/
readonly level: IoMessageLevel;

/**
* The action that triggered the message.
*/
readonly action: IoAction;

/**
* A short code uniquely identifying message type.
*/
readonly code: string;

/**
* The message text.
*/
readonly message: string;

/**
* If true, the message will be written to stdout
* regardless of any other parameters.
*
* @default false
*/
readonly forceStdout?: boolean;
}

export type IoMessageLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';

export type IoAction = 'synth' | 'list' | 'deploy' | 'destroy';

/**
* Options for the CLI IO host.
*/
interface CliIoHostOptions {
/**
* If true, the host will use TTY features like color.
*/
useTTY?: boolean;

/**
* Flag representing whether the current process is running in a CI environment.
* If true, the host will write all messages to stdout, unless log level is 'error'.
*
* @default false
*/
ci?: boolean;
}

/**
* A simple IO host for the CLI that writes messages to the console.
*/
export class CliIoHost {
private readonly pretty_messages: boolean;
private readonly ci: boolean;

constructor(options: CliIoHostOptions) {
this.pretty_messages = options.useTTY ?? process.stdout.isTTY ?? false;
this.ci = options.ci ?? false;
}

/**
* Notifies the host of a message.
* The caller waits until the notification completes.
*/
async notify(msg: IoMessage): Promise<void> {
const output = this.formatMessage(msg);

const stream = this.getStream(msg.level, msg.forceStdout ?? false);

return new Promise((resolve, reject) => {
stream.write(output, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

/**
* Determines which output stream to use based on log level and configuration.
*/
private getStream(level: IoMessageLevel, forceStdout: boolean) {
// For legacy purposes all log streams are written to stderr by default, unless
// specified otherwise, by passing `forceStdout`, which is used by the `data()` logging function, or
// if the CDK is running in a CI environment. This is because some CI environments will immediately
// fail if stderr is written to. In these cases, we detect if we are in a CI environment and
// write all messages to stdout instead.
if (forceStdout) {
return process.stdout;
}
if (level == 'error') return process.stderr;
return this.ci ? process.stdout : process.stderr;
}

/**
* Formats a message for console output with optional color support
*/
private formatMessage(msg: IoMessage): string {
// apply provided style or a default style if we're in TTY mode
let message_text = this.pretty_messages
? styleMap[msg.level](msg.message)
: msg.message;

// prepend timestamp if IoMessageLevel is DEBUG or TRACE. Postpend a newline.
return ((msg.level === 'debug' || msg.level === 'trace')
? `[${this.formatTime(msg.time)}] ${message_text}`
: message_text) + '\n';
}

/**
* Formats date to HH:MM:SS
*/
private formatTime(d: Date): string {
const pad = (n: number): string => n.toString().padStart(2, '0');
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
}

export const styleMap: Record<IoMessageLevel, (str: string) => string> = {
error: chalk.red,
warn: chalk.yellow,
info: chalk.white,
debug: chalk.gray,
trace: chalk.gray,
};
Loading

0 comments on commit 1c9103e

Please sign in to comment.