メトリクス・ログ・トレース、同じ障害を見るのになぜ3つ要るのか?
Grafana のダッシュボードで p99 レイテンシが跳ねているアラートを見た経験があるはずだ。グラフは確かに「遅くなった」ことを教えてくれる。しかし、どのリクエストの、どの処理が遅かったのかまでは、そのグラフ一枚からは分からない。これは監視の設定が甘いからではなく、メトリクスという道具がそもそもその質問に答えられるように作られていないからだ。
- メトリクス・ログ・トレースは同じ障害を異なる粒度とコストで捉えており、どれか一つが他を代替できるものではない
- メトリクスは個々のリクエストの情報を捨てて数値に集計するからこそ安く常時収集できる。その代わり「どのリクエストが遅かったか」は原理的に答えられない
- ログはサービスをまたぐと点でしかなく、trace-id という共通の識別子を埋め込んで初めてトレースという1本の線につながる
メトリクスは「遅くなった」は分かっても「誰が遅いか」は分からない
メトリクスとは、一定期間の測定値をカウンタ・ゲージ・ヒストグラムなどの数値として集計したデータのことだ。リクエスト数・エラー率・レイテンシの分布といった値を数秒〜数十秒おきに記録し、時系列として保存する。この集計というステップこそが要点で、個々のリクエストが「誰の何のリクエストだったか」という情報はここで捨てられる。捨てるからこそ、1 秒間に数万リクエストをさばくサービスでも保存コストを抑えたまま常時収集し続けられる。
ここで「ラベルを足せば個々のリクエストまで追えるのでは」と思うかもしれない。しかしラベルの値の組み合わせは、それぞれ独立した新しい時系列として保存される。この組み合わせの種類数をカーディナリティと呼ぶ。エンドポイントやステータスコードのように取りうる値が少ないラベルなら問題にならないが、ユーザー ID やリクエスト ID のように値が事実上無限に増え続けるラベルを付けると、時系列の数がリクエストの数だけ増殖してしまう。Prometheus の公式ドキュメントは、値の種類が絞られていないラベル(ユーザー ID やメールアドレスなど)をラベルに使わないよう明記しており、カーディナリティを低く保つことをメトリクス設計の基本としている。つまりメトリクスは「個々のリクエストを追えないように」設計上わざと粗くされている道具なのだ。
ログは詳細を持つが、サービスをまたぐと点でしかない
ログとは、ある時点で起きた出来事をテキストとして記録したものだ。エラーメッセージ、スタックトレース、リクエストのパラメータなど、メトリクスが捨てた文脈をそのまま保持できる。OpenTelemetry のドキュメントも、本番環境ではフィールドが決まった構造化ログを推奨している。スキーマが安定していれば機械的にパース・相関・大量分析ができるからだ。
問題は、1 つのリクエストが複数のサービスをまたぐ構成では、各サービスが自分の都合でログを吐いているだけだという点にある。API サーバーのログと、その先の認可サービスのログと、さらに先の DB プロキシのログは、それぞれ別のファイルや別のログ基盤に独立して記録される。タイムスタンプが近いというだけでは、それらが本当に同じ 1 件のリクエストに属するログなのか確証が持てない。同時刻に他のユーザーのリクエストも大量に処理されているからだ。
宅配便の伝票番号に近い。倉庫の出荷記録、配送センターの中継記録、配達員の受け渡し記録は、それぞれの拠点が自分の業務のために別々に残す記録であり、伝票番号というただ一つの共通の番号がなければ、後からそれらを同じ荷物の記録として突き合わせる手段がない。伝票番号があって初めて、倉庫から配達までの一本の道のりとして再構成できる。
トレースは trace-id で「1 リクエストの旅」をつなぎ合わせる
トレースとは、1 つのリクエストがシステム内を移動する経路を、スパン(処理の最小単位。開始・終了時刻と、どの処理から呼ばれたかという親子関係を持つ)の集まりとして記録したものだ。OpenTelemetry のドキュメントは、同じトレースに属する全スパンが共通の trace-id を持ち、親スパンの ID を通じて呼び出し階層を表現すると定義している。
この trace-id をサービス間で受け渡す標準形式が W3C の Trace Context 仕様が定める traceparent ヘッダーだ。
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
ハイフン区切りで「バージョン」「trace-id(トレース全体で不変)」「親スパンの ID」「サンプリング等のフラグ」の 4 つを持つ、ただのテキストの HTTP ヘッダーである。仕様上、このヘッダーを受け取ったサービスは次の呼び出し先にも転送する義務があり、転送のたびに自分のスパン ID に更新してよいが、trace-id だけは書き換えずに引き継ぐ。特別なプロトコルではなく、curl でも手で付け外しできる普通のヘッダーだという点が重要だ。
┌──────┐ ┌──────┐ ┌──────┐ │ API │──▶│ Auth │──▶│ DB │ └──────┘ └──────┘ └──────┘ span A span B span C └──── trace-id は全区間で共通 ────┘
三者を結びつけるのは、ログに埋め込まれた trace-id
実際の障害調査は、3 つを順番に乗り換えていく形で進む。まずメトリクスのアラートで「p99 レイテンシが閾値を超えた」という異常の発生と大まかな範囲(どのエンドポイントか)を知る。次に、その時間帯・そのエンドポイントに絞ってログを検索し、遅いリクエストや失敗したリクエストの詳細を探す。ここでログの各行に trace-id が埋め込まれていれば、その 1 行から「このリクエストのトレースを開く」という操作ができ、そこで初めて API・認可・DB のどのスパンが時間を食っていたのかが、スパンの親子関係と所要時間の内訳として見える。
つまり trace-id は、メトリクスが持たない個々のリクエストの識別子を、ログとトレースの双方に共通の鍵として持たせる仕組みだ。ログに trace-id を出力するようアプリケーションを計装しておかない限り、アラートで気づいた異常と、その原因となった具体的なリクエストの経路とは、決してつながらない。
Datadog や Grafana、New Relic のようなオブザーバビリティ基盤では、メトリクスのグラフから該当時間帯の関連トレース一覧へジャンプできるボタンが用意されていることが多い。トレースの詳細画面でエラーになったスパンを選ぶと、そのスパンに紐づくログ行が同じ画面に表示される——これが実現できているのは、アプリケーション側があらかじめ全ログ行に trace-id を出力するよう計装されているからであり、逆に計装が漏れているサービスでは、トレースまでは追えてもそこから先のログ検索だけ手作業に戻る、という体験の差が出る。
traceparent が特別な仕組みではなく、curl で手で付けられるただの HTTP ヘッダーであることを確認する。
curl -s -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \ https://httpbin.org/headers
レスポンスの JSON 内 "Traceparent" フィールドに、送った値がそのまま返ってくるはずだ。サーバー側で何か特殊な処理が行われたわけではなく、単に受け取ったヘッダーをそのまま読めているだけである。実際のサービス間ではこの値を次の呼び出し先へ手動で転送するのが計装ライブラリの役目であり、この転送が 1 か所でも抜けると、そこから先のスパンは trace-id を失って別のトレースとして記録されてしまう。
- メトリクスのラベルを増やせば増やすほど詳しく監視できる — ラベルの値の組み合わせごとに新しい時系列が生成されるため、値の種類が絞られていないラベル(ユーザー ID など)を足すと時系列数が際限なく増え、監視システム自体の負荷とコストが跳ね上がる。
- ログさえ全部集めておけば、後から障害の全体像は追える — マイクロサービス構成では各サービスが独立してログを吐くため、共通の識別子がなければタイムスタンプだけを頼りに他サービスのログと突き合わせるのは実質的に不可能に近い。
- トレースを導入すればメトリクスやログは不要になる — トレースが見せるのは 1 リクエストの経路と所要時間の内訳であり、全体傾向からの異常検知はメトリクスの、経路に現れない詳細なエラー内容はログの役割で、3 つは代替関係ではなく補完関係にある。
- メトリクス
- 一定期間の測定値を数値として集計したデータ。カウンタ・ゲージ・ヒストグラムなど。
- ログ
- ある時点で起きた出来事をテキストとして記録したもの。
- スパン (span)
- トレースを構成する処理の最小単位。開始・終了時刻と親子関係を持つ。
- trace-id
- 1 つのリクエストが辿る全スパンに共通して埋め込まれる識別子。
- traceparent
- trace-id・親スパン ID・サンプリングフラグを HTTP ヘッダーとして伝搬する W3C 標準の形式。
- カーディナリティ
- メトリクスのラベル値の組み合わせの種類数。増えるほど時系列の数と保存コストが増す。
- Traces | OpenTelemetry — trace-id・スパン・親子関係の定義を一次情報で確認できる。
- Logs | OpenTelemetry — 構造化ログが本番で推奨される理由を解説する公式ドキュメント。
- Trace Context | W3C — traceparent ヘッダーのフォーマットと伝搬ルールを定めた仕様そのもの。
- Metric and label naming | Prometheus — カーディナリティを低く保つべき理由と具体的な指針。