Blog • Claude Code / OpenTelemetry

Nach dem Upgrade auf Claude Code 2.1.312 fehlt claude_code.safety_event in OpenTelemetry. Welche Audit-JSON-Felder ändern?

Am 27. Oktober 2026 lieferte Anthropic Claude Code 2.1.312. OpenTelemetry sendet claude_code.safety_event nicht mehr; 2.1.307 vom 21. Oktober hatte es für von permission rule, auto, safety check, Hook oder Sandbox gestoppte tool calls ergänzt.

Dieselbe Version setzt mcp_server.name bei Remote-MCP mit fester URL aus offiziellen Marketplace-Plugins auf den echten Servernamen statt custom. Dashboards mit safety_event-Zählung oder custom-Gruppierung alarmieren falsch ohne Versions-Gate im lokalen JSON.

Blockgründe und auto-Evaluationskontext stehen in Managed Agents auto-Evaluation; lokale permission-Queue in Agent permission policy; JSON für nicht-interaktives MCP-Timeout in MCP elicitation timeout. Dieser Artikel behandelt nur OpenTelemetry-Feld-Auslauf in 2.1.312, mcp_server.name-Abgleich und telemetry_gate-Verzweigung.

Zuerst dies: 2.1.312 ändert OpenTelemetry-Ereignisnamen und MCP-Span-Attribute, nicht permission rules. Unten: Changelog, ausgelaufenes safety_event-Beispiel, Pipeline-JSON und Schema, mcp_server.name-Vergleich, Gate-Tabelle, telemetry_gate-Quelltext, Config-Prüfung in JSONNote.

2.1.312 stoppt safety_event in OpenTelemetry und korrigiert Marketplace-MCP-Namen

Änderungen stehen in Claude Code changelog, Tag-Seite Release v2.1.312. Gegenüber 2.1.307: claude_code.safety_event entfällt ganz, und mcp_server.name ist bei Marketplace-MCP mit fester URL nicht mehr custom.

permission rule, Hooks und Sandbox blockieren weiter; nur der dedizierte safety_event-Export entfällt. Audits sollen outcome auf tool spans oder eigenes approval JSON lesen, nicht auf claude_code.safety_event warten.

Wie claude_code.safety_event in 2.1.307 aussah und warum 312 es nicht mehr gibt

2.1.307 packte blockierte Aufrufe in claude_code.safety_event mit tool.name, outcome blocked, source permission_rule. Beispiel nur für Legacy-Form; 2.1.312-Clients dürfen den Namen nicht mehr senden.

Legacy-Form claude_code.safety_event aus 2.1.307 (in 312 gestoppt).
{
  "name": "claude_code.safety_event",
  "attributes": {
    "tool.name": "Bash",
    "outcome": "blocked",
    "source": "permission_rule"
  }
}

Erhält eine 312-Fleet das Ereignis noch, alte Binaries oder Collector-Replay prüfen. telemetry_gate soll unexpected_event liefern, wenn claudeCodeVersion mindestens 2.1.312 ist und span.name noch claude_code.safety_event heißt.

Wie Audit-Pipeline-JSON claudeCodeVersion und pipeline setzt

Jeden Export-Batch an claudeCodeVersion binden; pipeline deklariert safety_event-Zählung oder mcp_server.name-Gruppierung. count_safety_event auf 312 → retire_counter; bei Mix muss die Version vom Agent kommen.

Pipeline-Config: Version 2.1.312, pipeline und Marketplace-Flag.
{
  "claudeCodeVersion": "2.1.312",
  "pipeline": "count_safety_event",
  "marketplacePlugin": true
}

additionalProperties und enums per JSON Schema fixieren. Bei Schema-Fehler zuerst schema zurückgeben, nicht ok oder retire_counter bei fehlenden Feldern raten.

Pipeline-JSON-Schema: Form vor 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" }
  }
}

Warum mcp_server.name bei Marketplace-Remote-MCP von custom zum Plugin-Namen wechselt

Ab 2.1.312 schreibt Remote-MCP aus offiziellen Anthropic-Marketplace-Plugins mit fester URL den echten Servernamen (z. B. github) auf tools/call-Spans statt custom. Self-hosted ausgenommen; custom bei marketplacePlugin true bedeutet veraltete Gruppierung.

2.1.312: mcp_server.name ist echter Marketplace-Name.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "github",
    "tool.name": "create_issue"
  }
}
2.1.311 und früher: Marketplace-MCP mit fester URL oft custom.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "custom",
    "tool.name": "create_issue"
  }
}
Config und Span-Signaltelemetry_gate liefertOperator-Aktion
claudeCodeVersion mindestens 2.1.312, pipeline weiter count_safety_eventretire_countersafety_event-Zähler abschalten; stattdessen tool span outcome oder approval JSON.
Version mindestens 2.1.312, span.name noch claude_code.safety_eventunexpected_eventAlte Binaries oder Collector-Replay prüfen; nicht als normale Blocks zählen.
pipeline mcp_server_group, marketplacePlugin true, mcp_server.name noch customstale_mcp_aliasGrafana-Gruppen und Alert-Keys aktualisieren; custom auf echte Plugin-Namen mappen.
claudeCodeVersion unter 2.1.312, pipeline count_safety_event, Span ist kein safety_eventmissing_safetyAlte Clients lassen safety_event aus; Zähler lockern oder nach Version bucketen, nicht mit unexpected_event vermischen.

