博客 • 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 返回值。