エージェントオブザーバビリティ:フック、Alloy、Grafana
> Claude CodeとCodexを1つのGrafanaスタックにOpenTelemetryとAlloyで配線し、トレースとログを使ってエージェントの挙動の問題を根源から見つけて直す。
エージェントシステムは、思いもよらない形で壊れます。
モデルが原因のこともあれば、ツールが原因のこともあります。MCPサーバー自体は正常なのに、エージェントが誤った専門エージェントを選んでしまったり、想定外のシェル作業にセッションの半分を費やしていたり、外からは順調に見えるループの中で静かにコストを溶かしていたりすることもあります。
その違いを見分けられないなら、あなたはエージェントシステムを運用しているのではなく、ただ推測しているだけです。
そこで私たちは、自分たちのワークフロー向けにオブザーバビリティスタックを構築しました。Claude Code、Codex、Claudeのフックイベント、Codexの通知イベント、ネイティブのOpenTelemetry、Grafana Alloy、そしてその先にあるGrafana Cloudです。
興味深いのは「ダッシュボードを作った」という点ではありません。興味深いのは、単一のフィードでは全体像がつかめなかったため、テレメトリを2つの異なるストリームに分割せざるを得なかったという点です。
問題:エージェントのテレメトリは断片化している
現代のコーディングエージェントは、すでにある程度のテレメトリを出力しています。それは助けにはなりますが、十分ではありません。
ネイティブのOTELは、次のような問いに答えるのは得意です:
- いくつリクエストを送ったか?
- セッションのコストはいくらだったか?
- スパンやトレースはどこにあるか?
- レイテンシは急増したか?
一方で、次のような問いに答えるのはかなり苦手です:
- エージェントはどのMCPサーバーに依存していたか?
- この失敗は
Bash、組み込みのファイルツール、それともMCP呼び出しのどれで起きたのか? - 実際にどのスキルが発動したのか?
- どのタイプのサブエージェントがディスパッチされたのか?
- そのセッションは有益な作業をしていたのか、それとも空回りしていただけなのか?
この2つ目のカテゴリーの問いは、トレースよりもフックに近いところにあります。
しかし逆もまた真です。もっとも重要なパフォーマンスに関する問いのいくつかは、フックよりもトレースに近いところにあります。
実際にどこでレイテンシが積み上がったのか、どのスパンが遅かったのか、あるいはセッションがモデル呼び出しとツール実行のどちらに時間を費やしていたのかを知りたいなら、意味的なイベントに加えてトレースデータも必要になります。
最終的に行き着いたアーキテクチャ
私たちは2つのテレメトリ経路を並行して運用しています。
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操作に費やしていた」という事実との違いを生み出すものです。
実装の細部として、見た目ほど奇妙ではないものが1つあります。共有のLokiストリームは、イベントの発生元がCodexであってもservice_name="claude-code-hooks"というラベルを使い続けます。ランタイム間の本当の区別はagent_toolの側で行われます。
なぜAlloyが中間に位置するのか
この構成において、Grafana Alloyは単なるフォワーダーではありません。ポリシーの境界線です。
Claude CodeとCodexからのネイティブOTELストリームを、localhost:4318上のローカルAlloyプロキシに向け、Grafana Cloudへ転送する前にAlloyにペイロードを整理させています。
これが重要なのは、生のエージェントテレメトリには、分析には有用だがインデックス済みラベルとしては最悪な、高カーディナリティのフィールドが大量に含まれているからです:
session_idprompt_id- トークン数
- 所要時間
- ツールパラメータのblob
すべてをインデックスすると、ラベルの爆発的増加と、ろくでもない1日が待っています。
そのためAlloyには次の3つを担わせています:
- 低カーディナリティのラベルをごく少数だけインデックス化する。
- ノイズが多いが有用なフィールドを構造化メタデータへ移す。
- 純粋なノイズは完全に破棄する。
重要な考え方はシンプルです。観測は多く、インデックスは少なく。
なぜフックストリームはAlloyを迂回するのか
フックストリームはすでにLoki向けに整形済みです。
send_event.pyがイベントをプッシュする時点で、どのフィールドをラベルとして扱い、どのフィールドを構造化されたJSON本文に含めるかはすでに決まっています。このストリームはAlloyをもう一度経由することなく、Grafana CloudのOTLPゲートウェイへ直接送られます。
つまり、このシステムには明確な役割分担があります:
- Alloyは生のネイティブOTELストリームを飼いならします。
- フックによる拡充は意味的なイベントをクエリ可能にします。
これにより、すべてを1本の経路に無理やり通そうとするよりも、アーキテクチャがシンプルに保たれます。
ダッシュボードが実際に示しているもの
下のスクリーンショットは、私たちのエージェントワークフローを支えるオブザーバビリティダッシュボードの1つです。これはベンチマークではなく、数値はあくまである時点の断面にすぎません。重要なのはデータの形です。アクティビティフィード、ツール呼び出し、失敗、プロンプト、そしてエージェント別・組み込みツール別・MCP利用状況別・シェルコマンド別・スキル別の内訳です。
有用なのは、このダッシュボードが他のエージェントテレメトリと同じGrafanaスタック上に存在している点です。ソースとなるエージェントやツールファミリーでフィルタリングでき、システムごとに別々のオブザーバビリティの物語を作ることなく、ランタイムをまたいで俯瞰できます。
トレースが見た目以上に重要な理由
ログはどのカテゴリーの作業が行われたかを教えてくれます。トレースは、その作業が時間の経過とともにどう展開したかを教えてくれます。
この区別はエージェントシステムにおいて重要です。「遅い」という言葉だけでは、あまりに大雑把で役に立たないからです。
トレースを見れば、問題がどこから来ているのかがわかります:
- モデルのレイテンシ
- ツール実行時間
- 繰り返されるリトライ
- 特別にコストの高い1回のMCPインタラクション
- 個別に見れば無害に見える、小さな操作のロングテール
実際には、フックストリームとTempoのトレースを組み合わせて使っています。
- フックログが答えるのは:何が起きたか?
- トレースが答えるのは:時間はどこへ消えたか?
この組み合わせこそが、オブザーバビリティを単なるダッシュボードから「説明」へと変えるものです。
Codexの位置づけ
Codexは同じスタックの一部ですが、Claude Codeと同一というわけではありません。
Codexについては、次の2つを組み込んでいます:
- CodexのネイティブOTELをAlloyへ
- 通知webhookを
codex_notify.pyへ。これはターン完了をフックイベントで使っているのと同じLokiスキーマへマッピングします
これにより、同一のログストリーム内でagent_tool="codex-cli"のような統一フィルタが使えるようになります。
正直に言うと注意点もあります。Codexの通知ペイロードは、統合の性質そのものが異なるため、現時点ではClaude Codeのフックペイロードよりも薄いものです。現在の私たちの構成では、Codexのターン完了は共通スキーマへ正規化できますが、ツールごとの詳細な抽出については、通知ブリッジよりもネイティブOTELストリームの方が依然として優れています。
だからといってこの記事を書かない理由にはなりません。むしろそれこそがこの記事の要点です。本物のオブザーバビリティシステムは、不完全なシグナルを組み合わせて作られるものなのです。
MCP経由のGrafanaがゲームを変える
より大きな変化は、Grafanaが人間がブラウザで訪れるだけの場所ではなくなったという点です。
このリポジトリでは、GrafanaもMCP経由で公開しています。つまり、人間がまずダッシュボードを手動で確認するのを待つことなく、エージェントが直接Loki、Prometheus、Tempoへクエリを投げられるということです。
これにより、オブザーバビリティはワークフローへの受動的な報告手段ではなく、能動的な入力へと変わります。
エージェントは、次のような問いを立てられます:
- 直近1時間で最も失敗の多かったツールファミリーはどれか?
- どのMCPサーバーがセッションを支配していたか?
- 最近の変更はツールの失敗を減らしたのか、それとも同じミスをより多くのシェル中心の経路へ移しただけなのか?
- もっともレイテンシが高い、あるいはリトライが繰り返されているトレースはどれか?
ここまでくれば、自己改善ループはすぐ目の前です。
ダッシュボードからフィードバックループへ
ここが私たちにとって最も興味深い部分です。
オブザーバビリティスタックがエージェントレイヤーからクエリ可能になった瞬間、テレメトリは受動的な報告手段であることをやめ、制御シグナルになります。
ループは次のような形になります:
- エージェントの活動が、トレース、メトリクス、拡充されたフックログを生成する。
- Grafanaがその証拠をLoki、Tempo、そしてメトリクスが存在する場合はPrometheusに保存する。
- エージェントがGrafana MCP経由でその証拠にクエリを投げる。
- システムが、悪いツールの組み合わせ、壊れやすいスキル、弱いルーティング、あるいは避けられるはずのミスを繰り返し生むシェル中心のワークフローを特定する。
- エージェントやオペレーターが、プロンプト、エージェント設定、スキルの説明、ルーティングルール、ツールアクセスを調整する。
- 次のセッションが新しいテレメトリの形を生み出し、サイクルが繰り返される。
こうして「興味深いダッシュボード」から「測定可能な改善システム」へと移行していくのです。
目標は特定のツールカテゴリーを最大化することではありません。実際に行われている作業に対して、CLI、組み込みツール、MCP呼び出し、スキルの適切な組み合わせに落ち着くことです。
これによって何がわかるようになるか
両方のランタイムが同じGrafanaスタックに収まると、運用上の問いにはるかに速く答えられるようになります:
- 失敗は特定のツールファミリーに集中しているか?
- より高レベルなツールがあるべき場面で、シェル中心のワークフローが避けられるはずのミスを生んでいないか?
- どのMCPサーバーが作業負荷を担っているか?
- 意味のある進捗を生んでいないエージェントの活動に対価を払っていないか?
- セッションが不健全なのは、モデルのせいか、ツールのせいか、それともオーケストレーション層のせいか?
これはマルチエージェントのワークフローにおいて特に有用です。「エージェントが忙しかった」という説明では、ほとんど何もわからないからです。
特定の専門エージェントが繰り返しディスパッチされ、高い失敗率を出し続けているなら、それはルーティングかプロンプト設計の問題です。
1つのMCPサーバーがすべての呼び出しを占めているなら、それは優れたアーキテクチャの証かもしれませんし、他のすべてが無駄な重荷になっているサインかもしれません。
コストが高止まりしたままツールの失敗が急増しているなら、それは品質の問題ではなく運用上の問題です。
より高レベルなツールがあるべき場面で、シェル作業が予測可能かつ避けられる形で失敗し続けているなら、それはプロダクトへのシグナルです。
特定のスキルが常に発動しているのに成果が改善しないなら、それはプロンプトかルーティングへのシグナルです。
本当の教訓
ここでのより深い教訓は、エージェントのオブザーバビリティにはランタイムテレメトリとワークフローテレメトリの両方が必要だということです。
ランタイムテレメトリは、システムが何をしたかを教えてくれます。
ワークフローテレメトリは、エージェント自身が何をしているつもりだったかを教えてくれます。
その両方が必要です。
トレースとカウンターだけを残していると、意味的なレイヤーを見逃します。フックイベントだけを残していると、レイテンシやスパン、そしてより広いランタイム全体の姿を見逃します。
そして両方を残していても、それをエージェントレイヤーへフィードバックしなければ、それは「適応」ではなく単なる「監視」でしかありません。
この組み合わせこそが、システムを運用できるだけ説明可能にし、改善できるだけ調整可能にするものです。
まだ不完全な部分
まだ粗削りな部分は残っています。
- すべてのフックイベントに、欲しいだけの所要時間やトークンのデータが含まれているわけではない。
- タイミングに関する最良のビューの一部は、フックログではなく依然としてTempoのトレースから得られる。
- 拡充されたイベントストリームにおいて、現時点のCodexはClaude Codeほど意味的に豊かではない。
- ダッシュボードのスクリーンショットは、磨き上げられたマーケティング素材ではなく、実際の運用画面である。
最後の点は意図的なものです。エージェントシステムがまるで魔法のように自明であるかのように装うより、本物の計器パネルを見せる方を私たちは選びます。
これがMaguyvaにとって重要な理由
Maguyvaは、エージェントによりよいコードインテリジェンスを与えることを目的としています。しかし、エージェントが実際に有益な作業をこなし始めた瞬間、新たな要件が即座に浮かび上がります。それは、エージェントがどう振る舞っているかを可視化する必要がある、ということです。
検索の質、ルーティングの質、ツール選択、コンテキストの効率性は、すべて観測可能な問題になります。
だからこそ、これは書く価値があると私たちは考えています。未来のエージェントスタックは、プロンプトとツールだけでは成り立ちません。プロンプトとツール、そして全体が機能しているかどうかを教えてくれる計測レイヤーがあって初めて成り立つのです。
本気でエージェントワークフローを構築しているなら、オブザーバビリティはあってもなくてもいいインフラではありません。それはプロダクトの一部です。
関連記事
Maguyva開発ログのその他の記事
コード検索をvoyage-4-largeへアップグレードした理由_
私たちはコード埋め込みをvoyage-4-largeへ移行しました ―― 現在、公開RTEBコード検索リーダーボードのトップに立つモデルです。正直に言うと:私たちが受け入れているトレードオフ、実際に何をインデックス化しているか、そしてなぜプレミアムな埋め込みにお金を払うのか。
言語の再帰的自己改善:約280言語にわたるコードインテリジェンスの磨き上げ_
私たちは約280言語のコードインテリジェンスに対応しています。人間の手でそのすべてを監査することはできません。そこで私たちは、言語の再帰的自己改善ループ ―― 抜き取り検査、LLM-as-judge、1点修正、再検証 ―― を構築し、抽出が単に「グリーン」であるだけでなく実際に正しくなるまで、隔離されたエージェント群でこれを回し続けています。
マルチモーダル・フュージョン検索:あらゆるクエリに最適な検索エンジンを選ぶ_
「parseConfigはどこで定義されているか」のようなクエリと「認証はどう動いているか」のようなクエリでは、求められる検索の種類が異なります。Maguyvaは意図を分類し、それに応じて4つの検索モダリティに重みを付け、重み付き相互順位融合で結果を統合します。