一、日志系统概述:为什么需要日志系统、日志级别、核心设计原则
说实话,我刚开始写 Node.js 那会儿,对日志这事完全不上心。
觉得「console.log 打天下」就够用了。直到有一次线上服务半夜挂了,我盯着黑乎乎的终端,连错误栈都没留下…… 那叫一个抓瞎。
从那以后,我养成了一个习惯:任何上线的服务,日志系统必须优先设计。
1.1 为什么需要日志系统?
你想想看,一个 Node.js 应用跑在生产环境,你没法像开发时那样打断点调试。出了问题怎么办?
日志就是你的「黑匣子」。它记录了系统运行过程中的一切关键信息。
我个人总结,日志系统至少解决三个核心问题:
- 问题定位:线上报错了,是参数问题、数据库挂了、还是第三方接口超时?日志能告诉你真相。
- 行为审计:谁在什么时候做了什么操作?尤其是金融、电商类项目,日志是合规的底线。
- 性能分析:接口响应慢在哪一步?通过日志埋点,你能精确到毫秒级。
核心观点:没有日志的系统,就像蒙着眼睛开车。你永远不知道下一秒会撞上什么。
1.2 日志级别:从 debug 到 fatal
日志级别这东西,说白了就是给信息分个「轻重缓急」。我见过不少新手把所有日志都打成 info,结果排查问题时根本找不到重点。
Node.js 社区常用的日志级别,从低到高是这样的:
| 级别 | 含义 | 使用场景 | 生产环境是否输出 |
|---|---|---|---|
| DEBUG | 调试信息 | 开发阶段,打印变量值、函数调用栈 | 否 |
| INFO | 普通信息 | 记录系统正常运行状态,如「用户登录成功」「订单创建」 | 是 |
| WARN | 警告 | 潜在问题,但不影响当前功能。比如「磁盘使用率超过80%」 | 是 |
| ERROR | 错误 | 功能不可用,但系统还能继续运行。比如「数据库连接失败,已重试3次」 | 是 |
| FATAL | 致命错误 | 系统无法继续运行,必须立即停机。比如「内存耗尽」「配置文件损坏」 | 是 |
我的经验:DEBUG 日志在生产环境一定要关掉。我曾经见过一个项目,DEBUG 日志里打印了完整的 SQL 语句和用户密码哈希,这要是泄露出去…… 嗯,后果你懂的。
实际编码中,我们通常这样使用:
const logger = require('./logger');
// 调试阶段
logger.debug('用户输入参数:', req.body);
// 正常流程
logger.info('用户 %s 登录成功', userId);
// 潜在风险
logger.warn('第三方支付接口响应超时,已触发降级策略');
// 业务错误
logger.error('订单 %s 支付失败,原因:余额不足', orderId);
// 系统崩溃
logger.fatal('无法加载配置文件,系统退出');
1.3 日志系统的核心设计原则
设计一个日志系统,不是随便写个函数就完事了。我踩过不少坑,总结出下面几条原则:
原则一:结构化
别用纯文本。用 JSON 格式输出日志,这样后续分析、搜索、聚合都方便。
// 不推荐
[2024-01-15 10:30:00] ERROR 用户登录失败
// 推荐
{"timestamp":"2024-01-15T10:30:00.000Z","level":"ERROR","message":"用户登录失败","userId":12345,"ip":"192.168.1.1"}
原则二:可配置
日志级别、输出位置、格式,都应该通过配置文件控制。不要写死在代码里。
// config/logger.js
module.exports = {
level: process.env.LOG_LEVEL || 'info',
output: process.env.LOG_OUTPUT || 'file', // console / file / elasticsearch
format: process.env.LOG_FORMAT || 'json'
};
原则三:低开销
日志写入不能阻塞主流程。尤其是高并发场景,用异步写入、缓冲区、批量提交。
避坑指南:我曾经在某个高并发项目里,直接用 fs.writeFileSync 写日志。结果 QPS 一上来,整个进程卡死。后来改用 stream 和缓冲区,问题才解决。
原则四:上下文传递
每个请求的日志,应该能关联起来。比如带上 requestId、userId、traceId。
// 使用 AsyncLocalStorage 实现
const { AsyncLocalStorage } = require('async_hooks');
const storage = new AsyncLocalStorage();
// 中间件
app.use((req, res, next) => {
const context = { requestId: uuid(), userId: req.user?.id };
storage.run(context, () => next());
});
// 日志函数自动获取上下文
function log(level, message) {
const ctx = storage.getStore() || {};
console.log(JSON.stringify({ ...ctx, level, message }));
}
原则五:分级存储
不同级别的日志,存到不同的地方。DEBUG 日志可以本地保留3天,ERROR 日志要长期归档。
| 级别 | 存储位置 | 保留周期 |
|---|---|---|
| DEBUG | 本地文件 | 3天 |
| INFO | 本地文件 + 日志中心 | 30天 |
| WARN | 日志中心 + 告警 | 90天 |
| ERROR | 日志中心 + 即时告警 | 180天 |
| FATAL | 日志中心 + 电话告警 | 永久 |
1.4 小结
日志系统不是「锦上添花」,而是「雪中送炭」。
我见过太多项目,前期图省事用 console.log,后期线上出问题只能靠猜。说白了,日志系统设计得好不好,直接决定了你半夜被叫醒的次数。
嗯,这一章先聊到这。下一章我们动手实现一个 Node.js 日志库,从零开始写代码。
一句话总结:日志级别要分明,设计原则要牢记。别让「console.log」成为你职业生涯的绊脚石。
公众号:蓝海资料掘金营,微信deep3321