Pino 日志实战指南:高性能 Node.js 结构化日志的正确打开方式

Pino 是 Node.js 生态中性能最强的日志库之一,也是 Fastify 的默认日志器。本文讲解 Pino 的级别配置、时间戳与字段定制、child logger、错误与未捕获异常处理、transport 落盘与脱敏,并给出接入观测云集中分析的路径。

最佳实践
Pino 日志实战指南:高性能 Node.js 结构化日志的正确打开方式技术指南封面

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-prettynode 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 配置 logfilessource/service)。JSON 日志自动解析字段,Pipeline 只需确认 timestatus 标准字段映射正确(如 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 日志最佳实践十一条

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台