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