ps-chronicle
Version:
eGain PS logging wrapper utility on Winston
371 lines (278 loc) • 12.8 kB
Markdown
# 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