日志是应用可观测性的基石。无论是一个简单的 API 服务还是复杂的微服务集群,合理的日志记录能够帮助开发者在调试问题、监控运行状态和事后回溯故障时快速定位。Node.js 原生的 console.log 虽然简单,但缺乏级别控制、格式化输出和持久化能力,生产环境必须借助专门的日志框架。本节将详细介绍两个最主流的 Node.js 日志库——winston 和 pino,并覆盖日志分级、切割、收集等工程化实践。
14.3.1 日志框架选型:winston 与 pino
winston:功能全面,扩展灵活
winston 是 Node.js 社区老牌的日志库,设计上追求“传输(Transport)”与“格式(Format)”的解耦。它支持多种日志输出目标(文件、控制台、HTTP、数据库等),并可以通过组合格式器(如颜色、时间戳、JSON)自由定制输出样式。
核心特点:
- 日志级别可自定义,内置
error、warn、info、http、verbose、debug、silly等级别。 - 传输器生态丰富,除内置的 File Transport、Console Transport 外,社区提供了
winston-daily-rotate-file、winston-elasticsearch等扩展。 - 支持多种格式器:简单文本、JSON、彩色输出等,可组合多个格式器。
- 子 Logger 创建方便,适合按模块输出个性化日志。
pino:极致性能,生产优先
pino 由 Node.js 性能专家 Matteo Collina 创建,目标是为生产环境提供最低开销的日志记录。它输出结构化的 JSON 日志,并使用了大量底层优化(如避免 JSON.stringify、直接写入可写流等)。在高并发场景下,pino 的性能通常比 winston 高出数倍。
核心特点:
- 极致性能,尽可能减少日志调用对事件循环的影响。
- 默认输出 JSON 格式,便于日志收集系统(如 ELK、Loki)解析。
- 内置
pino-pretty模块,开发时可将 JSON 转为人类可读格式。 - 插件机制轻量,通过
pino.transport()实现异步传输。 - 支持日志级别动态调整,无需重启进程。
性能对比:社区基准测试中,pino 的吞吐量可以接近原始的 process.stdout.write,而 winston 由于格式化和传输器机制存在一些额外开销。对于对性能敏感的服务或者日志量巨大的应用,pino 是更优选择。
选型建议:
- 项目需要多目标输出、自定义复杂格式、或依赖现成企业级集成(如 Elasticsearch、Syslog),winston 生态更完善。
- 追求极致性能、使用微服务架构、且日志统一收集(如 ELK、Grafana Loki),pino 几乎是最佳组合。
14.3.2 日志分级:按紧迫程度有序记录
日志分级是规范日志输出的第一步。通过级别,开发者可以按需控制日志输出的详细程度,避免生产环境被大量调试信息淹没,同时也能在排查问题时临时开启低级别日志。
标准日志级别体系(以 RFC 5424 为参照,winston 与 pino 均有对应或自定义级别):
| 级别 | 典型用途 |
|-----------|--------------------------------------------|
| error | 无法恢复的错误,需要人工介入 |
| warn | 异常但不影响主流程,如配置缺失、降级 |
| info | 关键业务里程碑,如服务启动、用户登录等 |
| debug | 开发调试信息,生产环境通常关闭 |
| trace/silly | 极度详细的内部状态,用于深度诊断 |
在代码中使用日志级别的基本示例(以 winston 为例):
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info', // 可通过环境变量动态调整
transports: [new winston.transports.Console()]
});
logger.error('数据库连接失败', { host: 'db.local', err });
logger.warn('内存使用超过 80%', { usage: process.memoryUsage() });
logger.info('用户登录成功', { userId: 123 });
logger.debug('SQL 查询', { sql: 'SELECT ...' });
最佳实践:
- 生产环境默认日志级别设定为
info,关键节点用debug级别记录详细信息,当需要排查时临时调低级别。 - 避免在循环中大量输出
debug或trace,这些日志在开启时可能严重影响性能。 - 日志消息文本应具备人类可读性,而结构化信息使用元数据对象传递,便于机器解析。
14.3.3 winston 实战:多传输器、格式化与异常处理
基础配置
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'user-service' }, // 全局附加字段
transports: [
// 错误等级写入错误文件
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// 全部日志写入组合文件
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
// 开发环境额外输出到控制台,格式美化
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
关键配置说明:
format.combine拼接多个格式器:timestamp添加时间戳,errors({ stack: true })可以输出错误堆栈,json()转为方便机器处理的格式。- 文件传输器支持自动轮转:
maxsize限制单个文件大小,超过后自动创建新文件,maxFiles控制保留文件数。 - 通过环境区分控制台输出,开发时可读,生产时输出到文件或收集器。
使用 winston-daily-rotate-file 实现日志切割
npm install winston-daily-rotate-file
const DailyRotateFile = require('winston-daily-rotate-file');
const transport = new DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true, // 压缩旧日志
maxSize: '20m', // 单文件最大20M
maxFiles: '14d' // 保留14天
});
const logger = winston.createLogger({
transports: [transport]
});
该扩展每天生成一个新文件,并自动清理过期文件,不需要额外 crontab 脚本。
14.3.4 pino 实战:高性能结构化日志
基础配置
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
bindings: (bindings) => ({
pid: bindings.pid,
host: bindings.hostname,
node_version: process.version,
}),
level: (label) => ({ level: label }),
},
timestamp: pino.stdTimeFunctions.isoTime,
});
logger.info({ userId: 42, action: 'login' }, '用户登录');
// 输出:{"level":"info","time":"2025-03-15T10:30:00.000Z","pid":1234,"host":"server1","node_version":"v20.10.0","userId":42,"action":"login","msg":"用户登录"}
pino 的几个设计哲学:
- 首个参数为对象:作为日志的上下文数据,会自动变为 JSON 字段;第二个参数是消息字符串(
msg)。这种方式结构清晰,也便于后续日志检索。 - 子 Logger:通过
logger.child({ component: 'auth' })可创建带有固定上下文的子 Logger,适合模块化。 - 极端性能优化:pino 的字符串化和 JSON 构建是异步进行的,不会阻塞事件循环。
使用 pino-pretty 美化开发输出
npm install pino-pretty
配置时指定传输:
const logger = pino({
transport: {
target: 'pino-pretty',
options: { colorize: true }
}
});
注意:生产环境应避免使用
pino-pretty,直接输出原始 JSON 即可。如果必须输出文件,可以使用pino/file或直接将标准输出重定向。
pino 日志切割:搭配 Node.js 内置流或外部工具
pino 自身不内置文件轮转功能,但它接受任意可写流。我们可以借助 pino-roll 或系统工具实现轮转。
方案一:使用 pino-roll 库
npm install pino-roll
const pino = require('pino');
const roll = require('pino-roll');
const stream = roll({
file: 'logs/app.log',
frequency: 'daily',
mkdir: true,
size: '10m',
limit: { count: 10 },
});
const logger = pino(stream);
方案二:利用操作系统 logrotate
将 pino 输出到文件,然后配置 logrotate 在系统层面完成切割和压缩,保持 Node.js 应用的简单。
const fs = require('fs');
const pino = require('pino');
const logStream = fs.createWriteStream('logs/app.log', { flags: 'a' });
const logger = pino(logStream);
更推荐使用 pino-roll,方案统一且不依赖部署环境。
14.3.5 日志收集与集中管理
单机日志文件只适用于小型项目,一旦服务实例增多,日志会分散在多个容器或节点中,排查困难。解决思路是将日志统一收集至中央系统,常见的方案有:
1. 标准化输出 + 容器日志驱动
无论使用 winston 还是 pino,都让应用将日志输出到 stdout / stderr,而不是本地文件。容器编排平台(如 Docker、Kubernetes)会捕获这些流,并通过日志驱动(如 Fluentd、Filebeat)转发到日志中心(如 Elasticsearch、Kafka)。这也是云原生的推荐做法。
// winston
logger.add(new winston.transports.Console());
// pino 默认就是 stdout,无需额外配置
const logger = require('pino')();
启动容器时配置日志驱动(Docker Compose 示例):
services:
app:
image: node:20
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "app.{{.Name}}"
2. 对接 ELK Stack(Elasticsearch, Logstash, Kibana)
- winston:使用
winston-elasticsearch传输器直接将日志写入 Elasticsearch。 - pino:pino 的 JSON 格式天然被 Logstash 或 Fluentd 解析,无需额外转换;也可以通过
pino-elasticsearch直接发送。
3. 使用轻量化的 Grafana Loki + Promtail
适合云原生场景,配置简单。应用输出结构化 JSON 到文件或 stdout,Promtail 采集后推送至 Loki,Grafana 可视化查询。
14.3.6 日志最佳实践清单
- 分级记录:明确每个模块、每个操作的日志级别,不要滥用
info,也不要忽略debug。 - 结构化,非字符串拼接:例如
logger.info({ userId, action: 'purchase' }, '用户下单'),而不是logger.info('用户' + userId + '下单了')。这方便后续检索聚合。 - 敏感信息脱敏:不得在日志中明文记录密码、Token、身份证号等。可在序列化阶段添加过滤器。
- 合理使用异步日志:pino 自身就是极低延迟;winston 如有大量文件写入,建议使用异步传输或
winston.transports.Console。 - 日志轮转与清理:设定最大保留期限和磁盘用量,防止磁盘爆满。
- 关联分布式追踪 ID:在微服务中,在日志里附带
traceId,便于串联调用链(可结合async_hooks或 OpenTelemetry)。 - 避免日志风暴:不要在循环内部打日志,尤其是
error级别;对于高频路径可设置抽样记录。
日志系统虽看似基础,却是线上诊断和监控的窗口。选择合适的框架(winston 的全面或 pino 的极速),配合规范的分级、切割和收集策略,才能在问题发生时快速定位,保障服务的稳定运行。