UNPKG

@clipwhisperer/common

Version:

ClipWhisperer Common - Shared library providing core utilities, database schemas, authentication, bucket management, and common functionality across all ClipWhisperer microservices

444 lines (367 loc) 12.6 kB
import { randomUUID } from 'crypto'; import { createWriteStream, WriteStream } from 'fs'; import { mkdir } from 'fs/promises'; import { join } from 'path'; export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace'; export type LogContext = Record<string, any>; export interface LogEntry { timestamp: string; level: LogLevel; message: string; context?: LogContext; correlationId?: string; service?: string; component?: string; error?: { name: string; message: string; stack?: string; }; metadata?: { pid: number; hostname: string; version: string; }; } export interface LogTransport { name: string; write(entry: LogEntry): Promise<void>; flush?(): Promise<void>; close?(): Promise<void>; } /** * Console transport for logging to stdout/stderr */ export class ConsoleTransport implements LogTransport { public readonly name = 'console'; private readonly colors: boolean; constructor(colors = true) { this.colors = colors; } public async write(entry: LogEntry): Promise<void> { const formatted = this.formatEntry(entry); if (entry.level === 'error' || entry.level === 'warn') { console.error(formatted); } else { console.log(formatted); } } private formatEntry(entry: LogEntry): string { const timestamp = entry.timestamp; const level = this.colors ? this.colorizeLevel(entry.level) : entry.level.toUpperCase(); const service = entry.service ? `[${entry.service}]` : ''; const component = entry.component ? `[${entry.component}]` : ''; const correlationId = entry.correlationId ? `(${entry.correlationId.slice(0, 8)})` : ''; let message = `${timestamp} ${level} ${service}${component}${correlationId} ${entry.message}`; if (entry.context && Object.keys(entry.context).length > 0) { message += ` | Context: ${JSON.stringify(entry.context)}`; } if (entry.error) { message += `\n Error: ${entry.error.name}: ${entry.error.message}`; if (entry.error.stack) { message += `\n Stack: ${entry.error.stack}`; } } return message; } private colorizeLevel(level: LogLevel): string { if (!this.colors) return level.toUpperCase(); const colors = { error: '\x1b[31m', // Red warn: '\x1b[33m', // Yellow info: '\x1b[36m', // Cyan debug: '\x1b[35m', // Magenta trace: '\x1b[37m', // White }; const reset = '\x1b[0m'; return `${colors[level]}${level.toUpperCase()}${reset}`; } } /** * File transport for logging to files with rotation */ export class FileTransport implements LogTransport { public readonly name = 'file'; private writeStream: WriteStream | null = null; private readonly logPath: string; private readonly maxFileSize: number; private readonly maxFiles: number; private currentFileSize = 0; constructor( logPath: string, maxFileSize = 10 * 1024 * 1024, // 10MB maxFiles = 5 ) { this.logPath = logPath; this.maxFileSize = maxFileSize; this.maxFiles = maxFiles; } public async write(entry: LogEntry): Promise<void> { if (!this.writeStream) { await this.createWriteStream(); } const formatted = JSON.stringify(entry) + '\n'; // Check if we need to rotate the log file if (this.currentFileSize + formatted.length > this.maxFileSize) { await this.rotateLogFile(); } return new Promise((resolve, reject) => { this.writeStream!.write(formatted, (error) => { if (error) { reject(error); } else { this.currentFileSize += formatted.length; resolve(); } }); }); } public async flush(): Promise<void> { if (this.writeStream) { return new Promise((resolve) => { this.writeStream!.end(resolve); }); } } public async close(): Promise<void> { if (this.writeStream) { await this.flush(); this.writeStream = null; } } private async createWriteStream(): Promise<void> { // Ensure log directory exists await mkdir(join(this.logPath, '..'), { recursive: true }); this.writeStream = createWriteStream(this.logPath, { flags: 'a' }); this.currentFileSize = 0; // Reset size counter } private async rotateLogFile(): Promise<void> { if (this.writeStream) { await this.close(); } // Rotate existing files for (let i = this.maxFiles - 1; i > 0; i--) { const oldFile = `${this.logPath}.${i}`; const newFile = `${this.logPath}.${i + 1}`; try { const fs = await import('fs/promises'); await fs.rename(oldFile, newFile); } catch { // File doesn't exist, continue } } // Move current log to .1 try { const fs = await import('fs/promises'); await fs.rename(this.logPath, `${this.logPath}.1`); } catch { // File doesn't exist, continue } await this.createWriteStream(); } } /** * Enterprise-grade structured logger with multiple transports and correlation tracking */ export class Logger { private static instance: Logger; private transports: Map<string, LogTransport> = new Map(); private logLevel: LogLevel; private correlationId: string | null = null; private service: string | null = null; private component: string | null = null; private readonly logLevels: Record<LogLevel, number> = { error: 0, warn: 1, info: 2, debug: 3, trace: 4, }; private constructor() { this.logLevel = (process.env.LOG_LEVEL as LogLevel) || 'info'; this.initializeTransports(); } public static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } private async initializeTransports(): Promise<void> { // Add console transport this.addTransport(new ConsoleTransport(process.env.LOG_COLORS !== 'false')); // Add file transport if log directory is specified const logDir = process.env.LOG_DIRECTORY || './logs'; const logFile = join(logDir, 'application.log'); this.addTransport(new FileTransport(logFile)); } public setCorrelationId(correlationId: string): void { this.correlationId = correlationId; } public setService(service: string): void { this.service = service; } public setComponent(component: string): void { this.component = component; } public clearContext(): void { this.correlationId = null; this.service = null; this.component = null; } public child(context: { service?: string; component?: string; correlationId?: string }): Logger { const childLogger = Object.create(this); childLogger.service = context.service || this.service; childLogger.component = context.component || this.component; childLogger.correlationId = context.correlationId || this.correlationId; return childLogger; } public error(message: string, context?: LogContext, error?: Error): void { this.log('error', message, context, error); } public warn(message: string, context?: LogContext): void { this.log('warn', message, context); } public info(message: string, context?: LogContext): void { this.log('info', message, context); } public debug(message: string, context?: LogContext): void { this.log('debug', message, context); } public trace(message: string, context?: LogContext): void { this.log('trace', message, context); } public log(level: LogLevel, message: string, context?: LogContext, error?: Error): void { if (!this.shouldLog(level)) return; const entry: LogEntry = { timestamp: new Date().toISOString(), level, message, context, correlationId: this.correlationId || undefined, service: this.service || undefined, component: this.component || undefined, error: error ? { name: error.name, message: error.message, stack: error.stack, } : undefined, metadata: { pid: process.pid, hostname: require('os').hostname(), version: process.version, }, }; // Write to all transports (fire and forget for performance) this.writeToTransports(entry).catch(console.error); } private shouldLog(level: LogLevel): boolean { return this.logLevels[level] <= this.logLevels[this.logLevel]; } private async writeToTransports(entry: LogEntry): Promise<void> { const writePromises = Array.from(this.transports.values()).map(transport => transport.write(entry).catch(error => { console.error(`Failed to write to transport ${transport.name}:`, error); }) ); await Promise.allSettled(writePromises); } public addTransport(transport: LogTransport): void { this.transports.set(transport.name, transport); } public removeTransport(name: string): boolean { return this.transports.delete(name); } public getTransports(): LogTransport[] { return Array.from(this.transports.values()); } public async flush(): Promise<void> { const flushPromises = Array.from(this.transports.values()) .filter(transport => transport.flush) .map(transport => transport.flush!()); await Promise.allSettled(flushPromises); } public async close(): Promise<void> { const closePromises = Array.from(this.transports.values()) .filter(transport => transport.close) .map(transport => transport.close!()); await Promise.allSettled(closePromises); this.transports.clear(); } public async performance<T>(operation: string, fn: () => Promise<T>): Promise<T> { return this.performanceSync(operation, fn); } public async performanceSync<T>(operation: string, fn: () => Promise<T>): Promise<T> { const startTime = Date.now(); const correlationId = this.generateCorrelationId(); this.info(`Starting operation: ${operation}`, { operation, correlationId, startTime: new Date(startTime).toISOString() }); try { const result = await fn(); const duration = Date.now() - startTime; this.info(`Operation completed: ${operation}`, { operation, correlationId, duration, status: 'success' }); return result; } catch (error) { const duration = Date.now() - startTime; this.error(`Operation failed: ${operation}`, { operation, correlationId, duration, status: 'error' }, error instanceof Error ? error : new Error(String(error))); throw error; } } public metric(name: string, value: number, tags?: Record<string, string>): void { this.debug(`Metric: ${name}`, { metric: name, value, tags }); } public counter(name: string, increment = 1, tags?: Record<string, string>): void { this.metric(name, increment, { ...tags, type: 'counter' }); } public gauge(name: string, value: number, tags?: Record<string, string>): void { this.metric(name, value, { ...tags, type: 'gauge' }); } public histogram(name: string, value: number, tags?: Record<string, string>): void { this.metric(name, value, { ...tags, type: 'histogram' }); } public audit(action: string, resource: string, context?: LogContext): void { this.info(`Audit: ${action}`, { audit: true, action, resource, ...context }); } public security(event: string, details: LogContext): void { this.warn(`Security event: ${event}`, { security: true, event, ...details }); } public health(service: string, status: 'healthy' | 'unhealthy' | 'degraded', details?: LogContext): void { const level = status === 'healthy' ? 'info' : 'warn'; this.log(level, `Health check: ${service} is ${status}`, { health: true, service, status, ...details }); } public static generateCorrelationId(): string { return randomUUID(); } public generateCorrelationId(): string { return Logger.generateCorrelationId(); } } // Export singleton instance for convenience export default Logger.getInstance();