121 lines
3.5 KiB
JavaScript
121 lines
3.5 KiB
JavaScript
/**
|
|
* @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<string, unknown>) => void} debug
|
|
* @property {(msg: string, meta?: Record<string, unknown>) => void} info
|
|
* @property {(msg: string, meta?: Record<string, unknown>) => void} warn
|
|
* @property {(msg: string, meta?: Record<string, unknown>) => 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<string, unknown>|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<string, unknown>} [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),
|
|
};
|
|
}
|