Skip to content

필사 모드: オブザーバビリティ徹底解説:ログ・トレーシング・LLM監視

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに — オブザーバビリティとは何か

システムが遅くなりました。ユーザーは「決済ができない」と言います。ところがサーバーは問題なく動いていて、CPUも正常、エラーログも特にありません。いったいどこが問題なのか? この問いに答える能力こそが**オブザーバビリティ(observability)**です。

オブザーバビリティはモニタリングとよく混同されます。モニタリングが「あらかじめ決めた指標が正常範囲か」を見るものだとすれば、オブザーバビリティは「システムの外から投げる任意の問いに、内部状態で答えられるか」です。前者はすでに分かっている問題(known unknowns)を監視し、後者は予想外の問題(unknown unknowns)を掘り下げます。

この能力をつくる材料が、よく**三本柱(three pillars)**と呼ばれるログ・メトリクス・トレースです。この記事は三本柱を一つずつ押さえ、ログ保存先(Loki vs OpenSearch)と分散トレーシング(OpenTelemetry、Jaeger、Tempo)の選択肢を対比したうえで、最近新しく台頭した**LLMオブザーバビリティ**(Langfuseなど)まで続けて整理します。

三本柱 — ログ、メトリクス、トレース

三つの信号は、同じ出来事を異なる角度から記録します。

- **ログ(logs)**: 特定の時点に起きた個々の出来事の記録です。「12:03:11に注文4821の決済が失敗、理由:カード拒否」。もっとも詳細ですが、量が爆発しやすいです。

- **メトリクス(metrics)**: 時間に沿って集計された数値です。「秒あたりリクエスト数」「p99レイテンシ」「エラー率」。保存が安くダッシュボード・アラートに適しますが、個々の出来事の文脈はありません。

- **トレース(traces)**: 一つのリクエストが複数のサービスを経る全体の旅路です。APIゲートウェイから始まり、注文サービス、決済サービス、DBを通りながら、各区間がどれだけかかったかを示します。

ここに最近、**継続的プロファイリング(continuous profiling)**が四本目の柱としてよく挙がります。本番でCPU・メモリを関数単位で常時サンプリングし、「どの関数がCPUを食うか」をトレースより深いコードレベルで答えます。Grafana Pyroscope、Parcaといったツールが代表的です。

肝心なのは三つの信号を別々に見るのではなく、**結びつけて**見ることです。メトリクスで「エラー率が跳ねた」を見つけ、その時点のトレースへ移って「決済サービスが遅かった」を確認し、そのトレースに紐づくログで「カード会社のタイムアウト」という原因を読み取る。この流れがオブザーバビリティの核心です。

信号はどう結びつくか — trace_idとexemplar

三つの信号を結ぶ接着剤が**相関ID(correlation ID)**、なかでも`trace_id`です。

もっとも実用的な結びつきは**ログにtrace_idを埋め込むこと**です。構造化ログのフィールドとしてtrace_idを一緒に残せば、あるトレースを見ながら「このリクエストが残したログだけ」を絞り込めます。逆にエラーログを見ながら、そのtrace_idで全体のトレースを開くこともできます。

{

"timestamp": "2026-07-03T12:03:11.482Z",

"level": "ERROR",

"service": "payment-service",

"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",

"span_id": "00f067aa0ba902b7",

"message": "card declined",

"order_id": 4821

}

メトリクスとトレースは**exemplar(見本)**で結びつきます。exemplarはメトリクスのデータポイントに代表的なtrace_idを一つ付けておく手法です。たとえば「p99レイテンシ」ヒストグラムの遅いバケットに、実際に遅かったリクエストのtrace_idを付けておけば、ダッシュボードでグラフの跳ねた点をクリックして、そのままそのトレースへジャンプできます。メトリクス(何が)からトレース(なぜ)へ渡る橋です。

この結びつきを機能させることが、オブザーバビリティ設計の半分です。信号がいくらあっても互いにつながっていなければ、問題に出くわしたとき、三つの別々の窓を目で見比べる苦痛が残ります。

ログ — Loki vs OpenSearch

ログをどこにどう保存するかは、コストと検索体験を大きく分けます。代表的な二つの陣営がGrafana LokiとOpenSearch(旧Elasticsearch系)です。

