logger.ts•9.67 kB
/**
* Comprehensive logging system with structured logging
*/
import { appendFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: Record<string, any>;
error?: {
name: string;
message: string;
stack: string | undefined;
};
requestId?: string;
userId?: string;
operation?: string;
duration?: number;
}
export interface LoggerConfig {
level: LogLevel;
enableConsole: boolean;
enableFile: boolean;
logDirectory: string;
maxFileSize: number;
maxFiles: number;
enableStructured: boolean;
}
export class Logger {
private config: LoggerConfig;
private logLevels: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
constructor(config: Partial<LoggerConfig> = {}) {
this.config = {
level: 'info',
enableConsole: true,
enableFile: true,
logDirectory: './logs',
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
enableStructured: true,
...config
};
this.ensureLogDirectory();
}
/**
* Log debug message
*/
public debug(message: string, context?: Record<string, any>): void {
this.log('debug', message, context);
}
/**
* Log info message
*/
public info(message: string, context?: Record<string, any>): void {
this.log('info', message, context);
}
/**
* Log warning message
*/
public warn(message: string, context?: Record<string, any>): void {
this.log('warn', message, context);
}
/**
* Log error message
*/
public error(message: string, error?: Error, context?: Record<string, any>): void {
const errorContext = error ? {
name: error.name,
message: error.message,
stack: error.stack
} : undefined;
this.log('error', message, context, errorContext);
}
/**
* Log request start
*/
public logRequest(requestId: string, method: string, path: string, userId?: string): void {
this.log('info', `Request started: ${method} ${path}`, {
requestId,
userId,
operation: 'request_start',
method,
path
});
}
/**
* Log request completion
*/
public logResponse(
requestId: string,
statusCode: number,
duration: number,
userId?: string
): void {
this.log('info', `Request completed: ${statusCode}`, {
requestId,
userId,
operation: 'request_complete',
statusCode,
duration
});
}
/**
* Log API operation
*/
public logOperation(
operation: string,
success: boolean,
duration: number,
context?: Record<string, any>
): void {
const level = success ? 'info' : 'error';
const message = `Operation ${operation}: ${success ? 'SUCCESS' : 'FAILED'}`;
this.log(level, message, {
...context,
operation,
success,
duration
});
}
/**
* Log performance metrics
*/
public logPerformance(
operation: string,
metrics: {
duration: number;
memoryUsage?: number;
cpuUsage?: number;
[key: string]: any;
}
): void {
this.log('debug', `Performance metrics for ${operation}`, {
operation: 'performance',
operationName: operation,
...metrics
});
}
/**
* Core logging method
*/
private log(
level: LogLevel,
message: string,
context?: Record<string, any>,
error?: { name: string; message: string; stack: string | undefined }
): void {
// Check if log level is enabled
if (this.logLevels[level] < this.logLevels[this.config.level]) {
return;
}
const logEntry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
...(context && { context }),
...(error && { error }),
...(context?.requestId && { requestId: context.requestId }),
...(context?.userId && { userId: context.userId }),
...(context?.operation && { operation: context.operation }),
...(context?.duration && { duration: context.duration })
};
// Console logging
if (this.config.enableConsole) {
this.logToConsole(logEntry);
}
// File logging
if (this.config.enableFile) {
this.logToFile(logEntry);
}
}
/**
* Log to console with colors
*/
private logToConsole(entry: LogEntry): void {
const colors = {
debug: '\x1b[36m', // Cyan
info: '\x1b[32m', // Green
warn: '\x1b[33m', // Yellow
error: '\x1b[31m' // Red
};
const reset = '\x1b[0m';
if (this.config.enableStructured) {
console.log(`${colors[entry.level]}[${entry.timestamp}] ${entry.level.toUpperCase()}${reset}: ${entry.message}`);
if (entry.context) {
console.log(' Context:', JSON.stringify(entry.context, null, 2));
}
if (entry.error) {
console.log(' Error:', entry.error);
}
} else {
const contextStr = entry.context ? ` | ${JSON.stringify(entry.context)}` : '';
console.log(`${colors[entry.level]}[${entry.timestamp}] ${entry.level.toUpperCase()}${reset}: ${entry.message}${contextStr}`);
}
}
/**
* Log to file
*/
private logToFile(entry: LogEntry): void {
const logFile = join(this.config.logDirectory, `app-${new Date().toISOString().split('T')[0]}.log`);
const logLine = JSON.stringify(entry) + '\n';
try {
// Check file size and rotate if necessary
if (existsSync(logFile)) {
const stats = require('fs').statSync(logFile);
if (stats.size > this.config.maxFileSize) {
this.rotateLogFile(logFile);
}
}
appendFileSync(logFile, logLine);
} catch (error) {
console.error('Failed to write to log file:', error);
}
}
/**
* Rotate log files
*/
private rotateLogFile(currentFile: string): void {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const rotatedFile = currentFile.replace('.log', `-${timestamp}.log`);
try {
require('fs').renameSync(currentFile, rotatedFile);
// Clean up old log files
this.cleanupOldLogs();
} catch (error) {
console.error('Failed to rotate log file:', error);
}
}
/**
* Clean up old log files
*/
private cleanupOldLogs(): void {
try {
const fs = require('fs');
const files = fs.readdirSync(this.config.logDirectory)
.filter((file: string) => file.endsWith('.log'))
.map((file: string) => ({
name: file,
path: join(this.config.logDirectory, file),
stats: fs.statSync(join(this.config.logDirectory, file))
}))
.sort((a: any, b: any) => b.stats.mtime - a.stats.mtime);
// Keep only the most recent files
if (files.length > this.config.maxFiles) {
const filesToDelete = files.slice(this.config.maxFiles);
filesToDelete.forEach((file: any) => {
fs.unlinkSync(file.path);
});
}
} catch (error) {
console.error('Failed to cleanup old logs:', error);
}
}
/**
* Ensure log directory exists
*/
private ensureLogDirectory(): void {
if (!existsSync(this.config.logDirectory)) {
try {
mkdirSync(this.config.logDirectory, { recursive: true });
} catch (error) {
console.error('Failed to create log directory:', error);
this.config.enableFile = false;
}
}
}
/**
* Update logger configuration
*/
public updateConfig(config: Partial<LoggerConfig>): void {
this.config = { ...this.config, ...config };
this.ensureLogDirectory();
}
/**
* Get current configuration
*/
public getConfig(): LoggerConfig {
return { ...this.config };
}
/**
* Create child logger with additional context
*/
public child(context: Record<string, any>): ChildLogger {
return new ChildLogger(this, context);
}
}
/**
* Child logger that includes additional context in all log entries
*/
export class ChildLogger {
constructor(
private parent: Logger,
private context: Record<string, any>
) {}
public debug(message: string, additionalContext?: Record<string, any>): void {
this.parent.debug(message, { ...this.context, ...additionalContext });
}
public info(message: string, additionalContext?: Record<string, any>): void {
this.parent.info(message, { ...this.context, ...additionalContext });
}
public warn(message: string, additionalContext?: Record<string, any>): void {
this.parent.warn(message, { ...this.context, ...additionalContext });
}
public error(message: string, error?: Error, additionalContext?: Record<string, any>): void {
this.parent.error(message, error, { ...this.context, ...additionalContext });
}
public logRequest(requestId: string, method: string, path: string, userId?: string): void {
this.parent.logRequest(requestId, method, path, userId);
}
public logResponse(requestId: string, statusCode: number, duration: number, userId?: string): void {
this.parent.logResponse(requestId, statusCode, duration, userId);
}
public logOperation(operation: string, success: boolean, duration: number, context?: Record<string, any>): void {
this.parent.logOperation(operation, success, duration, { ...this.context, ...context });
}
public logPerformance(operation: string, metrics: { duration: number; [key: string]: any }): void {
this.parent.logPerformance(operation, { ...this.context, ...metrics });
}
}
// Export singleton instance
export const logger = new Logger();