logger.js 2.94 KB
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 || {} },
      })
    },
  }
}