@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
JavaScript
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,
};