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