Node.js 日志体系设计:结构化、分级与链路关联
背景与问题界定 日志是分布式系统中最基础也是最重要的可观测性数据来源。然而,在许多 Node.js 项目中,日志体系的设计仍然停留在"console.log 加个时间戳"的阶段。当系统流量上升、出现线上问题需要排查时,这种原始日志方案暴露出诸多问题:日志散落在标准输出和文件中,没有统一的查询入口;日志级别混乱——info、warn、error 的使用没有统一标准,导致告警噪音极高;关键的业务请求缺乏链路标识,无法将一个请求在多个微服务和函数调用间的日志串联起来。更严重的是,高性能场景下频繁的日志 I/O 可能导致线程阻塞,影响服务吞吐量。这些问题直指一个核心矛盾:日志既要详尽到可以排查问题,又要精简到不影响性能和存储成本。 目标拆解与工程约束 结构化日志输出:所有日志必须以 JSON 格式输出,包含时间戳、级别、模块名、请求 ID(traceId)和结构化消息体,禁止拼接字符串的日志模式。 日志级别的严格语义:trace 用于开发调式;debug 用于诊断;info 记录关键业务流程节点;warn 表示非正常但可恢复的状态;error 表示功能失败或异常。error 级别必须直接触发告警通知。 性能开销可控:日志写入不应导致请求处理的 TP99 增加超过 1ms。生产环境日志的采样率可通过配置动态调整,降级时关闭 debug+trace 级别。 链路追踪集成:请求进入服务时生成或传递 traceId,日志中自动携带该 ID,并与下游的 HTTP/gRPC 调用关联,形成完整的调用链视图。 方案设计 我们基于 pino + pino-opentelemetry 构建日志体系。pino 是 Node.js 中性能最好的日志库之一,相比 winston 吞吐量高出 5~10 倍,适合高并发场景。 日志核心封装 创建一个统一的日志工厂函数,确保所有模块的日志输出格式一致: import pino from 'pino' import { AsyncLocalStorage } from 'async_hooks' const asyncLocalStorage = new AsyncLocalStorage<{ traceId: string }>() function createLogger(module: string) { const transport = pino.transport({ target: 'pino/file', options: { destination: process.env.LOG_FILE || '/dev/stdout' } }) return pino( { level: process.env.LOG_LEVEL || 'info', mixin() { const store = asyncLocalStorage.getStore() return { module, traceId: store?.traceId, pid: process.pid, host: process.env.HOSTNAME } }, formatters: { level(label) { return { level: label } }, bindings() { return {} } // 不输出默认的 bindings }, timestamp: pino.stdTimeFunctions.isoTime }, transport ) } // 中间件设置 traceId async function tracingMiddleware(ctx, next) { const traceId = ctx.request.headers['x-trace-id'] || crypto.randomUUID() await asyncLocalStorage.run({ traceId }, next) ctx.set('x-trace-id', traceId) } 日志分级与动态采样 生产环境默认开启 info 及以上级别。当需要调试特定问题时,通过配置中心的动态开关,对指定模块或指定用户开启 debug 级别,同时通过采样率控制日志量: ...