logger.ts•8.69 kB
import { getConfig } from './config.js';
// Log levels
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
// Log entry interface
export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: Record<string, any>;
error?: Error;
duration?: number;
requestId?: string;
}
// Logger configuration
interface LoggerConfig {
level: LogLevel;
enableTimestamps: boolean;
enableColors: boolean;
enableJson: boolean;
enableFile: boolean;
logFile?: string;
}
// Color codes for console output
const COLORS = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m',
};
// Log level colors
const LEVEL_COLORS = {
[LogLevel.DEBUG]: COLORS.gray,
[LogLevel.INFO]: COLORS.blue,
[LogLevel.WARN]: COLORS.yellow,
[LogLevel.ERROR]: COLORS.red,
};
// Log level names
const LEVEL_NAMES = {
[LogLevel.DEBUG]: 'DEBUG',
[LogLevel.INFO]: 'INFO',
[LogLevel.WARN]: 'WARN',
[LogLevel.ERROR]: 'ERROR',
};
class Logger {
private config: LoggerConfig;
private logBuffer: LogEntry[] = [];
private requestCounter = 0;
constructor() {
const appConfig = getConfig();
this.config = {
level: this.parseLogLevel(appConfig.logging.level),
enableTimestamps: appConfig.logging.enableTimestamps,
enableColors: appConfig.logging.enableColors,
enableJson: process.env.NODE_ENV === 'production',
enableFile: process.env.LOG_FILE !== undefined,
logFile: process.env.LOG_FILE,
};
}
private parseLogLevel(level: string): LogLevel {
switch (level.toLowerCase()) {
case 'debug': return LogLevel.DEBUG;
case 'info': return LogLevel.INFO;
case 'warn': return LogLevel.WARN;
case 'error': return LogLevel.ERROR;
default: return LogLevel.INFO;
}
}
private shouldLog(level: LogLevel): boolean {
return level >= this.config.level;
}
private formatTimestamp(): string {
return new Date().toISOString();
}
private formatConsoleMessage(entry: LogEntry): string {
let message = '';
if (this.config.enableColors) {
const color = LEVEL_COLORS[entry.level];
const levelName = LEVEL_NAMES[entry.level].padEnd(5);
if (this.config.enableTimestamps) {
message += `${COLORS.gray}${entry.timestamp}${COLORS.reset} `;
}
message += `${color}${levelName}${COLORS.reset} `;
if (entry.requestId) {
message += `${COLORS.cyan}[${entry.requestId}]${COLORS.reset} `;
}
message += entry.message;
if (entry.duration !== undefined) {
message += ` ${COLORS.gray}(${entry.duration}ms)${COLORS.reset}`;
}
if (entry.context && Object.keys(entry.context).length > 0) {
message += `\n${COLORS.dim}${JSON.stringify(entry.context, null, 2)}${COLORS.reset}`;
}
if (entry.error) {
message += `\n${COLORS.red}${entry.error.stack || entry.error.message}${COLORS.reset}`;
}
} else {
const levelName = LEVEL_NAMES[entry.level].padEnd(5);
if (this.config.enableTimestamps) {
message += `${entry.timestamp} `;
}
message += `${levelName} `;
if (entry.requestId) {
message += `[${entry.requestId}] `;
}
message += entry.message;
if (entry.duration !== undefined) {
message += ` (${entry.duration}ms)`;
}
if (entry.context && Object.keys(entry.context).length > 0) {
message += `\n${JSON.stringify(entry.context, null, 2)}`;
}
if (entry.error) {
message += `\n${entry.error.stack || entry.error.message}`;
}
}
return message;
}
private formatJsonMessage(entry: LogEntry): string {
return JSON.stringify({
timestamp: entry.timestamp,
level: LEVEL_NAMES[entry.level],
message: entry.message,
context: entry.context,
error: entry.error ? {
message: entry.error.message,
stack: entry.error.stack,
name: entry.error.name,
} : undefined,
duration: entry.duration,
requestId: entry.requestId,
});
}
private writeLog(entry: LogEntry): void {
if (!this.shouldLog(entry.level)) {
return;
}
// Add to buffer for monitoring
this.logBuffer.push(entry);
if (this.logBuffer.length > 1000) {
this.logBuffer.shift();
}
// Console output
if (this.config.enableJson) {
console.log(this.formatJsonMessage(entry));
} else {
console.log(this.formatConsoleMessage(entry));
}
// File output (if enabled)
if (this.config.enableFile && this.config.logFile) {
// In a real implementation, you'd write to file here
// For now, we'll just use console
}
}
public generateRequestId(): string {
return `req-${Date.now()}-${++this.requestCounter}`;
}
public debug(message: string, context?: Record<string, any>, requestId?: string): void {
this.writeLog({
timestamp: this.formatTimestamp(),
level: LogLevel.DEBUG,
message,
context,
requestId,
});
}
public info(message: string, context?: Record<string, any>, requestId?: string): void {
this.writeLog({
timestamp: this.formatTimestamp(),
level: LogLevel.INFO,
message,
context,
requestId,
});
}
public warn(message: string, context?: Record<string, any>, requestId?: string): void {
this.writeLog({
timestamp: this.formatTimestamp(),
level: LogLevel.WARN,
message,
context,
requestId,
});
}
public error(message: string, error?: Error, context?: Record<string, any>, requestId?: string): void {
this.writeLog({
timestamp: this.formatTimestamp(),
level: LogLevel.ERROR,
message,
error,
context,
requestId,
});
}
public timing(message: string, startTime: number, context?: Record<string, any>, requestId?: string): void {
const duration = Date.now() - startTime;
this.writeLog({
timestamp: this.formatTimestamp(),
level: LogLevel.INFO,
message,
context,
duration,
requestId,
});
}
public getLogBuffer(): LogEntry[] {
return [...this.logBuffer];
}
public getStats(): {
totalLogs: number;
errorCount: number;
warnCount: number;
averageResponseTime: number;
} {
const totalLogs = this.logBuffer.length;
const errorCount = this.logBuffer.filter(entry => entry.level === LogLevel.ERROR).length;
const warnCount = this.logBuffer.filter(entry => entry.level === LogLevel.WARN).length;
const timingEntries = this.logBuffer.filter(entry => entry.duration !== undefined);
const averageResponseTime = timingEntries.length > 0
? timingEntries.reduce((sum, entry) => sum + (entry.duration || 0), 0) / timingEntries.length
: 0;
return {
totalLogs,
errorCount,
warnCount,
averageResponseTime,
};
}
}
// Global logger instance
const logger = new Logger();
// Helper functions for structured logging
export const log = {
debug: (message: string, context?: Record<string, any>, requestId?: string) =>
logger.debug(message, context, requestId),
info: (message: string, context?: Record<string, any>, requestId?: string) =>
logger.info(message, context, requestId),
warn: (message: string, context?: Record<string, any>, requestId?: string) =>
logger.warn(message, context, requestId),
error: (message: string, error?: Error, context?: Record<string, any>, requestId?: string) =>
logger.error(message, error, context, requestId),
timing: (message: string, startTime: number, context?: Record<string, any>, requestId?: string) =>
logger.timing(message, startTime, context, requestId),
generateRequestId: () => logger.generateRequestId(),
getStats: () => logger.getStats(),
getLogBuffer: () => logger.getLogBuffer(),
};
// Performance monitoring helpers
export function createPerformanceTimer(name: string, requestId?: string) {
const startTime = Date.now();
return {
end: (context?: Record<string, any>) => {
log.timing(`${name} completed`, startTime, context, requestId);
},
checkpoint: (checkpointName: string, context?: Record<string, any>) => {
log.timing(`${name} - ${checkpointName}`, startTime, context, requestId);
},
};
}
// Export the logger instance
export { logger };
export default log;