# Step 62: 遥测 / 日志 / 错误上报

一句话导读:给 agent 加结构化运行日志,但核心纪律是“记类型和规模,不记代码、路径、密钥和 prompt 原文”。


# 一、这一步做了什么(What)

step62 新增 services/telemetry/:Telemetry 负责记录事件、擦洗敏感字段、批量落盘;fromEngine.ts 负责把 EngineEvent 映射成安全遥测。终端还新增 /telemetry 命令,可以查看事件、开关遥测、演示 PII 擦洗。

这一步没有接远程上报,只写本地 log/telemetry.jsonl。教学重点不是“发到哪”,而是“什么东西绝不能发”。

# 二、面试官视角:为什么要做?(Why)

面试题:AI 编程工具为什么需要遥测?为什么又不能直接把日志全打出来?

需要遥测,是因为 agent runtime 很复杂:工具调用、权限拒绝、缓存命中、错误、耗时、成本都需要可观测,否则排障只能靠猜。不能全打,是因为用户的代码、路径、密钥、prompt 都可能是敏感信息。

应该记录 不应该记录
工具名、成功/失败、耗时 Bash 命令原文
token、cost、cacheRead 用户 prompt 原文
错误类型、擦洗后的短消息 文件路径、密钥、长代码

遥测的边界是:知道发生了什么类型的事,不知道用户具体做了什么内容。

# 三、原理:它是怎么工作的(How)

主角在 step62/services/telemetry/telemetry.ts:

export function scrubValue(v: string | number | boolean) {
  // 路径、密钥、长文本都会被替换成 [redacted:*]
}

export class Telemetry {
  logEvent(name: string, metadata: Metadata = {}) {
    const ev = { name, metadata: scrubMetadata(metadata), ts: Date.now() };
    this.queue.push(ev);
  }
}
1
2
3
4
5
6
7
8
9
10

引擎事件映射在 fromEngine.ts,只挑安全字段:

case "tool_result":
  t.logEvent("tool_use", { tool: ev.name, ok: ev.ok, ms: ev.elapsed });
  break;
case "round":
  t.logEvent("turn", { inT: ev.inT, outT: ev.outT, cacheRead: ev.cacheRead });
  break;
1
2
3
4
5
6

注意这里没有记录 ev.preview、没有记录工具参数、没有记录流式文本。index.ts 里把它接入事件循环:

const telemetry = new Telemetry({ enabled: !process.env["DISABLE_TELEMETRY"] });
recordEngineEvent(telemetry, ev);
telemetry.flush();
1
2
3

数据流:

EngineEvent
  → recordEngineEvent 只选安全字段
  → Telemetry.logEvent
  → scrubMetadata / scrubValue
  → queue
  → flush 到 log/telemetry.jsonl
1
2
3
4
5
6

# 四、深入追问(面试常见 follow-up)

Q:为什么工具名可以记,命令参数不能记?
A:工具名是短枚举,表达类型;命令参数可能包含路径、密钥、代码和业务数据,属于内容。

Q:错误 message 也可能有路径,怎么办?
A:logError() 也走 scrubValue。这就是擦洗层必须在统一入口做,而不是靠每个调用点自觉。

Q:为什么不记录 text/thinking 流式内容?
A:它们很可能直接包含用户代码或模型对代码的复述,且遥测分析价值低。记录 token/cost/耗时即可。

Q:opt-out 为什么重要?
A:遥测是信任边界。即便本地教学版不外发,也要保留 DISABLE_TELEMETRY 和 /telemetry off 这种退出机制。

# 五、踩坑 / 设计权衡

本步选择“保守擦洗”:像路径、密钥、长文本的字符串一律 redact。代价是有些无害长文本也会被盖掉,但遥测宁可少一点,也不能多泄一点。

# 六、与真实源码的对照

我们的实现 Claude Code 源码
Telemetry.logEvent(name, metadata) services/analytics/ 同款事件 API
scrubValue 挡路径/密钥/长文 真实用类型名和 PII 分列强约束
本地 JSONL 队列落盘 sink 批量上报、采样、killswitch
/telemetry on/off/scrub 动态配置、远程开关、采样率

# 七、一句话总结

遥测不是“把日志打全”,而是把运行过程结构化成安全元数据:工具名、耗时、token、成本可以记,代码、路径、密钥和 prompt 原文必须被挡在门外。

# 下一节预告

下一篇是 step63 源码对照②:把权限、沙箱、分类器、缓存、设置、遥测与真实 Claude Code 的安全/性能层逐项对齐。