UNPKG

ps-chronicle

Version:

eGain PS logging wrapper utility on Winston

371 lines (278 loc) 12.8 kB
# ps-chronicle A robust, extensible logger for Node.js, built on top of [Winston](https://github.com/winstonjs/winston). Designed for eGain PS and multi-tenant applications, with support for JSON and simple log formats, custom log levels, and per-request context. ## Features - **Easy-to-use API** with TypeScript support - **Custom log levels:** error, wspayload, warn, info, debug - **Log formats:** JSON (default) or simple - **Per-request context:** requestId, customerName, methodName, etc. - **Extensible:** add your own Winston transports - **Timezone:** All timestamps are in GMT - **Timestamp format:** Timestamps are in ISO 8601 (24-hour) format (e.g., `2024-06-01T17:45:23.123Z`) - **Sensitive data redaction:** Automatically redacts sensitive fields (e.g., password, token, secret, apiKey, authorization) from log output (case-insensitive, and custom keys are merged with defaults). The redaction string is configurable (default: ***) - **Performance metrics logging:** Easily log operation durations and memory usage - **Colorized console output:** Colorize log levels in the console for easier reading (dev/debug). **Note:** Only applies to `LogFormat.SIMPLE`. - **Structured error logging:** Error objects are logged with name, message, stack, status, code, and primitive custom fields; large/nested fields are summarized - **Dynamic log level adjustment:** Change log level at runtime with `setLogLevel()` --- ## Requirements - Node.js **16.0.0 or later** is required (uses Object.hasOwn and other modern features) ## Installation ```sh npm install ps-chronicle ``` --- ## Dual ESM & CommonJS Usage This package supports both CommonJS (`require`) and ESM (`import`) usage out of the box. Node.js and modern bundlers will automatically use the correct build for your environment. ### CommonJS (require) ```js // In Node.js or legacy projects const { PsChronicleLogger, LogLevel, LogFormat } = require('ps-chronicle'); ``` ### ESM (import) ```js // In ESM projects or with "type": "module" import { PsChronicleLogger, LogLevel, LogFormat } from 'ps-chronicle'; ``` - The correct build (CJS or ESM) is chosen automatically by Node.js and bundlers. - All features and types are available in both modes. --- ## Usage ### 1. Import the logger and enums ```js const { PsChronicleLogger, LogLevel, LogFormat } = require('ps-chronicle'); ``` or (ESM): ```js import { PsChronicleLogger, LogLevel, LogFormat } from 'ps-chronicle'; ``` ### 2. Initialize your logger ```js const logger = new PsChronicleLogger({ fileName: 'myfile.js', // Optional: will be included in log output logLevel: LogLevel.INFO, // Optional: default is LogLevel.DEBUG format: LogFormat.JSON, // Optional: 'json' (default) or 'simple' sensitiveKeys: ['password', 'token', 'secret', 'apiKey', 'authorization'], // Optional: customize redacted fields (merged with defaults, case-insensitive) colorize: true, // Optional: enable colorized console output (only applies to LogFormat.SIMPLE) redactionString: '***' // Optional: string to use for redacted fields (default: ***) }); // Note: If you use colorize: true with LogFormat.JSON, colorization will be ignored and a warning will be shown. ``` ### 3. Set per-request or per-operation context ```js logger.setRequestId(context.awsRequestId); // e.g., from AWS Lambda context.awsRequestId logger.setCustomerName('TMXXXX'); // Set the customer/tenant name logger.setMethodName('myFunction()'); // Set the current method name ``` > **Note:** Global context (customerName, requestId) is set via instance methods. These values are shared across all logger instances. ### 4. Log messages ```js logger.log(LogLevel.INFO, 'Informational message', { foo: 'bar' }); logger.log(LogLevel.ERROR, 'Error occurred', { error: new Error('Something went wrong') }); ``` ### 5. Wait for logger (for async/await environments) If your function is async (e.g., AWS Lambda), ensure all logs are flushed before exit: ```js await logger.waitForLogger(); ``` --- ## Colorized Console Output You can enable colorized log output in the console for easier reading during development or debugging. This is especially useful with `LogFormat.SIMPLE`. **Note:** The `colorize` option only applies to `LogFormat.SIMPLE`. If you use `colorize: true` with `LogFormat.JSON`, colorization will be ignored and a warning will be shown. **Example:** ```js const logger = new PsChronicleLogger({ fileName: 'color-demo.js', logLevel: LogLevel.DEBUG, format: LogFormat.SIMPLE, // Use SIMPLE for colorized output colorize: true }); logger.log(LogLevel.INFO, 'This is an info message'); logger.log(LogLevel.WARN, 'This is a warning'); logger.log(LogLevel.ERROR, 'This is an error'); logger.log(LogLevel.DEBUG, 'This is a debug message'); ``` - Colorization only affects console output, not file or JSON logs. - Each log level is shown in a different color for quick visual scanning. - If you use `LogFormat.JSON`, colorization is ignored. --- ## Performance Metrics Logging You can easily log operation durations and memory usage to monitor and optimize your application's performance. ### Timing Operations (Manual) ```js const start = logger.startTimer(); // ... your code ... logger.logPerformance('DB query', start); ``` ### Timing Async Functions (Automatic) ```js await logger.measurePerformance('fetchData', async () => { await fetchData(); }); ``` ### Logging Memory Usage ```js logger.logMemoryUsage('After processing'); ``` **Use cases:** - Measure how long a function or operation takes - Automatically log duration and errors for async functions - Log memory usage at any point in your code --- ## Structured Error Logging When you log an error object, the logger will automatically serialize it to include only the most relevant fields: - `name`, `message`, `stack`, `status`, `code`, and any primitive custom fields - Large or deeply nested fields (objects/arrays) are summarized as `[Object]` or `[Array]` **Example:** ```js try { // ... } catch (err) { logger.log(LogLevel.ERROR, 'API call failed', { error: err }); } ``` **Output:** ```json { "level": "error", "message": "API call failed", "xadditionalInfo": { "error": { "name": "Error", "message": "Something went wrong", "stack": "...", "status": 500, "code": "E_API_FAIL", "details": "[Object]" } }, "timestamp": "2024-06-01T17:45:23.123Z" } ``` --- ## Dynamic Log Level Adjustment You can change the log level at runtime for a logger instance: ```js logger.setLogLevel(LogLevel.ERROR); // Only log errors and above from now on logger.setLogLevel(LogLevel.DEBUG); // Log everything from debug and above ``` --- ## Sensitive Data Redaction You can automatically redact sensitive fields (such as `password`, `token`, `secret`, `apiKey`, `authorization`) from your log output. By default, these fields are redacted as `***`, but you can customize the string using the `redactionString` option in the constructor. **Custom keys are merged with the defaults, and redaction is case-insensitive.** **Example:** ```js const logger = new PsChronicleLogger({ fileName: 'example2.js', logLevel: LogLevel.INFO, format: LogFormat.JSON, sensitiveKeys: ['Authorization', 'PASSWORD'], // Custom keys (case-insensitive, merged with defaults) redactionString: '***' // Optional: string to use for redacted fields (default: ***) }); logger.log(LogLevel.INFO, 'Logging user data', { username: 'alice', password: 'supersecret', token: 'abc123', Authorization: 'Bearer xyz', profile: { apiKey: 'my-api-key', nested: { secret: 'hidden', PASSWORD: 'another' } } }); ``` **Output:** ```json { "level": "info", "message": "Logging user data", "xadditionalInfo": { "username": "alice", "password": "***", "token": "***", "Authorization": "***", "profile": { "apiKey": "***", "nested": { "secret": "***", "PASSWORD": "***" } } }, "timestamp": "2024-06-01T17:45:23.123Z" } ``` - The `redactionString` option is optional. If not provided, the default is `***`. - The `timestamp` field is always in ISO 8601 (24-hour) format. --- ## API ### Constructor ```js new PsChronicleLogger(options) ``` - `options.fileName` (string, optional): File name to include in logs - `options.logLevel` (LogLevel, optional): Minimum log level (default: DEBUG) - `options.format` (LogFormat, optional): Log output format (default: JSON) - `options.transports` (array, optional): Custom Winston transports - `options.sensitiveKeys` (array, optional): List of sensitive keys to redact from log output (merged with defaults, case-insensitive) - `options.colorize` (boolean, optional): Enable colorized console output (for development/debugging) - `options.redactionString` (string, optional): String to use for redacted sensitive fields (default: ***) ### Methods - `setRequestId(requestId: string)` - `setCustomerName(customerName: string)` - `setMethodName(methodName: string)` - `log(level: LogLevel, message: string, ...meta: object[])` - `waitForLogger(): Promise<void>` - `startTimer(): number` Start a timer for performance measurement - `logPerformance(operation: string, startTime: number, extraMeta?: object)` Log the duration of an operation - `measurePerformance(operation: string, fn: () => Promise<T>, extraMeta?: object)` Measure and log the duration of an async function - `logMemoryUsage(label?: string)` Log current memory usage - `setLogLevel(level: LogLevel)` Dynamically change the log level for this logger instance - `getLogLevel(): LogLevel` Get the current log level - `isLevelEnabled(level: LogLevel): boolean` Check if a log level is enabled - `getMethodName(): string` Get the current method name - `getCustomerName(): string | undefined` Get the global customer name - `getRequestId(): string | undefined` Get the global request ID ### Enums - `LogLevel`: `ERROR`, `WS_PAYLOAD`, `WARN`, `INFO`, `DEBUG` - `LogFormat`: `JSON`, `SIMPLE` --- ## Deferring Expensive Work If you have expensive computations for log metadata, you can avoid unnecessary work by checking if a log level is enabled before building the log message. Use the `isLevelEnabled(level)` method: ```js if (logger.isLevelEnabled(LogLevel.DEBUG)) { logger.log(LogLevel.DEBUG, 'Debug info', expensiveMeta()); } ``` This ensures that expensive work is only performed if the log will actually be emitted at the current log level. --- ## Global Context: customerName and requestId You can set `customerName` and `requestId` globally for all logger instances using the instance methods: ```js logger.setCustomerName('AcmeCorp'); logger.setRequestId('req-123'); ``` - These values will be included in all logs from any logger instance. - You only need to set them once (e.g., at app startup or per request). - The instance methods update global/static values shared by all logger instances. --- ## Logging Metadata: Objects, Strings, and More The `log` method always places additional metadata under the `xadditionalInfo` key. The structure depends on what you pass: - If you pass a single object, its fields are included under `xadditionalInfo`: ```js logger.log(LogLevel.INFO, 'User info', { userId: 123, name: 'Alice' }); // { ..., "xadditionalInfo": { "userId": 123, "name": "Alice" } } ``` - If you pass a single string, number, or boolean, it is logged as `{ '0': [value] }` under `xadditionalInfo`: ```js logger.log(LogLevel.INFO, 'Document retrieved', 'Document retrieved successfully'); // { ..., "xadditionalInfo": { "0": ["Document retrieved successfully"] } } ``` - If you pass multiple non-object values, they are logged as `{ '0': [array of values] }` under `xadditionalInfo`: ```js logger.log(LogLevel.INFO, 'IDs', 'a', 'b', 'c'); // { ..., "xadditionalInfo": { "0": ["a", "b", "c"] } } ``` - If you pass both objects and non-objects, the objects are merged and the non-objects are included in a `0` field under `xadditionalInfo`: ```js logger.log(LogLevel.INFO, 'User info', { userId: 123 }, 'extra1', 42); // { ..., "xadditionalInfo": { "userId": 123, "0": ["extra1", 42] } } ``` This makes it easy to log any kind of metadata without worrying about how to wrap your values. --- ## License ISC