可观测性基础设施:Claude Code 如何观测自身运行
这篇文档只回答一个问题:
src/ 里的可观测性基础设施,到底是怎样分层、初始化、采集、脱敏并导出的?
先给结论:
- 这套实现不是一个单点 telemetry 模块,而是四层协作:
src/services/analytics/:业务事件采集与分发src/utils/telemetry/:OpenTelemetry 指标、日志、追踪与导出器src/bootstrap/state.ts:全局 meter / logger / tracer / counter 状态中枢src/context/stats.tsx:会话内轻量统计与本地落盘
- 系统同时维护多种信号类型,而不是把所有信息都塞进同一条管线:
- analytics event:面向产品行为、权限决策、功能使用
- metrics:面向会话级计数、成本、token、活跃时长
- logs / events:面向 OTel 事件记录和内部事件批量上报
- traces:面向 interaction / tool / LLM request 的时序链路
- 这套实现非常强调隐私边界:
- 默认不允许任意字符串直接进入 analytics metadata
- MCP/tool 名称、prompt 内容、workspace 路径都要经过显式门控
_PROTO_*字段只允许流向 1P 特权后端,不会进入 Datadog 这类通用后端
可以先把整体链路记成下面这条:
调用点
-> analytics / OTel API
-> sink / provider / state
-> exporter
-> Datadog / 1P event logging / OTLP / BigQuery / Perfetto / 本地 session metrics
1. 边界:可观测性不是一个目录,而是一组协作层
如果只看目录名,很容易把可观测性理解成 services/analytics/。
但真实实现更接近“四层拼接”:
1.1 第一层:services/analytics/ 负责业务事件
这一层关注的是:
- 用户做了什么
- 权限如何被批准或拒绝
- API 请求是否成功
- 插件、Skill、MCP 等能力是否被启用
它的统一入口是 src/services/analytics/index.ts,核心 API 只有:
logEvent()logEventAsync()attachAnalyticsSink()
也就是说,这一层的抽象不是“日志行”,而是“业务事件”。
1.2 第二层:utils/telemetry/ 负责 OTel 信号
这一层处理的是 OpenTelemetry 语义下的三种核心信号:
- metrics
- logs / event
- traces
对应的核心文件包括:
src/utils/telemetry/instrumentation.tssrc/utils/telemetry/events.tssrc/utils/telemetry/sessionTracing.tssrc/utils/telemetry/bigqueryExporter.tssrc/utils/telemetry/perfettoTracing.tssrc/utils/telemetry/betaSessionTracing.ts
它不是“业务埋点系统”的附庸,而是一套独立的运行时遥测基础设施。
1.3 第三层:bootstrap/state.ts 负责全局观测状态注入
src/bootstrap/state.ts 并不直接导出 exporter,但它保存了:
metersessionCounterlocCounterprCountercommitCountercostCountertokenCountercodeEditToolDecisionCounteractiveTimeCounterloggerProvidereventLoggermeterProvidertracerProviderstatsStore
所以它是“观测运行时句柄”的集中注册表。
1.4 第四层:context/stats.tsx 负责会话内轻量统计
src/context/stats.tsx 这层不是标准 OTel,也不是远端 analytics。
它更像一个:
- 进程内统计 accumulator
- UI 渲染性能采样器
- session 结束时落盘的本地 metrics store
这层解释了为什么仓库里既有 OTel counter,又有 StatsStore.observe()。
两者不是重复设计,而是分工不同。
2. 业务事件链路:logEvent 如何走到后端
2.1 src/services/analytics/index.ts 是统一入口
这一层最关键的设计是“先记事件,再挂 sink”。
logEvent() 和 logEventAsync() 不直接依赖 Datadog 或 1P logger,而是:
- 如果 sink 还没挂上,就把事件放进内存队列
- 等
attachAnalyticsSink()被调用后,再异步 drain 这些事件
这意味着 analytics API 本身被做成了一个无依赖、低耦合的门面层:
- 避免 import cycle
- 允许启动早期先记录事件
- 把真正的路由逻辑延迟到应用初始化完成之后
2.2 src/utils/sinks.ts 负责真正挂接 sink
initSinks() 会按顺序做两件事:
initializeErrorLogSink()initializeAnalyticsSink()
这里的顺序不是随意的。errorLogSink 注释里明确要求它在 analytics sink 之前初始化,这样 analytics 路由过程中产生的问题也有地方可记。
2.3 src/services/analytics/sink.ts 负责 fanout
这个模块把业务事件从统一入口分发到两个后端:
- Datadog
- 1P event logging
它承担的不是单纯“转发”,而是整个 analytics dispatch policy:
shouldSampleEvent():按 event name 做采样initializeAnalyticsGates():初始化 Datadog gateisSinkKilled():按 sink 维度做 killswitchstripProtoFields():在通用后端前剥离_PROTO_*字段
也就是说,真正的 analytics 规则集中在这里,而不是散落在调用点。
2.4 Datadog 与 1P 后端不是对称关系
src/services/analytics/datadog.ts 和 src/services/analytics/firstPartyEventLogger.ts 看起来都在“发事件”,但角色不同。
datadog.ts 的特点:
- 只接受 allowlist 中的事件名
- 只在
production且 provider 为firstParty时发送 - 事件会做 cardinality reduction
_PROTO_*字段会在进入 Datadog 前被剥离- 以批量 HTTP POST 的方式发到 Datadog logs intake
firstPartyEventLogger.ts 的特点:
- 使用独立的
LoggerProvider - 通过
FirstPartyEventLoggingExporter批量发到/api/event_logging/batch - 支持采样配置、批处理配置、GrowthBook refresh 后重建 provider
- 失败事件可以落盘并重试
它们的关系更像:
- Datadog:通用事件分析后端
- 1P event logging:内部特权事件管线
2.5 初始化时序上,1P event logging 甚至早于完整 telemetry
src/entrypoints/init.ts 在 init() 里就会异步加载:
firstPartyEventLogger.jsgrowthbook.js
然后调用 initialize1PEventLogging()。
这样做的意义是:
- 即使完整 OTel telemetry 还没初始化
- 甚至 trust 后才初始化的那部分 telemetry 还未就绪
- 1P internal event logging 也可以更早开始工作
这能减少启动早期业务事件丢失。
3. OTel 基础设施:metrics、logs、traces 如何装起来
3.1 src/utils/telemetry/instrumentation.ts 是 OTel 装配核心
这个文件负责:
- 读取
OTEL_*环境变量 - 解析 metrics / logs / traces exporter 类型
- 动态导入不同 OTLP exporter
- 合并
resource信息 - 初始化
MeterProvider、LoggerProvider、BasicTracerProvider - 注册 shutdown / forceFlush 逻辑
- 初始化 Perfetto tracing
这里有两个重要特征。
第一,它大量使用 lazy import。
目的很明确:
- 避免 OpenTelemetry 和 gRPC 依赖在每次启动时都被提前加载
- 只有真正启用某类 exporter 时,才把对应实现拉进来
第二,它把 customer OTLP telemetry 与 1P event logging 分开装配。
firstPartyEventLogger.ts 使用的是自己的 LoggerProvider,并不会注册为全局 provider;而 instrumentation.ts 初始化的是 customer-facing 的 OTel provider。
这是一条明确的数据隔离边界。
3.2 src/entrypoints/init.ts 负责“信号装配”和“状态映射”
initializeTelemetryAfterTrust() 是完整 telemetry 初始化的入口。
它的时序不是“程序一启动就立刻做”,而是:
- 对 remote managed settings 用户,先等设置加载,再重新应用 env vars,再做 telemetry init
- 对普通路径,则直接初始化
- 在 headless + beta tracing 的场景下,还会走 eager init,避免首个 query 开始时 tracer 还没准备好
真正的初始化发生在 setMeterState():
- 动态导入
initializeTelemetry() - 拿到
meter - 基于
meter.createCounter()构造 attributed counter 工厂 - 调用
setMeter()把这些 counter 注册到bootstrap/state.ts - 补记一次
sessionCounter
所以 init.ts 的角色不是 exporter 本身,而是把 exporter 产出的运行时对象接入全局状态。
3.3 src/bootstrap/state.ts 是 OTel 运行时句柄的集中存储
setMeter() 会一次性构造多个标准计数器:
claude_code.session.countclaude_code.lines_of_code.countclaude_code.pull_request.countclaude_code.commit.countclaude_code.cost.usageclaude_code.token.usageclaude_code.code_edit_tool.decisionclaude_code.active_time.total
除此之外,bootstrap/state.ts 还提供:
setLoggerProvider()/getLoggerProvider()setEventLogger()/getEventLogger()setMeterProvider()/getMeterProvider()setTracerProvider()/getTracerProvider()
这使得业务代码不需要自己持有 provider,只需要从 state 中取句柄即可。
3.4 BigQuery metrics 是 metrics 管线的一部分,不是 analytics fanout 的一部分
src/utils/telemetry/bigqueryExporter.ts 实现了 PushMetricExporter,会把 OTel metrics 转成内部 API 可接受的 payload,然后发到 /api/claude_code/metrics。
它的特点是:
- 要求 trust 已建立或处于 non-interactive session
- 会检查组织级 metrics opt-out
- 导出的是 metrics,不是业务事件
- 使用 delta temporality,明确不走 cumulative
所以这条链路属于 metrics exporter,而不是 logEvent -> sink 那条业务事件链路。
4. 会话内统计:为什么还需要 StatsStore
4.1 src/context/stats.tsx 是一套轻量本地统计系统
这里的 StatsStore 支持四种操作:
increment()set()observe()add()
其中 observe() 不是简单相加,而是维护一个 histogram:
countsumminmax- reservoir sample
最终还能导出:
p50p95p99
4.2 它的核心用途是会话内统计和本地持久化
StatsProvider 会在进程退出时把 store.getAll() 的结果写入配置中的 lastSessionMetrics。
所以它更像:
- 本地 session summary
- UI / TUI 运行时指标
- 调试和回看辅助数据
而不是标准 OTel exporter 的替代品。
4.3 StatsStore 与 OTel counter 的职责不同
StatsStore 擅长:
- 轻量、局部、进程内聚合
- percentile 近似统计
- 会话结束后本地落盘
OTel counter 擅长:
- 远端导出
- 标准语义指标
- 带 attributes 的统一计量
所以可以把它们理解为:
StatsStore:本地运行态观测- OTel metrics:标准化外部观测
4.4 交互态会显式把 StatsStore 注入运行时
src/interactiveHelpers.tsx 的 getRenderContext() 会:
createStatsStore()setStatsStore(stats)- 把
stats交给App组件树
src/components/App.tsx 再通过 StatsProvider 注入 React 树。
这说明 StatsStore 是交互态 runtime 的一等公民,而不是某个页面私有的小工具。
5. Tracing 与 OTel 事件:运行链路如何被展开
5.1 src/utils/telemetry/sessionTracing.ts 负责 span 生命周期
这里定义的核心 span 类型包括:
interactionllm_requesttooltool.blocked_on_usertool.executionhook
这说明 tracing 不是只关心 API,而是把一次完整交互拆成多个运行阶段。
这个模块还做了几件非常关键的事:
- 用
AsyncLocalStorage保存当前 interaction / tool 上下文 - 给 orphan span 做 TTL 清理,避免内存泄漏
- 同时兼容 OTel tracing 与 Perfetto tracing
- 在 enhanced telemetry 和 beta tracing 两种模式下复用同一套 span 入口
5.2 src/utils/telemetry/events.ts 负责 OTel 事件日志
logOTelEvent() 不是业务 analytics,而是 OTel event record。
它会为每条事件补充:
- 通用 telemetry attributes
event.nameevent.timestampevent.sequenceprompt.id
此外还有一个非常重要的限制:
workspace.host_paths只写入 event attributes,不进入 metric dimensions
这体现了仓库对高基数路径字段的谨慎处理。
5.3 代表性调用点 1:用户输入
src/utils/processUserInput/processTextPrompt.ts 在处理用户输入时同时做了三件事:
startInteractionSpan(userPromptText)logOTelEvent('user_prompt', ...)logEvent('tengu_input_prompt', ...)
这非常能说明该仓库的设计风格:
- 同一个用户动作,可能同时产出 trace、OTel event、analytics event
- 但它们分别进入不同管线,承担不同用途
5.4 代表性调用点 2:API 请求
src/services/api/logging.ts 在 API 成功路径里会:
- 记录 analytics 事件,例如
tengu_api_query、成功/失败事件 - 记录 OTel 事件
api_request - 结束 LLM request span,并把 token、TTFT、输出信息挂到 span 上
这里可以清楚看到:
- analytics 负责产品/行为语义
- OTel event 负责结构化事件
- tracing 负责时序与上下文
5.5 代表性调用点 3:工具权限与执行
src/hooks/toolPermission/permissionLogging.ts 和 src/services/tools/toolExecution.ts 共同覆盖了工具权限决策链路。
这里会同时写入:
- analytics event,例如批准/拒绝事件
- OTel event
tool_decision - code edit tool counter
而 toolExecution.ts 还会在 headless 等非交互路径下补发这些信号,避免因为没有走 UI permission path 而丢观测数据。
这说明观测逻辑并不是只写在 UI 交互层,而是会在执行层补齐。
6. 隐私与数据治理:哪些内容能上报,哪些不能
6.1 metadata.ts 是 analytics 元数据的中心治理层
src/services/analytics/metadata.ts 做的不只是“补字段”,更像一个集中式 policy 层。
它负责:
- 构造公共 metadata
- 识别运行环境、平台、subscription、agent 身份
- 处理 MCP/tool 名称脱敏
- 处理文件扩展名提取
- 处理 bash 命令里可安全提取的扩展名
- 控制 tool input 在 telemetry 中的截断与深度
这比“调用点自己拼 metadata”要成熟得多。
6.2 类型标记本身就是一层制度约束
src/services/analytics/index.ts 和 metadata.ts 都定义了 marker type,例如:
AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHSAnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED
这些类型不会在运行时做校验,但在代码层表达了一个重要制度:
- 普通 analytics metadata 默认不应携带代码、路径或敏感原文
- 只有被显式标记为 PII-tagged 的字段,才允许进入特定后端
6.3 _PROTO_* 是一条双通道设计
这套实现里最关键的隐私机制之一,就是 _PROTO_* 字段。
它的工作方式是:
- 调用点可以把原始值放进
_PROTO_* stripProtoFields()会在发往 Datadog 等通用后端前剥离这些字段firstPartyEventLoggingExporter会识别并提升这些字段,送进 1P 特权 proto 列
所以同一条事件可以同时拥有:
- 通用后端可见的脱敏字段
- 仅 1P 后端可见的原始字段
这就是典型的 twin-column / dual-path 设计。
6.4 MCP/tool 名称默认不是随便上报的
默认情况下:
- MCP tool 名称会被
sanitizeToolNameForAnalytics()归一成mcp_tool - 只有满足特定条件时,才会通过
mcpToolDetailsForAnalytics()记录更细颗粒度的 MCP server / tool 名称
这些条件包括:
- local-agent 模式
- claude.ai proxy connector
- official registry 中的 MCP URL
也就是说,用户自定义 MCP 配置默认不会被当成普通低敏字段直接上报。
6.5 prompt、tool input、workspace path 都有显式门控
几个典型例子:
src/utils/telemetry/events.ts- 只有
OTEL_LOG_USER_PROMPTS=1时才会记录真实 prompt,否则写<REDACTED>
- 只有
src/services/analytics/metadata.ts- 只有
OTEL_LOG_TOOL_DETAILS=1时才会记录序列化后的 tool input - 而且会做长度截断、深度限制、键过滤
- 只有
src/utils/telemetry/events.tsworkspace.host_paths只进入 event,不进入 metric dimensions
这说明仓库对“可观测性”和“可泄漏性”是明确区分的。
7. 初始化顺序:这套基础设施何时变得可用
把关键顺序串起来,大致是这样:
程序启动
-> init()
-> 异步启动 1P event logging 与 GrowthBook
-> preAction / setup 挂 error log sink 和 analytics sink
-> trust 建立后执行 initializeTelemetryAfterTrust()
-> initializeTelemetry()
-> setMeterState()
-> bootstrap/state 注册 meter / logger / tracer / counters
-> 调用点开始持续写入 analytics / OTel / traces
这里有三个时序点值得特别记住。
第一,analytics API 可以早于 sink 调用。
因为 logEvent() 会先排队,等 sink 挂载后再 drain。
第二,1P event logging 可以早于完整 OTel telemetry。
因为 init() 里就已经启动了它。
第三,完整 telemetry 初始化要等 trust 和 remote managed settings 条件满足。
这也是为什么 initializeTelemetryAfterTrust() 不只是一个简单的“开关函数”,而是一段带条件和时序语义的编排逻辑。
8. 总结:这是一套分层而非单点的可观测性体系
最后可以把整个体系压缩成一句话:
这个仓库把“可观测性”拆成了三条主链路,再用全局 state 把它们接在一起:
services/analytics/:面向业务事件与产品行为utils/telemetry/:面向 OTel 指标、事件、追踪与导出context/stats.tsx:面向交互态本地统计与会话总结
它们共享一些调用点,但不会被粗暴混成一个系统。
所以如果要理解仓库里的 observability,最重要的不是记住某个 exporter,而是先分清三个问题:
- 这是 analytics event、metric、OTel event,还是 trace?
- 它进入的是通用后端、1P 特权后端,还是只留在本地 session?
- 它在上报前经过了哪些脱敏、采样、gate 和 killswitch?
把这三个问题想清楚,src/ 里的可观测性实现就基本能读通了。