Blog • Claude Code / OpenTelemetry

Après la mise à niveau vers Claude Code 2.1.312, claude_code.safety_event n'apparaît plus dans OpenTelemetry. Quels champs JSON d'audit modifier ?

Le 27 octobre 2026, Anthropic a livré Claude Code 2.1.312. L'export OpenTelemetry n'envoie plus claude_code.safety_event, ajouté le 21 octobre en 2.1.307 pour les tool calls stoppés par permission rule, mode auto, safety check, hook ou sandbox.

La même version remplace custom par le vrai nom serveur dans mcp_server.name pour MCP distant URL fixe des plugins marketplace officiels. Les tableaux qui comptent encore safety_event ou groupent sur custom fausseront sans seuil de version dans le JSON local.

Le contexte des blocages et de l'évaluation auto est dans évaluation auto Managed Agents; la file permission locale dans Agent permission policy; le JSON de timeout MCP non interactif dans MCP elicitation timeout. Cet article ne couvre que la retraite des champs OpenTelemetry en 2.1.312, l'alignement mcp_server.name et les branches de telemetry_gate.

Gardez ceci : 2.1.312 change les noms d'événements OpenTelemetry et les attributs de span MCP, pas les permission rules. Ensuite : changelog, échantillon safety_event retiré, JSON pipeline et schema, comparaison mcp_server.name, table du garde, source telemetry_gate, validation config dans JSONNote.

2.1.312 arrête safety_event dans OpenTelemetry et corrige les noms MCP marketplace

Les notes de version sont dans changelog Claude Code, page tag version v2.1.312. Par rapport à 2.1.307 : claude_code.safety_event disparaît et mcp_server.name n'est plus custom sur MCP marketplace URL fixe.

Les permission rules, hooks et sandbox bloquent encore ; seul l'export safety_event disparaît. L'audit doit lire outcome sur les tool spans ou votre JSON approval, sans attendre claude_code.safety_event.

À quoi ressemblait claude_code.safety_event en 2.1.307 et pourquoi 312 l'a retiré

2.1.307 enveloppait les appels bloqués dans claude_code.safety_event avec tool.name, outcome blocked, source permission_rule. L'échantillon sert à l'ancienne forme ; les clients 2.1.312 ne doivent plus émettre ce nom.

Forme legacy claude_code.safety_event de l'ère 2.1.307 (arrêtée en 312).
{
  "name": "claude_code.safety_event",
  "attributes": {
    "tool.name": "Bash",
    "outcome": "blocked",
    "source": "permission_rule"
  }
}

Si une flotte 312 reçoit encore l'événement, vérifiez binaires mélangés ou rejeu collector. telemetry_gate doit renvoyer unexpected_event si claudeCodeVersion est au moins 2.1.312 et span.name reste claude_code.safety_event.

Comment le JSON pipeline d'audit définit claudeCodeVersion et pipeline

Liez chaque lot d'export à claudeCodeVersion ; pipeline déclare comptage safety_event ou groupe mcp_server.name. count_safety_event sur 312 → retire_counter ; en mix la version vient de l'agent.

Config pipeline : version 2.1.312, pipeline et drapeau marketplace.
{
  "claudeCodeVersion": "2.1.312",
  "pipeline": "count_safety_event",
  "marketplacePlugin": true
}

Fixez additionalProperties et enums avec JSON Schema. Si schema échoue, telemetry_gate renvoie schema d'abord, sans deviner ok ou retire_counter.

JSON Schema pipeline : forme avant 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" }
  }
}

Pourquoi mcp_server.name sur MCP distant marketplace passe de custom au nom du plugin

À partir de 2.1.312, MCP distant URL fixe des plugins marketplace Anthropic écrit le vrai nom (ex. github) sur les spans tools/call au lieu de custom. Self-hosted hors scope ; custom avec marketplacePlugin true indique des règles de groupe obsolètes.

2.1.312 : mcp_server.name est le vrai nom marketplace.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "github",
    "tool.name": "create_issue"
  }
}
2.1.311 et avant : MCP marketplace URL fixe souvent custom.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "custom",
    "tool.name": "create_issue"
  }
}
Config et signal spantelemetry_gate renvoieAction opérateur
claudeCodeVersion au moins 2.1.312, pipeline toujours count_safety_eventretire_counterRetirer le compteur safety_event ; lire outcome sur tool span ou JSON approval.
Version au moins 2.1.312 mais span.name reste claude_code.safety_eventunexpected_eventVérifier binaires mélangés ou rejeu collector ; ne pas compter comme blocages normaux.
pipeline mcp_server_group, marketplacePlugin true, mcp_server.name encore customstale_mcp_aliasMettre à jour groupes Grafana et clés d'alerte ; remplacer custom par les vrais noms plugin.
claudeCodeVersion sous 2.1.312, pipeline count_safety_event, span n'est pas safety_eventmissing_safetyLes clients anciens peuvent omettre safety_event ; assouplir le compteur ou bucket par version, ne pas fusionner avec unexpected_event.

