Codex写日志为什么越加越难排查?用结构化日志和Trace ID定位线上问题
使用 Codex 修复线上 Bug 时,最常见的第一步往往是:
多加一点日志看看。
于是项目里逐渐出现:
console.log("进入这里");
console.log("user:", user);
console.log("request:", req);
console.log("error:", error);
短期看似信息更多了,但真正上线以后却经常遇到:
-
同一个请求的日志散落在几十行里;
-
多个用户同时访问时根本分不清谁是谁;
-
报错日志只有一句“请求失败”;
-
一个接口经过多个服务后无法串起来;
-
日志数量巨大,却找不到问题第一次发生的位置;
-
Token、手机号甚至完整请求体被打印进日志;
-
Codex 为了排查问题不断增加
console.log,最终日志越来越乱。
真正可用的线上日志,不是“打印得足够多”,而是每一条日志都能够被搜索、关联和解释。
一、为什么普通console.log不够用?
假设接口代码:
async function createOrder(req) {
console.log("开始创建订单");
const user = await getUser(req.userId);
console.log("用户查询完成");
const order = await saveOrder(req.body);
console.log("订单创建成功");
return order;
}
只有一个请求时很好理解。
但生产环境同时有100个请求:
开始创建订单
开始创建订单
用户查询完成
开始创建订单
订单创建成功
用户查询完成
...
你已经不知道:
哪一个“订单创建成功”
对应:
哪一个“开始创建订单”
所以线上日志首先要解决的不是“打印什么”,而是:
这些日志属于同一个请求吗?
二、给每个请求分配Trace ID
请求进入系统时生成一个唯一标识:
traceId = req_8f21a7
之后所有日志都带上它:
{
"level": "info",
"traceId": "req_8f21a7",
"event": "order_create_started",
"userId": 1001
}
数据库写入成功:
{
"level": "info",
"traceId": "req_8f21a7",
"event": "order_created",
"orderId": 90001
}
发生异常:
{
"level": "error",
"traceId": "req_8f21a7",
"event": "order_create_failed",
"errorType": "DatabaseTimeout"
}
排查时只需要搜索:
traceId=req_8f21a7
就能看到完整调用过程。
三、结构化日志比字符串日志更容易搜索
不推荐:
logger.info(
`用户 ${userId} 创建订单 ${orderId} 成功`
);
更推荐:
logger.info({
event: "order_created",
userId,
orderId,
traceId
});
结构化日志的好处是可以直接查询:
event = order_created
或者:
userId = 1001
甚至:
errorType = DatabaseTimeout
如果全部是自然语言字符串,后续统计和过滤都会更困难。
四、给日志事件固定命名
不要在不同地方分别写:
订单创建完成
订单生成成功
create order success
order ok
虽然人能理解,但机器搜索时属于四种不同事件。
可以统一:
order_create_started
order_created
order_create_failed
例如:
logger.info({
event: "order_create_started",
traceId
});
成功:
logger.info({
event: "order_created",
traceId,
orderId
});
失败:
logger.error({
event: "order_create_failed",
traceId,
errorType: error.name
});
事件命名稳定以后,监控和统计会简单很多。
五、Info、Warn、Error不要乱用
一个常见问题是所有日志都使用:
logger.error(...)
结果线上每天出现几万条“错误”,真正严重的问题反而被淹没。
可以简单区分:
DEBUG
开发排查细节:
请求参数解析结果
缓存命中情况
内部状态
INFO
正常业务事件:
用户登录成功
订单创建完成
任务处理完成
WARN
出现异常但系统仍能继续:
缓存读取失败,已回源数据库
第三方接口第一次超时,准备重试
ERROR
明确失败,需要关注:
数据库事务失败
订单创建失败
消息发送达到最大重试次数
日志级别应该代表问题严重程度,而不是“这条日志看起来重要”。
六、错误日志必须保留原始原因
错误写法:
catch (error) {
logger.error("创建订单失败");
}
日志只告诉你:
失败了
却不知道为什么。
更合理:
catch (error) {
logger.error({
event: "order_create_failed",
traceId,
errorType:
error instanceof Error
? error.name
: "UnknownError",
message:
error instanceof Error
? error.message
: String(error)
});
throw error;
}
如果错误支持 cause,也应该尽量保留错误链。
真正有价值的问题通常是:
谁失败了?
在哪里失败?
为什么失败?
而不是只记录:
操作失败
七、日志里不要直接打印完整对象
Codex 为了方便排查,经常会写:
console.log(req.body);
console.log(user);
console.log(headers);
这可能把敏感信息直接写入日志。
例如:
password
Authorization
cookie
accessToken
refreshToken
身份证号
银行卡
手机号
尤其是:
console.log(req.headers);
很容易把:
Authorization: Bearer xxx
一起保存。
日志系统通常保留时间很长,这会扩大数据泄露风险。
八、建立统一脱敏函数
例如:
function maskPhone(phone: string) {
return phone.replace(
/^(\d{3})\d{4}(\d{4})$/,
"$1****$2"
);
}
日志:
logger.info({
event: "sms_sent",
phone: maskPhone(phone)
});
对于Token:
禁止记录完整值
最多只记录:
tokenId
hash
前几位标识
日志的原则是:
排查问题所需的信息够用即可,不应该成为业务数据库的副本。
九、每个HTTP请求都记录哪些基础字段?
可以统一包含:
traceId
method
path
statusCode
duration
userId
tenantId
clientIp
例如:
{
"event": "http_request_completed",
"traceId": "req_a82f",
"method": "POST",
"path": "/api/orders",
"statusCode": 200,
"durationMs": 85,
"userId": 1001
}
这样可以直接分析:
哪个接口最慢?
哪个用户频繁报错?
哪些请求大量出现500?
十、duration比“开始/结束”两条日志更有价值
很多代码写:
logger.info("request start");
结束:
logger.info("request end");
真正排查性能问题时还需要手动计算时间。
更直接:
const startedAt = Date.now();
try {
return await run();
} finally {
logger.info({
event: "request_completed",
durationMs:
Date.now() - startedAt
});
}
然后就能统计:
P50
P95
P99
虽然日志不能完全替代指标系统,但 duration 是非常有价值的上下文字段。
十一、跨服务调用要继续传Trace ID
假设:
API Gateway
↓
Order Service
↓
Payment Service
↓
Notification Service
如果 Order Service 每次重新生成新的 Trace ID:
Gateway:A
Order:B
Payment:C
调用链仍然无法串起来。
应该把上游:
traceId
继续放入请求头:
X-Trace-Id: req_8f21a7
下游继续使用同一个标识。
这样一个用户请求经过多个服务后,仍然可以通过一个 Trace ID 搜索完整过程。
十二、Trace ID和Request ID可以分开
复杂系统中可以进一步区分:
Trace ID
=
整个调用链唯一ID
和:
Request ID
=
当前服务收到的单次请求ID
例如:
traceId = trace_1001
gateway requestId = req_A
order requestId = req_B
payment requestId = req_C
这样既能串联整个流程,也可以定位单个服务请求。
小项目不一定需要这么复杂,但概念要明确。
十三、后台任务也要有Trace信息
日志追踪不仅适用于HTTP请求。
例如:
订单创建
↓
消息队列
↓
后台Worker发送通知
Worker已经脱离原来的HTTP请求。
消息中可以保留:
{
"eventId": "evt_1001",
"traceId": "trace_8f21a7",
"orderId": 90001
}
Worker日志:
{
"event": "notification_started",
"traceId": "trace_8f21a7",
"eventId": "evt_1001"
}
这样异步流程也能继续关联。
十四、不要每行代码都打日志
错误做法:
进入函数A
变量x=1
开始调用函数B
函数B执行完成
准备return
return完成
日志数量很大,却没有真正业务价值。
更推荐记录关键节点:
任务开始
重要状态变化
外部依赖失败
任务完成
任务失败
一个很实用的判断:
如果这条日志线上出现100万次,我还希望保留它吗?
如果答案是否定的,它可能更适合 DEBUG,甚至根本不需要存在。
十五、成功日志也不能无限打印
例如高频接口:
GET /health
每秒几百次。
如果每次都记录:
health check success
日志存储成本会非常高。
对于超高频正常事件,可以:
-
不记录;
-
采样;
-
使用Metrics统计;
-
只记录异常。
日志不是越完整越好,还要考虑成本。
十六、采样适合高频日志
例如搜索接口每秒:
10000次
如果每个成功请求都保存详细日志,可能没有必要。
可以采用:
成功请求采样1%
错误请求100%记录
慢请求100%记录
这样既保留排查能力,又控制日志量。
错误和异常流量一般不应该被过度采样。
十七、日志和Metrics不要混为一谈
如果想知道:
今天有多少请求?
错误率多少?
P95多少?
更适合 Metrics。
如果想知道:
用户1001那一次请求为什么失败?
更适合日志。
简单来说:
Metrics
回答“系统发生了多少次”
Logs
回答“这一次具体发生了什么”
二者应该互相配合。
十八、给慢请求单独打Warn
例如接口要求:
正常应该 < 500ms
可以设置:
if (durationMs > 1000) {
logger.warn({
event: "slow_request",
traceId,
path,
durationMs
});
}
这样排查性能问题时,可以直接搜索:
event=slow_request
不用从海量正常请求中一点点寻找。
十九、日志里记录SQL要谨慎
开发环境可以记录完整 SQL。
但生产环境如果把:
SELECT *
FROM users
WHERE phone = '13800138000'
完整写进日志,会泄露业务数据。
更适合记录:
queryName
durationMs
rowCount
database
例如:
{
"event": "db_query_slow",
"queryName": "find_user_by_phone",
"durationMs": 1800
}
需要 SQL 时再通过受控方式查看。
二十、让Codex先做日志审查
遇到线上难排查的问题时,可以先要求:
请先不要新增日志。
分析当前日志体系:
1. 是否存在统一Logger;
2. 是否有Trace ID;
3. HTTP请求之间能否关联;
4. 跨服务是否传递Trace ID;
5. ERROR日志是否保留真实异常;
6. 是否记录敏感数据;
7. 哪些日志属于重复噪声;
8. 哪些关键业务步骤完全没有日志。
先整理已有日志,再决定哪里应该补。
不要默认:
排查不到问题
=
日志数量不够
很多时候真正的问题是日志没有结构。
二十一、把日志规则写进AGENTS.md
# Logging规则
- 生产日志必须优先使用结构化格式
- 每个请求必须带traceId
- 跨服务调用必须继续传递traceId
- 错误日志必须保留errorType和原始原因
- 禁止记录密码、Token、Cookie等敏感信息
- 用户隐私字段必须脱敏
- 禁止为了排查问题直接打印完整Request对象
- 高频正常日志需要评估采样
- 慢请求必须记录duration
- 新增日志必须说明它解决什么排查问题
这样 Codex 后续排查 Bug 时,就不会简单地到处插入 console.log()。
二十二、一个推荐的日志结构
可以统一为:
{
"timestamp": "2026-08-14T14:20:01Z",
"level": "error",
"event": "order_create_failed",
"traceId": "trace_82f1",
"requestId": "req_219a",
"userId": 1001,
"orderId": 90001,
"durationMs": 1380,
"errorType": "DatabaseTimeout",
"message": "database request timeout"
}
不要求所有字段每次都存在。
但整体结构应该稳定。
二十三、Plus还是Pro?
如果主要使用 Codex 处理:
-
单服务日志;
-
普通Node.js项目;
-
API错误排查;
-
简单Trace ID改造;
Plus通常已经能够覆盖大部分开发场景。
如果长期维护:
-
微服务;
-
大量生产日志;
-
多服务Trace;
-
消息队列和Worker;
-
复杂线上问题排查;
-
多轮日志分析和代码修改;
则可以根据实际开发强度评估 Pro。
不过更大的上下文只能帮助分析更多日志。
如果日志本身没有Trace ID和结构化字段,再多日志也很难还原真实调用链。
总结
Codex 写日志以后,日志越来越多却越来越难排查,根本原因通常不是“日志不够”,而是日志没有统一结构和关联关系。
通过:
结构化日志
Trace ID
固定事件名
正确日志级别
耗时字段
敏感信息脱敏
可以让日志真正具备线上排查价值。
真正好的日志系统,不应该要求开发者阅读几万行文本才能猜到发生了什么。
它应该让你在拿到一个 Trace ID 后,很快回答:
这个请求从哪里进入?经过了哪些步骤?在哪一步开始变慢?最终为什么失败?
CSDN文章描述
本文介绍 Codex 编写日志时常见的日志噪声和调用链断裂问题,并通过结构化日志、Trace ID、日志级别、耗时字段和敏感信息脱敏,提高线上故障排查效率。
更多推荐



所有评论(0)