成为全栈·Node 后端篇·结构化日志与请求链路追踪

成为全栈·Node 后端篇·结构化日志与请求链路追踪

开发时我们习惯了 console.log('到了这里')console.log(user) 这种"打印大法"。它管用,因为你就一个人、一个断点、一个终端。可一旦项目上线、流量进来、用户开始报"我刚才那条请求怎么没了"------你会发现 console.log 像沙滩上的字,潮水一冲啥都不剩。

这一篇聊日志与链路追踪。它不性感,但决定了你半夜被叫起来时,是十分钟定位问题、还是通宵瞎猜。

一、为什么需要 traceId(请求链路 ID)

想象一个请求进来的生命周期:路由校验 → 调 service → 查三次数据库 → 调一次存储 → 返回。期间可能打了十几条日志,分布在五六个文件里。现在用户说"下午三点那次提交失败了",你要在浩如烟海的日志里,把属于那一次请求的十几条日志挑出来------靠什么挑?靠时间?同一秒可能有上千请求。靠"提交失败"关键字?那只能捞出失败的那一条,前面成功的步骤全丢了。

答案是给每次请求发一个唯一 ID ,它贯穿这次请求的所有日志。我们的信封里已经埋好了这个 ID------回忆 M1-08 讲过的 requestId

ts 复制代码
const envelope = <T>(code: number, message: string, data: T | null): Envelope<T> => ({
  code, message, data,
  requestId: requestId(),          // 每次响应即时生成
  timestamp: new Date().toISOString(),
});

requestId 就是天然的 traceId(链路追踪 ID) 。用户报障时把响应里的 requestId 甩给你,你拿它去日志系统一搜,这次请求从进来到崩溃的完整轨迹全出来了。这就是"快递单号"的价值------没有它,包裹在仓库里就是一团乱麻。

二、日志分级:不是所有日志都一样重要

console.log 把所有信息混为一谈。生产环境我们需要分级,常见的四档:

  • debug:开发调试细节,生产默认关闭(量太大、含敏感风险)。
  • info:正常业务节点("用户 7 发布了文章 123"),生产可保留关键路径。
  • warn:不该发生但不致命("缓存未命中,降级查库")。
  • error:出错了(异常、外部调用失败),必须告警。

我们目前代码里最显眼的一处错误级日志,就在 src/middleware/error.ts 的兜底分支:

ts 复制代码
// 兜底:未知异常不应向客户端泄露堆栈
console.error('[unhandled]', err);
return failResponse(ErrCode.INTERNAL, 500);

未知异常来临时,它把完整错误打到服务端(注意:只打服务端、不返回客户端,呼应 M1-09 的"堆栈不泄露")。这是错误级日志的典范------它存在的唯一目的,就是出事时让你能顺着它查到根因。

分级的意义在于:生产环境可以把级别调到 info 或 warn,关掉 debug 的噪音,既省存储又让真正的异常不被淹没。级别是可调的旋钮,不是写死的。

三、结构化日志:让机器也能读懂

console.log('用户 ' + u.id + ' 登录了') 这种字符串拼接,人看着还行,机器处理起来是灾难------你想统计"今天登录失败多少次",得用正则去扒文本,脆弱又低效。

结构化日志的做法是:每条日志是一个 JSON 对象,字段固定、值规范:

json 复制代码
{ "level": "info", "ts": "2026-08-27T15:04:00Z", "msg": "user_login", "userId": 7, "traceId": "a1b2c3" }

好处是立竿见影的:

  1. 可检索level:error AND userId:7 一条查询就能定位,不用正则。
  2. 可聚合:统计、画仪表盘、设告警都建立在结构化字段上。
  3. 可演进:加字段不影响旧解析,字段命名有契约可依。

举个具体对比就懂差距了。字符串日志时代,你想查"用户 7 的所有登录失败",得写 grep "登录" app.log | grep "失败" | grep "userId=7"------可一旦某同事日志里写成 uid:7user 7,你的 grep 瞬间漏查。结构化日志时代,一条查询 level:error AND event:login_failed AND userId:7 精准命中,因为字段名稳定、值规范,不受文案怎么写影响。当日志量涨到每天千万条,这种"可检索性"的差距,就是"十分钟查到"和"查死也查不到"的区别。

我们信封里的 requestId / timestamp 其实就是"结构化思维"在响应层的体现------既然响应都结构化了,日志也没理由退回字符串。项目目前用 console.error 起步,但向生产演进时,第一件事就是换成结构化 logger(如 pino / winston),把字符串改成 JSON 对象。

四、请求链路串联:一条线串起所有日志

把第一、三节合起来,就是"链路追踪"的完整画面:

  1. 请求进入,生成 requestId(我们的信封每次响应都生成,handler 内部也可复用同一个)。
  2. 这次请求里每一条日志都带上 traceId: requestId
  3. 排查时,按 traceId 把分散在各文件、各函数的日志按时间排序,还原出完整时间线。

举个排查场景:用户说"我发布的文章没显示"。你拿到 requestId,一搜发现这条链路里:info 接收创建请求info 写入 articles 表成功(id=123)warn 标签关联 article_tags 插入失败,已降级info 返回 200。一眼就看出:文章存成功了,但标签关联挂了导致前端以为没发布成功。定位从"通宵"变成"十分钟"。

