Блог • Claude Code / OpenTelemetry

После обновления до Claude Code 2.1.312 в OpenTelemetry больше нет claude_code.safety_event. Какие поля audit JSON менять?

27 октября 2026 Anthropic выпустил Claude Code 2.1.312. Экспорт OpenTelemetry больше не шлёт claude_code.safety_event, добавленный 21 октября в 2.1.307 для tool calls, остановленных permission rule, auto, safety check, hook или sandbox.

Та же версия меняет mcp_server.name у remote MCP с фиксированным URL из официальных marketplace plugins с custom на реальное имя сервера. Дашборды с подсчётом safety_event или группировкой по custom дадут ложные срабатывания без порога версии в локальном JSON.

Контекст блокировок и auto evaluation — в auto evaluation Managed Agents; локальная очередь permission — в Agent permission policy; JSON таймаута неинтерактивного MCP — в MCP elicitation timeout. Эта статья только о снятии полей OpenTelemetry в 2.1.312, выравнивании mcp_server.name и ветвлении telemetry_gate.

Сначала это: 2.1.312 меняет имена событий OpenTelemetry и атрибуты span MCP, не сами permission rules. Далее: changelog, образец снятого safety_event, pipeline JSON и schema, сравнение mcp_server.name, таблица шлюза, исходник telemetry_gate, проверка config в JSONNote.

2.1.312 останавливает safety_event в OpenTelemetry и исправляет имена MCP marketplace

Изменения — в changelog Claude Code, страница тега релиз v2.1.312. По сравнению с 2.1.307: claude_code.safety_event снимают полностью, mcp_server.name для marketplace MCP с фиксированным URL больше не custom.

permission rule, hook и sandbox по-прежнему блокируют tool calls; исчез только экспорт safety_event. Аудит должен читать outcome на tool spans или свой approval JSON, не ждать claude_code.safety_event.

Как выглядел claude_code.safety_event в 2.1.307 и почему в 312 его нет

2.1.307 оборачивал блокировки в claude_code.safety_event с tool.name, outcome blocked, source permission_rule. Образец только для старой формы; клиенты 2.1.312 не должны слать это имя.

Legacy-форма claude_code.safety_event эпохи 2.1.307 (остановлена в 312).
{
  "name": "claude_code.safety_event",
  "attributes": {
    "tool.name": "Bash",
    "outcome": "blocked",
    "source": "permission_rule"
  }
}

Если fleet 312 всё ещё получает событие, проверьте смешение старых бинарников или replay collector. telemetry_gate должен вернуть unexpected_event при claudeCodeVersion не ниже 2.1.312 и span.name claude_code.safety_event.

Как audit pipeline JSON задаёт claudeCodeVersion и pipeline

Привяжите каждый batch экспорта к claudeCodeVersion; pipeline объявляет подсчёт safety_event или группировку по mcp_server.name. count_safety_event на 312 → retire_counter; при mix версия от agent, не от даты установки collector.

Конфиг pipeline: версия 2.1.312, pipeline и флаг marketplace.
{
  "claudeCodeVersion": "2.1.312",
  "pipeline": "count_safety_event",
  "marketplacePlugin": true
}

Зафиксируйте additionalProperties и enum через JSON Schema. При ошибке schema telemetry_gate сначала возвращает schema, не угадывая ok или retire_counter.

JSON Schema pipeline: форма до 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" }
  }
}

Почему mcp_server.name у marketplace remote MCP меняется с custom на имя плагина

С 2.1.312 remote MCP из официальных marketplace plugins Anthropic с фиксированным URL пишет реальное имя (например github) в spans tools/call вместо custom. Self-hosted вне scope; custom при marketplacePlugin true — устаревшие правила группировки.

2.1.312: mcp_server.name — реальное имя marketplace.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "github",
    "tool.name": "create_issue"
  }
}
2.1.311 и раньше: marketplace MCP с фиксированным URL часто custom.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "custom",
    "tool.name": "create_issue"
  }
}
Config и сигнал spantelemetry_gate возвращаетДействие оператора
claudeCodeVersion не ниже 2.1.312, pipeline всё ещё count_safety_eventretire_counterСнять счётчик safety_event; читать outcome tool span или approval JSON.
Версия не ниже 2.1.312, но span.name claude_code.safety_eventunexpected_eventПроверить смешение старых бинарников или replay 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; ослабить счётчик или bucket по версии, не смешивать с unexpected_event.

Как telemetry_gate выбирает между schema и пятью business-ветками

Порядок: validate config через pipeline JSON Schema; читать span.name и attributes.mcp_server.name; сравнить claudeCodeVersion с 2.1.312. На 312 имя safety_event → unexpected_event; 312 и count_safety_event → retire_counter; ниже 312 count pipeline, span не safety_event → 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"

Как проверять pipeline JSON и образцы span в JSONNote

  1. 1
    Зафиксировать config pipeline через Schema

    Откройте JSON Schema. Schema pipeline в первое поле, claudeCodeVersion и pipeline во второе. Ошибки enum или pattern сначала; при плохой форме не возвращать business-ветки.

  2. 2
    Format для атрибутов span

    Откройте JSON Формат, вставьте span tools/call из collector. Проверьте github или custom в mcp_server.name перед веткой stale_mcp_alias.

  3. 3
    Diff имён MCP до и после

    Откройте JSON Diff. Слева span 2.1.312, справа legacy с custom. Подсвеченный mcp_server.name — строка группировки Grafana для правки.

Вопросы

Будет ли 2.1.312 слать блокировки под другим именем события OpenTelemetry?

Changelog только снимает claude_code.safety_event; замены один к одному не обещано. Опирайтесь на tool spans, permission logs или approval JSON, не ждите новое имя safety.

Можно ли оставить pipeline count_safety_event на 2.1.312?

Поле можно оставить как маркер миграции, но telemetry_gate на 312 вернёт retire_counter, потому что счётчик не увидит событий. Смените enum pipeline или перейдите на метрики outcome tool span, не считайте retire_counter ok.

Будет ли у self-hosted MCP mcp_server.name custom?

stale_mcp_alias здесь только для remote MCP с фиксированным URL из официальных marketplace plugins при marketplacePlugin true. Именование self-hosted вне этого changelog 2.1.312; не считайте каждый custom ошибкой.

Как разделить missing_safety и unexpected_event при смешении 2.1.307 и 2.1.312?

unexpected_event: config не ниже 2.1.312, но claude_code.safety_event всё ещё приходит. missing_safety: config ниже 312, pipeline count_safety_event, span не это событие. Оба зависят от claudeCodeVersion, не от общего числа событий на collector.

Значит ли telemetry_gate ok, что изменение имён MCP можно игнорировать?

ok лишь значит, что этот config и образец span прошли все правила, например mcp_server_group с реальным именем плагина или без count_safety_event. Это не гарантия, что в будущих batch не будет legacy spans; продолжайте выборку.

Итог

Claude Code 2.1.312 убирает отдельный экспорт safety_event в OpenTelemetry и выравнивает mcp_server.name у marketplace remote MCP на реальные имена плагинов. Audit JSON должен фиксировать claudeCodeVersion и pipeline, чтобы telemetry_gate ветвился при смешанных fleet.

Решение: 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 для pipeline JSON, Format для атрибутов span, Diff mcp_server.name, отдельно отчитывать возвраты telemetry_gate.

Назад в блог