UNPKG

lever-ui-logger

Version:

Zero-dependency logging library with optional EventBus integration. Built-in PII redaction, multiple transports, and comprehensive logging capabilities.

1,768 lines (1,758 loc) 122 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { BUILT_IN_PATTERNS: () => BUILT_IN_PATTERNS, BaseTransport: () => BaseTransport, BrowserStyles: () => BrowserStyles, Colors: () => Colors, ConsoleTransport: () => ConsoleTransport, DEFAULT_LOGGER_CONFIG: () => DEFAULT_LOGGER_CONFIG, DEFAULT_LOG_LEVEL: () => DEFAULT_LOG_LEVEL, Environment: () => Environment, ErrorEvent: () => ErrorEvent, ErrorMessageSanitizer: () => ErrorMessageSanitizer, EventBusTransport: () => EventBusTransport, Formatters: () => Formatters, LOGGER_VERSION: () => LOGGER_VERSION, LOG_LEVEL_PRIORITY: () => LOG_LEVEL_PRIORITY, LogEvent: () => LogEvent, LoggerBaseEvent: () => LoggerBaseEvent, LoggerConfigChangedEvent: () => LoggerConfigChangedEvent, LoggerCreatedEvent: () => LoggerCreatedEvent, LoggerDestroyedEvent: () => LoggerDestroyedEvent, LoggerImpl: () => LoggerImpl, MetricEvent: () => MetricEvent, PIIWarningEvent: () => PIIWarningEvent, PII_FIELD_NAMES: () => PII_FIELD_NAMES, RedactionEngine: () => RedactionEngine, SecureTokenHandler: () => SecureTokenHandler, SendBeaconTransport: () => SendBeaconTransport, TRANSPORTS_VERSION: () => TRANSPORTS_VERSION, TransportErrorEvent: () => TransportErrorEvent, TransportEvent: () => TransportEvent, TransportMiddleware: () => TransportMiddleware, addMetadata: () => addMetadata, batchCompressionMiddleware: () => batchCompressionMiddleware, compressString: () => compressString, compressionMiddleware: () => compressionMiddleware, createConsoleTransport: () => createConsoleTransport, createEventBusTransport: () => createEventBusTransport, createLogger: () => createLogger, createSendBeaconTransport: () => createSendBeaconTransport, defaultErrorSanitizer: () => defaultErrorSanitizer, defaultSecureTokenHandler: () => defaultSecureTokenHandler, enrichErrors: () => enrichErrors, filterByLevel: () => filterByLevel, getEnabledPatterns: () => getEnabledPatterns, getRedactionEngine: () => getRedactionEngine, isCompressionSupported: () => isCompressionSupported, isErrorEvent: () => isErrorEvent, isLogEvent: () => isLogEvent, isLoggerEvent: () => isLoggerEvent, isMetricEvent: () => isMetricEvent, isPIIFieldName: () => isPIIFieldName, mergeConfig: () => mergeConfig, passesSampling: () => passesSampling, rateLimit: () => rateLimit, redactArgs: () => redactArgs, redactObject: () => redactObject, redactString: () => redactString, sample: () => sample, shouldLog: () => shouldLog, sortPatternsByPriority: () => sortPatternsByPriority, transformEvent: () => transformEvent }); module.exports = __toCommonJS(src_exports); // src/logger/events.ts var LoggerBaseEvent = class { /** * Creates a new logger base event. * * @param timestamp - Unix timestamp when event was created (defaults to Date.now()) * @param clientId - Unique client identifier for this browser session (auto-generated) */ constructor(timestamp = Date.now(), clientId = generateClientId()) { this.timestamp = timestamp; this.clientId = clientId; } }; var LogEvent = class extends LoggerBaseEvent { /** * Creates a new log event. * * @param level - Log level (trace, debug, info, warn, error) * @param message - Primary log message * @param context - Structured context data object * @param args - Additional arguments passed to the log call * @param component - Component or module name that generated the log * @param logger - Logger instance name * @param timestamp - Optional custom timestamp (defaults to current time) * @param clientId - Optional custom client ID (defaults to generated ID) */ constructor(level, message, context, args, component, logger, timestamp, clientId) { super(timestamp, clientId); this.level = level; this.message = message; this.context = context; this.args = args; this.component = component; this.logger = logger; } /** * Converts this event to the LogEventData format used by transports. * * @returns LogEventData object ready for transport processing * @example * ```typescript * const event = new LogEvent('info', 'test', {}, [], 'comp', 'logger'); * const transportData = event.toLogEventData(); * consoleTransport.write(transportData); * ``` */ toLogEventData() { return { level: this.level, message: this.message, timestamp: this.timestamp, context: this.context, args: this.args, component: this.component, logger: this.logger }; } }; var MetricEvent = class extends LoggerBaseEvent { /** * Creates a new metric event. * * @param name - Metric name (e.g., 'page_load_time', 'button_click') * @param fields - Metric data and measurements * @param context - Additional context for the metric * @param component - Component that recorded the metric * @param timestamp - Optional custom timestamp * @param clientId - Optional custom client ID */ constructor(name, fields, context, component, timestamp, clientId) { super(timestamp, clientId); this.name = name; this.fields = fields; this.context = context; this.component = component; } /** * Convert to metric data format */ toMetricData() { return { name: this.name, fields: this.fields, timestamp: this.timestamp, context: this.context, component: this.component }; } }; var ErrorEvent = class extends LoggerBaseEvent { constructor(error, handled, context, component, timestamp, clientId) { super(timestamp, clientId); this.error = error; this.handled = handled; this.context = context; this.component = component; } /** * Get error name (constructor name or custom name) */ get name() { return this.error.name || this.error.constructor.name || "Error"; } /** * Get error message */ get message() { return this.error.message || "Unknown error"; } /** * Get stack trace if available */ get stack() { return this.error.stack; } /** * Convert to error data format */ toErrorData() { return { name: this.name, message: this.message, stack: this.stack, handled: this.handled, timestamp: this.timestamp, context: this.context, component: this.component }; } }; var LoggerCreatedEvent = class extends LoggerBaseEvent { constructor(name, config, timestamp, clientId) { super(timestamp, clientId); this.name = name; this.config = config; } }; var LoggerDestroyedEvent = class extends LoggerBaseEvent { constructor(name, reason, timestamp, clientId) { super(timestamp, clientId); this.name = name; this.reason = reason; } }; var TransportEvent = class extends LoggerBaseEvent { constructor(transportName, operation, details = {}, timestamp, clientId) { super(timestamp, clientId); this.transportName = transportName; this.operation = operation; this.details = details; } }; var TransportErrorEvent = class extends LoggerBaseEvent { constructor(transportName, error, operation, details = {}, timestamp, clientId) { super(timestamp, clientId); this.transportName = transportName; this.error = error; this.operation = operation; this.details = details; } }; var PIIWarningEvent = class extends LoggerBaseEvent { constructor(field, value, pattern, suggestion, context, timestamp, clientId) { super(timestamp, clientId); this.field = field; this.value = value; this.pattern = pattern; this.suggestion = suggestion; this.context = context; } }; var LoggerConfigChangedEvent = class extends LoggerBaseEvent { constructor(loggerName, oldConfig, newConfig, changes, timestamp, clientId) { super(timestamp, clientId); this.loggerName = loggerName; this.oldConfig = oldConfig; this.newConfig = newConfig; this.changes = changes; } }; function generateClientId() { if (typeof crypto !== "undefined" && crypto.randomUUID) { return crypto.randomUUID(); } return "client-" + Math.random().toString(36).substr(2, 9) + "-" + Date.now().toString(36); } function isLogEvent(event) { return event instanceof LogEvent; } function isMetricEvent(event) { return event instanceof MetricEvent; } function isErrorEvent(event) { return event instanceof ErrorEvent; } function isLoggerEvent(event) { return event instanceof LoggerBaseEvent; } // src/logger/logger-config.ts var DEFAULT_LOG_LEVEL = "info"; var LOG_LEVEL_PRIORITY = { trace: 0, debug: 1, info: 2, warn: 3, error: 4 }; var DEFAULT_LOGGER_CONFIG = { level: DEFAULT_LOG_LEVEL, component: "default", defaultContext: {}, sampling: { trace: 1, debug: 1, info: 1, warn: 1, error: 1 }, redaction: { enabled: true, // Enable by default for security patterns: [], mode: "balanced" }, transports: [], captureUnhandledErrors: false, captureUnhandledRejections: false, captureConsoleErrors: false }; function mergeConfig(userConfig = {}) { return { ...DEFAULT_LOGGER_CONFIG, ...userConfig, defaultContext: { ...DEFAULT_LOGGER_CONFIG.defaultContext, ...userConfig.defaultContext }, sampling: { ...DEFAULT_LOGGER_CONFIG.sampling, ...userConfig.sampling }, redaction: { ...DEFAULT_LOGGER_CONFIG.redaction, ...userConfig.redaction, patterns: [ ...DEFAULT_LOGGER_CONFIG.redaction.patterns || [], ...userConfig.redaction?.patterns || [] ] }, transports: userConfig.transports || DEFAULT_LOGGER_CONFIG.transports }; } function shouldLog(level, minLevel) { return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[minLevel]; } function passesSampling(level, samplingRates) { const rate = samplingRates[level] ?? 1; return Math.random() < rate; } // src/logger/redaction-patterns.ts var BUILT_IN_PATTERNS = [ // Email addresses (high priority - very common) { name: "email", pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: "<email>", description: "Email addresses", defaultEnabled: true, priority: "high" }, // Phone numbers (various formats) { name: "phone-us", pattern: /(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b/g, replacement: "<phone>", description: "US phone numbers", defaultEnabled: true, priority: "high" }, // Social Security Numbers { name: "ssn", pattern: /\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, replacement: "<ssn>", description: "Social Security Numbers", defaultEnabled: true, priority: "high" }, // Credit card numbers (basic pattern) { name: "credit-card", pattern: /\b(?:\d{4}[-.\s]?){3}\d{4}\b/g, replacement: "<credit-card>", description: "Credit card numbers", defaultEnabled: true, priority: "medium" }, // IP addresses { name: "ipv4", pattern: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g, replacement: "<ip>", description: "IPv4 addresses", defaultEnabled: false, // Often needed for debugging priority: "low" }, // IPv6 addresses (simplified pattern) { name: "ipv6", pattern: /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g, replacement: "<ipv6>", description: "IPv6 addresses", defaultEnabled: false, priority: "low" }, // URLs with potential PII in query params { name: "url-params", pattern: /([?&](?:email|user|username|phone|ssn|token|key|secret|password|auth)=)[^&\s]+/gi, replacement: "$1<redacted>", description: "URL parameters containing PII", defaultEnabled: true, priority: "medium" }, // JWT tokens (basic pattern) { name: "jwt", pattern: /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9._-]*\.[A-Za-z0-9._-]*\b/g, replacement: "<jwt>", description: "JWT tokens", defaultEnabled: true, priority: "medium" }, // API keys (common patterns) { name: "api-key", pattern: /(?:(?:api[_-]?key|secret[_-]?key)\s*[:=]\s*|access[_-]?token\s+)["']?[A-Za-z0-9_-]{16,}["']?/gi, replacement: "<api-key>", description: "API keys and access tokens", defaultEnabled: true, priority: "high" }, // Generic secrets (high entropy strings) { name: "high-entropy", pattern: /\b[A-Za-z0-9+/]{32,}={0,2}\b/g, replacement: "<secret>", description: "High entropy strings (potential secrets)", defaultEnabled: false, // Can be noisy priority: "low" } ]; var PII_FIELD_NAMES = [ "password", "passwd", "secret", "token", "key", "auth", "authorization", "email", "mail", "phone", "tel", "telephone", "ssn", "social", "credit", "card", "cvv", "pin", "account", "username", "user", "login", "address", "street", "zip", "postal", "dob", "birthdate", "birthday", "age", "gender", "race", "ethnicity", "religion", "sexual", "political", "medical", "health", "diagnosis", "prescription", "insurance", "license", "passport", "visa", "fingerprint", "biometric" ]; function isPIIFieldName(fieldName) { const lowerFieldName = fieldName.toLowerCase(); return PII_FIELD_NAMES.some((piiField) => { if (lowerFieldName === piiField) { return true; } if (lowerFieldName.endsWith("_" + piiField) || lowerFieldName.endsWith("-" + piiField) || lowerFieldName.startsWith(piiField + "_") || lowerFieldName.startsWith(piiField + "-")) { return true; } if (lowerFieldName.includes(piiField + "id") || lowerFieldName.includes(piiField + "name") || lowerFieldName.includes(piiField + "email") || lowerFieldName.includes(piiField + "phone") || lowerFieldName.includes(piiField + "password") || lowerFieldName.includes(piiField + "token") || lowerFieldName.includes(piiField + "key") || lowerFieldName.includes(piiField + "address")) { return true; } if (piiField.length >= 4) { const camelCasePattern = new RegExp(piiField + "[A-Z]", "i"); if (camelCasePattern.test(fieldName)) { return true; } } if (piiField.length >= 5) { const wordBoundary = new RegExp(`\\b${piiField}\\b`, "i"); if (wordBoundary.test(fieldName)) { return true; } } return false; }); } function getEnabledPatterns(enabledPatterns, disabledPatterns) { return BUILT_IN_PATTERNS.filter((pattern) => { if (enabledPatterns && enabledPatterns.length > 0) { return enabledPatterns.includes(pattern.name); } const isDisabled = disabledPatterns?.includes(pattern.name) ?? false; return pattern.defaultEnabled && !isDisabled; }); } function sortPatternsByPriority(patterns) { const priorityOrder = { high: 0, medium: 1, low: 2 }; return [...patterns].sort((a, b) => { const aPriority = priorityOrder[a.priority]; const bPriority = priorityOrder[b.priority]; if (aPriority !== bPriority) { return aPriority - bPriority; } return a.name.localeCompare(b.name); }); } // src/logger/redaction.ts var RedactionEngine = class { // 1ms threshold constructor(config = {}) { this.performanceWarningThreshold = 1; this.config = this.mergeConfig(config); this.patterns = this.initializePatterns(); this.stats = this.initializeStats(); } /** * Redacts PII from a string value using configured patterns and custom redactor. * * @param value - The string to redact PII from * @returns The string with PII replaced by redaction tokens * * @example * ```typescript * const engine = new RedactionEngine(); * engine.redactString('Contact user@example.com for help'); * // Returns: 'Contact <email> for help' * * engine.redactString('Call us at (555) 123-4567'); * // Returns: 'Call us at <phone>' * ``` */ redactString(value) { if (!value || this.config.mode === "off" || typeof value !== "string") { return value; } const startTime = performance.now(); let result = value; for (const pattern of this.patterns) { const matches = result.match(pattern.pattern); if (matches) { result = result.replace(pattern.pattern, pattern.replacement); this.stats.patternHits[pattern.name] = (this.stats.patternHits[pattern.name] || 0) + matches.length; } } if (this.config.customRedactor) { result = this.config.customRedactor(result); } this.updateStats(startTime); return result; } /** * Redacts PII from an object, handling nested structures and circular references. * * @param obj - The object to redact PII from * @param visited - Internal WeakSet for tracking circular references * @returns The object with PII fields and string values redacted * * @example * ```typescript * const engine = new RedactionEngine(); * const user = { * name: 'John Doe', * email: 'john@example.com', * password: 'secret123', * profile: { * phone: '555-1234', * bio: 'Contact me at john@example.com' * } * }; * * engine.redactObject(user); * // Returns: { * // name: 'John Doe', * // email: '<redacted>', // Field name detected * // password: '<redacted>', // Field name detected * // profile: { * // phone: '<redacted>', // Field name detected * // bio: 'Contact me at <email>' // Pattern matched * // } * // } * ``` */ redactObject(obj, visited = /* @__PURE__ */ new WeakSet()) { if (this.config.mode === "off" || obj === null || obj === void 0) { return obj; } if (typeof obj === "string") { return this.redactString(obj); } if (typeof obj !== "object") { return obj; } if (visited.has(obj)) { return "<circular>"; } visited.add(obj); if (Array.isArray(obj)) { const result2 = obj.map((item) => this.redactObject(item, visited)); visited.delete(obj); return result2; } const result = {}; for (const [key, value] of Object.entries(obj)) { if (isPIIFieldName(key)) { result[key] = this.config.hashRedaction ? this.hashValue(value) : "<redacted>"; this.stats.fieldHits[key] = (this.stats.fieldHits[key] || 0) + 1; } else { if (typeof value === "string") { result[key] = this.redactString(value); } else { result[key] = this.redactObject(value, visited); } } } visited.delete(obj); return result; } /** * Redacts PII from log arguments array, processing each argument as an object. * * @param args - Array of log arguments to redact * @returns Array with PII redacted from each argument * * @example * ```typescript * const engine = new RedactionEngine(); * const logArgs = [ * 'User logged in', * { email: 'user@example.com', token: 'abc123' }, * 'Session started at user@example.com' * ]; * * engine.redactArgs(logArgs); * // Returns: [ * // 'User logged in', * // { email: '<redacted>', token: '<redacted>' }, * // 'Session started at <email>' * // ] * ``` */ redactArgs(args) { if (this.config.mode === "off" || !args || args.length === 0) { return args; } return args.map((arg) => this.redactObject(arg, /* @__PURE__ */ new WeakSet())); } /** * Adds Laplace noise to numeric values for differential privacy protection. * * @param value - The numeric value to add noise to * @param epsilon - Privacy budget parameter (default: 1.0, lower = more privacy) * @returns The value with differential privacy noise added * * @example * ```typescript * const engine = new RedactionEngine({ differentialPrivacy: true }); * * // Add noise with default epsilon (1.0) * const noisyValue = engine.addDifferentialPrivacyNoise(100); * // Returns: ~99.83 (varies each call) * * // More privacy (more noise) with lower epsilon * const privateValue = engine.addDifferentialPrivacyNoise(100, 0.1); * // Returns: ~107.42 (varies each call, larger deviation) * ``` */ addDifferentialPrivacyNoise(value, epsilon = 1) { if (this.config.mode === "off" || !this.config.differentialPrivacy) { return value; } const sensitivity = 1; const scale = sensitivity / epsilon; const u = Math.random() - 0.5; const noise = scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)); return value + noise; } /** * Gets current redaction statistics including operation counts and performance metrics. * * @returns Copy of current redaction statistics * * @example * ```typescript * const engine = new RedactionEngine(); * engine.redactString('Contact user@example.com'); * engine.redactString('Call (555) 123-4567'); * * const stats = engine.getStats(); * // Returns: { * // totalOperations: 2, * // totalTimeMs: 0.43, * // averageTimeMs: 0.215, * // patternHits: { email: 1, 'phone-us': 1 }, * // fieldHits: {} * // } * ``` */ getStats() { return { ...this.stats }; } /** * Resets redaction statistics to initial values. * * @example * ```typescript * const engine = new RedactionEngine(); * engine.redactString('user@example.com'); // Creates stats * * console.log(engine.getStats().totalOperations); // 1 * engine.resetStats(); * console.log(engine.getStats().totalOperations); // 0 * ``` */ resetStats() { this.stats.totalOperations = 0; this.stats.totalTimeMs = 0; this.stats.averageTimeMs = 0; this.stats.patternHits = {}; this.stats.fieldHits = {}; } /** * Validates potential PII in development mode, checking against all patterns. * * @param value - String to validate for potential PII * @param context - Optional context description for warnings * @returns Array of warning messages for detected PII patterns * * @example * ```typescript * const engine = new RedactionEngine(); * const warnings = engine.validateForPII( * 'Contact user@example.com or call 555-1234', * 'user input' * ); * * // Returns: [ * // 'Potential Email addresses detected in user input: email', * // 'Potential US phone numbers detected in user input: phone-us' * // ] * ``` */ validateForPII(value, context) { if (this.config.mode === "off" || typeof value !== "string") { return []; } const warnings = []; for (const pattern of BUILT_IN_PATTERNS) { const matches = value.match(pattern.pattern); if (matches) { const contextStr = context ? ` in ${context}` : ""; warnings.push(`Potential ${pattern.description} detected${contextStr}: ${pattern.name}`); } } return warnings; } /** * Merges user configuration with defaults */ mergeConfig(userConfig = {}) { return { enabled: userConfig.enabled ?? true, mode: userConfig.mode ?? "balanced", patterns: userConfig.patterns ?? [], enabledPatterns: userConfig.enabledPatterns, disabledPatterns: userConfig.disabledPatterns, customRedactor: userConfig.customRedactor, hashRedaction: userConfig.hashRedaction ?? false, differentialPrivacy: userConfig.differentialPrivacy ?? false, performanceThreshold: userConfig.performanceThreshold ?? 1 }; } /** * Initializes redaction patterns based on configuration */ initializePatterns() { if (!this.config.enabled || this.config.mode === "off") { return []; } let patterns = getEnabledPatterns( this.config.enabledPatterns, this.config.disabledPatterns ); patterns = this.filterPatternsByMode(patterns); if (this.config.patterns && this.config.patterns.length > 0) { const customPatterns = this.config.patterns.map((pattern, index) => ({ name: pattern.name || `custom-${index}`, pattern: pattern.pattern, replacement: pattern.replacement, description: pattern.description || "Custom pattern", defaultEnabled: pattern.defaultEnabled ?? true, priority: pattern.priority || "medium" })); patterns.push(...customPatterns); } return sortPatternsByPriority(patterns); } /** * Filters patterns based on redaction mode */ filterPatternsByMode(patterns) { switch (this.config.mode) { case "strict": return patterns; case "balanced": return patterns.filter((p) => p.priority !== "low" || p.name === "api-key"); case "permissive": return patterns.filter((p) => p.priority === "high"); case "off": return []; default: return patterns; } } /** * Hash-based redaction for analytics preservation */ hashValue(value) { if (value === null || value === void 0) { return "<redacted>"; } const str = String(value); if (str.length === 0) { return "<redacted>"; } let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } const hashStr = Math.abs(hash).toString(16).padStart(8, "0"); return `<hash:${hashStr}>`; } /** * Updates performance statistics */ updateStats(startTime) { const duration = performance.now() - startTime; this.stats.totalOperations++; this.stats.totalTimeMs += duration; this.stats.averageTimeMs = this.stats.totalTimeMs / this.stats.totalOperations; if (duration > this.performanceWarningThreshold) { if (typeof console !== "undefined") { console.warn(`Lever UI Logger: Redaction operation took ${duration.toFixed(2)}ms, consider optimizing patterns`); } } } /** * Initializes statistics tracking */ initializeStats() { return { totalOperations: 0, totalTimeMs: 0, averageTimeMs: 0, patternHits: {}, fieldHits: {} }; } }; var globalEngine = null; function getRedactionEngine(config) { if (!globalEngine || config) { globalEngine = new RedactionEngine(config); } return globalEngine; } function redactString(value, config) { return getRedactionEngine(config).redactString(value); } function redactObject(obj, config) { return getRedactionEngine(config).redactObject(obj, /* @__PURE__ */ new WeakSet()); } function redactArgs(args, config) { return getRedactionEngine(config).redactArgs(args); } // src/logger/transport-registry.ts var TransportRegistry = class { constructor() { this.transports = []; } /** * Add a transport to the registry * * @param transport - Transport instance to add * @throws {TypeError} If transport is invalid or duplicate name exists * * @example * ```typescript * const registry = new TransportRegistry(); * registry.add(new ConsoleTransport()); * registry.add(new EventBusTransport(eventBus)); * ``` */ add(transport) { if (!transport || typeof transport !== "object") { throw new TypeError("Transport must be a valid object"); } if (typeof transport.write !== "function") { throw new TypeError("Transport must have a write method"); } if (!transport.name || typeof transport.name !== "string") { throw new TypeError("Transport must have a valid name"); } if (this.transports.find((t) => t.name === transport.name)) { throw new Error(`Transport with name '${transport.name}' already exists`); } this.transports.push(transport); } /** * Remove a transport from the registry by name * * @param transportName - Name of transport to remove * @returns True if transport was found and removed, false otherwise * * @example * ```typescript * const removed = registry.remove('console'); * console.log(`Transport removed: ${removed}`); * ``` */ remove(transportName) { if (typeof transportName !== "string" || transportName.length === 0) { throw new TypeError("Transport name must be a non-empty string"); } const index = this.transports.findIndex((t) => t.name === transportName); if (index >= 0) { this.transports.splice(index, 1); return true; } return false; } /** * Write log event data to all registered transports * * Errors from individual transports are isolated and logged to console * to prevent one failing transport from breaking others. * * @param eventData - Log event data to write to all transports * * @example * ```typescript * const eventData = { * level: 'info', * message: 'User logged in', * timestamp: Date.now(), * context: { userId: '123' }, * args: [], * component: 'auth', * logger: 'main' * }; * registry.writeToAll(eventData); * ``` */ writeToAll(eventData) { this.transports.forEach((transport) => { try { const result = transport.write(eventData); if (result && typeof result.catch === "function") { result.catch((error) => { this.handleTransportError(transport.name, "write", error); }); } } catch (error) { this.handleTransportError(transport.name, "write", error); } }); } /** * Flush all registered transports in parallel * * Individual transport flush errors are isolated and logged. * The operation continues even if some transports fail to flush. * * @returns Promise that resolves when all transports have attempted to flush * * @example * ```typescript * await registry.flushAll(); * console.log('All transports flushed'); * ``` */ async flushAll() { const flushPromises = this.transports.map(async (transport) => { try { await Promise.resolve(transport.flush()); } catch (error) { this.handleTransportError(transport.name, "flush", error); } }); await Promise.all(flushPromises); } /** * Close all registered transports in parallel and clear the registry * * Individual transport close errors are isolated and logged. * The registry is cleared regardless of individual transport failures. * * @returns Promise that resolves when all transports have attempted to close * * @example * ```typescript * await registry.closeAll(); * console.log('All transports closed and registry cleared'); * ``` */ async closeAll() { const closePromises = this.transports.map(async (transport) => { try { await Promise.resolve(transport.close()); } catch (error) { this.handleTransportError(transport.name, "close", error); } }); await Promise.all(closePromises); this.transports.length = 0; } /** * Get the list of registered transport names * * @returns Array of transport names currently in the registry * * @example * ```typescript * const names = registry.getTransportNames(); * console.log('Registered transports:', names); * ``` */ getTransportNames() { return this.transports.map((t) => t.name); } /** * Get the number of registered transports * * @returns Number of transports in the registry * * @example * ```typescript * console.log(`Registry has ${registry.size} transports`); * ``` */ get size() { return this.transports.length; } /** * Check if registry has any transports * * @returns True if registry has no transports * * @example * ```typescript * if (registry.isEmpty) { * console.log('No transports registered'); * } * ``` */ get isEmpty() { return this.transports.length === 0; } /** * Get a transport by name * * @param name - Transport name to find * @returns Transport instance or undefined if not found * * @example * ```typescript * const consoleTransport = registry.getTransport('console'); * if (consoleTransport) { * console.log('Found console transport'); * } * ``` */ getTransport(name) { return this.transports.find((t) => t.name === name); } /** * Clear all transports from the registry without closing them * * Use this if you want to remove all transports without calling their close() methods. * For proper cleanup, use closeAll() instead. * * @example * ```typescript * registry.clear(); // Removes all transports without closing them * ``` */ clear() { this.transports.length = 0; } /** * Handle transport operation errors with consistent logging * * @private * @param transportName - Name of the failing transport * @param operation - Operation that failed (write, flush, close) * @param error - Error that occurred */ handleTransportError(transportName, operation, error) { console.error(`Transport ${transportName} ${operation} failed:`, error); } }; // src/logger/logger-configuration.ts var LOG_LEVEL_PRIORITY2 = { trace: 0, debug: 1, info: 2, warn: 3, error: 4 }; var DEFAULT_CONFIG = { level: "info", component: "default", defaultContext: {}, sampling: { trace: 1, debug: 1, info: 1, warn: 1, error: 1 }, redaction: { enabled: true, patterns: [], mode: "balanced" }, transports: [], captureUnhandledErrors: false, captureUnhandledRejections: false, captureConsoleErrors: false }; var LoggerConfiguration = class _LoggerConfiguration { /** * Creates a new configuration manager * * @param userConfig - User-provided configuration */ constructor(userConfig = {}) { this.componentLevels = /* @__PURE__ */ new Map(); this.originalConfig = { ...userConfig }; this.config = this.mergeConfig(userConfig); } /** * Get the current log level */ get level() { return this.config.level; } /** * Get the component name */ get component() { return this.config.component; } /** * Get the default context */ get defaultContext() { return { ...this.config.defaultContext }; } /** * Get the sampling configuration */ get sampling() { return { ...this.config.sampling }; } /** * Get the redaction configuration */ get redaction() { return { ...this.config.redaction, patterns: [...this.config.redaction.patterns || []] }; } /** * Get the transports configuration */ get transports() { return [...this.config.transports]; } /** * Get capture settings */ get captureSettings() { return { unhandledErrors: this.config.captureUnhandledErrors, unhandledRejections: this.config.captureUnhandledRejections, consoleErrors: this.config.captureConsoleErrors }; } /** * Get the full configuration object (frozen copy) */ get fullConfig() { return Object.freeze({ ...this.config }); } /** * Set the minimum log level * * @param level - New log level */ setLevel(level) { if (!(level in LOG_LEVEL_PRIORITY2)) { throw new TypeError(`Invalid log level: ${level}`); } this.config.level = level; } /** * Set log level for a specific component * * @param component - Component name * @param level - Log level for this component */ setComponentLevel(component, level) { if (!component || typeof component !== "string") { throw new TypeError("Component must be a non-empty string"); } if (!(level in LOG_LEVEL_PRIORITY2)) { throw new TypeError(`Invalid log level: ${level}`); } this.componentLevels.set(component, level); } /** * Remove component-specific log level * * @param component - Component name * @returns True if component level was removed */ removeComponentLevel(component) { return this.componentLevels.delete(component); } /** * Get effective log level for a component * * @param component - Component name (optional) * @returns Effective log level */ getEffectiveLevel(component) { if (component && this.componentLevels.has(component)) { return this.componentLevels.get(component); } return this.config.level; } /** * Check if a log should be processed based on level and sampling * * @param level - Log level to check * @param component - Optional component name for component-specific levels * @returns True if log should be processed */ shouldProcess(level, component) { const effectiveLevel = component ? this.getEffectiveLevel(component) : this.getEffectiveLevel(this.config.component); if (!this.shouldLog(level, effectiveLevel)) { return false; } return this.passesSampling(level); } /** * Update sampling rate for a specific level * * @param level - Log level * @param rate - Sampling rate (0-1) */ setSamplingRate(level, rate) { if (rate < 0 || rate > 1) { throw new RangeError("Sampling rate must be between 0 and 1"); } this.config.sampling[level] = rate; } /** * Update default context * * @param context - New context to merge with existing */ updateDefaultContext(context) { this.config.defaultContext = { ...this.config.defaultContext, ...context }; } /** * Clear all component-specific log levels */ clearComponentLevels() { this.componentLevels.clear(); } /** * Get all component-specific log levels */ getComponentLevels() { return new Map(this.componentLevels); } /** * Reset configuration to original values */ reset() { this.config = this.mergeConfig(this.originalConfig); this.componentLevels.clear(); } /** * Clone this configuration * * @param overrides - Optional configuration overrides * @returns New LoggerConfiguration instance */ clone(overrides) { const currentConfig = { level: this.config.level, component: this.config.component, defaultContext: { ...this.config.defaultContext }, sampling: { ...this.config.sampling }, redaction: { ...this.config.redaction, patterns: [...this.config.redaction.patterns || []] }, transports: [...this.config.transports], captureUnhandledErrors: this.config.captureUnhandledErrors, captureUnhandledRejections: this.config.captureUnhandledRejections, captureConsoleErrors: this.config.captureConsoleErrors }; const newConfig = { ...currentConfig, ...overrides }; const cloned = new _LoggerConfiguration(newConfig); this.componentLevels.forEach((level, component) => { cloned.setComponentLevel(component, level); }); return cloned; } /** * Merge user configuration with defaults * * @private */ mergeConfig(userConfig) { return { ...DEFAULT_CONFIG, ...userConfig, defaultContext: { ...DEFAULT_CONFIG.defaultContext, ...userConfig.defaultContext }, sampling: { ...DEFAULT_CONFIG.sampling, ...userConfig.sampling }, redaction: { ...DEFAULT_CONFIG.redaction, ...userConfig.redaction, patterns: [ ...DEFAULT_CONFIG.redaction.patterns || [], ...userConfig.redaction?.patterns || [] ] }, transports: userConfig.transports || DEFAULT_CONFIG.transports }; } /** * Check if a log level should be processed * * @private */ shouldLog(level, minLevel) { return LOG_LEVEL_PRIORITY2[level] >= LOG_LEVEL_PRIORITY2[minLevel]; } /** * Apply sampling to determine if log should be processed * * @private */ passesSampling(level) { const rate = this.config.sampling[level] ?? 1; return Math.random() < rate; } }; // src/logger/context-manager.ts var ContextManager = class _ContextManager { /** * Creates a new context manager * * @param baseContext - Base context that cannot be modified * @param parent - Optional parent context manager for inheritance */ constructor(baseContext = {}, parent) { this.additionalContext = {}; this.contextStack = []; this.baseContext = _ContextManager.deepCloneStatic(baseContext); this.parent = parent; if (parent) { this.additionalContext = { ...parent.getContext() }; } } /** * Get the current merged context * * @returns Merged context from all layers */ getContext() { let merged = {}; merged = { ...this.baseContext }; merged = { ...merged, ...this.additionalContext }; for (const stackContext of this.contextStack) { merged = { ...merged, ...stackContext }; } return merged; } /** * Get only the base context (immutable) */ getBaseContext() { return { ...this.baseContext }; } /** * Get only the additional context */ getAdditionalContext() { return { ...this.additionalContext }; } /** * Add or update context fields * * @param context - Context to add/merge */ add(context) { const cloned = this.deepClone(context); this.additionalContext = { ...this.additionalContext, ...cloned }; } /** * Set context (replaces additional context) * * @param context - New context to set */ set(context) { this.additionalContext = this.deepClone(context); } /** * Remove specific context fields * * @param keys - Keys to remove from context */ remove(...keys) { for (const key of keys) { delete this.additionalContext[key]; } } /** * Clear all additional context (keeps base context) */ clear() { this.additionalContext = {}; this.contextStack.length = 0; } /** * Push a temporary context onto the stack * * @param context - Temporary context to push * @returns Function to pop this context */ push(context) { this.contextStack.push({ ...context }); const stackIndex = this.contextStack.length - 1; return () => { if (this.contextStack.length > stackIndex) { this.contextStack.splice(stackIndex, 1); } }; } /** * Pop the most recent context from the stack * * @returns The popped context or undefined */ pop() { return this.contextStack.pop(); } /** * Execute a function with temporary context * * @param context - Temporary context * @param fn - Function to execute * @returns Result of the function */ withContext(context, fn) { const popContext = this.push(context); try { return fn(); } finally { popContext(); } } /** * Execute an async function with temporary context * * @param context - Temporary context * @param fn - Async function to execute * @returns Promise with result of the function */ async withContextAsync(context, fn) { const popContext = this.push(context); try { return await fn(); } finally { popContext(); } } /** * Create a child context manager * * @param additionalContext - Additional context for the child * @returns New ContextManager instance */ createChild(additionalContext = {}) { const child = new _ContextManager( { ...this.getContext(), ...additionalContext }, this ); return child; } /** * Clone this context manager * * @param includeStack - Whether to include the context stack * @returns New ContextManager instance */ clone(includeStack = false) { const cloned = new _ContextManager(this.baseContext); cloned.additionalContext = { ...this.additionalContext }; if (includeStack) { cloned.contextStack.push(...this.contextStack.map((ctx) => ({ ...ctx }))); } return cloned; } /** * Check if context has a specific key * * @param key - Key to check * @returns True if key exists in context */ has(key) { const context = this.getContext(); return key in context; } /** * Get a specific context value * * @param key - Key to get * @returns Value or undefined */ get(key) { const context = this.getContext(); return context[key]; } /** * Get the size of the current context * * @returns Number of keys in merged context */ get size() { return Object.keys(this.getContext()).length; } /** * Get the depth of the context stack * * @returns Stack depth */ get stackDepth() { return this.contextStack.length; } /** * Check if context is empty * * @returns True if no context is set */ get isEmpty() { return this.size === 0; } /** * Merge multiple contexts * * @param contexts - Contexts to merge * @returns Merged context */ static merge(...contexts) { return contexts.reduce((merged, context) => ({ ...merged, ...context }), {}); } /** * Filter context by allowed keys * * @param context - Context to filter * @param allowedKeys - Keys to allow * @returns Filtered context */ static filter(context, allowedKeys) { const filtered = {}; for (const key of allowedKeys) { if (key in context) { filtered[key] = context[key]; } } return filtered; } /** * Exclude keys from context * * @param context - Context to filter * @param excludedKeys - Keys to exclude * @returns Filtered context */ static exclude(context, excludedKeys) { const filtered = { ...context }; for (const key of excludedKeys) { delete filtered[key]; } return filtered; } /** * Static deep clone helper for use in constructor * * @private */ static deepCloneStatic(obj) { if (obj === null || typeof obj !== "object") { return obj; } const cloned = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; if (value === null || value === void 0) { cloned[key] = value; } else if (typeof value === "object") { if (Array.isArray(value)) { cloned[key] = [...value]; } else if (value instanceof Date) { cloned[key] = new Date(value.getTime()); } else { cloned[key] = _ContextManager.deepCloneStatic(value); } } else { cloned[key] = value; } } } return cloned; } /** * Deep clone an object (simple implementation for context objects) * * @private * @param obj - Object to clone * @returns Deep cloned object */ deepClone(obj) { return _ContextManager.deepCloneStatic(obj); } }; // src/logger/logger-impl.ts var LoggerImpl = class _LoggerImpl { /** Creates a new logger instance */ constructor(config = {}, loggerName = "default") { this.loggerName = loggerName; this.destroyed = false; this.configuration = new LoggerConfiguration(config); this.transportRegistry = new TransportRegistry(); this.contextManager = new ContextManager(this.configuration.defaultContext); this.redactionEngine = new RedactionEngine(this.configuration.redaction); this.configuration.transports.forEach((transport) => { this.transportRegistry.add(transport); }); } /** Logger name/identifier */ get name() { return this.loggerName; } /** Current minimum log level */ get level() { return this.configuration.level; } /** Logs a trace-level message */ trace(message, ...args) { if (typeof message !== "string") throw new TypeError("Message must be a string"); this.log("trace", message, ...args); } /** Logs a debug-level message */ debug(message, ...args) { if (typeof message !== "string") throw new TypeError("Message must be a string"); this.log("debug", message, ...args); } /** Logs an info-level message */ info(message, ...args) { if (typeof message !== "string") throw new TypeError("Message must be a string"); this.log("info", message, ...args); } /** Logs a warning-level message */ warn(message, ...args) { if (typeof message !== "string") throw new TypeError("Message must be a string"); this.log("warn", message, ...args); } /** Logs an error-level message */ error(message, ...args) { if (typeof message !== "string") throw new TypeError("Message must be a string"); this.log("error", message, ...args); } /** Records a structured metric */ metric(name, fields = {}) { if (this.destroyed) return; const context = this.redactionEngine.redactObject(this.contextManager.getContext()); const redactedFields = this.redactionEngine.redactObject(fields); const metricData = { name, fields: redactedFields, timestamp: Date.now(), context, component: this.configuration.component }; const eventData = { level: "info", message: `Metric: ${name}`, timestamp: metricData.timestamp, context: { ...context, ...redactedFields }, args: [metricData], component: this.configuration.component, logger: this.loggerName };