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
JavaScript
"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
};