import * as fs from 'fs'; import * as path from 'path'; export enum LogLevel { DEBUG = 'debug', INFO = 'info', WARN = 'warn', ERROR = 'error' } interface LogEntry { timestamp: string; level: LogLevel; message: string; context?: Record; } export class Logger { private static instance: Logger; private logDir: string; private logFile: string; private logLevel: LogLevel; private context: Record; private constructor(logDir: string = 'test-results/logs') { this.logDir = logDir; this.logFile = path.join(logDir, `test-run-${new Date().toISOString().replace(/[:.]/g, '-')}.log`); this.logLevel = this.parseLogLevel(process.env.LOG_LEVEL || 'info'); this.context = {}; this.ensureLogDir(); } static getInstance(logDir?: string): Logger { if (!Logger.instance) { Logger.instance = new Logger(logDir); } return Logger.instance; } private parseLogLevel(level: string): LogLevel { switch (level.toLowerCase()) { case 'debug': return LogLevel.DEBUG; case 'info': return LogLevel.INFO; case 'warn': return LogLevel.WARN; case 'error': return LogLevel.ERROR; default: return LogLevel.INFO; } } private ensureLogDir(): void { if (!fs.existsSync(this.logDir)) { fs.mkdirSync(this.logDir, { recursive: true }); } } setContext(context: Record): this { this.context = { ...this.context, ...context }; return this; } clearContext(): this { this.context = {}; return this; } private formatTimestamp(): string { return new Date().toISOString(); } private formatMessage(level: LogLevel, message: string): string { const timestamp = this.formatTimestamp(); const levelStr = level.toUpperCase().padEnd(5); const contextStr = Object.keys(this.context).length > 0 ? ` [${JSON.stringify(this.context)}]` : ''; return `${timestamp} | ${levelStr} |${contextStr} ${message}`; } private shouldLog(level: LogLevel): boolean { const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; return levels.indexOf(level) >= levels.indexOf(this.logLevel); } private writeToFile(entry: LogEntry): void { try { this.ensureLogDir(); const logLine = JSON.stringify(entry) + '\n'; fs.appendFileSync(this.logFile, logLine, 'utf-8'); } catch { // Ignore file write errors } } private log(level: LogLevel, message: string, context?: Record): void { if (!this.shouldLog(level)) return; const entry: LogEntry = { timestamp: this.formatTimestamp(), level, message, context: { ...this.context, ...context } }; this.writeToFile(entry); const formattedMessage = this.formatMessage(level, message); const colors: Record = { [LogLevel.DEBUG]: '\x1b[36m', [LogLevel.INFO]: '\x1b[32m', [LogLevel.WARN]: '\x1b[33m', [LogLevel.ERROR]: '\x1b[31m' }; const reset = '\x1b[0m'; console.log(`${colors[level]}${formattedMessage}${reset}`); } debug(message: string, context?: Record): void { this.log(LogLevel.DEBUG, message, context); } info(message: string, context?: Record): void { this.log(LogLevel.INFO, message, context); } warn(message: string, context?: Record): void { this.log(LogLevel.WARN, message, context); } error(message: string, error?: Error | unknown, context?: Record): void { const errorContext = error instanceof Error ? { errorMessage: error.message, stack: error.stack, ...context } : { error, ...context }; this.log(LogLevel.ERROR, message, errorContext); } section(title: string): void { const separator = '='.repeat(60); console.log(`\n${separator}`); console.log(` ${title}`); console.log(`${separator}\n`); this.info(`=== ${title} ===`); } progress(current: number, total: number, message: string): void { const percentage = Math.round((current / total) * 100); const bar = this.createProgressBar(percentage); process.stdout.write(`\r${bar} ${percentage}% - ${message}`); if (current === total) { process.stdout.write('\n'); } } private createProgressBar(percentage: number, width: number = 30): string { const filled = Math.round((percentage / 100) * width); const empty = width - filled; return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`; } getLogFile(): string { return this.logFile; } } export const logger = Logger.getInstance();