UNPKG

express-smart-logger-pro

Version:

Professional Express logging middleware with advanced features and zero configuration

283 lines (237 loc) 9.05 kB
const crypto = require('crypto'); // Sensitive fields to auto-mask const DEFAULT_SENSITIVE_FIELDS = ['password', 'token', 'secret', 'key', 'auth', 'authorization']; // Performance tracking const performanceStats = new Map(); const requestCorrelation = new Map(); // Generate short trace ID function generateTraceId() { return crypto.randomBytes(3).toString('hex'); } // Generate correlation ID for related requests function generateCorrelationId() { return crypto.randomBytes(4).toString('hex'); } // Mask sensitive data recursively function maskSensitiveData(obj, sensitiveFields) { if (typeof obj !== 'object' || obj === null) return obj; if (Array.isArray(obj)) { return obj.map(item => maskSensitiveData(item, sensitiveFields)); } const masked = {}; for (const [key, value] of Object.entries(obj)) { const lowerKey = key.toLowerCase(); if (sensitiveFields.some(field => lowerKey.includes(field))) { // Developer-friendly masking with asterisks const strValue = String(value); masked[key] = '*'.repeat(Math.min(strValue.length, 8)) + (strValue.length > 8 ? '...' : ''); } else if (typeof value === 'object' && value !== null) { masked[key] = maskSensitiveData(value, sensitiveFields); } else { masked[key] = value; } } return masked; } // Format duration function formatDuration(start) { const duration = Date.now() - start; return duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(2)}s`; } // Track performance trends function updatePerformanceStats(url, duration, status) { if (!performanceStats.has(url)) { performanceStats.set(url, { count: 0, totalTime: 0, avgTime: 0, minTime: Infinity, maxTime: 0, errorCount: 0, lastUpdated: Date.now() }); } const stats = performanceStats.get(url); stats.count++; stats.totalTime += duration; stats.avgTime = stats.totalTime / stats.count; stats.minTime = Math.min(stats.minTime, duration); stats.maxTime = Math.max(stats.maxTime, duration); if (status >= 400) { stats.errorCount++; } stats.lastUpdated = Date.now(); } // Enhanced error context function captureErrorContext(req, error) { return { traceId: req.traceId, correlationId: req.correlationId, url: req.url, method: req.method, headers: req.headers, body: req.body, timestamp: new Date().toISOString(), error: { message: error.message, stack: error.stack, name: error.name } }; } // Colored console output for dev function logDev(traceId, method, url, status, duration, response, requestBody, headers, logHeaders, requestSize, responseSize, logSize, correlationId, performanceInsights) { const statusColor = status >= 400 ? '\x1b[31m' : status >= 300 ? '\x1b[33m' : '\x1b[32m'; const reset = '\x1b[0m'; console.log(`[TRACE-${traceId}] ${method} ${url} ${statusColor}${status}${reset} (${duration})`); if (correlationId) { console.log(`Correlation ID: ${correlationId}`); } // Log sizes if enabled if (logSize) { if (requestSize > 0) console.log(`Request Size: ${requestSize} bytes`); if (responseSize > 0) console.log(`Response Size: ${responseSize} bytes`); } // Log request body if enabled if (requestBody && Object.keys(requestBody).length > 0) { console.log('Request Body:', JSON.stringify(requestBody, null, 2)); } // Log headers if enabled if (logHeaders && headers) { const userAgent = headers['user-agent'] || 'Unknown'; const ip = headers['x-forwarded-for'] || headers['x-real-ip'] || 'Unknown'; console.log(`User-Agent: ${userAgent}`); console.log(`IP: ${ip}`); } // Performance insights if (performanceInsights) { console.log(`Performance: Avg: ${performanceInsights.avgTime}ms, Min: ${performanceInsights.minTime}ms, Max: ${performanceInsights.maxTime}ms`); } // Log response if (response && Object.keys(response).length > 0) { console.log('Response:', JSON.stringify(response, null, 2)); } } // JSON output for prod function logProd(traceId, method, url, status, duration, response, requestBody, headers, logHeaders, requestSize, responseSize, logSize, correlationId, performanceInsights) { const logData = { traceId, timestamp: new Date().toISOString(), method, url, status, duration: parseInt(duration), response }; if (correlationId) { logData.correlationId = correlationId; } // Add sizes if enabled if (logSize) { if (requestSize > 0) logData.requestSize = requestSize; if (responseSize > 0) logData.responseSize = responseSize; } // Add request body if enabled if (requestBody && Object.keys(requestBody).length > 0) { logData.requestBody = requestBody; } // Add header info if enabled if (logHeaders && headers) { logData.userAgent = headers['user-agent'] || 'Unknown'; logData.ip = headers['x-forwarded-for'] || headers['x-real-ip'] || 'Unknown'; } // Add performance insights if (performanceInsights) { logData.performance = performanceInsights; } console.log(JSON.stringify(logData)); } // Main middleware function function smartLogger(options = {}) { const { mask = [], level = 'default', slowThreshold = 1000, logBody = false, logHeaders = false, logSize = false, enableCorrelation = true, enablePerformanceInsights = true, enableErrorContext = true } = options; const sensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...mask]; const isDev = process.env.NODE_ENV !== 'production'; return function(req, res, next) { const start = Date.now(); const traceId = generateTraceId(); // Add trace ID to request req.traceId = traceId; // Add correlation ID for related requests if (enableCorrelation) { const correlationId = req.headers['x-correlation-id'] || generateCorrelationId(); req.correlationId = correlationId; res.setHeader('x-correlation-id', correlationId); } // Capture request body for logging let requestBody = null; let requestSize = 0; if (logBody && req.body) { requestBody = maskSensitiveData(req.body, sensitiveFields); requestSize = JSON.stringify(req.body).length; } // Capture response const originalSend = res.send; let responseBody = null; let responseSize = 0; res.send = function(body) { responseBody = body; responseSize = typeof body === 'string' ? body.length : JSON.stringify(body).length; originalSend.call(this, body); }; // Enhanced error handling if (enableErrorContext) { const originalError = res.locals.error; res.locals.error = function(error) { const errorContext = captureErrorContext(req, error); console.error('Error Context:', JSON.stringify(errorContext, null, 2)); if (originalError) originalError.call(this, error); }; } // Log when response finishes res.on('finish', () => { const duration = Date.now() - start; const isSlow = duration > slowThreshold; // Update performance stats if (enablePerformanceInsights) { updatePerformanceStats(req.url, duration, res.statusCode); } // Get performance insights const performanceInsights = enablePerformanceInsights ? performanceStats.get(req.url) : null; // Mask sensitive data in response let maskedResponse = null; if (responseBody) { try { const parsed = typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody; maskedResponse = maskSensitiveData(parsed, sensitiveFields); } catch { maskedResponse = responseBody; } } // Log based on environment if (isDev) { logDev(traceId, req.method, req.url, res.statusCode, duration, maskedResponse, requestBody, req.headers, logHeaders, requestSize, responseSize, logSize, req.correlationId, performanceInsights); if (isSlow) { console.log(`\x1b[33m⚠️ Slow request detected: ${duration}\x1b[0m`); } } else { logProd(traceId, req.method, req.url, res.statusCode, duration, maskedResponse, requestBody, req.headers, logHeaders, requestSize, responseSize, logSize, req.correlationId, performanceInsights); } }); next(); }; } // Export additional utilities for advanced usage smartLogger.getPerformanceStats = () => Object.fromEntries(performanceStats); smartLogger.clearPerformanceStats = () => performanceStats.clear(); smartLogger.getRequestCorrelation = () => Object.fromEntries(requestCorrelation); module.exports = smartLogger;