14. 日志系统设计:日志级别、日志格式、日志存储、使用 Winston 库

日志这东西,说实话,很多新手容易忽视。

我刚开始做信令服务器那会儿,也觉得日志可有可无。直到有一次线上出了个诡异的问题——用户连不上,但代码看起来一切正常。我愣是盯着控制台看了半小时,啥也没看出来。后来加了日志,才发现是某个边缘情况下的 WebSocket 连接没释放。

嗯,从那以后,我再也不敢轻视日志系统了。

日志级别:不是所有消息都值得记录

日志级别,说白了就是给消息分个优先级。你想想看,如果每秒钟几百个信令消息都打印出来,那日志文件很快就炸了,真正有用的信息反而被淹没了。

我个人习惯用这五个级别:

级别 含义 使用场景
error 错误 连接失败、数据库异常、崩溃
warn 警告 重试次数过多、配置异常
info 信息 用户连接、房间创建、信令转发
debug 调试 ICE 候选者详情、SDP 内容
verbose 冗余 网络包细节、内部状态变化
我的经验:生产环境一般开到 info 级别就够了。debug 和 verbose 只在排查问题时临时开启,用完就关掉。我曾经见过有人把 verbose 开了一整周,结果日志文件撑爆了磁盘。

日志格式:一眼就能看懂

日志格式,我追求的是「一眼就能定位问题」。你想想看,如果日志长这样:

[2024-01-15 10:30:22] [info] user connected

嗯,信息太少了。谁连接的?从哪个房间?用了什么协议?

我建议的格式是这样的:

2024-01-15T10:30:22.123Z [info] [room:abc123] [user:user_456] User connected via WebSocket

关键信息一目了然:时间、级别、房间 ID、用户 ID、具体事件。

这里有个小技巧——时间戳用 ISO 格式,带毫秒。为什么?因为信令服务器处理消息的速度很快,毫秒级的时间戳能帮你精确还原事件顺序。

日志存储:别让日志成为负担

日志存哪里?这是个好问题。

我见过三种方案:

  • 文件存储:最简单,按天或按大小切割。适合单机部署。
  • 控制台输出:开发时用,方便实时查看。
  • 远程收集:生产环境推荐,比如用 ELK 或 Loki。方便集中管理和检索。

我个人习惯是:开发时同时输出到控制台和文件,生产环境只输出到文件,再通过 Filebeat 之类的工具转发到远程。

注意:千万不要把日志直接写到数据库里。我曾经见过有人这么干,结果日志写入成了性能瓶颈,信令延迟飙升。日志就是日志,别让它影响核心业务。

使用 Winston:Node.js 生态的日志利器

Winston 是我在 Node.js 项目里最常用的日志库。它灵活、稳定,而且社区活跃。

先安装:

npm install winston

然后配置一个基本的 logger:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }),
    winston.format.printf(({ timestamp, level, message, ...meta }) => {
      return `${timestamp} [${level}] ${message} ${JSON.stringify(meta)}`;
    })
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' })
  ]
});

module.exports = logger;

这段代码做了几件事:

  • 设置了默认级别为 info
  • 定义了日志格式,包含时间戳、级别、消息和附加信息
  • 同时输出到控制台和文件
  • 错误日志单独存一份,方便快速定位

用起来也很简单:

const logger = require('./logger');

logger.info('User connected', { roomId: 'abc123', userId: 'user_456' });
logger.error('WebSocket connection failed', { error: err.message });
一个小技巧:Winston 支持自定义传输器。我做过一个项目,把日志同时写到了 Kafka 里,方便实时流处理。如果你有类似需求,可以看看 winston-kafka-transport。

日志系统的核心逻辑

下面这张图展示了日志系统在信令服务器中的位置和作用:

日志系统核心逻辑 信令服务器 WebSocket / HTTP 请求 Winston Logger 日志级别过滤 日志输出 控制台 / 文件 / 远程 日志级别判断 日志格式化 日志存储策略 error / warn / info / debug / verbose 时间戳 + 级别 + 消息 + 元数据 文件切割 / 远程收集 / 归档 图:日志系统从产生到存储的完整流程

从图中可以看到,日志系统不是简单地把消息扔出去。它要经过级别判断、格式化、存储策略三个环节。每个环节都有讲究。

避坑指南

最后分享几个我踩过的坑:

  • 不要记录敏感信息:比如用户的 IP 地址、Token。我曾经在日志里不小心打了用户的 JWT,还好及时发现。建议加个过滤函数,把敏感字段替换掉。
  • 日志文件要轮转:用 winston-daily-rotate-file 插件,按天切割。不然一个文件几百兆,打开都费劲。
  • 异步写入:Winston 默认是异步的,但如果你用了同步的传输器,可能会阻塞事件循环。注意检查。
  • 日志别打太多:每秒钟几百条日志,磁盘 IO 扛不住。我一般把 verbose 级别的日志直接丢弃,除非调试模式开启。

好了,日志系统就聊到这儿。记住一句话:日志是你的眼睛,别让它蒙尘。


公众号:蓝海资料掘金营,微信deep3321