syntropylog
Version:
An instance manager with observability for Node.js applications
185 lines • 7.02 kB
JavaScript
/**
* @file src/logger/Logger.ts
* @description The core implementation of the ILogger interface.
*/
import * as util from 'node:util';
import { LOG_LEVEL_WEIGHTS } from './levels';
/**
* @class Logger
* @description The core logger implementation. It orchestrates the entire logging
* pipeline, from argument parsing and level checking to serialization, masking,
* and dispatching to transports.
*/
export class Logger {
level;
name;
transports;
bindings;
dependencies;
constructor(name, transports, dependencies, options = {}) {
this.name = name;
this.transports = transports;
this.dependencies = dependencies;
this.bindings = options.bindings ?? {};
this.level = options.level ?? 'info';
}
/**
* @private
* The core asynchronous logging method that runs the full processing pipeline.
* It handles argument parsing, level filtering, serialization, masking,
* and finally dispatches the processed log entry to the appropriate transports.
* @param {LogLevel} level - The severity level of the log message.
* @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to be logged, following the Pino-like signature (e.g., `(obj, msg, ...)` or `(msg, ...)`).
* @returns {Promise<void>}
*/
async _log(level, ...args) {
if (level === 'silent') {
return;
}
// Type-guarded access to weights
const weightedLevel = level;
const weightedThisLevel = this.level;
if (LOG_LEVEL_WEIGHTS[weightedLevel] < LOG_LEVEL_WEIGHTS[weightedThisLevel]) {
return;
}
// Build the base log entry with context and bindings
const context = this.dependencies.contextManager.getFilteredContext(level);
const logEntry = {
...context,
...this.bindings,
level,
timestamp: new Date().toISOString(),
service: this.name,
message: '', // Will be set below
};
// Parse arguments following Pino-like signature
let message;
let metadata = {};
if (args.length === 0) {
message = '';
}
else if (typeof args[0] === 'object' &&
args[0] !== null &&
!Array.isArray(args[0])) {
// First argument is metadata object: (metadata, message, ...formatArgs)
metadata = args[0];
message = args[1] || '';
const formatArgs = args.slice(2);
if (message && formatArgs.length > 0) {
message = util.format(message, ...formatArgs);
}
}
else {
// First argument is message: (message, ...formatArgs)
message = args[0] || '';
const formatArgs = args.slice(1);
if (message && formatArgs.length > 0) {
message = util.format(message, ...formatArgs);
}
}
// Ensure message is never undefined
logEntry.message = message || '';
// Merge metadata into log entry
Object.assign(logEntry, metadata);
// 1. Apply custom serializers (e.g., for Error objects)
const finalEntry = await this.dependencies.serializerRegistry.process(logEntry, this);
// 2. Apply masking to the entire, serialized entry.
const maskedEntry = this.dependencies.maskingEngine.process(finalEntry);
// Dispatch to transports
await Promise.all(this.transports.map((transport) => {
if (transport.isLevelEnabled(level)) {
// The type assertion is safe here because the masking engine preserves the structure.
return transport.log(maskedEntry);
}
return Promise.resolve();
}));
}
/**
* Logs a message at the 'info' level.
* @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
*/
info(...args) {
return this._log('info', ...args);
}
/**
* Logs a message at the 'warn' level.
* @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
*/
warn(...args) {
return this._log('warn', ...args);
}
/**
* Logs a message at the 'error' level.
* @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
*/
error(...args) {
return this._log('error', ...args);
}
/**
* Logs a message at the 'debug' level.
* @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
*/
debug(...args) {
return this._log('debug', ...args);
}
/**
* Logs a message at the 'trace' level.
* @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
*/
trace(...args) {
return this._log('trace', ...args);
}
/**
* Logs a message at the 'fatal' level.
* @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
*/
fatal(...args) {
return this._log('fatal', ...args);
}
/**
* Dynamically updates the minimum log level for this logger instance.
* Any messages with a severity lower than the new level will be ignored.
* @param {LogLevel} level - The new minimum log level.
*/
setLevel(level) {
this.level = level;
}
/**
* Creates a new child logger instance that inherits the parent's configuration
* and adds the specified bindings.
* @param {LogBindings} bindings - Key-value pairs to bind to the child logger.
* @returns {ILogger} A new logger instance with the specified bindings.
*/
child(bindings) {
const childLogger = new Logger(this.name, this.transports, this.dependencies, {
level: this.level,
bindings: { ...this.bindings, ...bindings },
});
return childLogger;
}
/**
* Creates a new logger instance with a `source` field bound to it.
* @param {string} source - The name of the source (e.g., 'redis', 'AuthModule').
* @returns {ILogger} A new logger instance with the `source` binding.
*/
withSource(source) {
return this.child({ source });
}
/**
* Creates a new logger instance with a `retention` field bound to it.
* @param {LogRetentionRules} rules - A JSON object containing the retention rules.
* @returns {ILogger} A new logger instance with the `retention` binding.
*/
withRetention(rules) {
return this.child({ retention: rules });
}
/**
* Creates a new logger instance with a `transactionId` field bound to it.
* @param {string} transactionId - The unique ID of the transaction.
* @returns {ILogger} A new logger instance with the `transactionId` binding.
*/
withTransactionId(transactionId) {
return this.child({ transactionId });
}
}
//# sourceMappingURL=Logger.js.map