UNPKG

@treasurenet/logging-middleware

Version:

A lightweight Express.js middleware for structured logging, request ID tracing, and response logging with log4js.

209 lines (191 loc) 5.6 kB
const log4js = require('log4js'); const requestContext = require('./requestContext'); const { v4: uuidv4 } = require('uuid'); const path = require('path'); require('dotenv').config(); const logLevel = process.env.LOG_LEVEL || 'info'; const isProd = process.env.NODE_ENV === 'production'; // Register custom JSON layout for file-based logging log4js.addLayout('json', () => { return (logEvent) => { return JSON.stringify({ timestamp: new Date(logEvent.startTime).toISOString(), level: logEvent.level.levelStr, category: logEvent.categoryName, requestId: requestContext.get('reqId') || 'N/A', data: logEvent.data, }); }; }); const logDir = path.join(process.cwd(), 'logs'); const fs = require('fs'); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } // Configure log4js appenders and categories log4js.configure({ appenders: { console: { type: 'stdout', layout: { type: 'pattern', pattern: '%[[%d] [%p] [%c] [id:%X{id}]%] %[%m%]', // Colored and context-aware output }, }, file: { type: 'dateFile', filename: path.join(logDir, 'app.log'), pattern: 'yyyy-MM-dd', // Rotate daily keepFileExt: true, numBackups: 14, compress: true, layout: { type: 'json' }, }, errorFile: { type: 'file', filename: path.join(logDir, 'error.log'), layout: { type: 'json' }, }, access: { type: 'dateFile', filename: path.join(logDir, 'access.log'), pattern: 'yyyy-MM-dd', keepFileExt: true, numBackups: 14, compress: true, layout: { type: 'json' }, }, 'logLevelFilter-error': { type: 'logLevelFilter', level: 'error', appender: 'errorFile', }, }, categories: { default: { appenders: isProd ? ['file', 'logLevelFilter-error'] : ['console', 'file', 'logLevelFilter-error'], level: logLevel, }, access: { appenders: isProd ? ['access'] : ['console', 'access'], // Output access logs to console in dev level: logLevel, }, api: { appenders: isProd ? ['file', 'logLevelFilter-error'] : ['console', 'file', 'logLevelFilter-error'], level: logLevel, }, sys: { appenders: isProd ? ['file', 'logLevelFilter-error'] : ['console', 'file', 'logLevelFilter-error'], level: logLevel, }, }, }); // Get loggers for specific categories const logger = log4js.getLogger('api'); const loggerAccess = log4js.getLogger('access'); const loggerSys = log4js.getLogger('sys'); /** * Middleware for logging requests and responses with UUID tracing */ const requestLogger = () => { return async (req, res, next) => { const reqId = uuidv4(); // Unique request ID req.id = reqId; const start = Date.now(); // Sanitize sensitive fields (e.g., password, token) const sanitize = (obj) => { const clone = typeof obj === 'object' && obj !== null ? { ...obj } : obj; ['password', 'token', 'authorization'].forEach((key) => { if (clone && typeof clone === 'object' && key in clone) { clone[key] = '***'; } }); return clone; }; requestContext.run(reqId, () => { // Inject request ID into logging context logger.addContext('id', reqId); loggerSys.addContext('id', reqId); loggerAccess.addContext('id', reqId); // Log incoming request loggerAccess.info({ type: 'request', id: reqId, method: req.method, url: req.originalUrl, headers: sanitize(req.headers), query: req.query, body: sanitize(req.body), }); // Intercept response to log output body const originalSend = res.send.bind(res); res.send = (body) => { const duration = Date.now() - start; loggerAccess.info({ type: 'response', id: reqId, status: res.statusCode, duration, body: sanitize(body), }); return originalSend(body); }; res.on('finish', () => { // Clean up request context after response ends logger.removeContext('id'); loggerSys.removeContext('id'); loggerAccess.removeContext('id'); }); next(); }); }; }; /** * Centralized error handler middleware * Logs error with trace ID and responds with 500 status */ const errorHandler = (err, req, res, next) => { const reqId = requestContext.get('reqId') || req.id || 'N/A'; logger.error({ type: 'error', id: reqId, message: err.message, stack: err.stack, }); res.status(500).json({ error: 'Internal Server Error', id: reqId }); }; /** * Middleware to handle unmatched routes (404) * Logs request as warning and returns standardized not found response */ const notFoundHandler = (req, res, next) => { const reqId = requestContext.get('reqId') || req.id || 'N/A'; logger.warn({ type: '404_not_found', id: reqId, method: req.method, url: req.originalUrl, message: 'Resource not found', }); res.status(404).json({ error: 'Not Found', id: reqId, }); }; // Export all loggers and middlewares module.exports = { logger, // For general API logging loggerSys, // For internal system logging requestLogger, // Middleware to log request/response lifecycle errorHandler, // Global error handler middleware notFoundHandler,// Middleware to catch unmatched routes (404) loggerAccess, };