에이전트 관찰가능성: 훅, Alloy, 그리고 Grafana
> 우리는 OpenTelemetry와 Alloy를 이용해 Claude Code와 Codex를 하나의 Grafana 스택에 연결한 뒤, 트레이스와 로그를 활용해 에이전트 동작 문제를 근원에서 찾아내고 고쳤습니다.
에이전트 시스템은 이상한 방식으로 고장 납니다.
때로는 모델이 문제입니다. 때로는 도구가 문제입니다. 때로는 MCP 서버는 멀쩡한데, 에이전트가 엉뚱한 전문 에이전트를 골랐거나, 세션의 절반을 예상치 못한 셸 작업에 쓰거나, 겉으로는 생산적으로 보이는 루프 안에서 조용히 비용만 태우기도 합니다.
이 차이를 볼 수 없다면, 여러분은 에이전트 시스템을 실제로 운영하는 것이 아니라 그저 추측하고 있는 것입니다.
그래서 우리는 우리 자신의 워크플로를 위한 관찰가능성 스택을 만들었습니다. Claude Code, Codex, Claude 훅 이벤트, Codex 알림(notify) 이벤트, 네이티브 OpenTelemetry, 그리고 반대편의 Grafana Alloy와 Grafana Cloud까지요.
흥미로운 부분은 “대시보드를 만들었다”가 아닙니다. 흥미로운 부분은 단일 피드로는 전체 그림을 얻을 수 없었기 때문에 텔레메트리를 두 개의 서로 다른 스트림으로 나눠야 했다는 점입니다.
문제: 에이전트 텔레메트리는 파편화되어 있다
최신 코딩 에이전트는 이미 어느 정도의 텔레메트리를 내보냅니다. 도움은 되지만 충분하지는 않습니다.
네이티브 OTEL은 다음과 같은 질문에 답하기 좋습니다.
- 요청을 몇 번 보냈는가?
- 세션 비용은 얼마였는가?
- 스팬과 트레이스는 어디에 있는가?
- 지연 시간이 튀었는가?
반면 다음과 같은 질문에는 훨씬 취약합니다.
- 에이전트가 어떤 MCP 서버에 의존했는가?
- 이 실패는
Bash, 내장 파일 도구, 아니면 MCP 호출에서 발생했는가? - 실제로 어떤 스킬이 활성화되었는가?
- 어떤 종류의 서브에이전트가 디스패치되었는가?
- 세션이 유의미한 작업을 하고 있었는가, 아니면 그냥 헛돌고 있었는가?
이 두 번째 부류의 질문은 트레이스보다 훅에 더 가까이 있습니다.
하지만 그 반대도 성립합니다. 가장 중요한 성능 관련 질문 중 일부는 훅보다 트레이스에 더 가까이 있습니다.
지연 시간이 실제로 어디서 쌓였는지, 어떤 스팬이 느렸는지, 아니면 세션이 도구 실행이 아니라 모델 호출에 시간을 태웠는지 알고 싶다면, 시맨틱 이벤트뿐 아니라 트레이스 데이터도 필요합니다.
최종적으로 도달한 아키텍처
우리는 두 개의 텔레메트리 경로를 나란히 운영합니다.
Claude Code
native OTEL -> Alloy -> Grafana Cloud
hooks -> send_event.py -> Grafana Cloud Loki
Codex
native OTEL -> Alloy -> Grafana Cloud
notify hook -> codex_notify.py -> shared Loki schema
이 분리는 의도적입니다.
또한 비대칭적이기도 합니다. Claude Code는 훨씬 풍부한 라이프사이클 훅 표면을 제공합니다. Codex는 네이티브 OTEL과 알림(notify) 표면을 제공하므로, 우리는 두 런타임이 동일한 제어 수단을 노출한다고 가정하는 대신 더 얇은 턴 완료 이벤트를 동일한 로그 스키마로 정규화합니다.
네이티브 OTEL은 기본 스트림을 제공합니다. 런타임 자체에서 나오는 로그와 트레이스, 그리고 런타임이 실제로 내보내는 경우의 메트릭입니다.
훅과 알림 이벤트는 시맨틱 레이어를 제공합니다. PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, SubagentStop, SkillActivated 같은 것들, 그리고 에이전트 동작을 디버깅할 때 우리가 실제로 신경 쓰는 분류된 메타데이터입니다. 여기서는 Claude Code가 더 풍부한 이벤트 스트림을 제공합니다. Codex는 더 얇지만 여전히 유용한 정규화된 스트림을 제공합니다.
훅이 애초에 존재하는 이유
우리의 훅 파이프라인은 이벤트가 Loki에 도달하기 전에 이를 보강합니다.
단순히 “도구가 실행됐다”라고만 말하는 대신, 우리는 이벤트를 다음과 같은 필드로 분류합니다.
tool_type: builtin, mcp, skill, agent, bashmcp_server: 어떤 MCP 백엔드가 호출을 처리했는지bash_cli: 셸 명령어 계열subagent_type: 어떤 종류의 전문 에이전트가 디스패치되었는지agent_tool: 출처가 Claude Code인지 Codex인지
이는 운영상 중요한 질문들을 던질 수 있다는 뜻입니다.
{service_name="claude-code-hooks"} | agent_tool="codex-cli"
{service_name="claude-code-hooks"} | tool_type="mcp"
{service_name="claude-code-hooks"} | json | bash_cli="git"
이런 필드들은 겉치레용이 아닙니다. “에이전트가 느리게 느껴졌다”와 “에이전트가 지난 10분을 도구 실패율이 높은 셸 위주의 git 작업에 썼다”의 차이를 만들어내는 것들입니다.
보기보다 이상하지 않은 구현 세부 사항 하나: 공유 Loki 스트림은 이벤트가 Codex에서 왔을 때도 여전히 service_name="claude-code-hooks"를 레이블로 사용합니다. 런타임 간의 진짜 구분은 agent_tool에서 일어납니다.
Alloy가 중간에 자리하는 이유
이 구성에서 Grafana Alloy는 단순한 포워더가 아닙니다. 정책 경계입니다.
우리는 Claude Code와 Codex의 네이티브 OTEL 스트림을 localhost:4318의 로컬 Alloy 프록시로 향하게 한 뒤, Alloy가 페이로드를 정리하게 하고 나서야 Grafana Cloud로 전달합니다.
이것이 중요한 이유는, 가공되지 않은 원본 에이전트 텔레메트리가 분석에는 유용하지만 인덱싱된 레이블로는 끔찍한 고카디널리티 필드로 가득하기 때문입니다.
session_idprompt_id- 토큰 수
- 소요 시간
- 도구 파라미터 블롭
모든 것을 인덱싱하면 레이블 폭발과 끔찍한 하루를 맞이하게 됩니다.
그래서 Alloy는 우리를 위해 세 가지 일을 합니다.
- 아주 적은 수의 저카디널리티 레이블만 인덱싱된 상태로 유지합니다.
- 시끄럽지만 유용한 필드는 구조화된 메타데이터로 옮깁니다.
- 순수한 노이즈는 완전히 버립니다.
핵심 아이디어는 단순합니다. 더 많이 관찰하고, 더 적게 인덱싱하라.
훅 스트림이 Alloy를 우회하는 이유
훅 스트림은 이미 Loki에 맞게 형태가 잡혀 있습니다.
send_event.py이 이벤트를 밀어넣을 때쯤이면, 어떤 필드가 레이블 처리를 받아야 하고 어떤 필드가 구조화된 JSON 본문에 속해야 하는지 이미 결정되어 있습니다. 이 스트림은 Alloy를 다시 거치지 않고 곧바로 Grafana Cloud의 OTLP 게이트웨이로 향합니다.
그래서 시스템은 명확한 역할 분담을 갖게 됩니다.
- Alloy는 가공되지 않은 네이티브 OTEL 스트림을 길들입니다.
- 훅 보강은 시맨틱 이벤트를 쿼리 가능하게 만듭니다.
이는 모든 것을 하나의 경로로 억지로 밀어넣으려는 것보다 아키텍처를 더 단순하게 유지합니다.
대시보드가 실제로 보여주는 것
아래 스크린샷은 우리 에이전트 워크플로 뒤에 있는 관찰가능성 대시보드 중 하나에서 가져온 것입니다. 벤치마크가 아니며, 수치는 특정 시점의 스냅샷일 뿐입니다. 중요한 것은 데이터의 형태입니다. 활동 피드, 도구 호출, 실패, 프롬프트, 그리고 에이전트별, 내장 도구별, MCP 사용별, 셸 명령별, 스킬별 분류입니다.
유용한 점은 이 대시보드가 나머지 에이전트 텔레메트리와 동일한 Grafana 스택 위에 있다는 것입니다. 우리는 출처 에이전트와 도구 계열로 필터링할 수 있고, 시스템마다 다른 관찰가능성 이야기를 새로 만들어낼 필요 없이 런타임 전반을 살펴볼 수 있습니다.
트레이스가 처음 보이는 것보다 더 중요한 이유
로그는 어떤 범주의 작업이 일어났는지 알려줍니다. 트레이스는 그 작업이 시간에 따라 어떻게 펼쳐졌는지 알려줍니다.
이 구분은 에이전트 시스템에서 중요한데, “느리다”는 표현이 쓸모 있기에는 너무 뭉툭하기 때문입니다.
트레이스는 고통의 원인이 다음 중 무엇이었는지 알려줄 수 있습니다.
- 모델 지연 시간
- 도구 실행 시간
- 반복된 재시도
- 유독 비용이 큰 하나의 MCP 상호작용
- 개별적으로는 무해해 보였던 작은 작업들의 긴 꼬리
실제로 우리는 훅 스트림과 Tempo 트레이스를 함께 사용합니다.
- 훅 로그는 답합니다. 어떤 종류의 일이 일어났는가?
- 트레이스는 답합니다. 시간이 어디로 갔는가?
이 조합이 바로 관찰가능성을 대시보드에서 설명으로 바꿔주는 것입니다.
Codex가 들어맞는 지점
Codex는 동일한 스택의 일부이지만 Claude Code와 똑같지는 않습니다.
Codex를 위해 우리는 두 가지를 연결합니다.
- Codex의 네이티브 OTEL을 Alloy로
- 턴 완료를 우리가 훅 이벤트에 사용하는 것과 동일한 Loki 스키마로 매핑하는 알림(notify) 웹훅을
codex_notify.py로
이는 동일한 로그 스트림 안에서 agent_tool="codex-cli" 같은 통합 필터를 가능하게 해줍니다.
솔직한 단서 조항: Codex의 알림 페이로드는 동일한 종류의 통합 표면이 아니기 때문에 현재 Claude Code의 훅 페이로드보다 얇습니다. 오늘날 우리 구성에서 Codex의 턴 완료는 공유 스키마로 정규화될 수 있지만, 도구별로 풍부한 추출은 알림 브리지보다 네이티브 OTEL 스트림에서 여전히 더 낫습니다.
이것이 이 글을 쓰지 말아야 할 이유는 아닙니다. 오히려 이 글의 요점입니다. 진짜 관찰가능성 시스템은 불완전한 신호들로 조립됩니다.
MCP를 통한 Grafana가 판을 바꾼다
더 큰 변화는 Grafana가 더 이상 사람이 브라우저에서 방문하는 장소만이 아니라는 점입니다.
이 저장소에서 우리는 Grafana를 MCP를 통해서도 노출합니다. 즉 에이전트가 사람이 먼저 대시보드를 수동으로 살펴보기를 기다리는 대신, Loki, Prometheus, Tempo를 직접 쿼리할 수 있다는 뜻입니다.
이는 관찰가능성을 워크플로에 대한 능동적인 입력으로 바꿔놓습니다.
에이전트는 다음과 같이 물을 수 있습니다.
- 지난 한 시간 동안 어떤 도구 계열이 가장 많이 실패했는가?
- 어떤 MCP 서버가 세션을 지배했는가?
- 최근 변경이 도구 실패를 줄였는가, 아니면 그저 같은 실수를 더 셸 위주의 경로로 옮겼을 뿐인가?
- 어떤 트레이스가 가장 높은 지연 시간이나 반복된 재시도를 보이는가?
이것을 갖추면 자기 개선 루프에 매우 가까워집니다.
대시보드에서 피드백 루프로
이 부분이 우리가 가장 흥미롭게 여기는 지점입니다.
관찰가능성 스택이 에이전트 레이어에서 쿼리 가능해지는 순간, 텔레메트리는 수동적인 보고 표면이기를 멈추고 제어 신호가 됩니다.
루프는 이렇게 생겼습니다.
- 에이전트 활동이 트레이스, 메트릭, 보강된 훅 로그를 내보냅니다.
- Grafana가 그 증거를 Loki, Tempo, 그리고 메트릭이 존재하는 Prometheus에 저장합니다.
- 에이전트가 Grafana MCP를 통해 그 증거를 쿼리합니다.
- 시스템이 나쁜 도구 조합, 취약한 스킬, 부실한 라우팅, 혹은 피할 수 있는 실수를 계속 만들어내는 셸 위주 워크플로를 식별합니다.
- 에이전트나 운영자가 프롬프트, 에이전트 설정, 스킬 설명, 라우팅 규칙, 혹은 도구 접근 권한을 조정합니다.
- 다음 세션은 새로운 텔레메트리 형태를 만들어내고, 순환이 반복됩니다.
이것이 “흥미로운 대시보드”에서 “측정 가능한 개선 시스템”으로 나아가는 방법입니다.
목표는 하나의 도구 범주를 극대화하는 것이 아닙니다. 실제로 수행되는 작업에 맞는 CLI, 내장 도구, MCP 호출, 스킬의 올바른 조합에 도달하는 것입니다.
이것으로 답할 수 있게 되는 것들
두 런타임이 동일한 Grafana 스택에 도달하면, 우리는 운영상의 질문에 훨씬 빠르게 답할 수 있습니다.
- 실패가 하나의 도구 계열에 집중되어 있는가?
- 더 상위 수준의 도구가 존재해야 할 자리에서 셸 위주 워크플로가 피할 수 있는 실수를 만들어내고 있는가?
- 어떤 MCP 서버가 작업 부하를 짊어지고 있는가?
- 유의미한 진전을 만들어내지 못하는 에이전트 활동에 비용을 지불하고 있는가?
- 세션이 모델, 도구, 혹은 오케스트레이션 레이어 때문에 상태가 나쁜가?
이는 특히 다중 에이전트 워크플로에서 유용한데, 여기서는 “에이전트가 바빴다”는 말이 거의 아무것도 알려주지 않기 때문입니다.
특정 전문 에이전트가 계속 디스패치되면서 높은 실패율을 만들어낸다면, 그것은 라우팅이나 프롬프트 설계 문제입니다.
하나의 MCP 서버가 모든 호출을 지배한다면, 이는 좋은 아키텍처일 수도 있고 나머지 전부가 죽은 무게라는 신호일 수도 있습니다.
비용은 높게 유지되는데 도구 실패가 급증한다면, 이는 품질 문제가 아니라 운영 문제입니다.
더 상위 수준의 도구가 존재해야 할 자리에서 셸 작업이 예측 가능하고 피할 수 있는 방식으로 계속 실패한다면, 이는 제품 신호입니다.
하나의 스킬이 끊임없이 활성화되지만 결과를 개선하지 못한다면, 이는 프롬프트나 라우팅 신호입니다.
진짜 교훈
여기서 더 깊은 교훈은 에이전트 관찰가능성에 런타임 텔레메트리와 워크플로 텔레메트리가 모두 필요하다는 것입니다.
런타임 텔레메트리는 시스템이 무엇을 했는지 알려줍니다.
워크플로 텔레메트리는 에이전트가 스스로 무엇을 하고 있다고 생각했는지 알려줍니다.
우리에게는 둘 다 필요합니다.
트레이스와 카운터만 유지한다면 시맨틱 레이어를 놓치게 됩니다. 훅 이벤트만 유지한다면 지연 시간, 스팬, 그리고 더 넓은 런타임 그림을 놓치게 됩니다.
그리고 둘 다 유지하면서도 이를 에이전트 레이어로 다시 피드백하지 않는다면, 여러분은 모니터링을 갖춘 것이지 적응을 갖춘 것이 아닙니다.
이 조합이야말로 시스템을 운영할 만큼 설명 가능하게 만들고, 개선할 만큼 조정 가능하게 만드는 것입니다.
아직 완벽하지 않은 것들
여전히 거친 부분들이 남아 있습니다.
- 모든 훅 이벤트가 우리가 원하는 소요 시간과 토큰 데이터를 포함하지는 않습니다.
- 최고의 타이밍 뷰 중 일부는 여전히 훅 로그가 아니라 Tempo 트레이스에서 나옵니다.
- 오늘날 Codex는 보강된 이벤트 스트림에서 Claude Code보다 시맨틱 측면이 덜 풍부합니다.
- 대시보드 스크린샷은 다듬어진 마케팅 자료가 아니라 실시간 운영 화면입니다.
마지막 항목은 의도적입니다. 우리는 에이전트 시스템이 마법처럼 스스로 설명된다고 가장하기보다는 진짜 계기판을 보여주는 쪽을 택합니다.
이것이 Maguyva에 중요한 이유
Maguyva는 에이전트에게 더 나은 코드 인텔리전스를 제공하는 것에 관한 것입니다. 하지만 에이전트가 실제로 유용한 작업을 하기 시작하면, 새로운 요구 사항이 즉시 나타납니다. 에이전트가 어떻게 행동하고 있는지 볼 수 있어야 한다는 것이죠.
검색 품질, 라우팅 품질, 도구 선택, 컨텍스트 효율성 모두가 관찰 가능한 문제가 됩니다.
이것이 우리가 이 글을 쓸 가치가 있다고 생각하는 이유입니다. 미래의 에이전트 스택은 단순히 프롬프트와 도구만이 아닙니다. 프롬프트, 도구, 그리고 전체 시스템이 제대로 작동하는지 알려주는 계측 레이어까지 포함합니다.
진지한 에이전트 워크플로를 구축하고 있다면, 관찰가능성은 선택적인 인프라가 아닙니다. 제품의 일부입니다.
관련 글
Maguyva 빌드 로그의 다른 글들
우리가 코드 검색을 voyage-4-large로 업그레이드한 이유_
우리는 코드 임베딩을 voyage-4-large로 옮겼습니다. 현재 공개된 RTEB 코드 검색 리더보드에서 1위인 모델입니다. 솔직한 버전을 말하자면, 우리가 감수하는 트레이드오프, 실제로 우리가 인덱싱하는 것, 그리고 프리미엄 임베딩에 비용을 지불하는 이유입니다.
언어 재귀적 자기 개선: 약 280개 언어에 걸친 코드 인텔리전스 갈아내기_
우리는 약 280개 언어에 대한 코드 인텔리전스를 지원합니다. 사람이 이를 일일이 검수할 수는 없습니다. 그래서 우리는 언어 재귀적 자기 개선 루프 — 무작위 점검, LLM 판정, 한 가지 수정, 재검증 — 를 만들었고, 추출이 단순히 초록불(green)이 아니라 실제로 옳아질 때까지 격리된 에이전트 플릿으로 이를 실행합니다.
다중 모달 융합 검색: 모든 쿼리에 맞는 검색기를 고르는 법_
"parseConfig는 어디서 정의되었나" 같은 쿼리는 "인증은 어떻게 동작하나"와는 다른 검색을 원합니다. Maguyva는 의도를 분류하고, 그에 맞춰 네 가지 검색 방식에 가중치를 부여한 뒤, 가중 상호 순위 융합(Reciprocal Rank Fusion)으로 결과를 결합합니다.