Loki — 「ログ界のPrometheus」

Grafana **Loki**の思想は一文で言えば「ログ本文はインデックスしない」です。Lokiはログの全文に転置インデックスを作らず、代わりに**ラベル(label)**だけをインデックスします。ログ本文そのものは圧縮して安価なオブジェクトストレージ(S3、GCSなど)にそのまま放り込みます。だからPrometheusがメトリクスをラベルで扱うように、Lokiはログをラベルで扱います。「ログ界のPrometheus」という別名はここから来ています。

クエリ言語は**LogQL**です。まずラベルでログストリームを絞り、その中でテキストをフィルタします。

{app="payment-service", level="error"} |= "card declined" | json | order_id="4821"

ここで前の波括弧の部分がラベルセレクタ(インデックスがかかった部分)で、後ろの`|=`からが本文フィルタです。この構造のおかげでインデックスが小さく、保存コストが低く運用が軽いです。一方で、ラベルで十分に絞らないまま広大な期間を全文検索すると遅くなり得ます。**ラベル設計**が成否を分けます。

注意すべき罠は**高カーディナリティ(high cardinality)のラベル**です。user_idやtrace_idのように値の種類が爆発するものをラベルに使うと、ストリームが数百万に分裂してLokiが急激に遅くなります。そうした値はラベルではなくログ本文に入れ、フィルタで探すべきです。

OpenSearch — 強力な全文検索

**OpenSearch**(そしてその源流であるElasticsearch)は正反対のアプローチです。ログのほぼすべてのフィールドに**転置インデックス(inverted index)**を作ります。おかげで任意のフィールドで複雑な全文検索、集計、ファセット分析を高速に行え、Kibana / OpenSearch Dashboardsという強力な探索UIが付きます。

同じクエリをOpenSearchのクエリDSLで書くとこうなります。

{

"query": {

"bool": {

"must": [

{ "match": { "service": "payment-service" } },

{ "match": { "message": "card declined" } },

{ "term": { "order_id": 4821 } }

]

}

}

}

代償は重さです。すべてをインデックスするので保存容量とメモリを多く使い、クラスタ運用(シャード、ヒープ、リバランス)も手がかかります。ログ量が増えるほどインフラコストが急に上がります。

いつどちらを選ぶか

- **Loki**: コストに敏感で、ログを主に「ラベルで絞って直近の区間を眺める」使い方をし、すでにGrafana・Prometheusエコシステムを使っているなら自然です。

- **OpenSearch**: 任意のフィールドで強力な全文検索や複雑な集計・分析が頻繁に必要で(セキュリティログ分析、監査、SIEMなど)、それだけのインフラコストを負担できるなら強力です。

構造化ロギングとログレベル

どの保存先を使うにせよ共通の原則があります。ログは人が読む文章ではなく、**機械がパースする構造化データ(JSON)**として残すのが良いです。`"2026-07-03 payment failed for order 4821"`のような文字列より、上で見たようにフィールドが分かれたJSONのほうがフィルタ・集計に圧倒的に有利です。

ログレベル(DEBUG、INFO、WARN、ERROR)は騒音を制御する最初のダイヤルです。そしてトラフィックの大きいサービスなら**サンプリング**が必須です。成功したリクエストのINFOログを100%残す必要はありません。たとえば正常リクエストは1%だけサンプリングし、エラーは100%残す、というふうにコストと情報量のバランスを取ります。

分散トレーシング — OpenTelemetryが標準になる

マイクロサービスでは、一つのユーザーリクエストが数多くのサービスを経ます。この旅路を再構成するのが**分散トレーシング**であり、この分野の事実上の標準が**OpenTelemetry(OTel)**です。

なぜOpenTelemetryか

OpenTelemetryはCNCF傘下の**ベンダー中立の標準**です。以前は観測ツールごとに計測SDKが別々で、バックエンドを変えるとコードの計測を全部作り替える必要がありました。OTelはこの計測層を標準化しました。コードはOTelで一度だけ計測し、データをどこへ送るか(Jaeger、Tempo、商用ベンダーなど)は後からエクスポーター設定を変えるだけで決めます。**ロックインを解くこと**が核心の価値です。

