/** * Log levels in order of severity */ export enum LogLevel { DEBUG = 'debug', INFO = 'info', WARN = 'warn', ERROR = 'error', } /** * Log level priorities for filtering */ const LOG_LEVEL_PRIORITY: Record = { [LogLevel.DEBUG]: 0, [LogLevel.INFO]: 1, [LogLevel.WARN]: 2, [LogLevel.ERROR]: 3, }; /** * Structured log entry */ export interface LogEntry { level: LogLevel; message: string; timestamp: Date; context?: Record; correlationId?: string; error?: Error; } /** * Log transport interface for custom log handlers */ export interface LogTransport { log(entry: LogEntry): void | Promise; } /** * Console transport with colored output */ export class ConsoleTransport implements LogTransport { private readonly colors = { debug: '\x1b[36m', // Cyan info: '\x1b[32m', // Green warn: '\x1b[33m', // Yellow error: '\x1b[31m', // Red reset: '\x1b[0m', }; log(entry: LogEntry): void { const color = this.colors[entry.level]; const reset = this.colors.reset; const timestamp = entry.timestamp.toISOString(); const level = entry.level.toUpperCase().padEnd(5); let message = `${color}[${timestamp}] ${level}${reset} ${entry.message}`; if (entry.correlationId) { message += ` ${color}[correlation: ${entry.correlationId}]${reset}`; } if (entry.context && Object.keys(entry.context).length > 0) { message += `\n Context: ${JSON.stringify(entry.context, null, 2)}`; } if (entry.error) { message += `\n Error: ${entry.error.message}`; if (entry.error.stack) { message += `\n${entry.error.stack}`; } } console.log(message); } } /** * JSON transport for structured logging */ export class JsonTransport implements LogTransport { log(entry: LogEntry): void { const jsonEntry = { level: entry.level, message: entry.message, timestamp: entry.timestamp.toISOString(), ...(entry.correlationId && { correlationId: entry.correlationId }), ...(entry.context && { context: entry.context }), ...(entry.error && { error: { message: entry.error.message, name: entry.error.name, stack: entry.error.stack, }, }), }; console.log(JSON.stringify(jsonEntry)); } } /** * Logger configuration */ export interface LoggerConfig { /** Minimum log level to output */ level: LogLevel; /** Log transports */ transports: LogTransport[]; /** Default context to include in all logs */ defaultContext?: Record; /** Whether to include timestamp */ includeTimestamp?: boolean; } /** * Structured logger with context and correlation support * * @example * ```typescript * const logger = new Logger({ * level: LogLevel.INFO, * transports: [new ConsoleTransport()], * defaultContext: { service: 'elasticsearch-client' } * }); * * logger.info('Connected to Elasticsearch', { node: 'localhost:9200' }); * * const childLogger = logger.withContext({ operation: 'bulk-index' }); * childLogger.debug('Processing batch', { size: 1000 }); * ``` */ export class Logger { private config: LoggerConfig; private context: Record; private correlationId?: string; constructor(config: Partial = {}) { this.config = { level: config.level || LogLevel.INFO, transports: config.transports || [new ConsoleTransport()], defaultContext: config.defaultContext || {}, includeTimestamp: config.includeTimestamp !== false, }; this.context = { ...this.config.defaultContext }; } /** * Create a child logger with additional context */ withContext(context: Record): Logger { const child = new Logger(this.config); child.context = { ...this.context, ...context }; child.correlationId = this.correlationId; return child; } /** * Create a child logger with correlation ID */ withCorrelation(correlationId: string): Logger { const child = new Logger(this.config); child.context = { ...this.context }; child.correlationId = correlationId; return child; } /** * Create a child logger for a specific namespace */ child(namespace: string): Logger { return this.withContext({ namespace }); } /** * Log at DEBUG level */ debug(message: string, meta?: Record): void { this.log(LogLevel.DEBUG, message, meta); } /** * Log at INFO level */ info(message: string, meta?: Record): void { this.log(LogLevel.INFO, message, meta); } /** * Log at WARN level */ warn(message: string, meta?: Record): void { this.log(LogLevel.WARN, message, meta); } /** * Log at ERROR level */ error(message: string, error?: Error, meta?: Record): void { this.log(LogLevel.ERROR, message, meta, error); } /** * Internal log method */ private log( level: LogLevel, message: string, meta?: Record, error?: Error ): void { // Check if we should log this level if (!this.shouldLog(level)) { return; } const entry: LogEntry = { level, message, timestamp: new Date(), context: { ...this.context, ...meta }, ...(this.correlationId && { correlationId: this.correlationId }), ...(error && { error }), }; // Send to all transports for (const transport of this.config.transports) { try { const result = transport.log(entry); // Handle async transports if (result && typeof result.then === 'function') { result.catch((err) => { console.error('Transport error:', err); }); } } catch (err) { console.error('Transport error:', err); } } } /** * Check if a log level should be output */ private shouldLog(level: LogLevel): boolean { return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.config.level]; } /** * Update logger configuration */ setLevel(level: LogLevel): void { this.config.level = level; } /** * Add a transport */ addTransport(transport: LogTransport): void { this.config.transports.push(transport); } /** * Get current log level */ getLevel(): LogLevel { return this.config.level; } } /** * Default logger instance */ export const defaultLogger = new Logger({ level: LogLevel.INFO, transports: [new ConsoleTransport()], });