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。
日志系统的核心逻辑
下面这张图展示了日志系统在信令服务器中的位置和作用:
从图中可以看到,日志系统不是简单地把消息扔出去。它要经过级别判断、格式化、存储策略三个环节。每个环节都有讲究。
避坑指南
最后分享几个我踩过的坑:
- 不要记录敏感信息:比如用户的 IP 地址、Token。我曾经在日志里不小心打了用户的 JWT,还好及时发现。建议加个过滤函数,把敏感字段替换掉。
- 日志文件要轮转:用 winston-daily-rotate-file 插件,按天切割。不然一个文件几百兆,打开都费劲。
- 异步写入:Winston 默认是异步的,但如果你用了同步的传输器,可能会阻塞事件循环。注意检查。
- 日志别打太多:每秒钟几百条日志,磁盘 IO 扛不住。我一般把 verbose 级别的日志直接丢弃,除非调试模式开启。
好了,日志系统就聊到这儿。记住一句话:日志是你的眼睛,别让它蒙尘。