Package Exports
- ps-chronicle
Readme
ps-chronicle
A robust, extensible logger for Node.js, built on top of 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
npm install ps-chronicleDual 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)
// In Node.js or legacy projects
const { PsChronicleLogger, LogLevel, LogFormat } = require('ps-chronicle');ESM (import)
// 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
const { PsChronicleLogger, LogLevel, LogFormat } = require('ps-chronicle');or (ESM):
import { PsChronicleLogger, LogLevel, LogFormat } from 'ps-chronicle';2. Initialize your logger
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
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 nameNote: Global context (customerName, requestId) is set via instance methods. These values are shared across all logger instances.
4. Log messages
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:
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:
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)
const start = logger.startTimer();
// ... your code ...
logger.logPerformance('DB query', start);Timing Async Functions (Automatic)
await logger.measurePerformance('fetchData', async () => {
await fetchData();
});Logging Memory Usage
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:
try {
// ...
} catch (err) {
logger.log(LogLevel.ERROR, 'API call failed', { error: err });
}Output:
{
"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:
logger.setLogLevel(LogLevel.ERROR); // Only log errors and above from now on
logger.setLogLevel(LogLevel.DEBUG); // Log everything from debug and aboveSensitive 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:
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:
{
"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
redactionStringoption is optional. If not provided, the default is***. - The
timestampfield is always in ISO 8601 (24-hour) format.
API
Constructor
new PsChronicleLogger(options)options.fileName(string, optional): File name to include in logsoptions.logLevel(LogLevel, optional): Minimum log level (default: DEBUG)options.format(LogFormat, optional): Log output format (default: JSON)options.transports(array, optional): Custom Winston transportsoptions.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 measurementlogPerformance(operation: string, startTime: number, extraMeta?: object)— Log the duration of an operationmeasurePerformance(operation: string, fn: () => Promise<T>, extraMeta?: object)— Measure and log the duration of an async functionlogMemoryUsage(label?: string)— Log current memory usagesetLogLevel(level: LogLevel)— Dynamically change the log level for this logger instancegetLogLevel(): LogLevel— Get the current log levelisLevelEnabled(level: LogLevel): boolean— Check if a log level is enabledgetMethodName(): string— Get the current method namegetCustomerName(): string | undefined— Get the global customer namegetRequestId(): string | undefined— Get the global request ID
Enums
LogLevel:ERROR,WS_PAYLOAD,WARN,INFO,DEBUGLogFormat: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:
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:
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: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] }underxadditionalInfo: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] }underxadditionalInfo: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
0field underxadditionalInfo: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