const winston = require('winston'); const path = require('path'); const fs = require('fs'); // Ensure logs directory exists const logsDir = path.join(__dirname, '../../logs'); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true, mode: 0o755 }); } /** * Custom format to sanitize sensitive data from logs * Removes passwords, tokens, secrets, and other sensitive information */ const sanitizeFormat = winston.format((info) => { // Convert info to string for pattern matching const infoStr = JSON.stringify(info); // Patterns to redact const sensitivePatterns = [ { pattern: /"password"\s*:\s*"[^"]*"/gi, replacement: '"password":"[REDACTED]"' }, { pattern: /"token"\s*:\s*"[^"]*"/gi, replacement: '"token":"[REDACTED]"' }, { pattern: /"secret"\s*:\s*"[^"]*"/gi, replacement: '"secret":"[REDACTED]"' }, { pattern: /"apiKey"\s*:\s*"[^"]*"/gi, replacement: '"apiKey":"[REDACTED]"' }, { pattern: /"api_key"\s*:\s*"[^"]*"/gi, replacement: '"api_key":"[REDACTED]"' }, { pattern: /"authorization"\s*:\s*"Bearer\s+[^"]*"/gi, replacement: '"authorization":"Bearer [REDACTED]"' }, { pattern: /"privateKey"\s*:\s*"[^"]*"/gi, replacement: '"privateKey":"[REDACTED]"' }, { pattern: /"private_key"\s*:\s*"[^"]*"/gi, replacement: '"private_key":"[REDACTED]"' } ]; let sanitized = infoStr; sensitivePatterns.forEach(({ pattern, replacement }) => { sanitized = sanitized.replace(pattern, replacement); }); try { return JSON.parse(sanitized); } catch (e) { return info; // Return original if parsing fails } }); /** * Production format: Structured JSON logs without sensitive data */ const productionFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), sanitizeFormat(), winston.format.json() ); /** * Development format: Human-readable with colors */ const developmentFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), sanitizeFormat(), winston.format.colorize(), winston.format.printf(({ timestamp, level, message, ...meta }) => { let msg = `${timestamp} [${level}]: ${message}`; if (Object.keys(meta).length > 0) { msg += ` ${JSON.stringify(meta, null, 2)}`; } return msg; }) ); const isProduction = process.env.NODE_ENV === 'production'; const logger = winston.createLogger({ level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'), format: productionFormat, defaultMeta: { service: 'streamflow-iptv' }, transports: [ // Error logs - separate file for errors only new winston.transports.File({ filename: path.join(logsDir, 'error.log'), level: 'error', maxsize: 5242880, // 5MB maxFiles: 5, tailable: true }), // Combined logs - all levels new winston.transports.File({ filename: path.join(logsDir, 'combined.log'), maxsize: 5242880, // 5MB maxFiles: 5, tailable: true }) ], // Don't exit on uncaught exceptions exitOnError: false }); // Console transport for development if (!isProduction) { logger.add(new winston.transports.Console({ format: developmentFormat, handleExceptions: true })); } // Security audit log helper logger.security = (action, details) => { logger.info('SECURITY_EVENT', { action, timestamp: new Date().toISOString(), ...details }); }; // Performance monitoring helper logger.performance = (operation, duration, details = {}) => { logger.info('PERFORMANCE', { operation, duration_ms: duration, timestamp: new Date().toISOString(), ...details }); }; module.exports = logger;