Pino 日志实战指南:高性能 Node.js 结构化日志的正确打开方式
Pino 是 Node.js 生态中性能最强的日志库之一,也是 Fastify 的默认日志器。本文讲解 Pino 的级别配置、时间戳与字段定制、child logger、错误与未捕获异常处理、transport 落盘与脱敏,并给出接入观测云集中分析的路径。
Pino 是一款以极致性能著称的 Node.js 结构化日志库,默认输出 JSON 格式日志,通过异步写入与 worker 线程 transport 把日志开销降到最低,因此被 Fastify 选作内置日志器。 如果你的 Node.js 服务对吞吐量敏感,Pino 通常是日志库的第一候选。
核心要点速览
- Pino 默认输出 JSON、开箱即用,级别用数字表示(30=info、50=error、60=fatal);
- 生产三件套:ISO 时间戳、大写级别名、
redact敏感字段脱敏; - 错误必须配
pino.stdSerializers.err才能带出完整堆栈;未捕获异常要监听uncaughtException/unhandledRejection并以fatal记录; - 落盘/外发走 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 是数字,对人和下游平台都不够友好。推荐这组"生产三件套":
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime, // ISO-8601 时间戳
formatters: {
level: (label) => ({ level: label.toUpperCase() }), // 数字级别转大写文本
bindings: (b) => ({ pid: b.pid, host: b.hostname, node_version: process.version }),
},
});
输出变为:
{"level":"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
错误处理:两个必配项
其一,记录 Error 对象时用标准序列化器,否则堆栈会丢:
const logger = pino({ serializers: { err: pino.stdSerializers.err } });
logger.error({ err, order_id: 'ORD-34567' }, '支付处理异常');
其二,Pino 不会自动接管未捕获异常,需手动监听并以 fatal 记录后退出:
process.on('uncaughtException', (err) => {
logger.fatal(err, '未捕获异常,进程退出');
process.exit(1);
});
process.on('unhandledRejection', (err) => {
logger.fatal(err, '未处理的 Promise 拒绝');
process.exit(1);
});
配合 PM2 或 Docker 的重启策略,保证进程崩溃后自动拉起。
日志去哪:transport 与落盘
Pino 默认写 stdout——这也是生产环境的推荐姿势(由容器平台或采集器接管,见下文)。确需落盘时用 v7+ 的 transport(运行在 worker 线程,不阻塞事件循环):
const transport = pino.transport({
target: 'pino/file',
options: { destination: './logs/app.log' },
});
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 可将字段彻底删除而非替换占位符。
接入观测云:采集与告警
观测云落地:推荐"解耦式"接入——应用只管把 JSON 写到 stdout(容器)或文件(虚拟机),采集交给 DataKit:容器环境用 stdout 采集,虚拟机用磁盘文件采集(logging.conf 配置 logfiles 与 source/service)。JSON 日志自动解析字段,Pipeline 只需确认 time、status 标准字段映射正确(如 level: FATAL/ERROR → status: error)。随后在日志查看器中按 request_id 追踪单次请求的全部日志、用聚类分析归并高频报错 ;在**监控器(日志检测)**中配置"fatal/error 日志出现即告警"或"错误数 5 分钟超阈值",经告警策略推送钉钉/企业微信/飞书 。若 Node.js 应用已通过观测云 APM(OpenTelemetry/ddtrace)接入链路追踪,将 trace_id 注入日志后还能实现链路与日志的双向跳转 。
总结
Pino 的配置主线:ISO 时间戳 + 文本级别 + err 序列化 + redact 脱敏是生产底线;写 stdout/文件、采集交给 DataKit、分析告警交给观测云,是与云原生架构最契合的组合。
常见问题(FAQ)
Q:Pino 的 level 数字和文本级别怎么对应?
10=trace、20=debug、30=info、40=warn、50=error、60=fatal。生产建议用 formatters.level 转成文本输出,下游平台(含观测云)才能直接按级别着色与筛选。
Q:Pino 日志可以直接写到日志平台吗?
技术上可通过自定义 transport 直推 HTTP,但不推荐:网络抖动时重试逻辑会拖累主线程。更稳妥的是写 stdout/文件,由 DataKit 这类采集器负责缓冲、重试与上报。
Q:Pino 支持日志文件轮转吗?
不支持,这是刻意的设计取舍(保持库本身轻量)。落盘场景用 logrotate 管理轮转即可,DataKit 采集能正确衔接轮转产生的新文件。
Q:Pino 和 Winston 怎么选?
高吞吐 API 服务优先 Pino(性能优势约 5 倍量级);需要复杂多目标路由、内置轮转的“全家桶”体验选 Winston。详细对比见本系列《Node.js 日志库八款对比》。
系列阅读:Winston 日志实战指南 | Node.js 日志最佳实践十一条