export type LogLevel = "debug" | "info" | "warn" | "error"; export interface LogRecord { timestamp: string; level: LogLevel; message: string; service: string; traceId?: string; spanId?: string; requestId?: string; attributes: Record; } export interface StructuredLogger { log(level: LogLevel, message: string, attributes?: Record): void; debug(message: string, attributes?: Record): void; info(message: string, attributes?: Record): void; warn(message: string, attributes?: Record): void; error(message: string, attributes?: Record): void; child(attributes: Record): StructuredLogger; } export interface StructuredLoggerOptions { service?: string; level?: LogLevel; now?: () => Date; sink?: (record: LogRecord, line: string) => void; redact?: readonly string[]; attributes?: Record; } const LEVELS: Record = { debug: 10, info: 20, warn: 30, error: 40 }; function scrub(value: Record, keys: Set): Record { return Object.fromEntries( Object.entries(value).map(([key, item]) => [ key, keys.has(key.toLowerCase()) ? "[REDACTED]" : item, ]), ); } export function createStructuredLogger(options: StructuredLoggerOptions = {}): StructuredLogger { const threshold = LEVELS[options.level ?? "info"]; const now = options.now ?? (() => new Date()); const sink = options.sink ?? ((_record, line) => console.log(line)); const redacted = new Set( (options.redact ?? ["authorization", "cookie", "password", "secret", "token"]).map((key) => key.toLowerCase(), ), ); const base = { ...(options.attributes ?? {}) }; const logger = (attributes: Record): StructuredLogger => { const log = (level: LogLevel, message: string, values: Record = {}) => { if (LEVELS[level] < threshold) return; const combined = scrub({ ...attributes, ...values }, redacted); const record: LogRecord = { timestamp: now().toISOString(), level, message, service: options.service ?? "wrnexus", ...(typeof combined.traceId === "string" ? { traceId: combined.traceId } : {}), ...(typeof combined.spanId === "string" ? { spanId: combined.spanId } : {}), ...(typeof combined.requestId === "string" ? { requestId: combined.requestId } : {}), attributes: combined, }; sink(record, JSON.stringify(record)); }; return { log, debug: (message, values) => log("debug", message, values), info: (message, values) => log("info", message, values), warn: (message, values) => log("warn", message, values), error: (message, values) => log("error", message, values), child: (values) => logger({ ...attributes, ...values }), }; }; return logger(base); }