블로그 • 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 placeholder에서 실제 서버명으로 바꾼다. 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부터 클라이언트는 claude_code.safety_event를 내지 않는다. 여전히 그 이름을 듣면 unexpected_event.
- pipeline이 count_safety_event인데 claudeCodeVersion이 2.1.312면 게이트는 retire_counter로 카운터 폐기를 알린다.
- 2.1.312에서 marketplace 원격 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 이벤트명과 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에서 눈에 띄는 변경은 claude_code.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 재등장을 기다리지 않는다.
2.1.307의 claude_code.safety_event와 312에서 사라지는 이유
2.1.307은 permission rule, auto, safety check, hook, sandbox로 막힌 호출을 claude_code.safety_event로 감쌌다. tool.name, outcome blocked, source permission_rule 등. 아래 샘플은 구 형태 대조용이며 312 클라이언트는 같은 이름을 내지 않는다.
{
"name": "claude_code.safety_event",
"attributes": {
"tool.name": "Bash",
"outcome": "blocked",
"source": "permission_rule"
}
}
312 클러스터에서 여전히 받으면 구 바이너리 혼용이나 collector의 구 배치 재생을 본다. 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. 혼용 시 버전은 collector 설치일이 아니라 agent 보고값.
{
"claudeCodeVersion": "2.1.312",
"pipeline": "count_safety_event",
"marketplacePlugin": true
}
JSON Schema로 additionalProperties와 enum을 고정해 pipeline 오타를 막는다. 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" }
}
}
marketplace 원격 MCP mcp_server.name이 custom에서 플러그인명으로 바뀌는 이유
2.1.312부터 공식 Anthropic marketplace 플러그인 고정 URL 원격 MCP는 tools/call span에 github 같은 실명을 쓰고 custom을 쓰지 않는다. 자체 호스팅 명명은 범위 밖. marketplacePlugin true인데 custom이면 그룹 규칙이 낡았다.
{
"name": "tools/call",
"attributes": {
"mcp_server.name": "github",
"tool.name": "create_issue"
}
}
{
"name": "tools/call",
"attributes": {
"mcp_server.name": "custom",
"tool.name": "create_issue"
}
}
| 설정과 span 신호 | telemetry_gate 반환 | 운영 조치 |
|---|---|---|
| claudeCodeVersion 2.1.312 이상인데 pipeline이 count_safety_event | retire_counter | safety_event 카운터를 내리고 tool span outcome 또는 approval JSON으로 전환. |
| 버전 2.1.312 이상인데 span.name이 claude_code.safety_event | unexpected_event | 구 바이너리 혼용이나 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를 생략할 수 있다. 집계를 완화하거나 버전별로 나누고 unexpected_event와 합치지 않는다. |
telemetry_gate가 schema와 다섯业务 분기를 어떻게 고르나
순서: 파이프라인 JSON Schema로 config 검증; span.name과 attributes.mcp_server.name 읽기; claudeCodeVersion을 2.1.312와 비교. 312에서 safety_event 이름 → unexpected_event; 312이고 count_safety_event → retire_counter; 312 미만인데 count인데 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"
JSONNote에서 파이프라인 JSON과 span 샘플을 어떻게 검증하나
- 1Schema로 파이프라인 설정 고정
열기 JSON Schema. 1칸에 파이프라인 schema, 2칸에 claudeCodeVersion과 pipeline 설정. enum이나 pattern 오류가 먼저 표시되며 형태가 깨지면业务 분기를 반환하지 않는다.
- 2포맷으로 span 속성 확인
열기 JSON 포맷, collector에서 뽑은 tools/call span을 붙인다. mcp_server.name이 github인지 custom인지 확인한 뒤 stale_mcp_alias 분기를 고른다.
- 3Diff로 MCP 이름 전후 비교
열기 JSON Diff. 왼쪽에 2.1.312 새 span, 오른쪽 custom legacy 샘플. Diff로 강조된 mcp_server.name이 Grafana 그룹 수정 줄.
자주 묻는 질문
2.1.312이 차단을 다른 OpenTelemetry 이벤트명으로 내보내나?
changelog은 claude_code.safety_event 중단만 말하고 1:1 대체 이벤트를 약속하지 않는다. 기존 tool span, permission 로그, 자체 approval JSON에 의존하고 새 safety 이벤트명을 기다리지 않는다.
2.1.312에서 pipeline을 count_safety_event로 유지할 수 있나?
필드는 마이그레이션 표시로 남길 수 있지만 312에서는 telemetry_gate가 retire_counter를 반환해 카운터가 이벤트를 못 본다. pipeline enum을 바꾸거나 tool span outcome 지표로 옮기고 retire_counter를 ok로 조용히 통과시키지 않는다.
자체 호스팅 MCP에서도 mcp_server.name이 custom인가?
이 글의 stale_mcp_alias는 공식 marketplace 플러그인이고 marketplacePlugin true인 고정 URL 원격 MCP만 해당한다. 자체 호스팅 명명은 2.1.312 changelog 밖이다. 모든 custom을 misconfig로 보지 않는다.
2.1.307과 2.1.312 혼용 시 missing_safety와 unexpected_event를 어떻게 나누나?
unexpected_event는 설정이 2.1.312 이상인데 claude_code.safety_event가 오는 경우다. missing_safety는 설정 버전이 312 미만이고 pipeline이 count_safety_event인데 span이 그 이벤트가 아닐 때다. 둘 다 claudeCodeVersion에 의존하며 collector 총량만으로는 나눌 수 없다.
telemetry_gate ok면 MCP 이름 변경을 무시해도 되나?
ok는 이 config와 span 샘플이 모든 규칙을 통과했다는 뜻일 뿐이다. 예: mcp_server_group에서 실제 플러그인명을 봤거나 count_safety_event를 내렸다. 미래 배치에 legacy 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 및 지표 변경. 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 반환값을 보고서에서 분리 집계.