codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
428 lines • 13.7 kB
JavaScript
import { mkdir, access } from 'fs/promises';
import { join } from 'path';
import { homedir } from 'os';
import chalk from 'chalk';
import * as api from '@opentelemetry/api';
import winston from 'winston';
/**
* Enterprise Logger with Winston - 2025 Best Practices
*
* Features:
* - Structured JSON logging for observability
* - High-performance async operations
* - OpenTelemetry integration
* - Production-grade error handling
* - Multiple transport support
*/
class Logger {
config;
winstonLogger; // Definite assignment assertion
logDirectory;
name;
constructor(nameOrConfig, config) {
// Handle overloaded constructor
if (typeof nameOrConfig === 'string') {
this.name = nameOrConfig;
this.config = {
level: 'info',
toFile: true,
toConsole: true,
maxFileSize: '10MB',
maxFiles: 5,
...config,
};
}
else {
this.config = {
level: 'info',
toFile: true,
toConsole: true,
maxFileSize: '10MB',
maxFiles: 5,
...nameOrConfig,
};
}
this.logDirectory = this.config.logDirectory || join(homedir(), '.codecrucible', 'logs');
this.setupWinstonLogger();
this.ensureLogDirectory();
}
/**
* Setup Winston logger with structured JSON format - 2025 Best Practice
*/
setupWinstonLogger() {
// Custom JSON formatter for structured logging
const jsonFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json(), winston.format.printf(({ timestamp, level, message, traceId, spanId, correlationId, userId, sessionId, ...meta }) => {
const logEntry = {
'@timestamp': timestamp,
level: level.toUpperCase(),
message,
service: this.name || 'codecrucible-synth',
...(traceId ? { traceId } : {}),
...(spanId ? { spanId } : {}),
...(correlationId ? { correlationId } : {}),
...(userId ? { userId } : {}),
...(sessionId ? { sessionId } : {}),
...meta,
};
return JSON.stringify(logEntry);
}));
// Console formatter with colors for development
const consoleFormat = winston.format.combine(winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.errors({ stack: true }), winston.format.printf(({ timestamp, level, message, traceId, ...meta }) => {
let output = `${timestamp} ${level} ${message}`;
if (Object.keys(meta).length > 0 && !meta.stack) {
output += `\n${JSON.stringify(meta, null, 2)}`;
}
if (meta.stack) {
output += `\n${meta.stack}`;
}
return output;
}));
const transports = [];
if (this.config.toConsole) {
transports.push(new winston.transports.Console({
level: this.config.level,
format: consoleFormat,
}));
}
if (this.config.toFile) {
transports.push(new winston.transports.File({
filename: join(this.logDirectory, 'codecrucible.log'),
level: this.config.level,
format: jsonFormat,
maxsize: this.parseFileSize(this.config.maxFileSize),
maxFiles: this.config.maxFiles,
tailable: true,
}));
// Separate error log file for high-priority issues
transports.push(new winston.transports.File({
filename: join(this.logDirectory, 'error.log'),
level: 'error',
format: jsonFormat,
maxsize: this.parseFileSize(this.config.maxFileSize),
maxFiles: this.config.maxFiles,
tailable: true,
}));
}
this.winstonLogger = winston.createLogger({
level: this.config.level,
levels: winston.config.npm.levels,
transports,
exitOnError: false,
handleExceptions: true,
handleRejections: true,
});
}
/**
* Parse file size string to bytes
*/
parseFileSize(sizeStr) {
const size = parseInt(sizeStr.match(/\d+/)?.[0] || '10');
const unit = sizeStr.match(/[a-zA-Z]+/)?.[0]?.toLowerCase() || 'mb';
const multipliers = {
b: 1,
kb: 1024,
mb: 1024 * 1024,
gb: 1024 * 1024 * 1024,
};
return size * (multipliers[unit] || multipliers.mb);
}
/**
* Ensure log directory exists
*/
async ensureLogDirectory() {
try {
await access(this.logDirectory);
}
catch {
try {
await mkdir(this.logDirectory, { recursive: true });
}
catch (error) {
console.warn(`Failed to create log directory: ${this.logDirectory}`);
}
}
}
/**
* Get numeric level for comparison
*/
getNumericLevel(level) {
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
return levels[level];
}
/**
* Check if message should be logged based on level
*/
shouldLog(level) {
return this.getNumericLevel(level) >= this.getNumericLevel(this.config.level);
}
/**
* Format timestamp for display
*/
formatTimestamp(date) {
return date.toISOString().replace('T', ' ').replace('Z', '');
}
/**
* Get colored prefix for console output
*/
getColoredPrefix(level) {
const timestamp = chalk.gray(this.formatTimestamp(new Date()));
switch (level) {
case 'debug':
return `${timestamp} ${chalk.blue('DEBUG')}`;
case 'info':
return `${timestamp} ${chalk.green(' INFO')}`;
case 'warn':
return `${timestamp} ${chalk.yellow(' WARN')}`;
case 'error':
return `${timestamp} ${chalk.red('ERROR')}`;
}
}
/**
* Format message for console output
*/
formatConsoleMessage(entry) {
const prefix = this.getColoredPrefix(entry.level);
let message = `${prefix} ${entry.message}`;
if (entry.data) {
const dataStr = typeof entry.data === 'string' ? entry.data : JSON.stringify(entry.data, null, 2);
message += `\\n${chalk.gray(dataStr)}`;
}
if (entry.error) {
message += `\\n${chalk.red(entry.error.stack || entry.error.message)}`;
}
return message;
}
/**
* Format message for file output
*/
formatFileMessage(entry) {
const timestamp = this.formatTimestamp(entry.timestamp);
const level = entry.level.toUpperCase().padEnd(5);
let message = `${timestamp} ${level} ${entry.message}`;
if (entry.data) {
const dataStr = typeof entry.data === 'string' ? entry.data : JSON.stringify(entry.data);
message += ` | Data: ${dataStr}`;
}
if (entry.error) {
message += ` | Error: ${entry.error.message}`;
if (entry.error.stack) {
message += `\\n${entry.error.stack}`;
}
}
return message;
}
/**
* Log a message with specified level - 2025 Winston Implementation
*/
async log(level, message, data, error) {
if (!this.shouldLog(level)) {
return;
}
// Get trace context from OpenTelemetry
const span = api.trace.getActiveSpan();
const spanContext = span?.spanContext();
// Prepare metadata for Winston
const metadata = {};
if (data)
metadata.data = data;
if (error)
metadata.error = error;
if (spanContext?.traceId)
metadata.traceId = spanContext.traceId;
if (spanContext?.spanId)
metadata.spanId = spanContext.spanId;
if (data?.correlationId)
metadata.correlationId = data.correlationId;
if (data?.userId)
metadata.userId = data.userId;
if (data?.sessionId)
metadata.sessionId = data.sessionId;
// Use Winston for structured logging - handles both console and file automatically
this.winstonLogger.log(level, message, metadata);
}
// Note: Winston handles file operations automatically - legacy queue methods removed
/**
* Debug level logging
*/
debug(message, data) {
this.log('debug', message, data);
}
/**
* Info level logging
*/
info(message, data) {
this.log('info', message, data);
}
/**
* Warning level logging
*/
warn(message, data) {
this.log('warn', message, data);
}
/**
* Error level logging
*/
error(message, error, data) {
if (error instanceof Error) {
this.log('error', message, data, error);
}
else {
// If second parameter is not an Error, treat it as data
this.log('error', message, error);
}
}
/**
* Update logger configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Flush pending logs (Winston handles automatically)
*/
async flush() {
// Winston handles flushing automatically
return Promise.resolve();
}
/**
* Create a child logger with additional context
*/
child(context) {
const childLogger = new Logger(this.config);
// Override log method to include context
const originalLog = childLogger.log.bind(childLogger);
childLogger.log = async (level, message, data, error) => {
return originalLog(level, `[${context}] ${message}`, data, error);
};
return childLogger;
}
/**
* Performance timing utility
*/
time(label) {
const start = Date.now();
return () => {
const duration = Date.now() - start;
this.debug(`Timer ${label}: ${duration}ms`);
};
}
/**
* Log performance of async operations
*/
async profile(label, operation) {
const start = Date.now();
try {
const result = await operation();
const duration = Date.now() - start;
this.debug(`Operation ${label} completed in ${duration}ms`);
return result;
}
catch (error) {
const duration = Date.now() - start;
this.error(`Operation ${label} failed after ${duration}ms`, error);
throw error;
}
}
/**
* Log system information
*/
logSystemInfo() {
this.info('System Information', {
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
cwd: process.cwd(),
});
}
/**
* Audit log for compliance and security
*/
audit(action, details) {
this.info(`AUDIT: ${action}`, {
...details,
auditType: 'security',
timestamp: new Date().toISOString(),
});
}
/**
* Security event logging
*/
security(event, details) {
this.warn(`SECURITY: ${event}`, {
...details,
eventType: 'security_event',
timestamp: new Date().toISOString(),
});
}
/**
* Performance metrics logging
*/
metric(name, value, unit = 'ms', tags) {
this.debug(`METRIC: ${name}`, {
metricName: name,
metricValue: value,
metricUnit: unit,
metricTags: tags,
metricType: 'performance',
timestamp: new Date().toISOString(),
});
}
/**
* Business event logging
*/
business(event, details) {
this.info(`BUSINESS: ${event}`, {
...details,
eventType: 'business_event',
timestamp: new Date().toISOString(),
});
}
}
// Create default logger instance
export const logger = new Logger();
// Export Logger class for custom instances
export { Logger };
// Convenience function to update global logger config
export function configureLogger(config) {
logger.updateConfig(config);
}
// Graceful shutdown - flush logs (only register once)
let loggerShutdownRegistered = false;
if (!loggerShutdownRegistered) {
loggerShutdownRegistered = true;
process.on('beforeExit', async () => {
try {
await logger.flush();
}
catch (error) {
console.error('Failed to flush logs on exit:', error);
}
});
process.on('SIGINT', async () => {
try {
await logger.flush();
}
catch (error) {
console.error('Failed to flush logs on SIGINT:', error);
}
process.exit(0);
});
process.on('SIGTERM', async () => {
try {
await logger.flush();
}
catch (error) {
console.error('Failed to flush logs on SIGTERM:', error);
}
process.exit(0);
});
}
//# sourceMappingURL=logger.js.map