express-smart-logger-pro
Version:
Professional Express logging middleware with advanced features and zero configuration
283 lines (237 loc) • 9.05 kB
JavaScript
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;