Winston 日志实战指南:Node.js 最流行日志库的配置与避坑

Winston 是 Node.js 生态下载量最高的日志库,但默认配置藏着不少坑。本文讲解 createLogger 的正确姿势、format 组合、errors 堆栈缺失的经典坑、transports 多目标输出与文件轮转、异常自动捕获,并给出接入观测云的路径。

最佳实践
Winston 日志实战指南:Node.js 最流行日志库的配置与避坑技术指南封面

Winston 是 Node.js 生态中最流行的日志库(周下载量超千万级),其核心设计是把级别、格式(format)与输出目标(transport)彻底解耦,让三者可以自由组合。 灵活是它的最大优点,但"默认配置不好用"也是出了名的——不改造就直接上生产,几乎一定会踩坑。

核心要点速览

  • 永远用 createLogger() 创建实例,不要用模块级默认 logger;
  • 经典坑:logger.error(new Error(...)) 默认会丢掉 message 和堆栈,必须加 errors({ stack: true }) format;
  • 同理,时间戳也不是默认带的,需要 timestamp() format;
  • 未捕获异常用 exceptionHandlers/rejectionHandlers 自动落盘;
  • 生产推荐 JSON 输出到 stdout/文件,由 DataKit 采集到观测云统一分析告警。

快速上手

npm install winston
import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

export default logger;
logger.info('服务启动完成');
logger.error('数据库连接失败');

输出:

{"level":"info","message":"服务启动完成"}

必踩的两个坑:时间戳与错误堆栈

坑一:错误对象被"吞掉"。直接记录 Error 实例,输出里既没有 message 也没有 stack:

logger.error(new Error('支付失败'));
// 输出只有:{"level":"error"} —— 排障时两眼一抹黑

修复:format 组合中加入 errors({ stack: true }):

const { combine, timestamp, json, errors } = winston.format;

const logger = winston.createLogger({
  level: 'info',
  format: combine(errors({ stack: true }), timestamp(), json()),
  transports: [new winston.transports.Console()],
});

坑二:默认不带时间戳。上面的 timestamp() 同时解决第二个坑。combine(errors({stack:true}), timestamp(), json()) 就是 Winston 的生产标配组合。

transports:一份日志,多个去处

Winston 的招牌能力是多目标输出,每个 transport 还能设独立级别:

transports: [
  new winston.transports.Console({ level: 'debug' }),              // 控制台全量
  new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),  // 错误单独存档
  new winston.transports.File({ filename: 'logs/app.log' }),       // 全量落盘
]

需要文件轮转时安装 winston-daily-rotate-file:

new winston.transports.DailyRotateFile({
  filename: 'logs/app-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxFiles: '14d',
  maxSize: '20m',
  zippedArchive: true,
})

选定文件 transport 后,可用 DataKit 日志采集把这一路 JSON 日志送入观测云。先检查 message、stack 和 timestamp 是否都在原始输出中,再配置 JSON 解析及 level 到 status 的映射;若上游已丢失堆栈,采集平台无法补回。

自动捕获未处理异常

const logger = winston.createLogger({
  // ...
  exceptionHandlers: [new winston.transports.File({ filename: 'logs/exceptions.log' })],
  rejectionHandlers: [new winston.transports.File({ filename: 'logs/rejections.log' })],
});

未捕获异常与未处理 Promise 拒绝会带着完整堆栈写入对应文件——这是 Winston 相比 Pino 开箱体验更好的一点。

其他实用能力

  • 上下文字段:logger.child({ service: 'order' }) 创建子 logger,公共字段自动附带;
  • 简单计时:const profiler = logger.startTimer(); ... profiler.done({message: 'xx'}),输出 durationMs;
  • 动态级别:logger.level = 'debug' 运行期改级别,配合配置中心可实现热调整(见本系列《动态调整日志级别》)。

总结

Winston 的正确用法一句话:createLogger + combine(errors({stack:true}), timestamp(), json()) 三件套打底,多 transport 分工,异常交给 handlers,采集交给 DataKit。配置到位后,它是 Node.js 生态里能力最全的日志库。

常见问题(FAQ)

Q:Winston 和 Pino 怎么选?
要极致性能选 Pino(异步序列化、worker transport);要开箱的多目标路由、内置轮转、异常自动捕获选 Winston。中小流量服务两者皆可,团队熟悉度优先。

Q:为什么生产环境不推荐 Console transport?
同步写控制台在高并发下会拖慢事件循环。生产只保留 File/轮转文件 transport,或干脆只写 stdout 由容器/采集器接管。

Q:child logger 和每次手动传字段有什么区别?
child logger 把公共字段(service、request_id)绑定一次,后续每条日志自动携带,避免漏写导致的上下文缺失——检索时才能按字段完整过滤出一次请求的全部日志。

Q:Winston 的 JSON 日志进观测云还要写 Pipeline 吗?
需先确认采集配置已启用适合当前输出的 JSON 解析。再用样本检查 level 到 status、时间到 time 的映射,以及堆栈字段是否完整;不能仅凭日志是 JSON 就省略验证。


系列阅读:Pino 日志实战指南 | Morgan 请求日志实战

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

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

立即开始

选择观测云版本

代码托管平台