ps-chronicle
Version:
eGain PS logging wrapper utility on Winston
419 lines • 15.8 kB
JavaScript
;
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