使用 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、日志级别、耗时字段和敏感信息脱敏,提高线上故障排查效率。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