4、Winston高级:自定义Transport、日志轮转、多传输通道配置、日志分级过滤

好,咱们继续深入Winston。前面几章我们把基础用法和核心概念都捋了一遍,现在该玩点真格的了。说实话,Winston真正强大的地方,就在于它的可扩展性。你想想看,一个日志库如果只能往控制台和文件里写,那跟咸鱼有什么区别?

这一章,我会带你搞定四个高级话题:自定义Transport日志轮转多传输通道、还有日志分级过滤。这些都是在生产环境里天天要用的东西。

4.1 自定义Transport:把日志送到任何地方

Winston内置了Console、File、Http这几个Transport。但实际项目中,你很可能需要把日志发到别的地方——比如发到企业微信、钉钉、或者存到数据库里。这时候就得自己写Transport了。

自定义Transport其实很简单。你只需要继承 winston.Transport 或者 winston.transports.Stream,然后实现一个 log 方法就行。我当年第一次写的时候,以为要搞什么高大上的东西,结果发现核心代码也就十几行。

const winston = require('winston');

// 自定义Transport:把错误日志发到企业微信
class WeChatTransport extends winston.Transport {
  constructor(options) {
    super(options);
    this.name = 'wechatTransport';
    this.webhookUrl = options.webhookUrl;
    this.level = options.level || 'error';
  }

  log(info, callback) {
    // 只处理error及以上级别
    if (info.level !== 'error' && info.level !== 'fatal') {
      return callback(null, true);
    }

    // 这里调用企业微信的API发送消息
    const message = `[${info.timestamp}] ${info.level}: ${info.message}`;
    
    // 异步发送,但不要阻塞主流程
    setImmediate(() => {
      this._sendToWeChat(message).catch(err => {
        console.error('发送到企业微信失败:', err);
      });
    });

    callback(null, true);
  }

  async _sendToWeChat(message) {
    // 实际发送逻辑,用axios或fetch都行
    // 这里省略具体实现
  }
}

// 使用自定义Transport
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new WeChatTransport({
      webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx',
      level: 'error'
    })
  ]
});
我的经验:自定义Transport里千万别做同步的网络请求。我曾经踩过这个坑,日志发送慢导致整个请求响应都变慢了。用 setImmediate 或者直接把任务丢到消息队列里,才是正确姿势。

4.2 日志轮转:别让日志撑爆磁盘

日志文件如果不做轮转,用不了多久就能把磁盘塞满。我记得有一次线上事故,就是因为日志文件涨到了几十个G,直接把服务器磁盘打满了,服务全挂。从那以后,日志轮转就成了我项目的标配。

Winston官方推荐用 winston-daily-rotate-file 这个包。它按天、按大小自动切割日志文件,还能设置保留天数。

npm install winston-daily-rotate-file

配置起来也很简单:

const DailyRotateFile = require('winston-daily-rotate-file');

const transport = new DailyRotateFile({
  filename: 'logs/application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,      // 压缩旧日志
  maxSize: '20m',           // 单个文件最大20MB
  maxFiles: '14d'           // 保留14天
});

const logger = winston.createLogger({
  transports: [transport]
});
配置项 说明 我的建议值
datePattern 文件名中的日期格式 YYYY-MM-DD
zippedArchive 是否压缩旧日志 true(省空间)
maxSize 单个文件大小上限 20m 或 50m
maxFiles 保留天数或文件数 14d 或 30d
注意:如果你同时设置了 maxSizemaxFiles,Winston会先按大小切割,再按天数清理。我曾经遇到过只设了 maxSize 没设 maxFiles,结果日志文件越切越多,最后还是把磁盘撑爆了。两个参数最好都设上。

4.3 多传输通道配置:不同日志走不同路

生产环境里,我们通常不会把所有日志都写到一个地方。比如:

  • debug/info 级别的日志,写到控制台和普通日志文件
  • warn 级别的日志,额外写到告警文件
  • error/fatal 级别的日志,不仅要写文件,还要发到监控系统