Wie telemetry_gate zwischen schema und fünf Fachzweigen wählt

Reihenfolge: config per Pipeline-JSON-Schema; span.name und attributes.mcp_server.name lesen; claudeCodeVersion mit 2.1.312 vergleichen. Auf 312 safety_event-Name → unexpected_event; 312 mit count_safety_event → retire_counter; unter 312 count-Pipeline, Span nicht safety_event → missing_safety; 312, mcp_server_group, marketplacePlugin, custom → stale_mcp_alias; sonst ok.

retire_counter und ok nicht zu einem passed-Bit zusammenlegen. Getrennt zählen, um ungeänderte pipeline-Felder und hängende custom-Gruppierung zu sehen.

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"

Wie validiert man Pipeline-JSON und Span-Beispiele in JSONNote

  1. 1
    Pipeline-Config per Schema fixieren

    Öffnen Sie JSON Schema. Pipeline-Schema ins erste Feld, claudeCodeVersion und pipeline ins zweite. Enum- oder Pattern-Fehler zuerst; bei schlechter Form keine Fachzweige.

  2. 2
    Format für Span-Attribute

    Öffnen Sie JSON formatieren, tools/call-Span vom Collector einfügen. Prüfen, ob mcp_server.name github oder custom ist, bevor stale_mcp_alias gewählt wird.

  3. 3
    Diff: MCP-Name vorher und nachher

    Öffnen Sie JSON Diff. Links 2.1.312-Span, rechts Legacy mit custom. Hervorgehobenes mcp_server.name ist die Grafana-Zeile zum Anpassen.

Häufige Fragen

Sendet 2.1.312 Blocks unter einem anderen OpenTelemetry-Ereignisnamen?

Das Changelog stoppt nur claude_code.safety_event; kein Ersatz-Ereignis ist zugesagt. Verlassen Sie sich auf tool spans, permission logs oder approval JSON, nicht auf einen neuen safety-Namen.

Kann pipeline auf 2.1.312 count_safety_event bleiben?

Das Feld darf als Migrationsmarkierung bleiben, aber telemetry_gate liefert auf 312 retire_counter, weil der Zähler nie Ereignisse sieht. pipeline enum ändern oder auf tool span outcome wechseln, retire_counter nicht als ok behandeln.

Zeigt Self-hosted-MCP weiter mcp_server.name custom?

stale_mcp_alias gilt hier nur für Remote-MCP mit fester URL aus offiziellen Marketplace-Plugins bei marketplacePlugin true. Self-hosted-Namen stehen außerhalb dieses 2.1.312-Changelogs; nicht jedes custom ist Fehlkonfiguration.

Wie trennt man missing_safety und unexpected_event bei Mix aus 2.1.307 und 2.1.312?

unexpected_event: Config mindestens 2.1.312, claude_code.safety_event kommt noch. missing_safety: Config unter 312, count_safety_event-Pipeline, Span ist nicht das Ereignis. Beides braucht claudeCodeVersion, nicht nur Collector-Gesamtzahl.

Bedeutet telemetry_gate ok, dass MCP-Namensänderungen ignoriert werden können?

ok heißt nur: dieses Config- und Span-Beispiel bestand alle Regeln, z. B. mcp_server_group mit echtem Plugin-Namen oder ohne count_safety_event. Keine Garantie, dass künftige Batches keine Legacy-Spans enthalten; weiter sampeln.

Fazit

Claude Code 2.1.312 entfernt den dedizierten OpenTelemetry-safety_event-Export und richtet mcp_server.name bei Marketplace-Remote-MCP auf echte Plugin-Namen aus. Audit-JSON braucht claudeCodeVersion und pipeline, damit telemetry_gate bei gemischten Flotten verzweigen kann.

Entscheidung: claudeCodeVersion mindestens 2.1.312 mit claude_code.safety_event → unexpected_event; gleiche Version mit count_safety_event → retire_counter und Metriken ändern; unter 2.1.312 safety_event zählen, Span nicht das Ereignis → missing_safety; auf 312 marketplacePlugin true und mcp_server.name custom → stale_mcp_alias; Schema scheitert → schema zuerst; sonst ok.

Als Nächstes: Pipeline-JSON per Schema, Format für Span-Attribute, Diff für mcp_server.name, telemetry_gate-Rückgaben getrennt berichten.

Zurück zum Blog