import { serializeLine, serializePrettyLine } from './serialize.js' /** * @typedef {import('./context.js').RequestContext} RequestContext */ /** * @typedef {Object} Logger * @property {(msg: string, fields?: Object) => void} debug * @property {(msg: string, fields?: Object) => void} info * @property {(msg: string, fields?: Object) => void} warn * @property {(msg: string, fields?: Object) => void} error fields 可含 `err`,会被结构化序列化 * @property {(ctx: RequestContext) => Logger} child 派生自动带 trace_id + ctx.fields 的 logger */ /** level 名 → 数值,用于过滤。 */ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 } /** * 默认 sink:写一行到 stdout。不在 import 期执行,仅作为函数默认参数被引用。 * @param {string} line */ function defaultSink(line) { process.stdout.write(line + '\n') } /** * 结构化日志工厂。`service` 名固定写入每条日志。 * * @param {Object} cfg * @param {string} cfg.service 服务/包名,如 'drama-service' * @param {('debug'|'info'|'warn'|'error')} [cfg.level='info'] * @param {('json'|'pretty')} [cfg.format='json'] * @param {(line: string) => void} [cfg.sink] 输出目标,默认写 stdout;测试可注入 buffer * @returns {Logger} */ export function createLogger(cfg) { const service = cfg && cfg.service const level = (cfg && cfg.level) || 'info' const format = (cfg && cfg.format) || 'json' const sink = (cfg && cfg.sink) || defaultSink const threshold = LEVELS[level] ?? LEVELS.info // bound:随 child(ctx) 绑定的 trace_id + ctx.fields;根 logger 无绑定字段。 return makeLogger({ service, threshold, format, sink, bound: null }) } /** * @param {{ service: string, threshold: number, format: 'json'|'pretty', sink: (line:string)=>void, bound: { trace_id: string, fields: Object } | null }} state * @returns {Logger} */ function makeLogger(state) { /** * @param {('debug'|'info'|'warn'|'error')} level * @param {string} msg * @param {Object} [fields] */ function emit(level, msg, fields) { if (LEVELS[level] < state.threshold) return // level 过滤 const record = { time: new Date().toISOString(), level, service: state.service, trace_id: state.bound ? state.bound.trace_id : undefined, msg, ...(state.bound ? state.bound.fields : null), ...(fields || null), } state.sink(state.format === 'pretty' ? serializePrettyLine(record) : serializeLine(record)) } return { debug: (msg, fields) => emit('debug', msg, fields), info: (msg, fields) => emit('info', msg, fields), warn: (msg, fields) => emit('warn', msg, fields), error: (msg, fields) => emit('error', msg, fields), child(ctx) { // 绑定 ctx 的 trace_id 与累积字段,派生新 logger;不修改当前 logger。 return makeLogger({ ...state, bound: { trace_id: ctx.traceId, fields: ctx.fields || {} }, }) }, } }