Winston的多传输通道配置,说白了就是给不同级别的日志配不同的Transport。我习惯这么配:

const logger = winston.createLogger({
  level: 'debug',
  transports: [
    // 所有日志都写到控制台
    new winston.transports.Console({
      level: 'debug',
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),

    // info及以上写到普通日志
    new winston.transports.File({
      filename: 'logs/info.log',
      level: 'info',
      maxsize: 10485760, // 10MB
      maxFiles: 5
    }),

    // error及以上写到错误日志,并发送告警
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      handleExceptions: true
    }),

    // 自定义Transport:错误日志发到企业微信
    new WeChatTransport({
      webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx',
      level: 'error'
    })
  ]
});

你看,每个Transport都可以单独设置 level。Winston在写日志的时候,会判断日志级别是否满足该Transport的要求。满足就写,不满足就跳过。说白了就是一层过滤。

4.4 日志分级过滤:精准控制输出

日志分级过滤,其实前面已经用到了。但这里我要讲一个更细的玩法——按模块过滤

比如你的项目里有用户模块、订单模块、支付模块。你只想看支付模块的debug日志,其他模块只看warn以上。怎么做?

Winston提供了 levelfilter 两种方式。我个人更推荐用 child logger 的方式:

// 创建基础logger
const baseLogger = winston.createLogger({
  level: 'warn',
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: 'logs/app-%DATE%.log',
      datePattern: 'YYYY-MM-DD'
    })
  ]
});

// 为支付模块创建子logger,级别设为debug
const paymentLogger = baseLogger.child({
  module: 'payment',
  level: 'debug'  // 注意:这里不能直接覆盖level
});

// 正确做法:单独创建一个带过滤的Transport
const paymentTransport = new winston.transports.Console({
  level: 'debug'
});

// 或者用filter函数
const paymentFilter = winston.format((info) => {
  if (info.module === 'payment' && info.level === 'debug') {
    return info;
  }
  return false; // 过滤掉
})();

const paymentLogger = winston.createLogger({
  level: 'debug',
  format: paymentFilter,
  transports: [
    new winston.transports.Console()
  ]
});
核心思路:format 里的filter函数,或者用 child loggerdefaultMeta,再配合Transport的 level 配置,就能实现非常精细的日志过滤。说白了就是「先分类,再过滤」。

4.5 实战组合:一个完整的生产配置

最后,我给你一个我在生产环境里实际用过的配置模板。你直接拿去改改就能用:

const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

// 自定义Transport:发送到监控系统
class MonitorTransport extends winston.Transport {
  constructor(options) {
    super(options);
    this.name = 'monitorTransport';
    this.apiUrl = options.apiUrl;
  }

  log(info, callback) {
    // 异步发送,不阻塞
    setImmediate(() => {
      fetch(this.apiUrl, {
        method: 'POST',
        body: JSON.stringify(info),
        headers: { 'Content-Type': 'application/json' }
      }).catch(() => {});
    });
    callback(null, true);
  }
}

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    // 控制台:开发环境用
    new winston.transports.Console({
      level: 'debug',
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),

    // 普通日志:按天轮转
    new DailyRotateFile({
      filename: 'logs/app-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      zippedArchive: true,
      maxSize: '20m',
      maxFiles: '14d',
      level: 'info'
    }),

    // 错误日志:单独存放
    new DailyRotateFile({
      filename: 'logs/error-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      zippedArchive: true,
      maxSize: '50m',
      maxFiles: '30d',
      level: 'error',
      handleExceptions: true
    }),

    // 监控系统:只发error及以上
    new MonitorTransport({
      apiUrl: 'https://monitor.example.com/logs',
      level: 'error'
    })
  ],
  exitOnError: false
});

module.exports = logger;

嗯,这一章的内容就这些。说白了就是三件事:自定义Transport让你能往任何地方写日志日志轮转防止磁盘爆炸多通道+分级过滤让你精准控制日志流向。下一章我们会聊聊日志的格式化输出和性能优化,到时候见。