pino — 日志不该阻塞热路径
已复核pino 是一个面向 Node.js 的 NDJSON 日志库:主线程尽快拼出一行 JSON,真正慢的着色、多目的地转发交给 worker。日常类比:厨师只把菜放到传菜口,传菜员在另一条通道决定送哪一桌。
你写:
import pino from "pino";const log = pino();log.info({ user: 1 }, "login");固定 10.3.1 默认 level 是 info,messageKey 为 msg,errorKey 为 err,base 为 { pid, hostname }。未指定 stream 且 process.stdout 未被篡改时,走 sonic-boom 写 stdout。浏览器打包应走独立入口 browser.js,不是 Node transport。
不理解 pino,下面这些事都没法解释:
- 为什么把 logger.level 设成
info之后,debug()连asJson都不会走 - 为什么 child logger 能带上
reqId却不必每次重新 stringify 整份父字段 - 为什么
pino.transport({ targets })能同时写文件和转发,而主线程仍只stream.write一次 - 为什么配置里的
prettyPrint不再是开关,而是直接抛错
固定源码把“快”拆成四步:
-
创建时预计算:
pino()规范化 options 与 stream,用safe-stable-stringify配depthLimit: 5、edgeLimit: 100,再genLsCache把每个 level 的前缀(默认{"level":30)预先 stringify 好。 -
setLevel 换 method:level 值为 trace=10、debug=20、info=30、warn=40、error=50、fatal=60。
setLevel按比较函数把低于阈值的方法赋成noop;达标方法才是genLog。热路径上没有if (level >= info)。 -
asJson 字符串拼接:
write处理 Error / mixin 后调用asJson。行首是 lsCache 前缀 + 时间戳 + 预拼的chindings,再拼本次字段和msg,最后补}\n。整段JSON.stringify只在需要序列化对象字段时出现。 -
transport 进 worker:
opts.transport用thread-stream建ThreadStream。target与targets互斥;pino/file映射到仓库file.js。主线程只跟这条 worker stream 打交道。fatal在写完后对当前 stream 调flushSync。
案例 1:默认 stdout 结构化日志
Section titled “案例 1:默认 stdout 结构化日志”import pino from "pino";const log = pino();log.info({ user: 1 }, "login");不传参数时,level 为 info,输出到 sonic-boom 包装的 stdout。第一个参数合并进 record,第二个是 msg。debug("x") 在默认 level 下是 noop,不会拼 JSON。
案例 2:child 预拼请求字段
Section titled “案例 2:child 预拼请求字段”const log = pino();app.use((req, res, next) => { req.log = log.child({ reqId: req.headers["x-request-id"] }); next();});无 options 的 child 做 Object.create(this),再用 asChindings 把字段预拼进 chindings。它复用父 logger 的 method 与 lsCache,不按请求 new 一套 level 函数。req.log.info("ok") 每行都带 reqId。
案例 3:transport 多目标
Section titled “案例 3:transport 多目标”const log = pino(pino.transport({ targets: [ { target: "pino/file", options: { destination: "/var/log/app.log" } }, { target: "pino-loki", options: { host: "http://loki:3100" } } ]}));targets 会把 worker 入口指到 lib/worker.js,并在 worker 里分发。pino/file 被解析成本仓库 file.js。不能同时传 target 和 targets;也不能把 opts.transport 和另一个 stream 一起用。主线程看不到各个 target 的 IO。
-
prettyPrint已删除:固定 10.3.1 看到该选项直接throw。开发着色要单独接pino-pretty包,不能当核心开关。 -
browser / Edge 不是同一条 Node 热路径:
package.json的browser字段指向browser.js,落到 console API,没有thread-stream。不能把 Node transport 性能故事搬到 Workers。 -
fatal 会
flushSync,但 process.exit 仍可能赶在 worker 就绪前:fatal对当前 stream 调flushSync;transport 的 processexit处理器还会flushSync+ 必要时sleep(100)。合规场景仍应单独验证同步落盘,不能只靠“打了 fatal”。 -
自定义 formatters.level + transport.targets 被拒绝:
createArgsNormalizer在两者同时出现时抛错。多目标 worker 依赖标准 level 前缀。 -
旧页里的吞吐倍数不能再用:本轮未跑 benchmark,不引用相对 winston 的固定倍数或绝对 ops/sec。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- Node 后端需要 NDJSON,希望主线程只做拼接和一次
stream.write - 用 child logger 在请求上注入
reqId/traceId,且不想为 child 重建 level 方法 - Fastify / Express / Koa 里替换
console.log,把收集留给 stdout 或 transport - 接受默认
sonic-boom/thread-stream依赖边界
不适用:
- 只在浏览器或 Edge 里打日志——应评估
browser.js,不要假定 worker transport - 短生命周期函数里为几个日志启动 worker——直接 stdout / sonic-boom 往往更简单
- 需要核心内置 pretty-print 开关——10.3.1 已删除该选项
- 审计日志必须同步落盘且不能依赖 worker 退出钩子——要单独设计同步 destination
固定版本边界
Section titled “固定版本边界”- 本文绑定
pinojs/pino@6b344980...,tag 与 npmpino@10.3.1的gitHead均为该提交。 - 默认:
level=info,timestamp=epochTime,messageKey=msg,errorKey=err,base={pid,hostname},depthLimit=5,edgeLimit=100。 - 依赖含
thread-stream@^4、sonic-boom@^4、@pinojs/redact、safe-stable-stringify、pino-abstract-transport。 prettyPrint不再受支持。许可为 MIT。- 本文未安装依赖、未运行上游测试、未测吞吐,状态保持
UNVERIFIED。
- 把“不该输出”变成空函数——少一个 level 分支,比在热路径里做比较更干净。
- 预拼接比通用序列化更接近热路径成本——lsCache 和 chindings 把不变的前缀提前付清。
- child 是原型委托,不是新引擎——无 options 的 child 只多一段预拼字段。
- 慢 IO 必须离开主线程,也必须承认退出窗口——transport 把转发交给 worker,fatal / process exit 只尽力 flush。
- 默认
pino()上调用log.debug("x"),会走到asJson吗? pino({ prettyPrint: true })在 10.3.1 会启用着色吗?pino.transport({ target: "pino/file", targets: [...] })合法吗?
检查点:
- 不会。默认 level 是 info,
setLevel把debug赋成noop。 - 不会。该选项会抛错,必须改用独立的 pino-pretty。
- 不合法。
target与targets只能选一个。
- 官方网站:getpino.io
- 固定源码:pinojs/pino —— 本文绑定提交
6b344980eae3ebed904fc87caf4bba0ab9dbe946 - 必读实现:
lib/proto.js、lib/levels.js、lib/tools.js、lib/transport.js - fastify —— 默认集成 pino
- sentry-javascript —— 错误上报是另一条管道,不能替代结构化日志
- fastify —— 默认 logger 就是 pino
- express —— 可用 pino-http 替换 morgan
- koa —— 同样不绑框架,middleware 里挂 child 即可
- sentry-javascript —— 异常信封走 Sentry SDK;pino 只管本地 / 收集器日志
- prom-client —— 指标是另一条可观测信号,不替代日志
- grafana —— Loki 常作为 pino transport 下游
- elasticsearch —— 另一类 NDJSON 收集终点
- prom-client —— prom-client — Node 服务暴露监控指标的事实标准 SDK