トレーシングの基本概念はこうです。

- **トレース(trace)**: 一つのリクエストの全旅路。固有のtrace_idで識別されます。

- **スパン(span)**: トレースを構成する一つの作業単位(例:「DBクエリ」「決済API呼び出し」)。開始・終了時刻を持ちます。

- **親子関係**: スパンは入れ子になります。「注文処理」スパンの中に「在庫確認」「決済」スパンが子として入ります。この木がトレースの形をつくります。

- **属性(attributes)とイベント(events)**: スパンに付けるキー・バリューのメタデータ(例:http.method、db.system)と、スパンの生存中に起きた時点の記録です。

コンテキスト伝播 — traceparentヘッダー

サービスAがサービスBを呼ぶとき、二つのサービスのスパンが同じトレースにつながるにはtrace_idが渡らなければなりません。この**コンテキスト伝播(context propagation)**の標準がW3C Trace Context、すなわちHTTPリクエストに載る`traceparent`ヘッダーです。値はおおよそバージョン、trace_id、親span_id、フラグをハイフンでつないだ形です。

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

│ │ │ │

バージョン trace_id (128ビット) 親span_id フラグ(サンプル済)

受信側サービスはこのヘッダーを読み、自分のスパンを同じtrace_idの下に子として付けます。こうしてサービス境界を越えてトレースが一つにつながります。

OTel Collectorと計測方式

**OTel Collector**は、アプリケーションとバックエンドの間に置く独立したプロセスです。多くのサービスが送ったテレメトリを受け取り(receive)、加工し(process:バッチ、サンプリング、属性の編集)、望むバックエンドへ送り出します(export)。アプリはCollector一つを知っていればよく、バックエンドの入れ替え・多重送信・サンプリングポリシーをCollectorで中央集権的に管理します。

計測には二つの方式があります。

- **自動計測(auto-instrumentation)**: 言語エージェントが人気ライブラリ(HTTPサーバー、DBドライバ、gRPCなど)を自動でフックしてスパンを作ってくれます。コード修正がほとんどなく、素早く始められます。

- **手動計測(manual instrumentation)**: SDKで直接スパンを開き、属性を付けます。ビジネスロジックの意味ある区間を細かく捉えたいときに必要です。

最小限のOTel SDK初期化はおおよそこんな形です(Node.jsの例)。

const { NodeSDK } = require("@opentelemetry/sdk-node");

const {

getNodeAutoInstrumentations,

} = require("@opentelemetry/auto-instrumentations-node");

const {

OTLPTraceExporter,

} = require("@opentelemetry/exporter-trace-otlp-http");

const sdk = new NodeSDK({

traceExporter: new OTLPTraceExporter({

url: "http://otel-collector:4318/v1/traces",

}),

instrumentations: [getNodeAutoInstrumentations()],

});

sdk.start();

バックエンド — JaegerとTempo

収集したトレースはトレーシングバックエンドで保存・照会します。

- **Jaeger**: Uber発の成熟したオープンソースのトレーシングシステムです。独自のUIでトレース検索・依存関係グラフ・タイムラインを提供します。

- **Grafana Tempo**: Lokiのトレース版にあたります。トレースをインデックスせず安価なオブジェクトストレージに丸ごと保存し、コストを大きく下げます。trace_idでの照会が基本で、Loki・PrometheusとGrafanaでなめらかに連携します。

サンプリング — head vs tail

トレースを100%保存すると量とコストが手に負えません。だから**サンプリング**をします。

- **ヘッドサンプリング(head-based)**: トレース開始時点で「これは保存、あれは破棄」を即座に決めます。たとえば「1%だけ保存」。単純で安いですが、まさに問題のある(遅い・エラーの)トレースを捨ててしまうことがあります。

- **テールサンプリング(tail-based)**: トレースが終わった後、全体を見て決めます。「エラーがある、またはp99より遅いトレースは必ず保存」といったルールをかけられ、正常なトラフィックは捨てつつ問題のあるトラフィックは確実に残します。代わりにトレースが終わるまでバッファリングする必要があり、Collectorに負荷とメモリがより多くかかります。

LLMオブザーバビリティ — なぜ特別なツールが必要か

