{"version":3,"file":"logger.mjs","names":[],"sources":["../../../../../../@warlock.js/logger/src/logger.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { LogChannel } from \"./log-channel\";\r\nimport { applyRedact, mergeRedact } from \"./redact\";\r\nimport type {\r\n  AutoFlushEvent,\r\n  LoggingData,\r\n  LogLevel,\r\n  OmittedLoggingData,\r\n  RedactConfig,\r\n} from \"./types\";\r\nimport { clearMessage } from \"./utils/clear-message\";\r\n\r\nconst SIGNAL_EVENTS: ReadonlySet<AutoFlushEvent> = new Set([\r\n  \"SIGINT\",\r\n  \"SIGTERM\",\r\n  \"SIGHUP\",\r\n  \"SIGBREAK\",\r\n  \"SIGUSR2\",\r\n]);\r\n\r\n/**\r\n * Severity ranks used by `setMinLevel`. Higher number = more severe. The\r\n * ordering matches conventional log-level hierarchies: `debug` is noisiest\r\n * and easiest to drop; `error` is the loudest and never dropped by the\r\n * minimum-level filter. `success` sits beside `info` — it's an informational\r\n * outcome, not a warning.\r\n */\r\nconst LEVEL_RANK: Record<LogLevel, number> = {\r\n  debug: 0,\r\n  info: 1,\r\n  success: 1,\r\n  warn: 2,\r\n  error: 3,\r\n  fatal: 4,\r\n};\r\n\r\nexport class Logger {\r\n  /**\r\n   * Current channel\r\n   */\r\n  public channels: LogChannel[] = [];\r\n\r\n  public id = \"logger-\" + Random.string(32);\r\n\r\n  /**\r\n   * Registered auto-flush handlers, keyed by event name. Stored so repeated\r\n   * calls to `enableAutoFlush` replace rather than stack, and so\r\n   * `disableAutoFlush` can remove them cleanly.\r\n   */\r\n  private autoFlushHandlers = new Map<AutoFlushEvent, () => void>();\r\n\r\n  /**\r\n   * Logger-wide minimum severity. When set, entries below this level are\r\n   * dropped before any channel is invoked — cheaper than per-channel `levels`\r\n   * filters because the fan-out loop is skipped entirely. `undefined` means\r\n   * no minimum (every entry reaches every channel that accepts it).\r\n   */\r\n  private minLevel?: LogLevel;\r\n\r\n  /**\r\n   * Logger-wide redaction floor. Applied once before fan-out — every\r\n   * channel receives an entry with these paths already censored. Channel\r\n   * configs can extend the path list (additive); they cannot remove paths\r\n   * set here.\r\n   */\r\n  private redactConfig?: RedactConfig;\r\n\r\n  /**\r\n   * Add a new channel\r\n   */\r\n  public addChannel(channel: LogChannel) {\r\n    this.channels.push(channel);\r\n\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Set base configurations\r\n   */\r\n  public configure(config: {\r\n    channels?: LogChannel[];\r\n    autoFlushOn?: AutoFlushEvent[];\r\n    minLevel?: LogLevel;\r\n    redact?: RedactConfig;\r\n  }) {\r\n    if (config.channels) {\r\n      this.channels = config.channels;\r\n    }\r\n\r\n    if (config.autoFlushOn) {\r\n      this.enableAutoFlush(config.autoFlushOn);\r\n    }\r\n\r\n    if (config.minLevel !== undefined) {\r\n      this.setMinLevel(config.minLevel);\r\n    }\r\n\r\n    if (config.redact !== undefined) {\r\n      this.setRedact(config.redact);\r\n    }\r\n\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Set the logger-wide redaction floor. Applied to every entry before\r\n   * fan-out; channel configs add more paths on top, never fewer. Pass\r\n   * `undefined` to clear.\r\n   *\r\n   * @example\r\n   * log.setRedact({\r\n   *   paths: [\"context.password\", \"context.*.token\"],\r\n   *   censor: \"[REDACTED]\",\r\n   * });\r\n   */\r\n  public setRedact(config: RedactConfig | undefined): this {\r\n    this.redactConfig = config;\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Read the active logger-wide redact config (or `undefined`).\r\n   */\r\n  public getRedact(): RedactConfig | undefined {\r\n    return this.redactConfig;\r\n  }\r\n\r\n  /**\r\n   * Drop every entry whose severity is below `level` before fan-out. Cheaper\r\n   * than per-channel `levels` filters because the loop never runs and no\r\n   * channel receives the entry. Pass `undefined` to clear and accept all\r\n   * levels again.\r\n   *\r\n   * @example\r\n   * // production: silence debug noise everywhere at once\r\n   * logger.setMinLevel(\"info\");\r\n   */\r\n  public setMinLevel(level: LogLevel | undefined): this {\r\n    this.minLevel = level;\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Read the active minimum severity (or `undefined` when none is set).\r\n   */\r\n  public getMinLevel(): LogLevel | undefined {\r\n    return this.minLevel;\r\n  }\r\n\r\n  /**\r\n   * Set channels\r\n   */\r\n  public setChannels(channels: LogChannel[]) {\r\n    this.channels = channels;\r\n\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Normalize log data to a single object\r\n   */\r\n  private normalizeLogData(\r\n    dataOrModule: LoggingData | OmittedLoggingData | string,\r\n    action?: string,\r\n    message: any = \"\",\r\n    level?: LogLevel,\r\n    context?: Record<string, any>,\r\n  ): LoggingData {\r\n    if (typeof dataOrModule === \"object\") {\r\n      // If level is provided, override type\r\n      return {\r\n        type: (level || (dataOrModule as any).type || \"info\") as LogLevel,\r\n        module: dataOrModule.module,\r\n        action: dataOrModule.action,\r\n        message: dataOrModule.message,\r\n        ...(context ? { context } : dataOrModule.context ? { context: dataOrModule.context } : {}),\r\n      };\r\n    }\r\n    return {\r\n      type: (level || \"info\") as LogLevel,\r\n      module: dataOrModule,\r\n      action: action as string,\r\n      message,\r\n      ...(context ? { context } : {}),\r\n    };\r\n  }\r\n\r\n  /**\r\n   * Make log\r\n   *\r\n   * Fans out a single log entry to every registered channel. Non-terminal\r\n   * channels receive a copy whose `message` has had ANSI color codes stripped\r\n   * — each channel sees its own shallow clone so one channel cannot observe\r\n   * another's mutations (e.g. a later terminal channel still sees the original\r\n   * colored message).\r\n   */\r\n  public async log(data: LoggingData) {\r\n    if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) {\r\n      return this;\r\n    }\r\n\r\n    // Apply the logger-wide redact floor once. Every channel sees the\r\n    // result; no channel can undo a logger-wide redaction (additive-only\r\n    // semantics).\r\n    const baseEntry = applyRedact(data, this.redactConfig);\r\n\r\n    for (const channel of this.channels) {\r\n      const channelRedact = channel.getRedactConfig?.();\r\n      const effectiveRedact = channelRedact\r\n        ? mergeRedact(this.redactConfig, channelRedact)\r\n        : undefined;\r\n\r\n      // When the channel adds paths, redact again from `data` rather than\r\n      // from `baseEntry` so the merged config (which already contains the\r\n      // logger-wide paths) does the full pass — avoids double-cloning the\r\n      // already-redacted base.\r\n      let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;\r\n\r\n      if (channel.terminal === false) {\r\n        payload = { ...payload, message: clearMessage(payload.message) };\r\n      }\r\n\r\n      channel.log(payload);\r\n    }\r\n\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Make debug log\r\n   */\r\n  public debug(\r\n    dataOrModule: OmittedLoggingData | string,\r\n    action?: string,\r\n    message: any = \"\",\r\n    context?: Record<string, any>,\r\n  ) {\r\n    const data = this.normalizeLogData(dataOrModule, action, message, \"debug\", context);\r\n    return this.log(data);\r\n  }\r\n\r\n  /**\r\n   * Make info log\r\n   */\r\n  public info(\r\n    dataOrModule: OmittedLoggingData | string,\r\n    action?: string,\r\n    message: any = \"\",\r\n    context?: Record<string, any>,\r\n  ) {\r\n    const data = this.normalizeLogData(dataOrModule, action, message, \"info\", context);\r\n    return this.log(data);\r\n  }\r\n\r\n  /**\r\n   * Make warn log\r\n   */\r\n  public warn(\r\n    dataOrModule: OmittedLoggingData | string,\r\n    action?: string,\r\n    message: any = \"\",\r\n    context?: Record<string, any>,\r\n  ) {\r\n    const data = this.normalizeLogData(dataOrModule, action, message, \"warn\", context);\r\n    return this.log(data);\r\n  }\r\n\r\n  /**\r\n   * Make error log\r\n   */\r\n  public error(\r\n    dataOrModule: OmittedLoggingData | string,\r\n    action?: string,\r\n    message: any = \"\",\r\n    context?: Record<string, any>,\r\n  ) {\r\n    const data = this.normalizeLogData(dataOrModule, action, message, \"error\", context);\r\n    return this.log(data);\r\n  }\r\n\r\n  /**\r\n   * Make success log\r\n   */\r\n  public success(\r\n    dataOrModule: OmittedLoggingData | string,\r\n    action?: string,\r\n    message: any = \"\",\r\n    context?: Record<string, any>,\r\n  ) {\r\n    const data = this.normalizeLogData(dataOrModule, action, message, \"success\", context);\r\n\r\n    return this.log(data);\r\n  }\r\n\r\n  /**\r\n   * Make fatal log — for unrecoverable failures where the application is going\r\n   * down (failed bootstrap, lost connection to a required dependency that the\r\n   * caller has decided not to retry, an `uncaughtException`).\r\n   *\r\n   * Identical shape to {@link error}; the level is purely informational —\r\n   * `fatal` does NOT auto-flush or exit. The caller decides whether to call\r\n   * `await log.flush()` and `process.exit(...)`.\r\n   */\r\n  public fatal(\r\n    dataOrModule: OmittedLoggingData | string,\r\n    action?: string,\r\n    message: any = \"\",\r\n    context?: Record<string, any>,\r\n  ) {\r\n    const data = this.normalizeLogData(dataOrModule, action, message, \"fatal\", context);\r\n\r\n    return this.log(data);\r\n  }\r\n\r\n  /**\r\n   * Log an `error` entry when `condition` is falsy. No-op otherwise — the\r\n   * entry is never built and channels are not invoked, so this is genuinely\r\n   * free in the happy path. Mirrors the spirit of `console.assert` but routes\r\n   * through the logger pipeline so persistent channels capture failures.\r\n   *\r\n   * @example\r\n   * log.assert(user !== null, \"auth\", \"session\", \"user vanished mid-flight\", { sessionId });\r\n   */\r\n  public assert(\r\n    condition: unknown,\r\n    module: string,\r\n    action: string,\r\n    message: any,\r\n    context?: Record<string, any>,\r\n  ): Promise<Logger> | Logger {\r\n    if (condition) return this;\r\n    return this.error(module, action, message, context);\r\n  }\r\n\r\n  /**\r\n   * Start a duration timer. The returned function emits an `info` entry\r\n   * with `completed in <ms>ms` and a `durationMs` field in `context` when\r\n   * called. Pass an object to `end()` to merge extra fields into context.\r\n   *\r\n   * @example\r\n   * const end = log.timer(\"db\", \"users.findById\");\r\n   * const user = await usersRepo.findById(id);\r\n   * end({ id, found: !!user });\r\n   */\r\n  public timer(\r\n    module: string,\r\n    action: string,\r\n  ): (extra?: Record<string, any>) => Promise<Logger> {\r\n    const startedAt = Date.now();\r\n    return (extra?: Record<string, any>) => {\r\n      const durationMs = Date.now() - startedAt;\r\n      return this.info(module, action, `completed in ${durationMs}ms`, {\r\n        durationMs,\r\n        ...(extra ?? {}),\r\n      });\r\n    };\r\n  }\r\n\r\n  /**\r\n   * Get channel by name\r\n   */\r\n  public channel(name: string) {\r\n    return this.channels.find((channel) => channel.name === name);\r\n  }\r\n\r\n  /**\r\n   * Synchronously flush logs\r\n   */\r\n  public flushSync() {\r\n    for (const channel of this.channels) {\r\n      if (channel.flushSync) {\r\n        channel.flushSync();\r\n      }\r\n    }\r\n  }\r\n\r\n  /**\r\n   * Asynchronously drain every channel that implements `flush()`.\r\n   *\r\n   * Unlike {@link flushSync}, this awaits each channel's async I/O — the\r\n   * correct call for a graceful shutdown that can afford to wait\r\n   * (`await log.flush()` after closing the HTTP server, before\r\n   * `process.exit`). A channel whose delivery is async (a network transport,\r\n   * an async disk write) implements `flush()`, not `flushSync()`.\r\n   *\r\n   * Channels are isolated: a channel whose flush rejects can neither prevent\r\n   * the others from draining nor escape as an unhandled rejection. Channels\r\n   * without `flush()` are skipped.\r\n   *\r\n   * @example\r\n   * async function shutdown() {\r\n   *   await httpServer.close();\r\n   *   await log.flush();\r\n   *   process.exit(0);\r\n   * }\r\n   */\r\n  public async flush(): Promise<void> {\r\n    await Promise.allSettled(\r\n      this.channels.map(async (channel) => {\r\n        if (!channel.flush) {\r\n          return;\r\n        }\r\n\r\n        try {\r\n          await channel.flush();\r\n        } catch {\r\n          // A single channel must never break shutdown for the others —\r\n          // a graceful drain is best-effort across every channel.\r\n        }\r\n      }),\r\n    );\r\n  }\r\n\r\n  /**\r\n   * Register one process-level handler per event that calls `flushSync()`\r\n   * before the process terminates.\r\n   *\r\n   * For signal events (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGBREAK`, `SIGUSR2`)\r\n   * the handler flushes and then re-raises the signal so Node's default exit\r\n   * behavior runs. For `beforeExit`, the handler flushes in place — Node exits\r\n   * naturally afterwards.\r\n   *\r\n   * Idempotent: calling with the same events replaces the previous handlers.\r\n   * Call `disableAutoFlush()` to unregister.\r\n   *\r\n   * @example\r\n   * log.configure({\r\n   *   channels: [new ConsoleLog(), new FileLog()],\r\n   *   autoFlushOn: [\"SIGINT\", \"SIGTERM\", \"beforeExit\"],\r\n   * });\r\n   */\r\n  public enableAutoFlush(events: AutoFlushEvent[]): this {\r\n    this.disableAutoFlush();\r\n\r\n    for (const event of events) {\r\n      const handler = SIGNAL_EVENTS.has(event)\r\n        ? () => {\r\n            this.flushSync();\r\n            process.off(event, handler);\r\n            process.kill(process.pid, event as NodeJS.Signals);\r\n          }\r\n        : () => {\r\n            this.flushSync();\r\n          };\r\n\r\n      process.on(event, handler);\r\n      this.autoFlushHandlers.set(event, handler);\r\n    }\r\n\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Remove every handler previously registered by `enableAutoFlush`.\r\n   * Safe to call when no handlers are registered.\r\n   */\r\n  public disableAutoFlush(): this {\r\n    for (const [event, handler] of this.autoFlushHandlers) {\r\n      process.off(event, handler);\r\n    }\r\n\r\n    this.autoFlushHandlers.clear();\r\n\r\n    return this;\r\n  }\r\n}\r\n\r\n/**\r\n * The package singleton. Use this for everyday logging — `log.info(...)`,\r\n * `log.error(...)`, `log.configure(...)`. Custom logger instances can be\r\n * created by instantiating `Logger` directly.\r\n *\r\n * The name is intentionally short: `log` reads naturally at the call site\r\n * (`log.info(\"auth\", \"login\", \"ok\")`) and matches the convention used in\r\n * pino, bunyan, and most JS logging tutorials.\r\n *\r\n * Note that `log` is a `Logger` instance, **not** a function — the bare\r\n * callable form was removed when the dual `log` / `logger` exports were\r\n * collapsed into a single name. Use `log.info(...)` (or any other level\r\n * shortcut) to emit entries.\r\n */\r\nexport const log = new Logger();\r\n"],"mappings":"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"}