syntropylog
Version:
An instance manager with observability for Node.js applications
169 lines • 7.78 kB
JavaScript
import { Logger } from './Logger';
import { MaskingEngine } from '../masking/MaskingEngine';
import { SerializerRegistry } from '../serialization/SerializerRegistry';
import { ConsoleTransport } from './transports/ConsoleTransport';
import { SanitizationEngine } from '../sanitization/SanitizationEngine';
/**
* @class LoggerFactory
* @description Manages the lifecycle and configuration of all logging components.
* An instance of this factory is created by `syntropyLog.init()` and acts as the central
* orchestrator for creating and managing logger instances and their dependencies.
*/
export class LoggerFactory {
/** @private The manager for handling asynchronous contexts. */
contextManager;
/** @private The main framework instance, used as a mediator. */
syntropyLogInstance;
/** @private The array of transports to which logs will be dispatched. */
transports;
/** @private The global minimum log level for all created loggers. */
globalLogLevel;
/** @private The global service name, used as a default for loggers. */
serviceName;
/** @private The engine responsible for serializing complex objects. */
serializerRegistry;
/** @private The engine responsible for masking sensitive data. */
maskingEngine;
/** @private A pool to cache logger instances by name for performance. */
loggerPool = new Map();
/**
* @constructor
* @param {SyntropyLogConfig} config - The global configuration object.
* @param {IContextManager} contextManager - The shared context manager instance.
* @param {SyntropyLog} syntropyLogInstance - The main framework instance for mediation.
* @description Initializes all core logging engines and orchestrates transport setup.
* It follows a key principle for transport configuration:
* - **If `config.logger.transports` is provided:** The factory trusts the user's
* configuration completely and uses the provided transports as-is. It is the user's
* responsibility to configure them correctly (e.g., adding sanitization).
* - **If no transports are provided:** The factory creates a single, production-safe
* `ConsoleTransport` by default, which includes a built-in `SanitizationEngine`.
*/
constructor(config, contextManager, syntropyLogInstance) {
this.contextManager = contextManager;
this.syntropyLogInstance = syntropyLogInstance;
// Configure the context manager by passing the entire context config object.
if (config.context) {
this.contextManager.configure(config.context);
}
// Configure the HTTP manager if http instances are provided
if (config.http?.instances) {
// This block is not provided in the original file, so it's commented out.
// If it were uncommented, it would likely involve configuring the HTTP manager
// with the provided instances.
}
// If the user provides a specific list of transports, we use them directly.
// We trust the user to have configured them correctly (e.g., providing a
// SanitizationEngine to their production transports).
if (config.logger?.transports) {
this.transports = config.logger.transports;
}
else {
// If no transports are provided, we create a safe, default production transport.
// This transport includes a default sanitization engine.
const sanitizationEngine = new SanitizationEngine();
this.transports = [new ConsoleTransport({ sanitizationEngine })];
}
this.globalLogLevel = config.logger?.level ?? 'info';
this.serviceName = config.logger?.serviceName ?? 'unknown-service';
this.serializerRegistry = new SerializerRegistry({
serializers: config.logger?.serializers,
timeoutMs: config.logger?.serializerTimeoutMs,
});
this.maskingEngine = new MaskingEngine({
rules: config.masking?.rules,
maskChar: config.masking?.maskChar,
preserveLength: config.masking?.preserveLength,
enableDefaultRules: config.masking?.enableDefaultRules !== false,
});
}
/**
* Retrieves a logger instance by name. If the logger does not exist, it is created
* and cached for subsequent calls.
* @param {string} [name='default'] - The name of the logger to retrieve.
* @param {Record<string, JsonValue>} [bindings] - Optional bindings to apply to the logger.
* @returns {ILogger} The logger instance.
*/
getLogger(name = 'default', bindings) {
// Create a stable cache key that doesn't depend on object reference
const cacheKey = this.createCacheKey(name, bindings);
if (this.loggerPool.has(cacheKey)) {
return this.loggerPool.get(cacheKey);
}
const loggerName = name === 'default' ? this.serviceName : name;
const dependencies = {
contextManager: this.contextManager,
serializerRegistry: this.serializerRegistry,
maskingEngine: this.maskingEngine,
syntropyLogInstance: this.syntropyLogInstance,
};
const logger = new Logger(loggerName, this.transports, dependencies, {
bindings,
});
logger.level = this.globalLogLevel;
this.loggerPool.set(cacheKey, logger);
return logger;
}
/**
* Creates a stable cache key for logger instances.
* @private
*/
createCacheKey(name, bindings) {
if (!bindings || Object.keys(bindings).length === 0) {
return name;
}
// Sort keys to ensure consistent cache keys regardless of property order
const sortedBindings = Object.keys(bindings)
.sort()
.reduce((result, key) => {
result[key] = bindings[key];
return result;
}, {});
try {
return `${name}:${JSON.stringify(sortedBindings)}`;
}
catch {
// Fallback for non-serializable objects
return `${name}:${Object.keys(sortedBindings).sort().join(',')}`;
}
}
/**
* Calls the `flush` method on all configured transports to ensure buffered
* logs are written before the application exits.
*/
async flushAllTransports() {
const flushPromises = this.transports.map((transport) => transport.flush().catch((err) => {
console.error(`Error flushing transport ${transport.constructor.name}:`, err);
}));
await Promise.allSettled(flushPromises);
}
/**
* Shuts down the logger factory and all its transports.
* This ensures that all buffered logs are written and resources are cleaned up.
*/
async shutdown() {
try {
// Flush all transports first
await this.flushAllTransports();
// Clear the logger pool
this.loggerPool.clear();
// Shutdown all transports if they have a shutdown method
const shutdownPromises = this.transports.map((transport) => {
if (typeof transport.shutdown ===
'function') {
return transport
.shutdown()
.catch((err) => {
console.error(`Error shutting down transport ${transport.constructor.name}:`, err);
});
}
return Promise.resolve();
});
await Promise.allSettled(shutdownPromises);
}
catch (error) {
console.error('Error during LoggerFactory shutdown:', error);
}
}
}
//# sourceMappingURL=LoggerFactory.js.map