博客 • 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 改的是 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 客戶端不應再產生同名事件。

2.1.307 時代仍可能出現的 claude_code.safety_event 形狀(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 安裝日。

流水線配置:版本 2.1.312,pipeline 與 marketplace 開關。
{
  "claudeCodeVersion": "2.1.312",
  "pipeline": "count_safety_event",
  "marketplacePlugin": true
}

用 JSON Schema 固定 additionalProperties 與 enum,避免運維手寫 pipeline 拼錯。schema 失敗時 telemetry_gate 應先返回 schema,不要在字段缺失時硬判 ok 或 retire_counter。

流水線 JSON Schema:先過形狀,再進 telemetry_gate。
{
  "$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 說明分組規則未更新。

2.1.312:mcp_server.name 爲 marketplace 插件真實名。
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "github",
    "tool.name": "create_issue"
  }
}
2.1.311 及更早:固定 URL marketplace MCP 常寫成 custom。
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "custom",
    "tool.name": "create_issue"
  }
}
配置與 span 現象telemetry_gate 返回運維動作
claudeCodeVersion 至少 2.1.312,pipeline 仍爲 count_safety_eventretire_counter下線 safety_event 計數器,改查 tool span outcome 或 approval JSON。
版本至少 2.1.312 仍收到 span.name claude_code.safety_eventunexpected_event查混跑舊二進制或 collector 重放,不要計入正常攔截。
pipeline 爲 mcp_server_group,marketplacePlugin true,mcp_server.name 仍爲 customstale_mcp_alias更新 Grafana 分組與告警鍵,把 custom 映射改爲插件真實名。
claudeCodeVersion 低於 2.1.312,pipeline count_safety_event,span 不是 safety_eventmissing_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。

telemetry_gate:schema、unexpected_event、retire_counter、missing_safety、stale_mcp_alias、ok。
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. 1
    用 Schema 固定流水線配置

    打開 JSON Schema。第一欄貼流水線 schema,第二欄貼 claudeCodeVersion 與 pipeline 配置。enum 或 pattern 錯誤會先標紅,telemetry_gate 不應在形狀錯誤時返回業務分支。

  2. 2
    用格式化覈對 span 屬性

    打開 JSON 格式化,貼從 collector 抽出的 tools/call span。確認 mcp_server.name 是 github 還是 custom,再決定要不要走 stale_mcp_alias 分支。

  3. 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 返回值。

返回博客