这正是《工程公约》里"日志要规范"的初衷 工程公约------规范的日志不是给写代码的人看的,是给三个月后凌晨两点的自己看的。

五、落盘与脱敏:日志也会泄密

日志写哪、写什么,两件事都不能含糊。

落盘:开发时打终端就够了;生产要把日志写到文件或集中收集系统(如 Loki / ELK),保留一段时间供检索。终端日志进程一退就没了,等于没记。同时还要考虑日志轮转(按大小或日期切割)和保留期------无限增长会撑爆磁盘,保留太久又增加敏感信息的暴露面,两者都要有个度,这正是运维和开发要一起商定的事。

脱敏 :这是最容易被忽略的安全坑。日志里绝不能出现密码、令牌、完整身份证号这类敏感字段 。我见过有人为了"方便排查"把登录请求的整个 body 打进日志,结果密码明文躺在日志系统里半年,等于把用户密码库白送攻击者。console.error('[unhandled]', err) 之所以安全,是因为 err 是程序异常对象,通常不含用户输入的明文密码;但如果你手滑 console.log('login body', body),那就是事故。铁律:日志只记"能公开给外人看"的信息, userId、操作类型、traceId 这些够了,密码令牌一个字都不许进日志。

还有个容易被忽视的点(P-50):日志字段命名也要跟契约对齐,别让它悄悄 stale 。我们信封的 requestId / timestamp 是契约定义的字段,如果将来引入结构化日志,它的字段名(如 traceId 还是 requestIdts 还是 timestamp)应当和契约词汇表保持一致,避免"代码里叫 traceId、契约里叫 requestId、文档里又叫 reqId"的三套名字漂移。注释和文档会随契约过时,日志字段名也是一样------凡是和契约沾边的命名,都以契约为准

六、诚实交代:我们目前到哪一步

写到这里得说句实话,免得你照着文章去 src/shared/logger.ts 找文件------当前冻结版本里并没有独立的 logger 模块 。我们有的是:信封自带的 requestId(链路追踪的地基已就位)、error.ts 里的 console.error('[unhandled]') 兜底(错误级日志的雏形)。

这是刻意的"轻量起步":先让关键信息(requestId、错误兜底)就位,不为了"看起来专业"而过早引入重型日志框架。等你项目真上了量,再引入 pino 这类零开销结构化 logger、接上集中收集,成本是低的------因为 requestId 已经贯穿全程,接入时只要把"每条日志带 traceId"这一步补上即可。架构的节奏感,往往比一步到位更重要。

七、小结与前瞻

日志与链路追踪,是把"能跑"推进到"可观测"的关键:

  1. traceId 是命脉 :每次请求一个唯一 ID,串起它所有的日志;我们的 requestId 已就位。
  2. 日志分级:debug/info/warn/error,生产可调级别、关掉 debug 噪音。
  3. 结构化优先:日志写 JSON 而非字符串,机器可检索、可聚合。
  4. 链路串联 :每条日志带 traceId,排查时按 ID 还原时间线。
  5. 落盘 + 脱敏:日志写文件/集中收集;密码令牌绝不进日志。
  6. P-50 :日志字段命名也跟契约对齐,别让 traceId/requestId 这类名字三套漂移。
  7. 诚实现状 :当前用 requestId 信封 + console.error 兜底起步,logger 模块留待上量时引入------地基已稳,演进成本低。

下一篇({{LINK:M1-12}})我们聊认证方案:JWT 还是 Session?两者真实差异在哪、无状态意味着什么代价、以及我们为什么选"JWT 访问令牌 + 有状态刷新令牌"这条折中路线。


如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

相关推荐
Flynt4 小时前
NestJS 12升级踩坑:从Webpack到Rspack,我折腾了一整个周末
typescript·node.js·nestjs
FungLeo17 小时前
成为全栈·Node 后端篇·配置管理:环境变量、多环境与密钥安全
node.js·环境变量·多环境配置·密钥安全
FungLeo18 小时前
成为全栈·Node 后端篇·错误处理:异常分层与全局捕获
node.js·错误处理·异常分层·全局捕获·成为全栈
百万运营Pro1 天前
用 Astro + Supabase 从零构建全网盘聚合搜索引擎:PGroonga 中文检索实战
搜索引擎·前端框架·node.js·个人开发·学习方法·ai编程·资源分享
小婉2 天前
我用 Next.js + React Flow 从零搭建了一个可视化 AI 工作流编排平台
前端·人工智能·node.js
抓不住时间的沙2 天前
N1搭建守护环境以及重装 Armbian 后完整恢复整套守护环境,清理日志步骤
node.js
meilindehuzi_a2 天前
从域名到数据库:React + Node.js 项目部署全流程与用户访问链路
数据库·react.js·node.js
李游Leo2 天前
Node.js 开发环境安装与 npm/pnpm 国内镜像配置(Windows / macOS / Linux)
npm·node.js·pnpm·前端开发·开发环境
阿黎梨梨3 天前
AI也有记忆?LangChain Memory 管理指南
langchain·node.js·llm