UNPKG

ps-chronicle

Version:

eGain PS logging wrapper utility on Winston

419 lines 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PsChronicleLogger = exports.LogFormat = exports.LogLevel = void 0; const winston_1 = require("winston"); /** * Supported log levels. */ var LogLevel; (function (LogLevel) { LogLevel["ERROR"] = "error"; LogLevel["WS_PAYLOAD"] = "wspayload"; LogLevel["WARN"] = "warn"; LogLevel["INFO"] = "info"; LogLevel["DEBUG"] = "debug"; })(LogLevel || (exports.LogLevel = LogLevel = {})); /** * Supported log output formats. */ var LogFormat; (function (LogFormat) { LogFormat["JSON"] = "json"; LogFormat["SIMPLE"] = "simple"; })(LogFormat || (exports.LogFormat = LogFormat = {})); /** * PsChronicleLogger: Extensible Winston logger wrapper. */ class PsChronicleLogger { /** * Convert bytes to a human-readable string (e.g., MB, GB). * @param bytes Number of bytes * @returns Human-readable string */ static formatBytes(bytes) { // If the input is 0 bytes, return '0 B' if (bytes === 0) return '0 B'; // 1 kilobyte = 1024 bytes const k = 1024; // Array of size units const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; // Determine which size unit to use const i = Math.floor(Math.log(bytes) / Math.log(k)); // Convert bytes to the appropriate unit and format to 2 decimal places return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Create a new logger instance. * @param options Logger configuration options */ constructor(options = {}) { /** * Method name. */ this.methodName = ""; /** * Default log level. */ this.defaultLogLevel = LogLevel.DEBUG; /** * List of sensitive keys to redact from log metadata (case-insensitive). */ this.sensitiveKeys = ['password', 'token', 'secret', 'apikey', 'authorization']; /** * Set for fast sensitive key lookup (case-insensitive). */ this.sensitiveKeySet = new Set(['password', 'token', 'secret', 'apikey', 'authorization']); /** * Enable colorized console output. */ this.colorize = false; /** * String to use for redacted sensitive fields. */ this.redactionString = '***'; /** * Add details to the log entry. */ this.addDetailsFormat = (0, winston_1.format)((info) => { const formatObj = Object.create(info); if (info.level) { formatObj.level = info.level; } if (this.fileName) { formatObj.fileName = this.fileName; } if (this.getMethodName()) { formatObj.methodName = this.getMethodName(); } if (PsChronicleLogger.globalCustomerName) { formatObj.customerName = PsChronicleLogger.globalCustomerName; } if (info.message) { formatObj.message = info.message; } if (PsChronicleLogger.globalRequestId) { formatObj.requestId = PsChronicleLogger.globalRequestId; } if (info.timestamp) { formatObj.timestamp = info.timestamp; } if (info.xadditionalInfo) { formatObj.xadditionalInfo = info.xadditionalInfo; } return formatObj; }); this.fileName = options.fileName; this.defaultLogLevel = options.logLevel || LogLevel.DEBUG; this.outputFormat = options.format || LogFormat.JSON; this.colorize = !!options.colorize; if (options.sensitiveKeys) { // Merge custom keys with default, case-insensitive, no duplicates const lowerDefaults = this.sensitiveKeys.map(k => k.toLowerCase()); const lowerCustom = options.sensitiveKeys.map(k => k.toLowerCase()); this.sensitiveKeys = Array.from(new Set([...lowerDefaults, ...lowerCustom])); this.sensitiveKeySet = new Set(this.sensitiveKeys); } else { this.sensitiveKeySet = new Set(this.sensitiveKeys.map(k => k.toLowerCase())); } if (options.redactionString) { this.redactionString = options.redactionString; } let transportFormat; if (this.outputFormat === LogFormat.SIMPLE) { transportFormat = winston_1.format.combine(this.addDetailsFormat(), ...(this.colorize ? [winston_1.format.colorize()] : []), winston_1.format.simple()); } else { if (this.colorize) { // Warn if colorize is set but format is not SIMPLE // eslint-disable-next-line no-console console.warn('[PsChronicleLogger] colorize option is only effective with LogFormat.SIMPLE. Ignoring colorize for non-simple formats.'); } transportFormat = winston_1.format.combine(this.addDetailsFormat(), winston_1.format.json()); } this.logger = (0, winston_1.createLogger)({ levels: { [LogLevel.ERROR]: 0, [LogLevel.WS_PAYLOAD]: 1, [LogLevel.WARN]: 2, [LogLevel.INFO]: 3, [LogLevel.DEBUG]: 4, }, format: winston_1.format.combine(winston_1.format.errors({ stack: true }), winston_1.format.timestamp({ format: () => new Date().toISOString() }), winston_1.format.metadata({ key: 'xadditionalInfo', fillExcept: ['message', 'level', 'timestamp', 'label'], })), transports: options.transports && options.transports.length > 0 ? options.transports : [ new winston_1.transports.Console({ level: this.defaultLogLevel, handleExceptions: true, format: transportFormat, }) ], exitOnError: false, }); } /** * Recursively redacts sensitive fields in an object. * Key comparison is case-insensitive. * @param obj The object to redact * @returns A new object with sensitive fields redacted (using this.redactionString) */ redactSensitiveData(obj) { if (Array.isArray(obj)) { return obj.map(item => this.redactSensitiveData(item)); } else if (obj && typeof obj === 'object') { const redacted = {}; for (const key of Object.keys(obj)) { if (this.sensitiveKeySet.has(key.toLowerCase())) { redacted[key] = this.redactionString; } else { redacted[key] = this.redactSensitiveData(obj[key]); } } return redacted; } return obj; } /** * Set the method name for the next log. */ setMethodName(methodName) { this.methodName = methodName; } /** * Get the current method name. */ getMethodName() { return this.methodName; } /** * Serialize an Error object to a structured object with name, message, stack, status, code, and primitive custom fields. * Nested objects/arrays are summarized as '[Object]' or '[Array]' to avoid logging large or sensitive data. * @param err The error object to serialize * @returns A plain object with error details */ serializeError(err) { if (err instanceof Error) { const errorObj = { name: err.name, message: err.message, stack: err.stack, }; if (Object.hasOwn(err, 'status')) { errorObj.status = err.status; } if (Object.hasOwn(err, 'code')) { errorObj.code = err.code; } // Only include primitive custom fields for (const key of Object.keys(err)) { if (!Object.hasOwn(errorObj, key)) { const value = err[key]; if (value === null || value === undefined || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { errorObj[key] = value; } else if (Array.isArray(value)) { errorObj[key] = '[Array]'; } else if (typeof value === 'object') { errorObj[key] = '[Object]'; } } } return errorObj; } return err; } /** * Recursively serialize Error objects in metadata to structured objects. * @param obj The metadata object * @returns The metadata with all Error objects serialized */ serializeErrorsInMeta(obj) { if (Array.isArray(obj)) { return obj.map(item => this.serializeErrorsInMeta(item)); } else if (obj && typeof obj === 'object') { if (obj instanceof Error) { return this.serializeError(obj); } const result = {}; for (const key of Object.keys(obj)) { result[key] = this.serializeErrorsInMeta(obj[key]); } return result; } return obj; } /** * Log a message with the given level and metadata. * Sensitive fields in metadata will be redacted. * Error objects in metadata will be serialized to structured objects (name, message, stack, status, ...). * @param level Log level * @param message Log message * @param xadditionalInfo Additional metadata objects */ log(level, message, ...xadditionalInfo) { try { if (!Object.values(LogLevel).includes(level)) { console.warn(`[PsChronicleLogger] Invalid log level '${level}', defaulting to 'info'.`); level = LogLevel.INFO; } let meta = {}; if (xadditionalInfo.length === 1) { const metaArg = xadditionalInfo[0]; if (metaArg && typeof metaArg === 'object' && !Array.isArray(metaArg)) { meta = metaArg; } else { meta = { '0': [metaArg] }; } } else if (xadditionalInfo.length > 1) { // If multiple non-object values, log as array under '0' const nonObjects = xadditionalInfo.filter(m => typeof m !== 'object' || m === null || Array.isArray(m)); const objects = xadditionalInfo.filter(m => m && typeof m === 'object' && !Array.isArray(m)); if (objects.length > 0) { meta = Object.assign({}, ...objects); if (nonObjects.length > 0) { meta['0'] = nonObjects; } } else { meta = { '0': nonObjects }; } } const metaWithErrors = this.serializeErrorsInMeta(meta); const redactedMeta = this.redactSensitiveData(metaWithErrors); this.logger.log(level, message, redactedMeta); } catch (err) { console.error('[PsChronicleLogger] Logging failed:', err); } } /** * Wait for the logger to finish processing logs (useful for async shutdown). */ async waitForLogger() { const loggerDone = new Promise(resolve => this.logger.on('finish', resolve)); this.logger.end(); return loggerDone; } /** * Start a timer for performance measurement. * @returns The current timestamp in milliseconds. */ startTimer() { return Date.now(); } /** * Log the duration of an operation. * @param operation Name of the operation * @param startTime Timestamp from startTimer() * @param extraMeta Additional metadata to include * @returns The duration in seconds */ logPerformance(operation, startTime, extraMeta = {}) { const durationInSeconds = (Date.now() - startTime) / 1000; this.log(LogLevel.INFO, `Performance: ${operation}`, { "Duration in seconds": durationInSeconds, ...extraMeta }); return durationInSeconds; } /** * Measure and log the duration of an async function. * Logs duration and errors if thrown. * @param operation Name of the operation * @param fn Async function to measure * @param extraMeta Additional metadata to include */ async measurePerformance(operation, fn, extraMeta = {}) { const start = Date.now(); try { const result = await fn(); const durationInSeconds = (Date.now() - start) / 1000; this.log(LogLevel.INFO, `Performance: ${operation}`, { "Duration in seconds": durationInSeconds, ...extraMeta }); return result; } catch (err) { const durationInSeconds = (Date.now() - start) / 1000; this.log(LogLevel.ERROR, `Performance (error): ${operation}`, { "Duration in seconds": durationInSeconds, error: err, ...extraMeta }); throw err; } } /** * Log current memory usage in a human-readable format. * @param label Optional label for the log entry */ logMemoryUsage(label = 'MemoryUsage') { const memory = process.memoryUsage(); const memoryUsage = {}; for (const key in memory) { if (Object.hasOwn(memory, key)) { memoryUsage[key] = PsChronicleLogger.formatBytes(memory[key]); } } this.log(LogLevel.INFO, label, { "Memory usage": memoryUsage }); } /** * Dynamically set the log level for this logger instance. * Updates all transports and the defaultLogLevel field. * @param level The new log level (must be a valid LogLevel) */ setLogLevel(level) { if (!Object.values(LogLevel).includes(level)) { console.warn(`[PsChronicleLogger] Invalid log level '${level}', keeping previous level '${this.defaultLogLevel}'.`); return; } this.defaultLogLevel = level; this.logger.transports.forEach((t) => { t.level = level; }); } /** * Get the current log level for this logger instance. * @returns The current log level */ getLogLevel() { return this.defaultLogLevel; } /** * Check if a log level is enabled for this logger instance. * Use before doing expensive work for logs. * @param level Log level to check * @returns true if enabled, false otherwise */ isLevelEnabled(level) { return this.logger.isLevelEnabled(level); } /** * Set the global customer name for all logger instances. */ setCustomerName(customerName) { PsChronicleLogger.globalCustomerName = customerName; } /** * Get the global customer name for all logger instances. */ getCustomerName() { return PsChronicleLogger.globalCustomerName; } /** * Set the global request ID for all logger instances. */ setRequestId(requestId) { PsChronicleLogger.globalRequestId = requestId; } /** * Get the global request ID for all logger instances. */ getRequestId() { return PsChronicleLogger.globalRequestId; } } exports.PsChronicleLogger = PsChronicleLogger; //# sourceMappingURL=index.js.map