你的 Span 名称是未记录的 API:Agent 团队之间的遥测契约
凌晨 3 点让财务部门收到告警的成本飙升其实并不是真正的成本飙升。那只是一个 Span 重命名。Agent 平台团队的某个人觉得 llm.completion.synthesis 应该改为 llm.generate.answer,因为这样读起来更自然。他们提交了一个小的 PR,运行了测试,然后发布了。三天后,财务的月度 Token 消耗仪表盘显示下降了 60%。没有人削减支出。聚合规则仍然按旧名称分组,而新的 Span 流向了一个仪表盘甚至没有渲染的 “其他” 桶中。账单没有变。仪表盘变了。
这是我一直看到团队在重复经历的一类事故。Span 名称和属性键并不是为了让人在追踪 UI 中阅读而存在的标签。它们是一个未公开 API 的公开 Schema,其消费者是生产团队从未谋面的——过滤它们的评估流水线、按它们分组的成本仪表盘、根据其持续时间触发的 SLO 告警、汇总其 Token 属性的 FinOps 报告。一个团队内部 “无害的重命名”,对于另外四个从未看过该 PR 的团队来说,就是一个网络协议破坏。
所需的认知转变虽不舒服但很简单:如果你的 Protobuf 定义拥有 Schema 注册表、兼容模式和 CI 门禁,那么你的 OpenTelemetry 属性集也理应享有同样的治理。遥测是一种契约。请像对待契约一样对待它。
为什么 Span 在无意间变成了契约
没有人打算通过发出一个 Span 来发布 API。这个链条是循序渐进的。团队 A 为其 Agent 进行了埋点,并选择了一个听起来合理的 Span 名称。平台团队的收集器将其吸纳并路由到追踪存储中。另一个组织的分析师注意到,通过 tool.call.search 过滤追踪可以为工具成功率分析提供有用的切片,于是写了一个查询。一位 FinOps 工程师看到了 gen_ai.usage.input_tokens 属性,并将其添加到 Looker 模型中。一位 SRE 针对 latency p99 where span.name = "llm.completion.synthesis" 编写了一个告警。
这些消费者从未与团队 A 沟通。他们都不在团队 A 的 CODEOWNERS 中。他们的查询都没有在团队 A 的仓库中被引用。但现在每一个消费者都成了对团队 A 所选字符串的承重依赖。契约从未经过协商;它是通过某处某人编写了一个依赖于该名称的查询这一行为而具象化的。
这在结构上与海勒姆定律(Hyrum's Law)在 HTTP API 上的运作方式完全相同:只要有足够多的消费者,你系统的每一个可观察行为都会被某人所依赖。区别在于,HTTP API 团队知道自己有一个 API。而发出 Span 的团队通常认为他们只是在写一行内部日志。
OpenTelemetry 社区一直在直接应对这一问题。Span 名称应该是低基数且稳定的;属性键应该遵循语义规范。 截至 2026 年初,针对 Agent 和 LLM 客户端的 GenAI 语义规范仍处于实验阶段,并附带了 OTEL_SEMCONV_STABILITY_OPT_IN 机制——即选择性地同时发出旧的和新的属性名称,以便消费者进行迁移。这一机制之所以存在,是因为维护者非常清楚,当你重命名一个全世界都在依赖的属性时会发生什么。
失败模式是隐蔽的
让遥测中断特别棘手的原因在于,它们往往是静默失败的。Kafka 生产者中的 Protobuf 字段重命名通常会因反序列化错误而导致下游消费者崩溃——Schema 注册表会拒绝发布,或者消费者会抛出异常,或者死信队列会开始堆积。这种破坏会自我宣告。
Span 重命名很少产生错误。它产生的是一个返回更少行、或没有行、或行数相同但缺少关键分类的查询。追踪存储是正常的。收集器是正常的。生产者团队的测试是绿色的。消费者的仪表盘变平了,或者更糟——看起来很合理,但实际上正在系统性地少算某个部分。
我见过这种失败的四种典型表现:
- 直线化的仪表盘。 一个原本正常波动的 KPI 突然固定在零或某个常数。这通常会被忽略好几天,因为平直的面板看起来就像 “现在一切都很稳定”。
- 静默的重分类。 一个之前按工具类型细分的多序列图表开始将所有内容坍缩到 “其他” 或单一类别中,因为区分属性发生了变化。
- 不完整的迁移。 一半的生产者发布了新名称,一半仍在使用旧名称。当两个名称在某个时间窗口重叠时,聚合会重复计算,而当一方完成滚动更新时,又会出现漏计。
- 缺失的告警。 一个 SLO 告警静默地停止触发,因为它的过滤器不再匹配任何内容。直到发生了一场本该被该告警捕获的事故并持续了数小时后,才有人察觉。
共同点是:没有异常,没有 500 错误,没有失败的部署。损害在于衍生数据的含义,而检测它的唯一方法是知道数据原本应该代表什么。
借鉴你已有的 API 规范
如果你曾在拥有真正 Schema 注册中心(如 Confluent 用于 Avro 和 Protobuf 的方案,或 Buf 用于 Proto 的方案)的系统上工作过,你已经熟悉这套流程了。Schema 是带有所有者的版本化产物。在生产者发布新版本之前,CI 会强制执行兼容性模式(BACKWARD、FORWARD、FULL)。破坏性变更需要一个显式的、经过评审的、选择性加入(opt-in)的过程,并且包含新旧字段并存的弃用窗口期。由于消费者已注册,生产者对谁在调用自己了如指掌。
这套流程的几乎每个部分都可以直接迁移到遥测(Telemetry)领域,而 OpenTelemetry 一直在悄悄构建让其落地的工具。OTel Weaver 就是类似于注册中心的工具:你在结构化注册中心里描述你的 Span、属性和指标,对照它验证代码,生成文档和类型化的插桩助手,并运行 weaver registry diff 来检测版本间的破坏性变更。这个差异检测(diff)可以集成到 CI 中,如果有人引入了向后不兼容的变更,且没有进行刻意的版本升级和迁移计划,就可以直接报错拦截 PR。Weaver 还支 持自定义 Rego 策略 —— 当你的内部规则超出 OTel 约定(例如,“注册中心里没有所属团队的 Span 不允许命名”)时,这非常有用。
这种模式的一一对应关系如下:
- Schema 即产物。 Span 名称和属性 Schema 存储在注册中心仓库中,而不是散落在各服务中的字符串字面量里。生产者从注册中心拉取定义;消费者引用其目标注册中心版本。
- 版本控制和兼容性模式。 注册中心版本是变更的最小单元。添加新的可选属性是向后兼容的;重命名或更改现有属性的类型则不是。CI 会强制执行这一规则。
- 有归属权的、有弃用窗口期的变更。 破坏性变更采用双重发射(dual-emission)方式:生产者在定义的窗口期内同时发射旧名称和新名称。OTel 的
OTEL_SEMCONV_STABILITY_OPT_IN环境变量在约定层面实现了这种模式 —— 你可以在组织层面复制这种做法。 - 已知的消费者。 当 Span Schema 发生变化时,生产者可以看到谁在对其进行过滤。这要求消费者针对注册中心注册他们的依赖项(如 Looker 模型、Grafana 面板、SLO 查询)—— 虽然设置起来很麻烦,但在需要变更时却能救命。
导致这种模式失效的组织缝隙
工具解决了技术层面的问题。文化层面则更难,而且几乎总是断裂在同一条裂缝上:谁拥有遥测栈的哪个部分。
有两种反复出现的失败模式。第一种是平台团队拥有“可观测性 ” —— 包括 Collector(收集器)、Trace 存储和仪表盘。他们对系统负责,但对应用团队发射的字符串没有管理权。他们眼睁睁地看着重命名发生,却没有任何政策杠杆来阻止它。第二种是每个团队拥有自己的 Span,平台团队只负责管道,没有共享的 Schema。跨团队的消费者学会了忍受脆弱的字符串过滤器,并编写每周都会崩溃的防御性代码。
能够行之有效的安排更接近成熟组织中 API 设计的方式:平台团队拥有 注册中心和策略,应用团队拥有 其注册中心切片的内容,而消费者注册他们的依赖。破坏性变更评审是实实在在的评审,拥有一群真正的评审员 —— 即受影响的消费者团队。在满足以下两个条件之一前,生产者不能合并重命名 PR:(a) 没有已注册的消费者依赖于旧名称,或者 (b) 已通知消费者并安排了弃用窗口期。
这听起来很繁重。但比起凌晨三点反复出现的告警,以及第三次“仪表盘数据错误”的财务升级事件后导致的信任崩溃,这要轻得多。
如果你还没达到那个阶段,该从哪里开始
大多数阅读本文的团队离基于 Weaver 的注册中心还很远,这没关系 —— 落地路径是渐进式的。
首先,盘点你已有的资产。从 Trace 存储中选出调用量排名前三到五的 Span 名称,然后问:谁在消费这些数据?你会感到惊讶,答案很少仅仅是“发射它们的那个团队”。记录下这些消费者。现在,你就有了一份契约草案。
接下 来,添加一个 CI 检查:如果任何 PR 在没有对应更新仓库中 TELEMETRY_CHANGES.md 文件的情况下,添加、重命名或删除了 Span 名称或属性键,则该 PR 失败。这种方式虽然笨拙且原始,但非常有效。它迫使对话在重命名上线 之前 发生,而不是在之后。
当你从原始阶段进化到正式阶段时,OTel Weaver 是显而易见的下一步 —— 在平台团队控制的 Span 中采用它,随着治理能力的成熟扩展到应用团队,并将 CI 中的注册中心 diff 作为兼容性关卡。配合 Collector 端的归一化层使用:当重命名确实发布时,通过处理器运行两个名称,在定义好的窗口期内将旧名称映射到新名称(反之亦然),这样现有的仪表盘就能继续工作,同时消费者可以进行迁移。
最后,像对待数据库索引设计一样,对 Span 属性的基数(Cardinality)保持极度警惕。将请求 ID 或用户 ID 放入 Span 名称中,在遥测领域相当于对未索引的表执行 SELECT * —— 它不会立即导致崩溃,但终有一天会以极其昂贵的代价爆发。OTel 命名约定对此有明确规定是有原因的。
真正的教训是关于“可观测性的可观测性”
更深层次的转变是,不要再将遥测数据仅仅视为一种只写流,而是要将其视为一个拥有独立用户、SLA 和变更管理规范的系统。成本仪表盘、评估流水线、SLO 告警——这些都是构建在你的 span 数据之上的“应用程序”,而依赖它们的人就是该 Schema 的付费客户,即便其中并不涉及金钱交易。
那些处理得当的团队始终坚持一个习惯:他们会记录遥测契约(telemetry contract)的每一次变更,并像审查公开 API 一样严格审查这些变更。而表现不佳的团队则会不断在凌晨 3 点因为那些并非真实波动的“成本飙升”而被叫醒,并逐渐对自己的仪表盘失去信任。
如果你的 Protobuf 值得拥有一个 Schema 注册表,那么你的 span 数据也同样值得。账单可能没动,但仪表盘的数据却变了——这其实是更糟糕的问题,因为当你意识到这一点时,你可能已经基于错误的数据做了数周的决策。
- https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/
- https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/
- https://opentelemetry.io/blog/2025/otel-weaver/
- https://github.com/open-telemetry/weaver
- https://opentelemetry.io/docs/specs/otel/schemas/
- https://opentelemetry.io/docs/specs/semconv/general/naming/
- https://opentelemetry.io/blog/2025/how-to-name-your-span-attributes/
- https://last9.io/blog/otel-naming-best-practices/
- https://last9.io/blog/otel-weaver-consistent-observability-with-semantic-conventions/
- https://buf.build/blog/why-a-protobuf-schema-registry
- https://docs.confluent.io/platform/current/schema-registry/fundamentals/serdes-develop/serdes-protobuf.html
- https://www.dash0.com/blog/observability-cost-out-of-control
