Skip to content

Commit 069b72c

Browse files
authored
chore: change logging to use a single base logging method (#32081)
Closes #32039 Modified the existing logging mechanism to route to a single log function. Adapted the existing structure so that very minimal changes were required for existing logging functions and how they are used today ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent be000a2 commit 069b72c

File tree

6 files changed

+445
-90
lines changed

6 files changed

+445
-90
lines changed

packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ArtifactMetadataEntryType, type MetadataEntry } from '@aws-cdk/cloud-as
33
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
44
import * as chalk from 'chalk';
55
import { ResourceEvent, StackEventPoller } from './stack-event-poller';
6-
import { error, logLevel, LogLevel, setLogLevel } from '../../../logging';
6+
import { error, LogLevel, setLogLevel } from '../../../logging';
77
import type { ICloudFormationClient } from '../../aws-auth';
88
import { RewritableBlock } from '../display';
99

@@ -102,7 +102,7 @@ export class StackActivityMonitor {
102102
};
103103

104104
const isWindows = process.platform === 'win32';
105-
const verbose = options.logLevel ?? logLevel;
105+
const verbose = options.logLevel ?? LogLevel.INFO;
106106
// On some CI systems (such as CircleCI) output still reports as a TTY so we also
107107
// need an individual check for whether we're running on CI.
108108
// see: https://discuss.circleci.com/t/circleci-terminal-is-a-tty-but-term-is-not-set/9965
@@ -626,7 +626,7 @@ export class CurrentActivityPrinter extends ActivityPrinterBase {
626626
*/
627627
public readonly updateSleep: number = 2_000;
628628

629-
private oldLogLevel: LogLevel = LogLevel.DEFAULT;
629+
private oldLogLevel: LogLevel = LogLevel.INFO;
630630
private block = new RewritableBlock(this.stream);
631631

632632
constructor(props: PrinterProps) {
@@ -674,8 +674,7 @@ export class CurrentActivityPrinter extends ActivityPrinterBase {
674674
public start() {
675675
// Need to prevent the waiter from printing 'stack not stable' every 5 seconds, it messes
676676
// with the output calculations.
677-
this.oldLogLevel = logLevel;
678-
setLogLevel(LogLevel.DEFAULT);
677+
setLogLevel(LogLevel.INFO);
679678
}
680679

681680
public stop() {

packages/aws-cdk/lib/cli.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { realHandler as docs } from '../lib/commands/docs';
2222
import { realHandler as doctor } from '../lib/commands/doctor';
2323
import { MIGRATE_SUPPORTED_LANGUAGES, getMigrateScanType } from '../lib/commands/migrate';
2424
import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init';
25-
import { data, debug, error, print, setLogLevel, setCI } from '../lib/logging';
25+
import { data, debug, error, print, setCI, setLogLevel, LogLevel } from '../lib/logging';
2626
import { Notices } from '../lib/notices';
2727
import { Command, Configuration, Settings } from '../lib/settings';
2828
import * as version from '../lib/version';
@@ -48,8 +48,20 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
4848

4949
const argv = await parseCommandLineArguments(args, makeBrowserDefault(), await availableInitLanguages(), MIGRATE_SUPPORTED_LANGUAGES as string[], version.DISPLAY_VERSION, yargsNegativeAlias);
5050

51+
// if one -v, log at a DEBUG level
52+
// if 2 -v, log at a TRACE level
5153
if (argv.verbose) {
52-
setLogLevel(argv.verbose);
54+
let logLevel: LogLevel;
55+
switch (argv.verbose) {
56+
case 1:
57+
logLevel = LogLevel.DEBUG;
58+
break;
59+
case 2:
60+
default:
61+
logLevel = LogLevel.TRACE;
62+
break;
63+
}
64+
setLogLevel(logLevel);
5365
}
5466

5567
// Debug should always imply tracing

packages/aws-cdk/lib/logging.ts

+170-82
Original file line numberDiff line numberDiff line change
@@ -2,114 +2,202 @@ import { Writable } from 'stream';
22
import * as util from 'util';
33
import * as chalk from 'chalk';
44

5-
type StyleFn = (str: string) => string;
6-
const { stdout, stderr } = process;
7-
8-
type WritableFactory = () => Writable;
5+
/**
6+
* Available log levels in order of increasing verbosity.
7+
*/
8+
export enum LogLevel {
9+
ERROR = 'error',
10+
WARN = 'warn',
11+
INFO = 'info',
12+
DEBUG = 'debug',
13+
TRACE = 'trace',
14+
}
915

10-
export async function withCorkedLogging<A>(block: () => Promise<A>): Promise<A> {
11-
corkLogging();
12-
try {
13-
return await block();
14-
} finally {
15-
uncorkLogging();
16-
}
16+
/**
17+
* Configuration options for a log entry.
18+
*/
19+
export interface LogEntry {
20+
level: LogLevel;
21+
message: string;
22+
timestamp?: boolean;
23+
prefix?: string;
24+
style?: ((str: string) => string);
25+
forceStdout?: boolean;
1726
}
1827

28+
const { stdout, stderr } = process;
29+
30+
// Corking mechanism
1931
let CORK_COUNTER = 0;
2032
const logBuffer: [Writable, string][] = [];
2133

22-
function corked() {
23-
return CORK_COUNTER !== 0;
24-
}
34+
// Style mappings
35+
const styleMap: Record<LogLevel, (str: string) => string> = {
36+
[LogLevel.ERROR]: chalk.red,
37+
[LogLevel.WARN]: chalk.yellow,
38+
[LogLevel.INFO]: chalk.white,
39+
[LogLevel.DEBUG]: chalk.gray,
40+
[LogLevel.TRACE]: chalk.gray,
41+
};
2542

26-
function corkLogging() {
27-
CORK_COUNTER += 1;
28-
}
43+
// Stream selection
44+
let CI = false;
2945

30-
function uncorkLogging() {
31-
CORK_COUNTER -= 1;
32-
if (!corked()) {
33-
logBuffer.forEach(([stream, str]) => stream.write(str + '\n'));
34-
logBuffer.splice(0);
46+
/**
47+
* Determines which output stream to use based on log level and configuration.
48+
* @param level - The log level to determine stream for
49+
* @param forceStdout - Whether to force stdout regardless of level
50+
* @returns The appropriate Writable stream
51+
*/
52+
const getStream = (level: LogLevel, forceStdout?: boolean): Writable => {
53+
// Special case - data() calls should always go to stdout
54+
if (forceStdout) {
55+
return stdout;
3556
}
57+
if (level === LogLevel.ERROR) return stderr;
58+
return CI ? stdout : stderr;
59+
};
60+
61+
const levelPriority: Record<LogLevel, number> = {
62+
[LogLevel.ERROR]: 0,
63+
[LogLevel.WARN]: 1,
64+
[LogLevel.INFO]: 2,
65+
[LogLevel.DEBUG]: 3,
66+
[LogLevel.TRACE]: 4,
67+
};
68+
69+
let currentLogLevel: LogLevel = LogLevel.INFO;
70+
71+
/**
72+
* Sets the current log level. Messages with a lower priority level will be filtered out.
73+
* @param level - The new log level to set
74+
*/
75+
export function setLogLevel(level: LogLevel) {
76+
currentLogLevel = level;
3677
}
3778

38-
const logger = (stream: Writable | WritableFactory, styles?: StyleFn[], timestamp?: boolean) => (fmt: string, ...args: unknown[]) => {
39-
const ts = timestamp ? `[${formatTime(new Date())}] ` : '';
79+
/**
80+
* Sets whether the logger is running in CI mode.
81+
* In CI mode, all non-error output goes to stdout instead of stderr.
82+
* @param newCI - Whether CI mode should be enabled
83+
*/
84+
export function setCI(newCI: boolean) {
85+
CI = newCI;
86+
}
4087

41-
let str = ts + util.format(fmt, ...args);
42-
if (styles && styles.length) {
43-
str = styles.reduce((a, style) => style(a), str);
44-
}
88+
/**
89+
* Formats a date object into a timestamp string (HH:MM:SS).
90+
* @param d - Date object to format
91+
* @returns Formatted time string
92+
*/
93+
function formatTime(d: Date): string {
94+
const pad = (n: number): string => n.toString().padStart(2, '0');
95+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
96+
}
4597

46-
const realStream = typeof stream === 'function' ? stream() : stream;
98+
/**
99+
* Executes a block of code with corked logging. All log messages during execution
100+
* are buffered and only written after the block completes.
101+
* @param block - Async function to execute with corked logging
102+
* @returns Promise that resolves with the block's return value
103+
*/
104+
export async function withCorkedLogging<T>(block: () => Promise<T>): Promise<T> {
105+
CORK_COUNTER++;
106+
try {
107+
return await block();
108+
} finally {
109+
CORK_COUNTER--;
110+
if (CORK_COUNTER === 0) {
111+
logBuffer.forEach(([stream, str]) => stream.write(str + '\n'));
112+
logBuffer.splice(0);
113+
}
114+
}
115+
}
47116

48-
// Logger is currently corked, so we store the message to be printed
49-
// later when we are uncorked.
50-
if (corked()) {
51-
logBuffer.push([realStream, str]);
117+
/**
118+
* Core logging function that handles all log output.
119+
* @param entry - LogEntry object or log level
120+
* @param fmt - Format string (when using with log level)
121+
* @param args - Format arguments (when using with log level)
122+
*/
123+
export function log(entry: LogEntry): void;
124+
export function log(level: LogLevel, fmt: string, ...args: unknown[]): void;
125+
export function log(levelOrEntry: LogLevel | LogEntry, fmt?: string, ...args: unknown[]): void {
126+
// Normalize input
127+
const entry: LogEntry = typeof levelOrEntry === 'string'
128+
? { level: levelOrEntry as LogLevel, message: util.format(fmt!, ...args) }
129+
: levelOrEntry;
130+
131+
// Check if we should log this level
132+
if (levelPriority[entry.level] > levelPriority[currentLogLevel]) {
52133
return;
53134
}
54135

55-
realStream.write(str + '\n');
56-
};
136+
// Format the message
137+
let finalMessage = entry.message;
57138

58-
function formatTime(d: Date) {
59-
return `${lpad(d.getHours(), 2)}:${lpad(d.getMinutes(), 2)}:${lpad(d.getSeconds(), 2)}`;
60-
61-
function lpad(x: any, w: number) {
62-
const s = `${x}`;
63-
return '0'.repeat(Math.max(w - s.length, 0)) + s;
139+
// Add timestamp first if requested
140+
if (entry.timestamp) {
141+
finalMessage = `[${formatTime(new Date())}] ${finalMessage}`;
64142
}
65-
}
66143

67-
export enum LogLevel {
68-
/** Not verbose at all */
69-
DEFAULT = 0,
70-
/** Pretty verbose */
71-
DEBUG = 1,
72-
/** Extremely verbose */
73-
TRACE = 2,
74-
}
144+
// Add prefix AFTER timestamp
145+
if (entry.prefix) {
146+
finalMessage = `${entry.prefix} ${finalMessage}`;
147+
}
75148

76-
export let logLevel = LogLevel.DEFAULT;
77-
export let CI = false;
149+
// Apply custom style if provided, otherwise use level-based style
150+
const style = entry.style || styleMap[entry.level];
151+
finalMessage = style(finalMessage);
78152

79-
export function setLogLevel(newLogLevel: LogLevel) {
80-
logLevel = newLogLevel;
81-
}
153+
// Get appropriate stream - pass through forceStdout flag
154+
const stream = getStream(entry.level, entry.forceStdout);
82155

83-
export function setCI(newCI: boolean) {
84-
CI = newCI;
85-
}
156+
// Handle corking
157+
if (CORK_COUNTER > 0) {
158+
logBuffer.push([stream, finalMessage]);
159+
return;
160+
}
86161

87-
export function increaseVerbosity() {
88-
logLevel += 1;
162+
// Write to stream
163+
stream.write(finalMessage + '\n');
89164
}
90165

91-
const stream = () => CI ? stdout : stderr;
92-
const _debug = logger(stream, [chalk.gray], true);
93-
94-
export const trace = (fmt: string, ...args: unknown[]) => logLevel >= LogLevel.TRACE && _debug(fmt, ...args);
95-
export const debug = (fmt: string, ...args: unknown[]) => logLevel >= LogLevel.DEBUG && _debug(fmt, ...args);
96-
export const error = logger(stderr, [chalk.red]);
97-
export const warning = logger(stream, [chalk.yellow]);
98-
export const success = logger(stream, [chalk.green]);
99-
export const highlight = logger(stream, [chalk.bold]);
100-
export const print = logger(stream);
101-
export const data = logger(stdout);
102-
103-
export type LoggerFunction = (fmt: string, ...args: unknown[]) => void;
166+
// Convenience logging methods
167+
export const error = (fmt: string, ...args: unknown[]) => log(LogLevel.ERROR, fmt, ...args);
168+
export const warning = (fmt: string, ...args: unknown[]) => log(LogLevel.WARN, fmt, ...args);
169+
export const info = (fmt: string, ...args: unknown[]) => log(LogLevel.INFO, fmt, ...args);
170+
export const print = (fmt: string, ...args: unknown[]) => log(LogLevel.INFO, fmt, ...args);
171+
export const data = (fmt: string, ...args: unknown[]) => log({
172+
level: LogLevel.INFO,
173+
message: util.format(fmt, ...args),
174+
forceStdout: true,
175+
});
176+
export const debug = (fmt: string, ...args: unknown[]) => log(LogLevel.DEBUG, fmt, ...args);
177+
export const trace = (fmt: string, ...args: unknown[]) => log(LogLevel.TRACE, fmt, ...args);
178+
179+
export const success = (fmt: string, ...args: unknown[]) => log({
180+
level: LogLevel.INFO,
181+
message: util.format(fmt, ...args),
182+
style: chalk.green,
183+
});
184+
185+
export const highlight = (fmt: string, ...args: unknown[]) => log({
186+
level: LogLevel.INFO,
187+
message: util.format(fmt, ...args),
188+
style: chalk.bold,
189+
});
104190

105191
/**
106-
* Create a logger output that features a constant prefix string.
107-
*
108-
* @param prefixString the prefix string to be appended before any log entry.
109-
* @param fn the logger function to be used (typically one of the other functions in this module)
110-
*
111-
* @returns a new LoggerFunction.
192+
* Creates a logging function that prepends a prefix to all messages.
193+
* @param prefixString - String to prepend to all messages
194+
* @param level - Log level to use (defaults to INFO)
195+
* @returns Logging function that accepts format string and arguments
112196
*/
113-
export function prefix(prefixString: string, fn: LoggerFunction): LoggerFunction {
114-
return (fmt: string, ...args: any[]) => fn(`%s ${fmt}`, prefixString, ...args);
115-
}
197+
export function prefix(prefixString: string, level: LogLevel = LogLevel.INFO): (fmt: string, ...args: unknown[]) => void {
198+
return (fmt: string, ...args: unknown[]) => log({
199+
level,
200+
message: util.format(fmt, ...args),
201+
prefix: prefixString,
202+
});
203+
}

packages/aws-cdk/test/api/exec.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ beforeEach(() => {
3737
});
3838

3939
afterEach(() => {
40-
setLogLevel(LogLevel.DEFAULT);
40+
setLogLevel(LogLevel.INFO);
4141

4242
sinon.restore();
4343
bockfs.restore();

0 commit comments

Comments
 (0)