블로그 • 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이 바꾸는 것은 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 클라이언트는 같은 이름을 내지 않는다.

2.1.307 시대 claude_code.safety_event 형태(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 보고값.

파이프라인 설정: 버전 2.1.312, pipeline 및 marketplace 플래그.
{
  "claudeCodeVersion": "2.1.312",
  "pipeline": "count_safety_event",
  "marketplacePlugin": true
}

JSON Schema로 additionalProperties와 enum을 고정해 pipeline 오타를 막는다. schema 실패 시 telemetry_gate는 먼저 schema를 반환하고 누락 필드로 ok나 retire_counter를 추측하지 않는다.

파이프라인 JSON Schema: 형태 통과 후 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" }
  }
}

marketplace 원격 MCP mcp_server.name이 custom에서 플러그인명으로 바뀌는 이유

2.1.312부터 공식 Anthropic marketplace 플러그인 고정 URL 원격 MCP는 tools/call span에 github 같은 실명을 쓰고 custom을 쓰지 않는다. 자체 호스팅 명명은 범위 밖. marketplacePlugin true인데 custom이면 그룹 규칙이 낡았다.

2.1.312: mcp_server.name은 marketplace 실제명.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "github",
    "tool.name": "create_issue"
  }
}
2.1.311 이전: 고정 URL marketplace MCP는 custom이 흔했다.
{
  "name": "tools/call",
  "attributes": {
    "mcp_server.name": "custom",
    "tool.name": "create_issue"
  }
}
설정과 span 신호telemetry_gate 반환운영 조치
claudeCodeVersion 2.1.312 이상인데 pipeline이 count_safety_eventretire_countersafety_event 카운터를 내리고 tool span outcome 또는 approval JSON으로 전환.
버전 2.1.312 이상인데 span.name이 claude_code.safety_eventunexpected_event구 바이너리 혼용이나 collector 재생을 조사하고 정상 차단 집계에 넣지 않는다.
pipeline mcp_server_group, marketplacePlugin true, mcp_server.name이 customstale_mcp_aliasGrafana 그룹과 알림 키를 갱신하고 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에 머무는지 본다.

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"

JSONNote에서 파이프라인 JSON과 span 샘플을 어떻게 검증하나

  1. 1
    Schema로 파이프라인 설정 고정

    열기 JSON Schema. 1칸에 파이프라인 schema, 2칸에 claudeCodeVersion과 pipeline 설정. enum이나 pattern 오류가 먼저 표시되며 형태가 깨지면业务 분기를 반환하지 않는다.

  2. 2
    포맷으로 span 속성 확인

    열기 JSON 포맷, collector에서 뽑은 tools/call span을 붙인다. mcp_server.name이 github인지 custom인지 확인한 뒤 stale_mcp_alias 분기를 고른다.

  3. 3
    Diff로 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 반환값을 보고서에서 분리 집계.

블로그로 돌아가기