UNPKG

@vitaly-yosef/node-smart-logger

Version:

Universal logger for Node.js applications with support for both ESM and CommonJS. It provides advanced features, such as structured logging in JSON format, integration with AWS CloudWatch Logs, and contextual logging.

369 lines 15 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.shutdown = exports.generateLoggerTraceId = exports.clearLoggerContext = exports.setLoggerContext = exports.getLoggerContext = exports.createErrorLogger = exports.createHttpLogger = exports.createLogger = void 0; const winston = __importStar(require("winston")); const morgan_1 = __importDefault(require("morgan")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const os_1 = __importDefault(require("os")); const async_hooks_1 = require("async_hooks"); require("winston-cloudwatch"); const uuid_1 = require("uuid"); const validation_1 = require("./validation"); const graceful_shutdown_1 = require("./graceful-shutdown"); // Creating AsyncLocalStorage for storing the logging context const asyncLocalStorage = new async_hooks_1.AsyncLocalStorage(); const levels = { critical: 0, alert: 1, error: 2, warn: 3, info: 4, http: 5, debug: 6, }; const colors = { critical: 'red', alert: 'red', error: 'magenta', warn: 'yellow', info: 'green', http: 'cyan', debug: 'blue', }; winston.addColors(colors); const level = () => { const env = process.env.NODE_ENV || 'development'; return env === 'development' ? 'debug' : 'info'; }; /** * Generates a unique identifier for tracking requests * @returns {string} Unique identifier */ function generateTraceId() { return (0, uuid_1.v4)(); } /** * Sets the context for the current request or operation. * @param {LoggerContext} context - Object with context data. */ function setContext(context) { // Validate and sanitize context data const validatedContext = (0, validation_1.validateLoggerContext)(context); const currentContext = asyncLocalStorage.getStore() || {}; asyncLocalStorage.enterWith({ ...currentContext, ...validatedContext }); } /** * Gets the current logging context. * @returns {LoggerContext} Current logging context */ function getContext() { return asyncLocalStorage.getStore() || {}; } /** * Clears the context for the current request */ function clearContext() { asyncLocalStorage.enterWith({}); } /** * Sets the log format based on an environment variable * @returns {string} 'text' OR 'json' */ const logFormat = () => { // By default, use text format for development and json for production. const defaultFormat = process.env.NODE_ENV === 'development' ? 'text' : 'json'; // but it possible to customize that via LOG_FORMAT variable return process.env.LOG_FORMAT || defaultFormat; }; /** * @param {string} service * @param {string|null} customLogDir * @param {LoggerOptions} options - Additional options for the logger * @returns {ExtendedLogger} */ function createLoggerFunction(service, customLogDir = null, options = {}) { // Validate service name const validatedServiceName = (0, validation_1.validateServiceName)(service); if (!service) { service = 'default'; console.warn('Logger service name not provided, using "default".'); } const env = process.env.NODE_ENV || 'development'; const hostname = os_1.default.hostname(); let baseDir; if (customLogDir && path_1.default.isAbsolute(customLogDir)) { baseDir = customLogDir; } else if (customLogDir) { baseDir = path_1.default.resolve(process.cwd(), customLogDir); } else { baseDir = path_1.default.resolve(process.cwd(), 'logs'); } const logDir = path_1.default.join(baseDir, service); if (!fs_1.default.existsSync(logDir)) { try { fs_1.default.mkdirSync(logDir, { recursive: true }); } catch (err) { console.error(`Failed to create log directory ${logDir}:`, err); } } const consoleFormat = logFormat() === 'text' ? winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.colorize({ all: true }), winston.format.printf((info) => { const context = getContext(); const traceId = context.traceId || '-'; const requestId = context.requestId || '-'; // Sanitize message to prevent log injection const sanitizedMessage = (0, validation_1.sanitizeForLogging)(info.message); return `${info.timestamp} [${service}] ${info.level} [${traceId}] [${requestId}]: ${sanitizedMessage}${info.stack ? '\n' + info.stack : ''}`; })) : winston.format.json(); const fileFormat = logFormat() === 'text' ? winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.printf((info) => { const context = getContext(); const traceId = context.traceId || '-'; const requestId = context.requestId || '-'; const operationId = context.operationId || '-'; const deviceId = context.deviceId || '-'; const userId = context.userId || '-'; let contextStr = `[trace:${traceId}]`; if (operationId !== '-') contextStr += ` [op:${operationId}]`; if (deviceId !== '-') contextStr += ` [device:${deviceId}]`; if (userId !== '-') contextStr += ` [user:${userId}]`; // Sanitize message to prevent log injection const sanitizedMessage = (0, validation_1.sanitizeForLogging)(info.message); return `${info.timestamp} [${info.service}] ${info.level.toUpperCase()} ${contextStr}: ${sanitizedMessage}${info.stack ? '\n' + info.stack : ''}`; })) : winston.format.json(); const cloudwatchFormat = winston.format.combine(winston.format.timestamp(), winston.format.json()); const addMetadata = winston.format((info) => { const context = getContext(); info.service = service; info.hostname = hostname; info.environment = env; if (context.traceId) info.traceId = context.traceId; if (context.requestId) info.requestId = context.requestId; if (context.userId) info.userId = context.userId; if (context.deviceId) info.deviceId = context.deviceId; // Sanitize metadata to prevent log injection if (info.message) { info.message = (0, validation_1.sanitizeForLogging)(info.message); } return info; }); const transports = [ new winston.transports.Console({ format: consoleFormat, }), ]; // Adding file transports transports.push(new winston.transports.File({ filename: path_1.default.join(logDir, 'error.log'), level: 'error', format: winston.format.combine(addMetadata(), fileFormat), maxsize: 10 * 1024 * 1024, // 10MB maxFiles: 5, tailable: true, zippedArchive: true })); transports.push(new winston.transports.File({ filename: path_1.default.join(logDir, 'combined.log'), format: winston.format.combine(addMetadata(), fileFormat), maxsize: 10 * 1024 * 1024, // 10MB maxFiles: 5, tailable: true, zippedArchive: true })); // Add CloudWatch transport if configured if (process.env.AWS_CLOUDWATCH_ENABLED === 'true') { try { transports.push(new winston.transports.CloudWatch({ logGroupName: process.env.AWS_CLOUDWATCH_GROUP || `IoTMonSys-${service}`, logStreamName: process.env.AWS_CLOUDWATCH_STREAM || `${hostname}-${new Date().toISOString().slice(0, 10)}`, awsRegion: process.env.AWS_REGION || 'us-east-1', messageFormatter: ({ level, message, ...meta }) => { return JSON.stringify({ level, message, ...meta, timestamp: new Date().toISOString() }); }, format: winston.format.combine(addMetadata(), cloudwatchFormat), })); } catch (err) { console.error('Failed to initialize CloudWatch transport:', err); } } const logger = winston.createLogger({ level: level(), levels, format: winston.format.combine(winston.format.errors({ stack: true }), addMetadata()), transports, exitOnError: false, }); // Extending the logger with methods for working with context logger.setContext = setContext; logger.getContext = getContext; logger.clearContext = clearContext; logger.generateTraceId = generateTraceId; // Adding a convenient method for creating an operational context logger.withOperationContext = function (contextData = {}, callback) { // Validate and sanitize context data const validatedContext = (0, validation_1.validateLoggerContext)(contextData); const operationId = validatedContext.operationId || (0, uuid_1.v4)(); const previousContext = getContext(); let callbackResult = undefined; // Run the callback within the new context asyncLocalStorage.run({ ...previousContext, ...validatedContext, operationId }, () => { if (callback) { try { callbackResult = callback(); } finally { // Restore the previous context after callback execution asyncLocalStorage.enterWith(previousContext); } } }); return callbackResult ?? operationId; }; return logger; } /** * Creates middleware for logging HTTP requests. * Using Morgan for logging HTTP-requests, sends output to the provided Winston-logger. * @param {ExtendedLogger} loggerInstance - Instance of Winston logger, built by createLogger. * @param {HttpLoggerOptions} [options={}] - Options. * @returns {Function} - Morgan middleware. */ function createHttpLoggerMiddleware(loggerInstance, options = {}) { const env = process.env.NODE_ENV || 'development'; const defaultFormat = env === 'development' ? 'dev' : 'combined'; const format = options.format || defaultFormat; const logOnlyAuthErrors = options.logOnlyAuthErrors || false; const skipLogging = options.skipLogging || false; if (skipLogging) { return (req, res, next) => next(); } // Creating middleware to add traceId and requestId to the request const traceMiddleware = (req, res, next) => { // Sanitize headers to prevent log injection const traceId = (0, validation_1.sanitizeForLogging)(req.headers['x-trace-id']) || loggerInstance.generateTraceId(); const requestId = (0, validation_1.sanitizeForLogging)(req.headers['x-request-id']) || loggerInstance.generateTraceId(); // Run the rest of the middleware chain within the context asyncLocalStorage.run({ traceId, requestId }, () => { res.setHeader('X-Trace-ID', traceId); res.setHeader('X-Request-ID', requestId); res.on('finish', () => { clearContext(); }); next(); }); }; const morganOptions = { stream: { write: (message) => { const level = logOnlyAuthErrors ? 'warn' : 'http'; loggerInstance[level](message.trim()); }, }, }; if (logOnlyAuthErrors && env === 'production') { morganOptions.skip = (req, res) => res.statusCode !== 401 && res.statusCode !== 403; } else if (logOnlyAuthErrors && env !== 'production') { return (req, res, next) => next(); } // Combining middleware for tracing with morgan return (req, res, next) => { traceMiddleware(req, res, () => { (0, morgan_1.default)(format, morganOptions)(req, res, next); }); }; } /** * Creates middleware for error handling and logging * @param {ExtendedLogger} loggerInstance - Instance of Winston logger * @returns {Function} - Error handling middleware */ function createErrorLoggerMiddleware(loggerInstance) { return (err, req, res, next) => { const context = loggerInstance.getContext(); const traceId = context.traceId || '-'; const requestId = context.requestId || '-'; loggerInstance.error(`Error processing request: ${err.message}`, { error: err.stack, url: req.originalUrl, method: req.method, body: req.body, params: req.params, query: req.query, traceId, requestId }); next(err); }; } const loggerLibrary = { createLogger: createLoggerFunction, createHttpLoggerMiddleware: createHttpLoggerMiddleware, createErrorLoggerMiddleware: createErrorLoggerMiddleware, setContext, getContext, clearContext, generateTraceId, shutdownLogger: graceful_shutdown_1.shutdownLogger }; exports.createLogger = loggerLibrary.createLogger; exports.createHttpLogger = loggerLibrary.createHttpLoggerMiddleware; exports.createErrorLogger = loggerLibrary.createErrorLoggerMiddleware; exports.getLoggerContext = loggerLibrary.getContext; exports.setLoggerContext = loggerLibrary.setContext; exports.clearLoggerContext = loggerLibrary.clearContext; exports.generateLoggerTraceId = loggerLibrary.generateTraceId; exports.shutdown = loggerLibrary.shutdownLogger; exports.default = loggerLibrary; //# sourceMappingURL=index.js.map