Comment telemetry_gate choisit entre schema et cinq branches métier

Ordre : valider config avec JSON Schema pipeline ; lire span.name et attributes.mcp_server.name ; comparer claudeCodeVersion à 2.1.312. Sur 312 nom safety_event → unexpected_event ; 312 et count_safety_event → retire_counter ; sous 312 pipeline count mais span pas safety_event → missing_safety ; 312, mcp_server_group, marketplacePlugin, custom → stale_mcp_alias ; sinon ok.

Ne fusionnez pas retire_counter et ok en un seul passed. Comptez séparément pour voir les équipes sans pipeline à jour et le groupe MCP bloqué sur 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"

Comment valider JSON pipeline et échantillons span dans JSONNote

  1. 1
    Fixer la config pipeline avec Schema

    Ouvrez JSON Schema. Schema pipeline au premier champ, claudeCodeVersion et pipeline au second. Erreurs enum ou pattern d'abord ; pas de branche métier si la forme est mauvaise.

  2. 2
    Format pour les attributs span

    Ouvrez JSON Format, collez un span tools/call du collector. Vérifiez si mcp_server.name est github ou custom avant la branche stale_mcp_alias.

  3. 3
    Diff des noms MCP avant/après

    Ouvrez JSON Diff. À gauche span 2.1.312, à droite legacy custom. mcp_server.name surligné est la ligne Grafana à changer.

Questions fréquentes

2.1.312 émettra-t-il les blocages sous un autre nom d'événement OpenTelemetry ?

Le changelog retire seulement claude_code.safety_event ; aucun événement de remplacement n'est promis. Appuyez-vous sur tool spans, logs permission ou JSON approval, sans attendre un nouveau nom safety.

pipeline peut-il rester count_safety_event sur 2.1.312 ?

Le champ peut rester comme marqueur de migration, mais telemetry_gate renvoie retire_counter sur 312 car le compteur ne verra jamais d'événements. Changez l'enum pipeline ou passez aux métriques outcome sur tool span, ne traitez pas retire_counter comme ok.

Le MCP self-hosted aura-t-il encore mcp_server.name custom ?

stale_mcp_alias ici ne vise que MCP distant URL fixe des plugins marketplace officiels avec marketplacePlugin true. Le nommage self-hosted est hors changelog 2.1.312 ; ne traitez pas tout custom comme erreur.

Comment séparer missing_safety et unexpected_event en mixant 2.1.307 et 2.1.312 ?

unexpected_event : config au moins 2.1.312 et claude_code.safety_event arrive encore. missing_safety : config sous 312, pipeline count_safety_event, span pas cet événement. Les deux dépendent de claudeCodeVersion, pas du volume total au collector.

telemetry_gate ok signifie-t-il qu'on peut ignorer le changement de nom MCP ?

ok signifie seulement que ce config et cet échantillon span ont passé toutes les règles, par ex. mcp_server_group avec vrai nom plugin ou sans count_safety_event. Cela ne garantit pas l'absence de spans legacy futurs ; continuez l'échantillonnage.

Conclusion

Claude Code 2.1.312 supprime l'export OpenTelemetry dédié safety_event et aligne mcp_server.name sur MCP distant marketplace sur les vrais noms plugin. Le JSON d'audit doit fixer claudeCodeVersion et pipeline pour que telemetry_gate branche en période mixte.

Décision : claudeCodeVersion au moins 2.1.312 avec claude_code.safety_event → unexpected_event ; même version et pipeline count_safety_event → retire_counter et changer les métriques ; sous 2.1.312 comptage safety_event mais span pas l'événement → missing_safety ; sur 312 marketplacePlugin true et mcp_server.name custom → stale_mcp_alias ; schema échoue → schema d'abord ; sinon ok.

Ensuite : Schema pour JSON pipeline, Format pour attributs span, Diff mcp_server.name, compter séparément les retours telemetry_gate.

Retour au blog