type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: Record<string, unknown>;
error?: Error;
}
class Logger {
private isDevelopment = process.env.NODE_ENV !== 'production';
private formatLog(entry: LogEntry): string {
const { timestamp, level, message, context, error } = entry;
let log = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
if (context && Object.keys(context).length > 0) {
log += ` ${JSON.stringify(context)}`;
}
if (error && this.isDevelopment) {
log += `\n${error.stack}`;
}
return log;
}
private log(level: LogLevel, message: string, context?: Record<string, unknown>, error?: Error) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
context,
error,
};
const formatted = this.formatLog(entry);
if (level === 'error') {
console.error(formatted);
} else if (level === 'warn') {
console.warn(formatted);
} else if (level === 'debug' && !this.isDevelopment) {
// Skip debug logs in production
return;
} else {
console.log(formatted);
}
}
debug(message: string, context?: Record<string, unknown>) {
this.log('debug', message, context);
}
info(message: string, context?: Record<string, unknown>) {
this.log('info', message, context);
}
warn(message: string, context?: Record<string, unknown>) {
this.log('warn', message, context);
}
error(message: string, error?: Error, context?: Record<string, unknown>) {
this.log('error', message, context, error);
}
}
export const logger = new Logger();
// Request logging middleware
export function requestLogger(req: any, res: any, next: any) {
const start = Date.now();
const requestId = Math.random().toString(36).substring(7);
logger.info('Incoming request', {
requestId,
method: req.method,
path: req.path,
ip: req.ip,
});
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('Request completed', {
requestId,
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
}