UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

412 lines (360 loc) 10.8 kB
/** * Enterprise Structured Logging System * Implements correlation IDs, structured JSON logs, and multiple transport layers */ import winston from 'winston'; import crypto from 'crypto'; import { AsyncLocalStorage } from 'async_hooks'; export interface LogEntry { timestamp: string; level: string; message: string; service: string; version: string; correlationId: string; traceId?: string; spanId?: string; userId?: string; sessionId?: string; operation?: string; duration?: number; statusCode?: number; error?: { name: string; message: string; stack: string; code?: string; }; metadata: Record<string, any>; } export interface LogConfig { level: string; format: 'json' | 'text'; environment: string; service: string; version: string; outputs: { console: boolean; file: boolean; remote?: { endpoint: string; apiKey: string; }; }; rotation: { enabled: boolean; maxSize: string; maxFiles: number; }; } // Context storage for correlation IDs const correlationStorage = new AsyncLocalStorage<string>(); export class StructuredLogger { private logger: winston.Logger; private config: LogConfig; private service: string; private version: string; constructor(config: LogConfig) { this.config = config; this.service = config.service; this.version = config.version; this.logger = winston.createLogger({ level: config.level, format: this.createFormat(), defaultMeta: { service: this.service, version: this.version, environment: config.environment, }, transports: this.createTransports(), }); // Handle uncaught exceptions and unhandled rejections this.logger.exceptions.handle(new winston.transports.File({ filename: 'exceptions.log' })); this.logger.rejections.handle(new winston.transports.File({ filename: 'rejections.log' })); } /** * Create Winston format with structured logging */ private createFormat(): winston.Logform.Format { return winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json(), winston.format.printf(({ timestamp, level, message, service, version, ...meta }) => { const correlationId = this.getCorrelationId(); const logEntry: LogEntry = { timestamp: timestamp as string, level: level as string, message: message as string, service: service as string, version: version as string, correlationId, metadata: meta, }; // Add tracing information if available if (meta.traceId) logEntry.traceId = meta.traceId as string; if (meta.spanId) logEntry.spanId = meta.spanId as string; if (meta.userId) logEntry.userId = meta.userId as string; if (meta.sessionId) logEntry.sessionId = meta.sessionId as string; if (meta.operation) logEntry.operation = meta.operation as string; if (meta.duration) logEntry.duration = meta.duration as number; if (meta.statusCode) logEntry.statusCode = meta.statusCode as number; // Handle error objects if (meta.error && meta.error instanceof Error) { logEntry.error = { name: meta.error.name, message: meta.error.message, stack: meta.error.stack || '', code: (meta.error as any).code, }; } return JSON.stringify(logEntry); }) ); } /** * Create Winston transports based on configuration */ private createTransports(): winston.transport[] { const transports: winston.transport[] = []; // Console transport if (this.config.outputs.console) { transports.push( new winston.transports.Console({ format: this.config.format === 'text' ? winston.format.combine(winston.format.colorize(), winston.format.simple()) : winston.format.json(), }) ); } // File transport if (this.config.outputs.file) { transports.push( new winston.transports.File({ filename: 'application.log', maxsize: this.parseSize(this.config.rotation.maxSize), maxFiles: this.config.rotation.maxFiles, tailable: true, }) ); // Separate error log transports.push( new winston.transports.File({ filename: 'error.log', level: 'error', maxsize: this.parseSize(this.config.rotation.maxSize), maxFiles: this.config.rotation.maxFiles, tailable: true, }) ); } // Remote transport (e.g., ELK, Splunk, DataDog) if (this.config.outputs.remote) { // In production, add HTTP transport to send logs to centralized system transports.push( new winston.transports.Http({ host: new URL(this.config.outputs.remote.endpoint).hostname, port: parseInt(new URL(this.config.outputs.remote.endpoint).port) || 443, path: new URL(this.config.outputs.remote.endpoint).pathname, ssl: this.config.outputs.remote.endpoint.startsWith('https'), headers: { Authorization: `Bearer ${this.config.outputs.remote.apiKey}`, 'Content-Type': 'application/json', }, }) ); } return transports; } /** * Parse size string to bytes */ private parseSize(size: string): number { const units = { k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024 }; const match = size.toLowerCase().match(/^(\d+)([kmg]?)b?$/); if (!match) return 10 * 1024 * 1024; // Default 10MB const value = parseInt(match[1]); const unit = match[2] as keyof typeof units; return value * (units[unit] || 1); } /** * Get or generate correlation ID */ private getCorrelationId(): string { return correlationStorage.getStore() || crypto.randomUUID(); } /** * Set correlation ID for async context */ setCorrelationId(id: string): void { correlationStorage.enterWith(id); } /** * Generate new correlation ID */ generateCorrelationId(): string { const id = crypto.randomUUID(); this.setCorrelationId(id); return id; } /** * Run function with correlation ID context */ withCorrelationId<T>(id: string, fn: () => T): T { return correlationStorage.run(id, fn); } /** * Structured logging methods */ info(message: string, metadata: Record<string, any> = {}): void { this.logger.info(message, metadata); } warn(message: string, metadata: Record<string, any> = {}): void { this.logger.warn(message, metadata); } error(message: string, error?: Error, metadata: Record<string, any> = {}): void { this.logger.error(message, { ...metadata, error }); } debug(message: string, metadata: Record<string, any> = {}): void { this.logger.debug(message, metadata); } /** * Operation-specific logging with timing */ async logOperation<T>( operation: string, fn: () => Promise<T>, metadata: Record<string, any> = {} ): Promise<T> { const startTime = Date.now(); const operationId = crypto.randomUUID(); this.info(`Operation started: ${operation}`, { operation, operationId, ...metadata, }); try { const result = await fn(); const duration = Date.now() - startTime; this.info(`Operation completed: ${operation}`, { operation, operationId, duration, status: 'success', ...metadata, }); return result; } catch (error) { const duration = Date.now() - startTime; this.error(`Operation failed: ${operation}`, error as Error, { operation, operationId, duration, status: 'error', ...metadata, }); throw error; } } /** * HTTP request logging */ logHttpRequest(req: any, res: any, duration: number): void { this.info('HTTP request processed', { method: req.method, url: req.originalUrl, statusCode: res.statusCode, duration, userAgent: req.get('User-Agent'), ip: req.ip, userId: req.user?.userId, sessionId: req.sessionId, }); } /** * Security event logging */ logSecurityEvent(event: string, metadata: Record<string, any> = {}): void { this.warn(`Security event: ${event}`, { securityEvent: event, timestamp: new Date().toISOString(), ...metadata, }); } /** * Performance metrics logging */ logPerformanceMetric( metric: string, value: number, unit: string, metadata: Record<string, any> = {} ): void { this.info(`Performance metric: ${metric}`, { metric, value, unit, timestamp: new Date().toISOString(), ...metadata, }); } /** * Business event logging */ logBusinessEvent(event: string, metadata: Record<string, any> = {}): void { this.info(`Business event: ${event}`, { businessEvent: event, timestamp: new Date().toISOString(), ...metadata, }); } /** * Express middleware for request logging */ middleware() { return (req: any, res: any, next: any) => { // Generate correlation ID for request const correlationId = req.headers['x-correlation-id'] || this.generateCorrelationId(); req.correlationId = correlationId; res.setHeader('X-Correlation-ID', correlationId); const startTime = Date.now(); // Log request this.info('HTTP request received', { method: req.method, url: req.originalUrl, userAgent: req.get('User-Agent'), ip: req.ip, correlationId, }); // Override res.end to log response const originalEnd = res.end; res.end = function (chunk: any, encoding: any) { const duration = Date.now() - startTime; // Use the logger instance through closure (req as any).logger.logHttpRequest(req, res, duration); originalEnd.call(this, chunk, encoding); }; // Attach logger to request for use in route handlers req.logger = this; this.withCorrelationId(correlationId, () => next()); }; } /** * Child logger with additional context */ child(metadata: Record<string, any>): StructuredLogger { const childLogger = Object.create(this); childLogger.logger = this.logger.child(metadata); return childLogger; } /** * Flush logs (useful for graceful shutdown) */ async flush(): Promise<void> { return new Promise(resolve => { this.logger.on('finish', resolve); this.logger.end(); }); } }