ここで最近急浮上した新しい領域へ移ります。LLMアプリケーション(チャットボット、RAG、エージェント)は、従来の観測ツールだけでは不十分です。理由は明確です。

- **非決定性**: 同じ入力でも出力が毎回異なります。「成功/失敗」という二値の判断では品質を捉えられません。

- **プロンプト・バージョンのドリフト**: プロンプトを一行変えたら品質が崩れることがよくあります。どのプロンプトのバージョンがどの結果を出したかを追う必要があります。

- **コスト**: LLM呼び出しはトークン単位で課金されます。どのリクエストが、どのチェーン段階がトークンをどれだけ食うかを見て初めてコストを制御できます。

- **品質評価**: レイテンシが正常でも、答えが間違っていたり、幻覚(hallucination)を作り出したりします。正答かどうかを別途評価(eval)する必要があります。

- **RAGデバッグ**: 答えがおかしいとき、原因が検索(retrieval)が見当違いの文書を引いてきたせいか、生成(generation)が文書を無視したせいかを区別する必要があります。

Langfuse — LLM呼び出しのトレース

**Langfuse**は、この問題に正面から取り組むオープンソースのLLMオブザーバビリティプラットフォームです。概念的には分散トレーシングに似ています。一つのユーザー対話を一つのトレースとして捉え、その中の各段階を入れ子のスパンとして記録します。ただしスパンの内容がLLMに特化しています。各LLM呼び出しごとにプロンプト、完了結果、トークン使用量、コスト、レイテンシを収め、チェーン・エージェント・検索の段階を親子に入れ子にします。

RAGパイプライン一つのトレースの形をごく単純化するとこうなります。

{

"trace_id": "t_9f2c",

"name": "rag-chat",

"input": "返金ポリシーはどうなっていますか?",

"spans": [

{

"name": "retrieval",

"type": "retriever",

"input": "返金ポリシー",

"output": ["doc-12", "doc-45"],

"latency_ms": 82

},

{

"name": "llm-answer",

"type": "generation",

"model": "claude-x",

"prompt_tokens": 1240,

"completion_tokens": 180,

"cost_usd": 0.0042,

"latency_ms": 1310

}

],

"scores": [{ "name": "helpfulness", "value": 0.9 }]

}

Langfuseが与える核心機能はこうです。

- **入れ子トレース**: チェーン・エージェント・ツール呼び出し・検索を階層構造で可視化します。エージェントがどのツールを何回呼んだかが一目で分かります。

- **プロンプトのバージョン管理**: プロンプトをバージョンごとに保存・デプロイし、どのバージョンがどのトレースを出したかを結びつけます。

- **評価とスコア(evals/scores)**: トレースにスコアを付けます。LLM-as-a-judgeの自動評価、ルールベースのスコア、人によるラベリングをすべて収められます。

- **ユーザーフィードバック**: エンドユーザーの高評価/低評価のようなフィードバックを該当トレースに結びつけ、実際の品質信号として使います。

他の選択肢 — LangSmith、Helicone、Phoenix

Langfuseだけではありません。

- **LangSmith**: LangChainチームが作った商用プラットフォームです。LangChain/LangGraphと緊密に統合され、トレーシング・評価・データセット管理が緻密です。

- **Helicone**: プロキシ方式が特徴です。LLM APIの前にプロキシとして載せれば、コード変更を最小限にしながらロギング・キャッシュ・コスト追跡を付けられます。

- **Arize Phoenix**: オープンソースで、OpenTelemetryベース(OpenInference)である点が強みです。トレーシングと評価に加え、埋め込み・ドリフト分析のようなML観測機能が強いです。

選ぶ基準はおおむねこうです。LangChain中心ならLangSmith、コード変更なしで素早く付けたいならHelicone、オープンソースとOTel標準を重視するならPhoenixやLangfuseが自然です。

一つにまとめる — OpenTelemetry中心のスタック

では全体を一つの絵に結んでみましょう。最近よくある構成は**OpenTelemetryをハブ**に置くことです。

アプリ(サービス群) ── OTel SDK ──▶ [ OTel Collector ]

┌─────────────────────────┼─────────────────────────┐

▼ ▼ ▼

