platform-tooling/run/utils/logger.ts

360 lines
9.4 KiB
TypeScript

/**
* Structured logging utility for the run CLI
*
* Provides:
* - Configurable log levels
* - Structured output with timestamps
* - Context-aware logging
* - Silent mode for scripts
*/
import { colors } from './colors.js';
import { FileLogger, type FileLoggerOptions } from './file-logger.js';
// =============================================================================
// Types
// =============================================================================
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
export interface LoggerConfig {
/** Minimum level to output */
level: LogLevel;
/** Context prefix for log messages */
context?: string;
/** Include timestamps in output */
timestamps?: boolean;
/** Suppress all output (for scripts) */
silent?: boolean;
}
// =============================================================================
// Log Level Hierarchy
// =============================================================================
const LOG_LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
silent: 4,
};
// =============================================================================
// Logger Class
// =============================================================================
export class Logger {
private config: Required<LoggerConfig>;
private suppressPatterns: RegExp[] = [];
private fileLogger?: FileLogger;
private verboseMode = false;
constructor(config: Partial<LoggerConfig> = {}) {
this.config = {
level: config.level ?? (process.env.DEBUG ? 'debug' : 'info'),
context: config.context ?? '',
timestamps: config.timestamps ?? true,
silent: config.silent ?? false,
};
}
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
/**
* Set log level
*/
setLevel(level: LogLevel): void {
this.config.level = level;
}
/**
* Set context prefix
*/
setContext(context: string): void {
this.config.context = context;
}
/**
* Enable silent mode
*/
setSilent(silent: boolean): void {
this.config.silent = silent;
}
/**
* Add patterns to suppress from output
*/
suppress(...patterns: (string | RegExp)[]): void {
for (const pattern of patterns) {
this.suppressPatterns.push(
typeof pattern === 'string' ? new RegExp(pattern) : pattern
);
}
}
/**
* Create a child logger with additional context
*/
child(context: string): Logger {
const child = new Logger({
...this.config,
context: this.config.context
? `${this.config.context}:${context}`
: context,
});
child.suppressPatterns = [...this.suppressPatterns];
return child;
}
/**
* Enable verbose mode with file logging
*/
async setVerbose(enabled: boolean, logFilePath?: string): Promise<void> {
this.verboseMode = enabled;
if (enabled && logFilePath) {
try {
this.fileLogger = new FileLogger({ logPath: logFilePath });
await this.fileLogger.init();
} catch (error) {
console.error(`Failed to initialize file logger: ${error instanceof Error ? error.message : 'Unknown error'}`);
console.error('Continuing with console-only logging');
this.fileLogger = undefined;
}
} else if (!enabled && this.fileLogger) {
await this.fileLogger.close();
this.fileLogger = undefined;
}
}
/**
* Close file logger if active
*/
async close(): Promise<void> {
if (this.fileLogger) {
await this.fileLogger.close();
this.fileLogger = undefined;
}
}
// ---------------------------------------------------------------------------
// Logging Methods
// ---------------------------------------------------------------------------
debug(message: string, ...args: unknown[]): void {
this.log('debug', message, args);
}
info(message: string, ...args: unknown[]): void {
this.log('info', message, args);
}
warn(message: string, ...args: unknown[]): void {
this.log('warn', message, args);
}
error(message: string, error?: Error): void {
this.log('error', message, error ? [error] : []);
if (error?.stack && this.shouldLog('debug')) {
console.error(colors.muted(error.stack));
}
}
success(message: string): void {
if (this.shouldLog('info')) {
this.write(`${colors.symbols.success} ${message}`);
}
}
/**
* Log a muted/secondary message (dimmed text)
*/
muted(message: string): void {
if (this.shouldLog('info')) {
this.write(colors.muted(message));
}
}
/**
* Log verbose message (only in verbose mode)
*/
verbose(message: string, ...args: unknown[]): void {
if (this.verboseMode) {
this.log('debug', message, args);
}
}
/**
* Log detailed operation info (only in verbose mode)
*/
detail(context: string, operation: string, data?: Record<string, unknown>): void {
if (this.verboseMode) {
const dataStr = data ? ` ${JSON.stringify(data)}` : '';
this.log('debug', `[${context}] ${operation}${dataStr}`, []);
}
}
// ---------------------------------------------------------------------------
// Structured Output
// ---------------------------------------------------------------------------
/**
* Print a section header
*/
section(title: string): void {
if (this.shouldLog('info')) {
this.write('');
this.write(colors.accent(`${title}`));
}
}
/**
* Print a stage header (for multi-phase operations)
*/
stage(name: string, description?: string): void {
if (this.shouldLog('info')) {
const desc = description ? ` - ${description}` : '';
this.write('');
this.write(colors.primary.bold(`━━━ ${name}${desc} ━━━`));
}
}
/**
* Print a header box
*/
header(title: string): void {
if (this.shouldLog('info')) {
const line = '━'.repeat(54);
this.write('');
this.write(colors.primary(line));
this.write(colors.primary.bold(` ${title}`));
this.write(colors.primary(line));
}
}
/**
* Print a key-value pair
*/
item(label: string, value: string | number, color?: 'success' | 'warning' | 'error'): void {
if (this.shouldLog('info')) {
const colorFn = color ? colors[color] : colors.accent;
this.write(` ${colors.symbols.bullet} ${label}: ${colorFn(String(value))}`);
}
}
/**
* Print a list of items
*/
list(items: string[], indent = 2): void {
if (this.shouldLog('info')) {
const prefix = ' '.repeat(indent);
for (const item of items) {
this.write(`${prefix}${colors.muted('•')} ${item}`);
}
}
}
/**
* Print a horizontal rule
*/
hr(): void {
if (this.shouldLog('info')) {
this.write(colors.muted('─'.repeat(54)));
}
}
/**
* Print blank line
*/
blank(): void {
if (this.shouldLog('info')) {
this.write('');
}
}
// ---------------------------------------------------------------------------
// Private Methods
// ---------------------------------------------------------------------------
private shouldLog(level: LogLevel): boolean {
if (this.config.silent) return false;
return LOG_LEVELS[level] >= LOG_LEVELS[this.config.level];
}
private shouldSuppress(message: string): boolean {
return this.suppressPatterns.some(pattern => pattern.test(message));
}
private log(level: LogLevel, message: string, args: unknown[]): void {
if (this.shouldSuppress(message)) return;
// Always write to file if verbose mode is enabled
if (this.verboseMode && this.fileLogger?.isReady()) {
// Write plain text to file (no ANSI colors)
const plainMessage = args.length > 0
? `${message} ${args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')}`
: message;
this.fileLogger.write(level, this.config.context, plainMessage);
}
// Write to console if not suppressed by level
if (!this.shouldLog(level)) return;
const formattedMessage = this.format(level, message);
const output = level === 'error' ? console.error : console.log;
output(formattedMessage, ...args);
}
private format(level: LogLevel, message: string): string {
const parts: string[] = [];
// Timestamp
if (this.config.timestamps) {
parts.push(colors.muted(this.timestamp()));
}
// Level badge
const levelColors: Record<LogLevel, typeof colors.info> = {
debug: colors.debug,
info: colors.info,
warn: colors.warning,
error: colors.error,
silent: colors.muted,
};
parts.push(levelColors[level](`[${level.toUpperCase()}]`));
// Context
if (this.config.context) {
parts.push(colors.primary(`[${this.config.context}]`));
}
// Message
parts.push(message);
return parts.join(' ');
}
private timestamp(): string {
return new Date().toISOString().split('T')[1]!.split('.')[0]!;
}
private write(text: string): void {
console.log(text);
}
}
// =============================================================================
// Default Logger Instance
// =============================================================================
export const logger = new Logger({ context: 'Run' });
// Suppress common noisy warnings
logger.suppress(
/DomainEventsEmitter/,
/\[Nest\].*WARN/,
/ExperimentalWarning/
);