Блог • 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 клиент не шлёт claude_code.safety_event; слушатели должны считать имя unexpected_event.
- Если pipeline остаётся count_safety_event и claudeCodeVersion 2.1.312, шлюз должен вернуть retire_counter.
- На 2.1.312 у marketplace remote MCP 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 и атрибуты 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 не должны слать это имя.
{
"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.
{
"claudeCodeVersion": "2.1.312",
"pipeline": "count_safety_event",
"marketplacePlugin": true
}
Зафиксируйте additionalProperties и enum через JSON Schema. При ошибке 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" }
}
}
Почему 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 — устаревшие правила группировки.
{
"name": "tools/call",
"attributes": {
"mcp_server.name": "github",
"tool.name": "create_issue"
}
}
{
"name": "tools/call",
"attributes": {
"mcp_server.name": "custom",
"tool.name": "create_issue"
}
}
| Config и сигнал span | telemetry_gate возвращает | Действие оператора |
|---|---|---|
| claudeCodeVersion не ниже 2.1.312, pipeline всё ещё count_safety_event | retire_counter | Снять счётчик safety_event; читать outcome tool span или approval JSON. |
| Версия не ниже 2.1.312, но span.name claude_code.safety_event | unexpected_event | Проверить смешение старых бинарников или replay 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; ослабить счётчик или 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.
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Зафиксировать config pipeline через Schema
Откройте JSON Schema. Schema pipeline в первое поле, claudeCodeVersion и pipeline во второе. Ошибки enum или pattern сначала; при плохой форме не возвращать business-ветки.
- 2Format для атрибутов span
Откройте JSON Формат, вставьте span tools/call из collector. Проверьте github или custom в mcp_server.name перед веткой stale_mcp_alias.
- 3Diff имён 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.