Pino vs Winston:Node.js 日志库怎么选?

如果你正在给 Express、Fastify、NestJS 或普通 Node.js 服务选日志库,核心问题不是“哪个更有名”,而是你的日志链路更需要低开销结构化输出,还是更需要进程内多路投递和格式化能力。

目录

  1. 先给结论
  2. 核心差异对比
  3. 什么时候选 Pino
  4. 什么时候选 Winston
  5. 代码示例
  6. 迁移和落地建议

1. 先给结论

新项目、API 服务、高并发服务、Kubernetes/容器环境,优先选 Pino。它默认输出结构化 JSON,设计目标是低开销,适合把日志直接交给 stdout、采集器和可观测平台处理。

老项目、脚本工具、需要在应用进程内同时写文件、控制台、HTTP、自定义目标时,Winston 仍然很合适。它的 transport 和 format 体系成熟,配置表达力强,对传统“应用自己管理日志投递”的模式更友好。

Pino

更像“高速 JSON 日志管道”

Pino 的默认姿势是把日志作为结构化事件写出去,尽量减少主业务线程开销。开发时可以用 pino-pretty 看人类友好的输出,生产环境通常保留 JSON。

Winston

更像“可编排日志工具箱”

Winston 强项是 transports、formats、levels 的组合。你可以在一个 logger 里同时配置 console、file、HTTP、第三方 transport,并为不同目标设置不同格式。

2. 核心差异对比

维度PinoWinston
默认输出 JSON 结构化日志,天然适合日志采集与查询。 可通过 format 配置 JSON、文本、彩色输出等。
性能取向 明确以低开销和高吞吐为核心目标。 更重视灵活性;复杂格式化和多 transport 会增加开销。
日志投递 推荐把日志写到 stdout 或通过 transport/worker 交给外部系统。 transport 是核心能力,可直接写 console、file、HTTP 或自定义目标。
格式化 生产环境倾向保持 JSON;开发环境用 pino-pretty format.combine() 非常灵活,适合复杂的人类可读格式。
上下文日志 child logger 很自然,适合 request id、user id、trace id。 也支持 child logger 和 defaultMeta,但通常配置略重。
框架生态 Fastify 默认偏向 Pino,现代 Node 服务生态支持较好。 历史生态广,很多旧项目、教程和第三方 transport 仍基于 Winston。
学习曲线 基础用法简单,理念是“少做格式化,多交给日志平台”。 基础简单,但 format/transport/level 组合多,长期配置会更复杂。

3. 什么时候选 Pino

  • 服务跑在 Docker、Kubernetes、Serverless 或云平台中,日志主要从 stdout 被采集。
  • 你希望日志默认就是 JSON,能被 Elasticsearch、Loki、Datadog、Better Stack、OpenTelemetry pipeline 等系统稳定解析。
  • API 请求量较大,日志调用不能明显拖慢主业务路径。
  • 你使用 Fastify,或希望为每个请求创建带 requestIdtraceId 的 child logger。
  • 团队能接受“生产日志不是给人直接读的,而是给机器检索和聚合的”。

4. 什么时候选 Winston

  • 你的应用需要在同一进程内同时写多个目标,例如控制台、普通文件、错误文件和远程 HTTP endpoint。
  • 你需要非常定制的人类可读格式,比如时间戳、颜色、标签、堆栈格式、按环境切换输出样式。
  • 项目已经用了 Winston,并且围绕它写了大量 transport、format 或内部封装。
  • 这是 CLI、后台脚本、桌面应用后端,而不是高吞吐 Web API。
  • 你更看重配置表达力,而不是极致低开销。

5. 代码示例

Pino:生产 JSON,开发可读

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: ['req.headers.authorization', 'password'],
  transport: process.env.NODE_ENV === 'development'
    ? { target: 'pino-pretty', options: { colorize: true } }
    : undefined
});

const requestLogger = logger.child({ requestId: 'req_123' });

requestLogger.info({ userId: 42 }, 'user login success');
requestLogger.error({ err: new Error('database timeout') }, 'query failed');

这类日志适合直接进入采集系统。开发时看漂亮文本,生产时保持稳定 JSON,是 Pino 最常见的落地方式。

Winston:多个 transport,各自配置

import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  defaultMeta: { service: 'billing-api' },
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' })
  ]
});

logger.info('user login success', { userId: 42, requestId: 'req_123' });
logger.error('query failed', { error: new Error('database timeout') });

这类配置适合应用自己管理日志文件和投递目标的场景。注意文件日志需要配合轮转策略,否则长期运行会产生大文件问题。

6. 迁移和落地建议

从 Winston 迁到 Pino

  • 先定义统一的业务日志接口,例如 logger.info(message, meta)logger.info(meta, message),不要让业务代码散落依赖具体库 API。
  • 把“写文件、发 HTTP、格式化文本”等职责尽量移到日志采集层,而不是继续放在应用进程内。
  • 统一字段名:requestIdtraceIduserIdserviceenv,这些比选择哪个库更影响后续排障效率。
  • 明确敏感字段脱敏策略,Pino 可用 redact,Winston 可在 format 层处理。

不要只按“性能 benchmark”选型

日志库性能很重要,但生产体验还取决于日志字段是否稳定、错误堆栈是否完整、request id 是否贯穿、采集系统是否能解析、告警规则是否容易写。对于大多数团队,日志结构设计比库本身更容易决定排障质量。

最终建议

如果你没有历史包袱,选择 Pino,并把生产日志作为结构化事件处理。如果你的项目已经深度使用 Winston,或者确实需要复杂 transport 编排,继续用 Winston 没问题,但建议逐步收敛格式和字段,避免每个模块都输出不同形状的日志。