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,
})

自动捕获未处理异常

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 以 JSON 输出到 stdout(容器)或 logs/app.log(虚拟机),采集交给 DataKit——容器用 stdout 采集,虚拟机用磁盘文件采集并在 logging.conf 中配置 source/service 标签 。JSON 字段自动解析,Pipeline 负责把 level 映射为观测云标准 status 字段、确认 time 提取正确 。之后:在日志查看器按字段过滤(如 level: error)、对报错做聚类分析 ;在**监控器(日志检测)**配置"exceptions.log 出现新记录即告警"或"error 级别日志突增",通过告警策略推送到钉钉/企业微信/飞书 。

不建议在应用内通过第三方 transport 直推日志平台:网络异常时的重试会占用业务进程资源。采集职责交给独立运行的 DataKit 更稳妥。

总结

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 自动切割),但建议用 Pipeline 做两件标准化:把 level 映射为标准 status(决定查看器着色与级别筛选),确认时间字段提取为 time


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

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

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

立即开始

选择观测云版本

代码托管平台