/** * @fileoverview Namespaced debug logger with runtime enable/disable. * @module debug/logger */ import { appendFileSync } from 'node:fs'; import { loadDebugConfig, shouldLog } from './config.js'; import { redact } from './redact.js'; import { getTraceId } from './trace-context.js'; /** * @typedef {Object} Debugger * @property {(msg: string, meta?: Record) => void} debug * @property {(msg: string, meta?: Record) => void} info * @property {(msg: string, meta?: Record) => void} warn * @property {(msg: string, meta?: Record) => void} error * @property {string} namespace */ /** * Optional sink for tests — when set, lines go here instead of/in addition to stderr. * @type {null|((line: string) => void)} */ let testSink = null; /** * Install a test sink that captures formatted log lines. * Used only by unit tests; do not use in production code. * * @param {null|((line: string) => void)} sink - Callback or null to clear. * @returns {void} */ export function setDebugTestSink(sink) { testSink = sink; } /** * Format a single log line. * * @param {object} parts * @param {string} parts.level * @param {string} parts.namespace * @param {string} parts.message * @param {Record|undefined} parts.meta * @param {string|null} parts.traceId * @returns {string} */ export function formatLogLine({ level, namespace, message, meta, traceId }) { const ts = new Date().toISOString(); const tracePart = traceId ? ` [trace=${traceId}]` : ''; const metaPart = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(redact(meta))}` : ''; return `${ts} ${level.toUpperCase()} ${namespace}${tracePart} ${message}${metaPart}`; } /** * Emit a log line to stderr, optional file, and optional test sink. * * @param {string} line - Full formatted line. * @param {string|null} filePath - Optional file from config. * @returns {void} */ function emit(line, filePath) { if (testSink) { testSink(line); } else { // eslint-disable-next-line no-console console.error(line); } if (filePath) { try { appendFileSync(filePath, `${line}\n`, 'utf8'); } catch { // ignore file errors in debug path } } } /** * Create a namespaced debugger bound to current env configuration. * * Configuration is re-read on every log call so tests can toggle `DEBUG_VERAE` * without reloading modules. Slight cost is acceptable for debug-only paths. * * @param {string} namespace - Namespace such as `auth`, `nats`, `jobs`. * @returns {Debugger} Logger with debug/info/warn/error methods. * * @example * const log = createDebugger('jobs'); * log.debug('poll start', { jobId: 'abc' }); */ export function createDebugger(namespace) { const ns = String(namespace || 'app').toLowerCase(); /** * @param {'debug'|'info'|'warn'|'error'} level * @param {string} message * @param {Record} [meta] */ function write(level, message, meta) { const config = loadDebugConfig(); if (!shouldLog(config, ns, level)) return; const line = formatLogLine({ level, namespace: ns, message: String(message), meta, traceId: getTraceId(), }); emit(line, config.filePath); } return { namespace: ns, debug: (message, meta) => write('debug', message, meta), info: (message, meta) => write('info', message, meta), warn: (message, meta) => write('warn', message, meta), error: (message, meta) => write('error', message, meta), }; }