章节导航 第 14 章 / 共 21 章
Tracing 与可观测性
自研 Span 体系、@observe 装饰器、SpanController、Processor 插件、远程 Trace 传播、Phase 系统
Nexus 自研 Tracing 的设计考量
Nexus 的 Trace 系统是自研的——nexus/trace/ 目录,约 70KB 代码。当初选择自研有三点考量:
- 轻量需求:不需要 collector、exporter、SDK 那套复杂架构
- 跨进程传播:需要在 Host ↔ Runtime 之间传播 Span,OTel 的跨进程方案偏重
- 训练集成:HyTuner Processor 需要 token-level 的精细数据,OTel 的 attribute 模型不够灵活
然而,OTel 生态在 2025-2026 年快速进化,前两条理由已被部分解决:
- 轻量化:agentrace-ai 仅依赖
otel-api+otel-sdk两个包,2 行代码接入(@trace_llm/@observe装饰器),已证明 OTel 可以做到足够轻量。traceAI 更提供 50+ 集成(OpenAI/Anthropic/Gemini/LangChain/MCP 等),sub-millisecond 开销。2026.06 更新:traceAI 新增 MCP、Claude Agent SDK、SmolAgents、Pydantic AI、AWS Strands 等集成,并新增 GUARDRAIL 和 EVALUATOR span kinds(共 9 种);agentrace-ai 0.2.0 新增@trace_retrieval和@observe通用装饰器,与 Nexus 的@observeAPI 设计相似度更高,桥接成本进一步降低 - 跨进程:traceAI 支持 OTLP 传播,agentrace-ai 支持跨进程 trace propagation
自研的核心理由已进一步退化:MCP 2026-07-28 规范(SEP-414)已将 W3C trace context(traceparent/tracestate/baggage)标准化为协议级跨进程传播答案,GenAI semantic conventions 也已扩展出 invoke_agent/execute_tool 等 agent span types——前两条理由(轻量需求、跨进程传播)在协议层与规范层都有了标准解。Nexus 自研仅剩一条真实理由:token-level 结构化训练数据(prompt_ids / response_ids / response_mask / logprobs)无法高效塞进 OTel 的键值对 attribute 模型,需借助 SpanEvent 或自定义扩展承载。
结论:如果 Nexus 今天重新设计 Tracing,很可能选择 OTel 为底座 + 自定义 SpanEvent 扩展训练数据的混合方案。
核心数据结构
class Span(BaseModel):
span_id: str
parent_id: str | None
trace_id: str
name: str
start_time: datetime
end_time: datetime | None
status: Status # OK / ERROR
attributes: dict # 键值对元数据
events: list[Event] # 时间点事件
class RemoteTraceContext(BaseModel):
trace_id: str
parent_span_id: str | None
@observe 装饰器
@observe 是 Trace 的核心 API。它包装一个函数,自动创建和管理 Span:
@observe(name='agent.main_agent', attributes={'type': 'agent'})
async def run_main_agent(input): ...
Agent、Task、Tool 在注册时被 Processor 管道自动包装了 @observe——你写扩展代码时不需要手动加。
也支持上下文管理器模式:
with span_context('my_operation', attributes={...}) as span:
# span 自动在 exit 时结束
update_span(attributes={'result': 'success'})
SpanController
SpanController(nexus/trace/controller.py)管理全局的 Span 生命周期——一个事件驱动的系统:
class SpanController:
def add_event(self, event: SpanEvent):
if isinstance(event, SpanStartEvent):
self._active_spans[event.span_id] = new_span
elif isinstance(event, SpanUpdateEvent):
self._active_spans[event.span_id].update(...)
elif isinstance(event, SpanEndEvent):
span = self._active_spans.pop(event.span_id)
self._notify_processors(span)
三种事件:SpanStartEvent、SpanUpdateEvent、SpanEndEvent。Span 结束时通知所有注册的 Processor。
Processor 插件体系
Processor 消费 Span 数据,产出各种形式的输出。通过 processor_context 管理生命周期:
with processor_context([chat_tracker, token_counter], finalize_on_exit=True):
await task_fn(input)
# finalize_on_exit=True 确保所有 Processor 的 finalize() 被调用
内置 Processor 详解
ChatTracker(chat_tracker.py,10KB):
- 追踪每次 LLM completion 的完整 messages
- 输出:有序的消息列表,每条含 role / content / tool_calls / timing
TokenCounter(token_counter.py,3.7KB):
- 统计 prompt_tokens / completion_tokens / total_tokens
- 支持按 client_key 过滤(如只统计
mainclient)
HyTuner Processor(hytuner.py,23.4KB):
- 最复杂的 Processor——采集 token-level 训练数据
- 输出:prompt_ids / response_ids / response_mask / logprobs
- 处理 multi-turn 和 context compaction 产生的多段轨迹
ChromeTrace(chrome_trace.py,6.3KB):
- 输出 Chrome Trace 格式(JSON),可用
chrome://tracing可视化时间线
JsonLine(json_line.py,2.3KB):
- 流式写入 JSONL 文件,每个 Span 一行
SocketIO Broadcaster(socketio.py,3.6KB):
- 将 Span 事件通过 Socket.IO 广播给 Host(用于 RemoteRuntime 的 Span 传播)
远程 Trace 传播
Host 和 Runtime 的 Span 需要关联。机制如下:
- Host 调用
runtime.invoke()时,在InvokeRequest.trace_context中填入当前 Span 的trace_id和parent_span_id - Runtime Server 接收到请求后,用
with_remote_trace_context(trace_id, parent_span_id)设置上下文 - Runtime 内部的
@observe创建的 Span 自动成为 Host Span 的子 Span - Runtime 通过
SocketIOSpanBroadcaster将 Span 事件推送回 Host - Host 的
RemoteRuntime接收事件,通过SpanController.add_event()回放
最终效果:Host 和 Runtime 的 Span 形成一棵完整的调用树。
Phase 系统
nexus/trace/phases.py(4.3KB)提供结构化的阶段划分:
with phase('agent'):
with phase('inference'):
completion = await client.create_completion(...)
with phase('observation'):
result = await tool_executor.call(...)
with phase('compaction'):
await compaction_manager.check_and_compact(...)
Phase 是语义化的——不像 Span 那样精确追踪函数调用,而是标记”现在在做什么阶段”。HyTuner Processor 用 Phase 来区分 prompt 和 response 的边界。
与 OpenTelemetry 的桥接
Nexus 自研 Span 体系与 OTel Span 的结构高度相似(span_id / parent_id / trace_id 直接对应),这为桥接提供了天然基础:
Span → OTel Span 映射
| Nexus 字段 | OTel Span 字段 | 说明 |
|---|---|---|
span_id | span_id | 直接映射 |
parent_id | parent_span_id | 直接映射 |
trace_id | trace_id | 直接映射 |
name | name | 直接映射 |
start_time / end_time | start_time / end_time | 直接映射 |
attributes | attributes | 直接映射 |
events | events (SpanEvent) | 直接映射 |
GenAI Conventions 的现状:独立仓库与属性名变动
桥接需注意:OTel GenAI Semantic Conventions 在 v1.42.0(2026-06-12 发布)作为 breaking change 从主仓库 deprecated,并迁至独立的 semantic-conventions-genai 仓库;官方 CHANGELOG 中 v1.43.0(2026-07-03)与 v1.44.0(2026-08-04)两版均未见”移除”条目,主仓库的正式表述止于 deprecated。属性名仍在持续变动——gen_ai.system → gen_ai.provider.name(v1.37.0)、prompt_tokens/completion_tokens → gen_ai.usage.input_tokens/output_tokens(v1.27.0)、invoke_agent 拆分为 client/internal spans(v1.41.0)。gen_ai.operation.name 现覆盖 create_agent/invoke_agent/invoke_workflow/execute_tool/retrieval/plan/memory。这提醒读者:桥接实现应做新旧属性名 coalesce 双字段查询,并维护版本化的内部 schema,避免上游属性名变动导致映射失效。
2026-09-16 实测两条硬证据,可把上述”保险起见”升格为必需:主仓 open-telemetry/semantic-conventions 的 releases.atom 最新仍为 v1.44.0(与本章一致);而迁往的新仓 open-telemetry/semantic-conventions-genai 的 releases.atom 返回 0 条 entry——截至该日,新仓库连第一个 tag 都还没有发布,尽管仓库本身仍在活跃更新。迁移尚未完成到这个程度,桥接层做 coalesce 与版本化内部 schema 就不是可选加固。📌 该状态随时会变,引用时请带抓取日期。
桥接方案
-
SocketIOSpanBroadcaster → OTLP Exporter 替换:当前 Span 通过 SIO 广播给 Host。可以增加 OTLP Exporter,将相同的 Span 数据发送到 OTel Collector,实现与 Grafana/Jaeger/Datadog 的无缝集成
-
HyTuner token-level 数据作为 OTel SpanEvent:prompt_ids / response_ids 等训练数据可作为 OTel SpanEvent 附加到 Span 上,而非塞进 attribute。SpanEvent 支持
attributes: dict[str, Any],比 attribute 更灵活。
不过要有一个量化预期。同样是”内容型数据”,Claude Code 官方为 OTel 内容属性设了 CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH,默认 61440(即 60 KB)截断,理由是主流后端对单个 attribute 值的上限是 64 KB;工具输入输出内容(OTEL_LOG_TOOL_CONTENT)默认禁用、且需要 tracing 才生效(其覆盖范围在 2.1.283 / 2026-09-25 扩大到 MCP 工具、WebFetch 与 WebSearch 三类输出,引用本条所估算的数据量请带版本)。这个数字说明:即便改走 SpanEvent,工业实现也普遍给内容设上限。Nexus 若要无截断地承载 prompt_ids / logprobs(HyTuner Processor 的 token-level 数据),基本只能走自研通道——有了这条量化对照,本章”自研仅剩一条真实理由”的结论更站得住。
- Agent Observatory 的 fail-open 模式参考:Agent Observatory 提供了一种中间路径——自研 session/span 模型但可桥接到 OTel。不配置全局 OTel 状态、不拥有 tracer provider,streaming-first 追踪。这避免了 OTel 的初始化开销,同时保留桥接能力
实施优先级
对于需要将 Nexus 接入企业可观测性栈的用户,最低成本的路径是:在 SocketIOSpanBroadcaster 旁新增 OTLPSpanExporter,通过配置开关启用。无需修改 Span 数据结构,仅在广播时增加一路 OTLP 输出。
这条结论只在客户端能决定目的地的前提下成立。 在受管环境下不成立——Claude Code 的官方模式就是现成的反例:OTEL_METRICS_EXPORTER / OTEL_LOGS_EXPORTER / OTEL_TRACES_EXPORTER 走 per-key 优先级(v2.1.223+),开发者侧的设置仍可关闭信号或改到 console exporter,所以想锁定目的地必须一并写进 managed settings;若 detailed beta tracing 激活,目的地还会被 BETA_TRACING_ENDPOINT 接管,而只要任一 managed setting 决定了这两个信号的目的地,Claude Code 就会移除开发者自行设置的 BETA_TRACING_ENDPOINT(v2.1.251 之前行为相反:开发者设置的 endpoint 会重定向 managed 的导出)。
对 Nexus 桥接的启示是:OTLPSpanExporter 除了”能不能发”,还要回答”被策略关掉时怎么办”。建议设计成 fail-open——导出失败不阻断 Span 主流程,与上文桥接方案 3 提到的 Agent Observatory fail-open 模式一致(本条为编辑推断 📌)。
但 fail-open 自带一个副作用:失败变得不可见。客户端侧的对应修复是——当配置好的 otelHeadersHelper 失败时新增启动告警,理由原文是”sessions that silently export no telemetry are noticed”(Claude Code 2.1.275,2026-09-17)。因此完整建议是 fail-open + 失败可见化:导出失败不阻断主流程,但必须以启动告警、失败计数器或定期自检的方式显式暴露,且该告警本身不能依赖同一条已失败的导出链路——否则失败与”失败的通知”会一起消失。
被解构对象的原生 OTel 实践:Claude Code
作为逆向工程对象,Claude Code 自身已原生拥抱 OTel。三个信号各自独立开关,全部通过环境变量配置,无需改动调用方式:
- 总开关:
CLAUDE_CODE_ENABLE_TELEMETRY=1 - Metrics(GA):session.count / token.usage / cost.usage / lines_of_code.count 等,经
OTEL_METRICS_EXPORTER=otlp导出 - Log events:见下方事件清单(27 种),经
OTEL_LOGS_EXPORTER=otlp导出 - Traces(beta):官方要求三个变量一并设置——
CLAUDE_CODE_ENABLE_TELEMETRY=1+CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1+OTEL_TRACES_EXPORTER=otlp。总开关是三者之一,缺任一都不出 traces;只设OTEL_TRACES_EXPORTER无效
traces 的 span 层级为 claude_code.interaction(根,一次完整 agent turn)→ llm_request / hook / tool,其中 tool 下再分 tool.blocked_on_user(权限等待)与 tool.execution(实际执行)两个子 span。子 agent 经 Task 工具派生时,其 span 会嵌套在父 span 之下,天然形成多智能体调用树。 与正文「子 agent 经 Task 工具派生时…」直接相连,中间无换行,破坏段落排版;已将其移至段末 —>
⚠️ 一处必须补的限定:上文的
claude_code.hookspan 只在 detailed beta tracing 激活时才出现——官方原文是”This span appears only when detailed beta tracing is active”,并明确CLAUDE_CODE_ENHANCED_TELEMETRY_BETA单独设置不会产生它。只按上一节那两个变量配置,是看不到 hook span 的。
事件层:27 种 log event
span 树只覆盖调用结构,Claude Code 的大部分可观测信息在事件层——日志型的时间点记录,与 span 是两层。官方事件共 27 种(2026-10-06 按官方 monitoring-usage 文档中「Event Name」条目计数;2026-09-16 时为 26 种):
user_prompt、assistant_response、tool_result、api_request、api_error、api_refusal、api_request_body、api_response_body、tool_decision、permission_mode_changed、auth、mcp_server_connection、internal_error、plugin_installed、plugin_loaded、skill_activated、at_mention、api_retries_exhausted、hook_registered、hook_execution_start、hook_execution_complete、hook_plugin_metrics、compaction、subagent_completed、feedback_survey、retention_sweep、managed_settings_resolved。
新增的 managed_settings_resolved(2.1.274 / 2026-09-17)记录受管设置的来源、策略辅助进程状态,以及机器拒绝启动的原因;开启 OTEL_LOG_MANAGED_SETTINGS=1 时还会输出设置内容与 resolved_sha256。它对本节的补强在于:受管环境下的可观测性不只关乎”信号发到哪”,还关乎”策略本身解析成了什么”——这与上面 §实施优先级 讨论的”目的地由谁决定”是同一条主线。
⚠️ 同一份内容可能同时存在于多个字段。 客户端实测:
user_prompt事件新增了prompt_text,其内容与prompt完全相同,存在的理由只是”部分后端会嵌套点号键”(Claude Code 2.1.287,2026-10-01);官方原文要求 “drop or mask it wherever you drop or maskprompt”。这条对 Nexus 的启示是:内容型数据的脱敏 / 截断策略必须按”内容”定义,而不是按”字段名”定义。HyTuner 的
prompt_ids/logprobs一旦走 OTLP,并因后端兼容产生多字段承载,只按字段名过滤的管线会留下完整副本——表现为”已经脱敏了”,实际等于没脱敏。同一风险族另见 Ch12 §安全管线层 的 PII 脱敏(Presidio)。
两层的关键设计差异在排序方式:span 靠父子关系,event 靠 event.sequence(per-process 计数器)。这条与 Nexus 的 SpanController 事件驱动模型(SpanStartEvent / SpanUpdateEvent / SpanEndEvent 按到达顺序驱动状态机)是同一个思路——本章”自研 Span 体系并非孤例”的判断又多一条例证。
第二层:detailed beta tracing
官方遥测实际上有两层 beta,上节写的只是第一层。
| 第一层 traces beta | 第二层 detailed beta tracing | |
|---|---|---|
| 启用 | CLAUDE_CODE_ENABLE_TELEMETRY=1 + CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 + OTEL_TRACES_EXPORTER=otlp | 再加 ENABLE_BETA_TRACING_DETAILED=1 + BETA_TRACING_ENDPOINT |
| 产出 | span 树 | claude_code.hook span、query_source、system_prompt_preview、tool_input、response.model_output 等内容型属性 |
| 稳定性 | — | 官方明确这些不属于稳定 span schema |
| 放行 | 无特殊要求 | 交互式 CLI 会话需组织被 allowlist 放行;Agent SDK 与 -p 非交互会话不需要 |
| 目的地 | 走 logs / traces exporter | logs 与 traces 的目的地被改到 BETA_TRACING_ENDPOINT,不再走原 exporter |
| 设置位置 | 任意 | 在 project 与 local settings 中被忽略,只能设在 shell / user settings / managed settings |
这一层的存在恰好印证本章的核心判断:连官方也要为内容型数据单独开一层、单独设上限、并声明其不稳定。它给”token-level 结构化训练数据塞不进 attribute 模型”提供了来自被解构对象自身的证据。
Compaction 作为一等可观测事件
官方把 compaction 建模为一等事件(event.name: "compaction"),属性包括 trigger(auto / manual)、success、duration_ms、pre_tokens、post_tokens、error,以及 precompute_reuse。
最后这个属性最值得注意:自动 compaction 会在上下文窗口填满之前于后台预先准备一份摘要,/compact 触发时记录是否复用了这份预计算摘要——hit 表示复用;miss_custom_instructions / miss_hook / miss_not_ready 给出重新计算的原因。(要求 v2.1.153+;且该属性只在 trigger 为 "manual" 时才设置。)
第三个面:网关层。 开启 CLAUDE_CODE_GATEWAY_HINT_HEADERS=1 后,x-claude-code-compaction 与 x-claude-code-context-compacted 会作为请求头出现在每一次 LLM 请求上(2.1.273,2026-09-15)——压缩状态被外化到请求本身,网关无需订阅遥测即可感知。这条对 Nexus 尤其合用:Runtime Server 的 LLM Proxy(见 Ch12)天然处在每一次 LLM 请求的路径上,采集压缩信号不必等到 Processor 落地,Proxy 层就能先拿到。(另注:同一版本还加了 x-claude-code-request-class / x-claude-code-agent-type / x-claude-code-prev-tool-durations;x-claude-code-prompt-id 于 2.1.283 加入,用于把服务同一次用户提示的请求归组。)
对 Nexus 有两条直接对位:
- 观测缺口:
CompactionManager目前只在 Phase 系统留下一个phase('compaction')语义标记,没有前后 token 数、没有耗时、没有失败原因。建议由 HyTuner Processor 或新增 Processor 采集pre_tokens/post_tokens/duration_ms/success——这对本章 §训练一致性 要求的”精确复现压缩前后 token 序列”是直接增强:压缩事件本身目前没有结构化记录。 - 机制对位:官方的”后台预计算 + 命中复用”是 Ch15 §Factory Anchored Iterative Summarization 讨论的”增量合并 vs 每次全量重建”之外的第三条路——预计算但可失效。Ch15 须同步补上。
这验证了章节”若今天重新设计很可能选 OTel 底座”的判断——被解构对象已经用 OTel 统一了自身的 agent 可观测性,Nexus 的桥接方向与业界原生实践一致。tool.blocked_on_user 这一 span 尤其值得注意:它把”权限拦截等待”单独建模为一个可度量阶段,与 Nexus 的 Phase 系统用 phase('observation') 标记阶段的思路异曲同工,可作为 Nexus Phase 语义对齐 OTel 的参考。
⚠️ 但该 span 的关键属性在非交互形态下曾长期不可用。 其
source与decision在-p会话、SDK 会话,以及 PreToolUse hook 审批这三种场景下曾恒为unknown(Claude Code 2.1.288,2026-10-02 修复)。Nexus 的RemoteRuntime恰恰运行在同形态(非交互 / 类 SDK,见 Ch12)。因此把这个 span 作为参考时,须同时确认”权限来源”与”决策结果”两个属性在你的运行形态下是否会退化为unknown——若会,则该 span 只能度量”等待时长”,不能度量”为何被拦”。