博客 • Claude Code / OpenTelemetry
升级到 Claude Code 2.1.312 之后,OpenTelemetry 里不再出现 claude_code.safety_event,审计 JSON 要怎样改字段?
2026 年 10 月 27 日,Anthropic 发布 Claude Code 2.1.312。OpenTelemetry 导出里不再发送事件 claude_code.safety_event;该事件曾在 10 月 21 日的 2.1.307 为被 permission rule、auto 模式、safety check、hook 或 sandbox 拦下的 tool call 新增。
同版还把官方 marketplace 插件里固定 URL 远程 MCP 的 span 属性 mcp_server.name 从占位值 custom 改成真实插件服务器名。仍按 safety_event 计数或仍把 custom 当分组键的 Grafana 看板会在升级后误报,需要本地 JSON 配置加版本门槛。
拦截原因与 auto 评测上下文见 Managed Agents auto 评测;本地 permission 队列写法见 Agent permission policy;非交互 MCP 超时 JSON 见 MCP elicitation timeout。本篇只写 2.1.312 的 OpenTelemetry 字段退役与 mcp_server.name 对齐,以及 telemetry_gate 怎样分支。
- 2.1.312 起客户端不再发出 claude_code.safety_event;仍监听该事件名应判 unexpected_event。
- pipeline 仍为 count_safety_event 且版本已写 2.1.312 时,闸门应返回 retire_counter 提醒下线计数器。
- marketplace 远程 MCP 在 2.1.312 上 mcp_server.name 应为 github 等插件名;仍见 custom 且 marketplacePlugin 为 true 时返回 stale_mcp_alias。
- 混跑 2.1.307 与 2.1.312 时,旧端缺少 safety_event 应返回 missing_safety,不要与 unexpected_event 混为一谈。
先记住这一点:2.1.312 改的是 OpenTelemetry 事件名与 MCP span 属性,不是 permission rule 本身。下面按 changelog、已退役的 safety_event 样本、流水线 JSON 与 schema、mcp_server.name 对比、闸门对照表、telemetry_gate 源码,以及 JSONNote 里怎样校验配置。
2.1.312 在 OpenTelemetry 里停发 safety_event 并修正 marketplace MCP 名称
变更说明见 Claude Code changelog,标签页见 v2.1.312 release。相对 2.1.307,telemetry 面最显眼的两处是 safety_event 整条下线,以及 marketplace 固定 URL MCP 不再把 mcp_server.name 写成 custom。
permission rule、hook、sandbox 仍会拦截 tool call,只是不再单独导出 safety_event 事件。审计应改查既有 tool span 的 outcome 或你们自有的 approval JSON,而不是等 claude_code.safety_event 重新出现。
claude_code.safety_event 在 2.1.307 长什么样、312 为何不再出现?
2.1.307 把被 permission rule、auto、safety check、hook 或 sandbox 挡下的调用封装成 OpenTelemetry 事件 claude_code.safety_event,常见属性含 tool.name、outcome blocked 与 source permission_rule。下面样本仅用于对照旧导出形状,312 客户端不应再产生同名事件。
{
"name": "claude_code.safety_event",
"attributes": {
"tool.name": "Bash",
"outcome": "blocked",
"source": "permission_rule"
}
}
若 312 集群仍收到该事件,优先查是否混跑旧二进制或 collector 重放旧批次。telemetry_gate 在 claudeCodeVersion 至少 2.1.312 且 span.name 仍为 claude_code.safety_event 时应返回 unexpected_event,而不是当作正常拦截计数。
审计流水线 JSON 怎样写 claudeCodeVersion 与 pipeline?
把每条导出批次绑到 claudeCodeVersion,并用 pipeline 声明你在数 safety_event 还是按 mcp_server.name 分组。312 仍写 count_safety_event 会触发 retire_counter;混跑旧客户端时版本字段必须来自 agent 自报,而不是 collector 安装日。
{
"claudeCodeVersion": "2.1.312",
"pipeline": "count_safety_event",
"marketplacePlugin": true
}
用 JSON Schema 固定 additionalProperties 与 enum,避免运维手写 pipeline 拼错。schema 失败时 telemetry_gate 应先返回 schema,不要在字段缺失时硬判 ok 或 retire_counter。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["claudeCodeVersion", "pipeline"],
"properties": {
"claudeCodeVersion": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
"pipeline": { "enum": ["count_safety_event", "mcp_server_group"] },
"marketplacePlugin": { "type": "boolean" }
}
}
marketplace 远程 MCP 的 mcp_server.name 为何从 custom 改成插件名?
2.1.312 起,来自 Anthropic 官方 marketplace 插件、固定 URL 的远程 MCP 在 tools/call span 上写入真实插件服务器名(例如 github),不再用 custom 占位。自托管或手写 URL 的 MCP 命名规则不在本篇范围;marketplacePlugin 为 true 时仍见 custom 说明分组规则未更新。
{
"name": "tools/call",
"attributes": {
"mcp_server.name": "github",
"tool.name": "create_issue"
}
}
{
"name": "tools/call",
"attributes": {
"mcp_server.name": "custom",
"tool.name": "create_issue"
}
}
| 配置与 span 现象 | telemetry_gate 返回 | 运维动作 |
|---|---|---|
| claudeCodeVersion 至少 2.1.312,pipeline 仍为 count_safety_event | retire_counter | 下线 safety_event 计数器,改查 tool span outcome 或 approval JSON。 |
| 版本至少 2.1.312 仍收到 span.name claude_code.safety_event | unexpected_event | 查混跑旧二进制或 collector 重放,不要计入正常拦截。 |
| pipeline 为 mcp_server_group,marketplacePlugin true,mcp_server.name 仍为 custom | stale_mcp_alias | 更新 Grafana 分组与告警键,把 custom 映射改为插件真实名。 |
| claudeCodeVersion 低于 2.1.312,pipeline count_safety_event,span 不是 safety_event | missing_safety | 旧线客户端可能未导出 safety_event;放宽计数或按版本分桶,勿与 unexpected_event 合并。 |
telemetry_gate 怎样在 schema 与五条业务分支间选择?
实现顺序:先用流水线 JSON Schema 校验 config;读 span.name 与 attributes.mcp_server.name;比较 parse 后的 claudeCodeVersion 与 2.1.312。312 上仍见 safety_event 名 → unexpected_event;312 且 pipeline count_safety_event → retire_counter;低于 312 且 pipeline 要数 safety_event 但 span 不是 → missing_safety;312、mcp_server_group、marketplacePlugin 且 custom → stale_mcp_alias;否则 ok。
不要把 retire_counter 与 ok 合成单一 passed 位。报表分开计数,才能看到还有多少团队未改 pipeline 字段,以及 MCP 分组是否仍卡在 custom。
def actual_type(data: object) -> str:
if data is None:
return "null"
if isinstance(data, bool):
return "boolean"
if isinstance(data, list):
return "array"
if isinstance(data, int):
return "integer"
if isinstance(data, float):
return "number"
if isinstance(data, str):
return "string"
if isinstance(data, dict):
return "object"
raise TypeError(type(data))
def validate(data: object, schema: dict) -> list[str]:
errors: list[str] = []
if not isinstance(schema, dict):
return errors
types = schema.get("type")
if isinstance(types, str):
types = [types]
actual = actual_type(data)
if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
errors.append("type")
if "enum" in schema and not any(value == data for value in schema["enum"]):
errors.append("enum")
if isinstance(data, str) and "minLength" in schema and len(data) < schema["minLength"]:
errors.append("minLength")
if isinstance(data, dict) and schema.get("properties"):
for key in schema.get("required") or []:
if key not in data:
errors.append(f"required:{key}")
if schema.get("additionalProperties") is False:
for key in data:
if key not in schema["properties"]:
errors.append(f"additional:{key}")
for key, sub in schema["properties"].items():
if key in data and data[key] is not None:
errors.extend(validate(data[key], sub))
return errors
def parse_version(text: str) -> tuple[int, int, int]:
parts = text.split(".")
if len(parts) != 3:
return (0, 0, 0)
try:
return (int(parts[0]), int(parts[1]), int(parts[2]))
except ValueError:
return (0, 0, 0)
def telemetry_gate(config: dict, span: dict, schema: dict) -> str:
if validate(config, schema):
return "schema"
version = parse_version(str(config.get("claudeCodeVersion", "0.0.0")))
attrs = span.get("attributes") if isinstance(span.get("attributes"), dict) else {}
name = span.get("name")
if version >= (2, 1, 312) and name == "claude_code.safety_event":
return "unexpected_event"
if version >= (2, 1, 312) and config.get("pipeline") == "count_safety_event":
return "retire_counter"
if version < (2, 1, 312) and config.get("pipeline") == "count_safety_event" and name != "claude_code.safety_event":
return "missing_safety"
if (
version >= (2, 1, 312)
and config.get("pipeline") == "mcp_server_group"
and config.get("marketplacePlugin")
and attrs.get("mcp_server.name") == "custom"
):
return "stale_mcp_alias"
return "ok"
在 JSONNote 里怎样校验流水线 JSON 与 span 样本?
- 1用 Schema 固定流水线配置
打开 JSON Schema。第一栏贴流水线 schema,第二栏贴 claudeCodeVersion 与 pipeline 配置。enum 或 pattern 错误会先标红,telemetry_gate 不应在形状错误时返回业务分支。
- 2用格式化核对 span 属性
打开 JSON 格式化,贴从 collector 抽出的 tools/call span。确认 mcp_server.name 是 github 还是 custom,再决定要不要走 stale_mcp_alias 分支。
- 3用 Diff 对比 MCP 名称前后
打开 JSON Diff。左边放 2.1.312 新 span,右边放仍写 custom 的旧样本。Diff 高亮的 mcp_server.name 就是 Grafana 分组最该改的一行。
常见问题
2.1.312 还会把拦截写成别的 OpenTelemetry 事件名吗?
changelog 只声明停发 claude_code.safety_event,没有承诺用一对一替代事件。审计应依赖既有 tool span、permission 日志或你们自有的 approval JSON,而不是等待新 safety 事件名。
能否在 2.1.312 上继续用 count_safety_event .pipeline?
字段可以留作迁移期标记,但 telemetry_gate 会对 312 返回 retire_counter,提醒计数器永远等不到事件。应改 pipeline 枚举或切到 tool span outcome 指标,而不是把 retire_counter 当 ok 静默通过。
自托管 MCP 还会看到 mcp_server.name 等于 custom 吗?
本篇 stale_mcp_alias 只针对官方 marketplace 插件且 marketplacePlugin 为 true 的固定 URL 远程 MCP。自托管命名不在 2.1.312 这条 changelog 里;不要把所有 custom 都当成配置错误。
混跑 2.1.307 与 2.1.312 时 missing_safety 与 unexpected_event 怎么分?
unexpected_event 要求配置声明至少 2.1.312 却仍收到 claude_code.safety_event。missing_safety 要求配置版本低于 312、pipeline 要数 safety_event,而 span 不是该事件。两者都依赖 claudeCodeVersion 字段,不能只看 collector 收到的事件总量。
telemetry_gate 返回 ok 是否表示可以忽略 MCP 名称变化?
ok 只表示当前 config 与 span 样本通过全部规则,例如已改用 mcp_server_group 且见到真实插件名,或已下线 count_safety_event。它不代表未来批次不会混进旧 span;持续抽样仍必要。
总结
Claude Code 2.1.312 把 OpenTelemetry 里的 safety_event 专用导出拿掉,并把 marketplace 远程 MCP 的 mcp_server.name 对齐到真实插件名。审计 JSON 必须写清 claudeCodeVersion 与 pipeline,让 telemetry_gate 在混跑与迁移期给出可执行分支。
直接结论:claudeCodeVersion 至少 2.1.312 仍见 claude_code.safety_event 返回 unexpected_event;同版本且 pipeline 仍为 count_safety_event 返回 retire_counter 并改指标;低于 2.1.312 要数 safety_event 而 span 不是该事件返回 missing_safety;312 上 marketplacePlugin 为 true 且 mcp_server.name 仍为 custom 返回 stale_mcp_alias;配置不过 schema 先返回 schema;其余 ok。
下一步:用 Schema 固定流水线 JSON,用格式化看清 span 属性,用 Diff 对比 mcp_server.name 前后,并在报表里分开统计 telemetry_gate 返回值。