Pino 日志实战指南:高性能 Node.js 结构化日志的正确打开方式
Pino 是 Node.js 生态中的结构化日志库,也是 Fastify 显式启用日志后使用的内置日志器。本文讲解 Pino 的级别配置、时间戳与字段定制、child logger、错误与未捕获异常处理、transport 落盘与脱敏,并给出接入观测云集中分析的路径。
Pino 是一款以极致性能著称的 Node.js 结构化日志库,默认输出 JSON 格式日志,通过异步写入与 worker 线程 transport 分担输出处理开销,因此被 Fastify 用作内置日志器(需显式启用 Fastify 日志)。 如果你的 Node.js 服务对吞吐量敏感,Pino 通常是日志库的第一候选。
核心要点速览
- Pino 默认输出 JSON、开箱即用,级别用数字表示(30=info、50=error、60=fatal);
- 生产配置:按需使用 ISO 时间戳,保留数字级别,使用
redact敏感字段脱敏; - 默认 err 序列化器已处理 Error 堆栈;崩溃诊断需选同步路径并保留进程失败语义;
- 落盘/外发走 v7+ 的 transport(worker 线程),主线程仍需序列化和入队,背压也可能影响延迟;文件轮转交给 logrotate;
- 接入观测云推荐"应用写 stdout/文件 + DataKit 采集"的解耦方式,而非应用内直推。
快速上手
npm install pino
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
});
logger.info('服务启动完成');
logger.error('数据库连接失败');
输出即结构化 JSON:
{"level":30,"time":1756100000000,"pid":5422,"hostname":"prod-01","msg":"服务启动完成"}
生产化配置:时间戳、级别名与全局字段
默认输出的 time 是 Unix 毫秒、level 是数字。可按下游格式要求调整时间戳;transport 级别过滤应保留数字 level:
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime, // ISO-8601 时间戳
formatters: {
level: (label, number) => ({ level: number, level_name: label.toUpperCase() }), // 保留多目标路由所需数字
bindings: (b) => ({ pid: b.pid, host: b.hostname, node_version: process.version }),
},
});
输出变为:
{"level":30,"level_name":"INFO","time":"2026-08-25T11:10:52.206Z","pid":5422,"host":"prod-01","node_version":"v22.16.0","msg":"服务启动完成"}
上下文复用:child logger
同一请求/模块的公共字段不必每条都写,用 child logger 绑定一次即可:
const reqLogger = logger.child({ request_id: 'req-abc-123', service: 'order' });
reqLogger.info('订单创建成功'); // 自动携带 request_id 与 service
错误处理:两个配置要点
其一,Pino 默认已为 err 提供标准序列化器,下例显式写出方便自定义时核对:
const logger = pino({ serializers: { err: pino.stdSerializers.err } });
logger.error({ err, order_id: 'ORD-34567' }, '支付处理异常');
其二,Pino 不会自动接管未捕获异常,可用 monitor 配合同步目标记录诊断而不阻止默认崩溃退出(启动参数显式使用 --unhandled-rejections=strict):
const crashLogger = pino(pino.destination({ dest: 2, sync: true }));
process.on('uncaughtExceptionMonitor', (err, origin) => {
crashLogger.fatal({ err, origin }, '未捕获异常');
});
配合 PM2 或 Docker 的重启策略,保证进程崩溃后自动拉起。
日志去哪:transport 与落盘
Pino 默认写 stdout——这也是生产环境的推荐姿势(由容器平台或采集器接管,见下文)。确需落盘时用 v7+ 的 transport(在 worker 线程处理输出,但主线程仍有日志调用与序列化开销):
const transport = pino.transport({
target: 'pino/file',
options: { destination: './logs/app.log', mkdir: true },
});
const logger = pino({ level: 'info' }, transport);
注意:Pino 自身不带文件轮转,落盘文件务必配合 logrotate(见本系列《Logrotate 日志轮转实战》)。开发期想要可读输出,管道接 pino-pretty:node index.js | pino-pretty。
敏感数据:redact 一行配置
const logger = pino({
redact: ['password', 'profile.phone', 'req.headers.authorization'],
});
logger.info({ username: 'john', password: 'secret' }, '登录');
// password 字段输出为 "[Redacted]"
设 remove: true 可将字段彻底删除而非替换占位符。redact 仅匹配结构化路径,不会自动清理 msg 文本或 Error 消息中的敏感值。
接入观测云:采集与告警
应用输出 JSON 后,在 DataKit 配置文件路径或容器日志采集路径,并设置来源和服务标识 [1]。为 Pino 的输出编写 Pipeline:提取 msg、time、level,将级别映射到标准 status,同时保留 request_id [2]。
先在查看器中用一次请求的 request_id 检查日志是否齐全,再按服务统计错误数量并配置日志检测 [4]。需要从 APM 链路查看日志时,按日志关联文档注入真实 trace_id;业务请求编号可以保留,但两者不是同一个字段。
总结
Pino 的配置主线:一致的时间/级别、正确的 Error 序列化与敏感字段治理是生产配置重点;应用负责输出一致的字段,采集器负责读取和上报,便于分别检查日志生成与采集环节的问题。
常见问题(FAQ)
Q:Pino 的 level 数字和文本级别怎么对应?
10=trace、20=debug、30=info、40=warn、50=error、60=fatal。数字级别可由下游映射;文本标签是可选项,多个 transport target 路由需要保留数字 level。
Q:Pino 日志可以直接写到日志平台吗?
技术上可通过自定义 transport 直推 HTTP,但不推荐:worker 中的网络重试不必直接运行于主线程,但队列、背压和失败处理仍会影响服务。更稳妥的是写 stdout/文件,由 DataKit 这类采集器负责缓冲、重试与上报。
Q:Pino 支持日志文件轮转吗?
不支持,这是刻意的设计取舍(保持库本身轻量)。轮转需验证写端重新打开文件与采集端位点;rename 后不重新打开可能继续写旧文件,copytruncate 也存在丢失窗口。
Q:Pino 和 Winston 怎么选?
比较时使用相同字段、输出目标和负载,观察序列化、写入及队列开销;Pino 的 JSON 输出与 transport 机制、Winston 的多 transport 配置是各自值得考察的方向。详细对比见本系列《Node.js 日志库八款对比》。
系列阅读:Winston 日志实战指南 | Node.js 日志最佳实践十一条
[1]: 观测云文档《DataKit 日志采集综述》https://docs.guance.com/datakit/datakit-logging/
[2]: 观测云文档《日志采集》https://docs.guance.com/logs/manual/
[4]: 观测云文档《监控》https://docs.guance.com/monitoring/monitor/monitor-rule/