1. 先给结论
新项目、API 服务、高并发服务、Kubernetes/容器环境,优先选 Pino。它默认输出结构化 JSON,设计目标是低开销,适合把日志直接交给 stdout、采集器和可观测平台处理。
老项目、脚本工具、需要在应用进程内同时写文件、控制台、HTTP、自定义目标时,Winston 仍然很合适。它的 transport 和 format 体系成熟,配置表达力强,对传统“应用自己管理日志投递”的模式更友好。
更像“高速 JSON 日志管道”
Pino 的默认姿势是把日志作为结构化事件写出去,尽量减少主业务线程开销。开发时可以用 pino-pretty 看人类友好的输出,生产环境通常保留 JSON。
更像“可编排日志工具箱”
Winston 强项是 transports、formats、levels 的组合。你可以在一个 logger 里同时配置 console、file、HTTP、第三方 transport,并为不同目标设置不同格式。
2. 核心差异对比
| 维度 | Pino | Winston |
|---|---|---|
| 默认输出 | 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,或希望为每个请求创建带
requestId、traceId的 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、格式化文本”等职责尽量移到日志采集层,而不是继续放在应用进程内。
- 统一字段名:
requestId、traceId、userId、service、env,这些比选择哪个库更影响后续排障效率。 - 明确敏感字段脱敏策略,Pino 可用
redact,Winston 可在 format 层处理。
不要只按“性能 benchmark”选型
日志库性能很重要,但生产体验还取决于日志字段是否稳定、错误堆栈是否完整、request id 是否贯穿、采集系统是否能解析、告警规则是否容易写。对于大多数团队,日志结构设计比库本身更容易决定排障质量。
最终建议
如果你没有历史包袱,选择 Pino,并把生产日志作为结构化事件处理。如果你的项目已经深度使用 Winston,或者确实需要复杂 transport 编排,继续用 Winston 没问题,但建议逐步收敛格式和字段,避免每个模块都输出不同形状的日志。