人人都会AI编程

14.3 日志系统:winston /pino 日志框架、分级、切割、收集

更新时间:2026-07-10

日志是应用可观测性的基石。无论是一个简单的 API 服务还是复杂的微服务集群,合理的日志记录能够帮助开发者在调试问题、监控运行状态和事后回溯故障时快速定位。Node.js 原生的 console.log 虽然简单,但缺乏级别控制、格式化输出和持久化能力,生产环境必须借助专门的日志框架。本节将详细介绍两个最主流的 Node.js 日志库——winstonpino,并覆盖日志分级、切割、收集等工程化实践。

14.3.1 日志框架选型:winston 与 pino

winston:功能全面,扩展灵活

winston 是 Node.js 社区老牌的日志库,设计上追求“传输(Transport)”与“格式(Format)”的解耦。它支持多种日志输出目标(文件、控制台、HTTP、数据库等),并可以通过组合格式器(如颜色、时间戳、JSON)自由定制输出样式。

核心特点:

  • 日志级别可自定义,内置 errorwarninfohttpverbosedebugsilly 等级别。
  • 传输器生态丰富,除内置的 File Transport、Console Transport 外,社区提供了 winston-daily-rotate-filewinston-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 级别记录详细信息,当需要排查时临时调低级别。
  • 避免在循环中大量输出 debugtrace,这些日志在开启时可能严重影响性能。
  • 日志消息文本应具备人类可读性,而结构化信息使用元数据对象传递,便于机器解析。

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 日志最佳实践清单

  1. 分级记录:明确每个模块、每个操作的日志级别,不要滥用 info,也不要忽略 debug
  2. 结构化,非字符串拼接:例如 logger.info({ userId, action: 'purchase' }, '用户下单'),而不是 logger.info('用户' + userId + '下单了')。这方便后续检索聚合。
  3. 敏感信息脱敏:不得在日志中明文记录密码、Token、身份证号等。可在序列化阶段添加过滤器。
  4. 合理使用异步日志:pino 自身就是极低延迟;winston 如有大量文件写入,建议使用异步传输或 winston.transports.Console
  5. 日志轮转与清理:设定最大保留期限和磁盘用量,防止磁盘爆满。
  6. 关联分布式追踪 ID:在微服务中,在日志里附带 traceId,便于串联调用链(可结合 async_hooks 或 OpenTelemetry)。
  7. 避免日志风暴:不要在循环内部打日志,尤其是 error 级别;对于高频路径可设置抽样记录。

日志系统虽看似基础,却是线上诊断和监控的窗口。选择合适的框架(winston 的全面或 pino 的极速),配合规范的分级、切割和收集策略,才能在问题发生时快速定位,保障服务的稳定运行。