const winston = require('winston'); const DailyRotateFile = require('winston-daily-rotate-file'); // Define log levels and colors const logLevels = { error: 0, warn: 1, info: 2, http: 3, debug: 4, }; winston.addColors({ error: 'red', warn: 'yellow', info: 'green', http: 'magenta', debug: 'white', }); // Determine log level based on environment const level = () => { const env = process.env.NODE_ENV || 'dev'; const isDevelopment = env === 'dev' || env === 'development'; return isDevelopment ? 'debug' : process.env.LOG_LEVEL || 'info'; }; // Custom format to extract stack traces from Error objects in metadata const extractErrorStack = winston.format((info) => { // Check if any metadata value is an Error object and extract its stack Object.keys(info).forEach(key => { if (info[key] instanceof Error) { info[key] = { message: info[key].message, stack: info[key].stack, name: info[key].name }; } }); return info; }); // Define log format const logFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.colorize({ all: true }), extractErrorStack(), winston.format.printf((info) => { const { timestamp, level, message, stack, ...metadata } = info; let output = `${timestamp} ${level}: ${message}`; // Include relevant metadata in console output const metaKeys = Object.keys(metadata).filter(key => !['splat', 'Symbol(level)', 'Symbol(message)'].includes(key) && !key.startsWith('Symbol') ); if (metaKeys.length > 0) { const metaOutput = {}; metaKeys.forEach(key => { // For Error objects, extract message and stack if (metadata[key] && metadata[key].stack) { metaOutput[key] = { message: metadata[key].message, stack: metadata[key].stack }; } else { metaOutput[key] = metadata[key]; } }); output += ` ${JSON.stringify(metaOutput, null, 2)}`; } // Check for stack trace in the info object itself if (stack) { output += `\nStack: ${stack}`; } return output; }), ); // Define JSON format for file logging const jsonFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.errors({ stack: true }), extractErrorStack(), winston.format.json() ); // Create transports array const transports = [ // Console transport for development new winston.transports.Console({ level: level(), format: logFormat, }), // Daily rotate file for all logs new DailyRotateFile({ filename: 'logs/application-%DATE%.log', datePattern: 'YYYY-MM-DD', maxSize: '20m', maxFiles: '14d', level: 'info', format: jsonFormat, }), // Daily rotate file for error logs only new DailyRotateFile({ filename: 'logs/error-%DATE%.log', datePattern: 'YYYY-MM-DD', maxSize: '20m', maxFiles: '14d', level: 'error', format: jsonFormat, }), ]; // Create the logger const logger = winston.createLogger({ level: level(), levels: logLevels, format: jsonFormat, transports, exceptionHandlers: [ new winston.transports.File({ filename: 'logs/exceptions.log' }) ], rejectionHandlers: [ new winston.transports.File({ filename: 'logs/rejections.log' }) ], }); // Create a stream object for Morgan logger.stream = { write: (message) => { // Remove trailing newline from Morgan and log as http level logger.http(message.trim()); }, }; // Add request ID correlation function logger.withRequestId = (requestId) => { return { error: (message, meta = {}) => logger.error(message, { ...meta, requestId }), warn: (message, meta = {}) => logger.warn(message, { ...meta, requestId }), info: (message, meta = {}) => logger.info(message, { ...meta, requestId }), http: (message, meta = {}) => logger.http(message, { ...meta, requestId }), debug: (message, meta = {}) => logger.debug(message, { ...meta, requestId }), }; }; // Add sanitization helper for sensitive data logger.sanitize = (data) => { if (typeof data !== 'object' || data === null) { return data; } const sensitiveFields = [ 'password', 'token', 'secret', 'key', 'authorization', 'cookie', 'ssn', 'credit_card', 'cvv', 'pin', 'account_number' ]; const sanitized = JSON.parse(JSON.stringify(data)); const sanitizeObject = (obj) => { for (const [key, value] of Object.entries(obj)) { const lowerKey = key.toLowerCase(); if (sensitiveFields.some(field => lowerKey.includes(field))) { obj[key] = '[REDACTED]'; } else if (typeof value === 'object' && value !== null) { sanitizeObject(value); } } }; sanitizeObject(sanitized); return sanitized; }; module.exports = logger;