[ Loki (ログ) ] [ Tempo (トレース) ] [ Prometheus (メトリクス) ]

└───────────── Grafana で統合照会 ──────────────┘

LLM呼び出し ────────────────▶ [ Langfuse (LLMトレース・コスト・評価) ]

核心の原理は先に強調した**trace_id相関**です。アプリはOTelで計測してCollectorへ送り、CollectorがログはLoki、トレースはTempo、メトリクスはPrometheusへ振り分けます。Grafanaで三つを一画面に並べ、trace_idでログ↔トレースを、exemplarでメトリクス→トレースを行き来します。LLM部分はLangfuseが別途捉えますが、同じtrace_idを載せておけば通常のトレースともつなげられます。

よくハマる罠 — コストとカーディナリティ

- **カーディナリティ爆発**: メトリクスのラベルやLokiのラベルに、user_id、request_id、trace_idのように値が無限に増えるものを入れると、時系列・ストリームが激増して保存先が崩れます。高カーディナリティの値はログ本文やトレース属性に入れ、ラベルは種類が限られたもの(サービス名、リージョン、ステータスコードなど)だけにします。

- **無分別な100%保存**: すべてのログ・トレースを全量保存するとコストが手に負えません。正常はサンプリングし、問題(エラー・遅いリクエスト)は確実に残すポリシーを初期に立てます。

- **つながっていない信号**: trace_idをログに埋め込まないと三本柱が別々に動きます。計測の初期に必ずtrace_idの注入を入れてください。

- **LLMコストの放置**: プロンプトが長くなったり再試行が増えたりすると、トークンコストが静かに膨れます。Langfuseのようなツールでリクエスト・段階ごとのコストを常時観察します。

決定チェックリスト

- ログ保存先: コスト・ラベル中心なら**Loki**、強力な全文検索・分析なら**OpenSearch**。

- 計測: 新しく始めるなら最初から**OpenTelemetry**で。ベンダーロックインを避けられます。

- トレースバックエンド: 低コスト・Grafanaエコシステムなら**Tempo**、独立した成熟UIなら**Jaeger**。

- サンプリング: 問題トレースを逃したくないなら**テールサンプリング**、単純・低コストなら**ヘッドサンプリング**。

- LLM: LLMアプリを運用するなら別途**Langfuse / LangSmith / Helicone / Phoenix**のいずれかを必ず導入。

- 相関: 何を使うにせよ**trace_idをすべてのログに埋め込んで**信号をつなぐ。

おわりに

オブザーバビリティはツール一つで終わるものではなく、ログ・メトリクス・トレースという三本柱(そして台頭するプロファイリング)をtrace_idで結び、「任意の問いに答えられるシステム」をつくる営みです。ログはLokiとOpenSearchがコストと検索力の両極をなし、トレーシングはOpenTelemetryが標準を統一しJaeger・Tempoがそれを受け、その上にLLMという新しい層をLangfuseのようなツールが埋めていきます。

肝心なのは「何が最高か」ではなく「何がこのシステムに合うか」、そして何より「信号どうしがつながっているか」です。三本柱をそれぞれよく立て、trace_idで固く結んでおけば、次の午前3時の障害で三つの窓を目で見比べる代わりに、一度のクリックで原因まで下りていけます。

参考資料

- OpenTelemetry公式ドキュメント: https://opentelemetry.io/docs/

- W3C Trace Context仕様: https://www.w3.org/TR/trace-context/

- Grafana Lokiドキュメント: https://grafana.com/docs/loki/latest/

- Grafana Tempoドキュメント: https://grafana.com/docs/tempo/latest/

- Jaeger公式サイト: https://www.jaegertracing.io/

- OpenSearchドキュメント: https://opensearch.org/docs/latest/

- Langfuseドキュメント: https://langfuse.com/docs

- Arize Phoenix: https://docs.arize.com/phoenix

현재 단락 (1/154)

システムが遅くなりました。ユーザーは「決済ができない」と言います。ところがサーバーは問題なく動いていて、CPUも正常、エラーログも特にありません。いったいどこが問題なのか? この問いに答える能力こそが...

작성 글자: 0원문 글자: 10,374작성 단락: 0/154