// Define types locally to avoid circular dependency export enum LogLevel { TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4, FATAL = 5, } export interface ILoggerComponent { init(): Promise; setLevel(level: LogLevel): void; trace(message: string, ...args: unknown[]): void; debug(message: string, ...args: unknown[]): void; info(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; fatal(message: string, ...args: unknown[]): void; child(context: string): ILoggerComponent; } // ANSI color codes for terminal output const COLORS = { RESET: '\x1b[0m', BRIGHT: '\x1b[1m', DIM: '\x1b[2m', RED: '\x1b[31m', GREEN: '\x1b[32m', YELLOW: '\x1b[33m', BLUE: '\x1b[34m', MAGENTA: '\x1b[35m', CYAN: '\x1b[36m', WHITE: '\x1b[37m', GRAY: '\x1b[90m', }; const LEVEL_COLORS = { [LogLevel.TRACE]: COLORS.GRAY, [LogLevel.DEBUG]: COLORS.CYAN, [LogLevel.INFO]: COLORS.GREEN, [LogLevel.WARN]: COLORS.YELLOW, [LogLevel.ERROR]: COLORS.RED, [LogLevel.FATAL]: COLORS.BRIGHT + COLORS.RED, }; const LEVEL_NAMES = { [LogLevel.TRACE]: 'TRACE', [LogLevel.DEBUG]: 'DEBUG', [LogLevel.INFO]: 'INFO ', [LogLevel.WARN]: 'WARN ', [LogLevel.ERROR]: 'ERROR', [LogLevel.FATAL]: 'FATAL', }; export class LoggerComponent implements ILoggerComponent { private level: LogLevel; private context: string; private static globalLevel: LogLevel = LogLevel.INFO; constructor(context = 'main') { this.context = context; this.level = LoggerComponent.globalLevel; // Check environment variable for log level const envLevel = process.env.LOG_LEVEL?.toUpperCase(); if (envLevel && LogLevel[envLevel as keyof typeof LogLevel] !== undefined) { this.level = LogLevel[envLevel as keyof typeof LogLevel]; LoggerComponent.globalLevel = this.level; } else if (process.env.DEBUG === 'true') { this.level = LogLevel.DEBUG; LoggerComponent.globalLevel = LogLevel.DEBUG; } } async init(): Promise { this.info(`Logger initialized for context: ${this.context}`); } setLevel(level: LogLevel): void { this.level = level; LoggerComponent.globalLevel = level; } private formatMessage( level: LogLevel, message: string, args: unknown[], ): string { const timestamp = new Date().toISOString(); const levelColor = LEVEL_COLORS[level]; const levelName = LEVEL_NAMES[level]; // Format the main message let formattedMessage = message; // Defensive check: ensure args is defined and is an array if (args && Array.isArray(args) && args.length > 0) { // Handle object arguments const argStrings = args.map((arg) => { // Special handling for Error objects if (arg instanceof Error) { return `${arg.name}: ${arg.message}${arg.stack ? '\n' + arg.stack : ''}`; } if (typeof arg === 'object' && arg !== null) { try { // Check if it's an empty object const str = JSON.stringify(arg, null, 2); if (str === '{}' && Object.getOwnPropertyNames(arg).length > 0) { // Object has non-enumerable properties, try to get more info return `${arg.constructor.name}: ${String(arg)}`; } return str; } catch { return String(arg); } } return String(arg); }); formattedMessage = `${message} ${argStrings.join(' ')}`; } // Build the log line with colors return `${COLORS.DIM}[${timestamp}]${COLORS.RESET} ${levelColor}[${levelName}]${COLORS.RESET} ${COLORS.BRIGHT}[${this.context}]${COLORS.RESET} ${formattedMessage}`; } private log(level: LogLevel, message: string, args: unknown[] = []): void { if (level < this.level) return; const formattedMessage = this.formatMessage(level, message, args || []); switch (level) { case LogLevel.TRACE: case LogLevel.DEBUG: console.log(formattedMessage); break; case LogLevel.INFO: console.log(formattedMessage); break; case LogLevel.WARN: console.warn(formattedMessage); break; case LogLevel.ERROR: case LogLevel.FATAL: console.error(formattedMessage); break; } } trace(message: string, ...args: unknown[]): void { this.log(LogLevel.TRACE, message, args); } debug(message: string, ...args: unknown[]): void { this.log(LogLevel.DEBUG, message, args); } info(message: string, ...args: unknown[]): void { this.log(LogLevel.INFO, message, args); } warn(message: string, ...args: unknown[]): void { this.log(LogLevel.WARN, message, args); } error(message: string, ...args: unknown[]): void { this.log(LogLevel.ERROR, message, args); } fatal(message: string, ...args: unknown[]): void { this.log(LogLevel.FATAL, message, args); } child(context: string): ILoggerComponent { const childLogger = new LoggerComponent(`${this.context}:${context}`); childLogger.level = this.level; return childLogger; } } // Cache logger instances by context to avoid recreating them const loggerCache = new Map(); export function getLogger(context = 'main'): ILoggerComponent { // Return cached logger for this context if it exists if (loggerCache.has(context)) { return loggerCache.get(context)!; } // Create new logger for this context const newLogger = new LoggerComponent(context); loggerCache.set(context, newLogger); return newLogger; } // Export default instance export const logger = new LoggerComponent('main');