import { format } from 'util';
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
TRACE = 4
}
export interface LogContext {
[key: string]: any;
}
export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: LogContext;
error?: Error;
stack?: string;
}
class Logger {
private static instance: Logger;
private logLevel: LogLevel;
private isProduction: boolean;
private constructor() {
this.logLevel = this.parseLogLevel(process.env.LOG_LEVEL || 'INFO');
this.isProduction = process.env.NODE_ENV === 'production';
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
private parseLogLevel(level: string): LogLevel {
switch (level.toUpperCase()) {
case 'ERROR': return LogLevel.ERROR;
case 'WARN': return LogLevel.WARN;
case 'INFO': return LogLevel.INFO;
case 'DEBUG': return LogLevel.DEBUG;
case 'TRACE': return LogLevel.TRACE;
default: return LogLevel.INFO;
}
}
private shouldLog(level: LogLevel): boolean {
return level <= this.logLevel;
}
private formatMessage(entry: LogEntry): string {
const levelStr = LogLevel[entry.level].padEnd(5);
if (this.isProduction) {
// Structured JSON logging for production
return JSON.stringify({
timestamp: entry.timestamp,
level: LogLevel[entry.level],
message: entry.message,
...(entry.context && { context: entry.context }),
...(entry.error && {
error: {
name: entry.error.name,
message: entry.error.message,
stack: entry.error.stack
}
})
});
} else {
// Human-readable format for development
let output = `[${entry.timestamp}] ${levelStr} ${entry.message}`;
if (entry.context && Object.keys(entry.context).length > 0) {
output += ` | ${JSON.stringify(entry.context)}`;
}
if (entry.error) {
output += `\n Error: ${entry.error.message}`;
if (entry.error.stack) {
output += `\n Stack: ${entry.error.stack}`;
}
}
return output;
}
}
private 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,
error
};
const formattedMessage = this.formatMessage(entry);
// Write to stderr to avoid mixing with MCP protocol messages on stdout
console.error(formattedMessage);
}
error(message: string, error?: Error, context?: LogContext): void {
this.log(LogLevel.ERROR, message, context, error);
}
warn(message: string, context?: LogContext): void {
this.log(LogLevel.WARN, message, context);
}
info(message: string, context?: LogContext): void {
this.log(LogLevel.INFO, message, context);
}
debug(message: string, context?: LogContext): void {
this.log(LogLevel.DEBUG, message, context);
}
trace(message: string, context?: LogContext): void {
this.log(LogLevel.TRACE, message, context);
}
// Helper method for timing operations
time(label: string): () => void {
const start = Date.now();
this.debug(`Timer started: ${label}`);
return () => {
const duration = Date.now() - start;
this.debug(`Timer completed: ${label}`, { duration_ms: duration });
};
}
// Helper method for logging method entry/exit
methodEntry(className: string, methodName: string, args?: any): void {
this.trace(`Entering ${className}.${methodName}`, { args });
}
methodExit(className: string, methodName: string, result?: any): void {
this.trace(`Exiting ${className}.${methodName}`, { result });
}
// Helper method for logging async operations
async asyncOperation<T>(
operationName: string,
operation: () => Promise<T>,
context?: LogContext
): Promise<T> {
const endTimer = this.time(operationName);
this.debug(`Starting async operation: ${operationName}`, context);
try {
const result = await operation();
this.debug(`Completed async operation: ${operationName}`, { ...context, success: true });
return result;
} catch (error) {
this.error(`Failed async operation: ${operationName}`, error as Error, context);
throw error;
} finally {
endTimer();
}
}
}
// Export singleton instance
export const logger = Logger.getInstance();
// Export helper function for child loggers with context
export function createLogger(context: LogContext): {
error: (message: string, error?: Error, additionalContext?: LogContext) => void;
warn: (message: string, additionalContext?: LogContext) => void;
info: (message: string, additionalContext?: LogContext) => void;
debug: (message: string, additionalContext?: LogContext) => void;
trace: (message: string, additionalContext?: LogContext) => void;
} {
return {
error: (message, error, additionalContext) =>
logger.error(message, error, { ...context, ...additionalContext }),
warn: (message, additionalContext) =>
logger.warn(message, { ...context, ...additionalContext }),
info: (message, additionalContext) =>
logger.info(message, { ...context, ...additionalContext }),
debug: (message, additionalContext) =>
logger.debug(message, { ...context, ...additionalContext }),
trace: (message, additionalContext) =>
logger.trace(message, { ...context, ...additionalContext })
};
}