observability-logs-search について
ES|QLによる反復的な絞り込みでElasticsearchログを調査するスキルです。時間推移とメッセージ分類を使い、可観測性の担当者がログを絞ってエラーや根本原因を探せるようにします。
主な機能
- 01ES|QLによるログ検索
- 02時系列ヒストグラムと件数の表示
- 03ログメッセージのサンプルと分類
- 04KQLでノイズを反復的に除外
- 05関連ログの確認やエンティティへの切り替え
活用シーン
- 01インシデント調査でログ範囲を絞る
- 02サービスログ急増の原因を特定する
- 03繰り返す正常メッセージを結果から除外する
- 04重要イベント前後のログを調べる
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elastic/ agent-skills --skill observability-logs-searchインストール後、Claude Code で/observability-logs-searchを入力して読み込まれたか確認します。
同梱ファイルreferences/log-search-reference.md
詳細
ログ検索
インシデント調査に役立てるため、ログを検索して絞り込みます。このワークフローは Kibana Discover と同様です。時間範囲とスコープフィルターを適用し、対象となるログが少数残るまで除外フィルター(NOT)を繰り返し追加します。残ったログは根本原因または重要なドキュメントのいずれかです。必要に応じて、そのドキュメントの前後のログを表示したり、別のエンティティに切り替えて新しい検索を開始したりできます。ES|QL のみを使用し(POST /_query)、Query DSL は使用しないでください。
使用しない場合
- メトリクスまたはトレース — 専用のメトリクスツールまたはトレースツールを使用してください。
パラメーターの規約
Observability のログ検索では、一貫した名前を使用してください。
| パラメーター | 型 | 説明 |
|---|---|---|
start | string | 時間範囲の開始(Elasticsearch の日付計算。例:now-1h) |
end | string | 時間範囲の終了(例:now) |
kqlFilter | string | 結果を絞り込む KQL クエリ文字列。query、filter、kql は使用しないでください。 |
limit | number | 返すログサンプルの最大数(例:10~100) |
groupBy | string | ヒストグラムをグループ化する任意のフィールド(例:log.level、service.name) |
エンティティフィルターには ECS フィールド名を使用してください:service.name、host.name、service.environment、kubernetes.pod.name、kubernetes.namespace。ECS 名のみをクエリしてください。Observability インデックスでは、OpenTelemetry のエイリアスが自動的にマッピングされます。
コンテキストの最小化
コンテキストウィンドウを小さく保ってください。クエリのサンプル分岐では、一部のフィールドのみを KEEP してください。既定ではドキュメント全体を返さないでください。KEEP を使用した小さな要約(例:10 件のドキュメント)は約 1000 トークン以内に収まりますが、JSON ドキュメント全体を 1 件返すだけで 4000 トークンを超える場合があります。
ログサンプルに推奨される KEEP リスト:
message、error.message、service.name、container.name、host.name、container.id、agent.name、kubernetes.container.name、kubernetes.node.name、kubernetes.namespace、kubernetes.pod.name
メッセージのフォールバック: 存在する場合は、次のうち最初の空でない値を使用してください:body.text(OTel)、message、error.message、event.original、exception.message、error.exception.message、attributes.exception.message(OTel)。Observability のインデックステンプレートでは、これらにエイリアスが設定されていることがよくあります。表示用に単一の「メッセージ」を作成する場合は、この順序を優先してください。
サンプル数の制限: クエリごとのサンプルは、既定で少数(ログ 10~20 件)にしてください。上限は 500 件とし、1 回の呼び出しで数千件を取得しないでください。各絞り込みステップは次の呼び出しを決めるためだけに使用します。コンテキストに保持して要約するのは、最終的に絞り込んだ結果のみです。
ファネルワークフロー
必ず反復してください。 1 回のクエリで止めないでください。残るログパターン(異なるメッセージカテゴリ)が20 個未満になるまで、NOT でノイズを除外し続けてください。反復時は必ずフィルター全体を維持してください。 新しい NOT を以前の KQL に追加してください。「範囲を広げる」ことや、以前の除外条件を削除することはしないでください。
- ラウンド 1 — 広範囲: スコープフィルター(例:
service.name: advertService)と時間範囲のみを指定してクエリを実行します。合計件数、ヒストグラム、サンプルログ、メッセージの分類(一般的なパターンとまれなパターン)を取得します。 - 確認: ヒストグラム(急増や減少が発生した時点)、サンプルメッセージ、分類されたパターン(fork4 = 件数上位のパターン、fork5 = まれなパターン)を確認します。ヒストグラムに特定の時刻の急増が示されている場合は、次のラウンドでその急増の前後に時間範囲(t_start、t_end)を絞り込みます。分類結果から異なるログパターンの数を数え、除外する大量のノイズを特定します。
- ラウンド 2 — ノイズの除外: 主なノイズパターンについて、KQL フィルターに
NOT句を追加します。完全なフィルター(以前の NOT と新しい NOT をすべて含む)でクエリを再実行します。 - 繰り返し:
NOT句を追加し、完全なフィルターで再実行し続けます。1、2 回のラウンドで止めないでください。ログパターンが 20 個未満になるまで続けます(分類結果を使って異なるメッセージカテゴリの数を数えます)。残りが少なくなると、興味深い情報(エラー、異常、根本原因)を解釈しやすくなります。 - ピボット(任意): ファネルによって特定のエンティティ(例:
container.id、host.name)が絞り込まれたら、そのエンティティに焦点を当てたクエリをもう 1 つ実行し、「最期の言葉」や前後のコンテキストを確認します。 - 必要に応じて見直す: ファネルで根本原因が判明しない場合は、ログをコンテキストとともに(重要なドキュメントの前後も含めて)確認するか、別のエンティティを調べて新たに検索を開始することを検討します。
ログパターンが 20 個未満になる前に止めると、実際の障害ではなくノイズを報告することになります。各中間結果は次の呼び出しを決めるためだけに使います。コンテキストに保持して要約するのは、最終的に絞り込んだ結果だけにしてください。
ログ検索の ES|QL パターン
ES|QL(POST /_query)のみを使用してください。Query DSL は使用しないでください。1 回のリクエストで必ず、時系列ヒストグラム、合計件数、少数のサンプルログ、メッセージの分類(一般的なパターンとまれなパターン)を返してください。ヒストグラムは主要なシグナルです。急増や減少が発生した時点を示し、次のフィルターを導きます。FORK を使って、トレンド、合計、サンプル、分類を 1 つのクエリで算出してください。
FORK 出力の解釈: 応答には、_fork 列(または同等の列)で識別される複数の結果セットが含まれます。次のように対応付けます。fork1 = トレンド(時間バケットごとの件数)、fork2 = 合計件数(1 行)、fork3 = サンプルログ、fork4 = 一般的なメッセージパターン(最大 10k 件のログから件数上位 20 件)、fork5 = まれなメッセージパターン(最大 10k 件のログから件数下位 20 件)。fork1 を使って時間範囲を絞り込むタイミングを見極め、fork2 で残っているノイズの量を確認し、fork3 で次に追加する NOT を判断します。fork4 と fork5 を使って残っている異なるログパターンの数を確認し、次に除外する条件を選びます。ログパターンが 20 個未満になるまで反復を続けてください。
KQL ガイダンス
- 対象のテキストが想定どおりにトークン化される場合は、具体性を高めるためにフレーズクエリを優先します(例:
message: "GET /health"、service.name: "advertService")。 - 対象が単一の語句としてトークン化されない場合は、ワイルドカードを使います(例:
message: *Returning*、message: *WARNING*)。引用符で囲んだフレーズの中にワイルドカード文字を入れないでください。 - 明示的なフィールド指定の KQL を使います:
service.name: "payment-api"、message: "GET /health"、NOT kubernetes.namespace: "kube-system"、error.message: * AND NOT message: "Known benign warning"。 log.levelによるフィルタリング(例:log.level: error)が役立つこともありますが、問題がある場合がよくあります。多くのログではレベルのメタデータが欠落していたり、誤っていたりします(例:すべてが「info」になっている、またはレベルがメッセージ本文にしか含まれていない)。失敗を調査するときは、メッセージの内容またはerror.messageで絞り込むことを優先し、log.levelは信頼できるフィルターではなくヒントとして扱ってください。- 「error」のような語句をランダムに全文検索する方法にも、問題がある場合がよくあります。無害な言及(例:「no error」、「error code 0」、その語句を含むスタックトレース)にも一致します。単一のキーワードに頼るのではなく、サービスやエンティティで対象を絞り、実際のメッセージパターンに対する NOT 除外を繰り返しながら調査してください。
ヒストグラム、サンプル、カテゴリ分類を使った基本的なログ検索
ログパターンの種類ごとの件数を数え、20 件未満になるまで繰り返し調査できるよう、メッセージのカテゴリ分類を含めます。5 つに分岐する FORK を使います:トレンド、合計、サンプル、一般的なパターン、まれなパターン。
POST /_query
{
"query": "FROM logs-* METADATA _id, _index | WHERE @timestamp >= TO_DATETIME(\"2025-03-06T10:00:00.000Z\") AND @timestamp <= TO_DATETIME(\"2025-03-06T11:00:00.000Z\") | FORK (STATS count = COUNT(*) BY bucket = BUCKET(@timestamp, 1m) | SORT bucket) (STATS total = COUNT(*)) (SORT @timestamp DESC | LIMIT 10 | KEEP _id, _index, message, error.message, service.name, container.name, host.name, kubernetes.container.name, kubernetes.node.name, kubernetes.namespace, kubernetes.pod.name) (LIMIT 10000 | STATS COUNT(*) BY CATEGORIZE(message) | SORT `COUNT(*)` DESC | LIMIT 20) (LIMIT 10000 | STATS COUNT(*) BY CATEGORIZE(message) | SORT `COUNT(*)` ASC | LIMIT 20)"
}
- fork4(一般的):最大 10,000 件のログから、件数の多い上位 20 件のメッセージパターンを取得します。主なノイズを NOT で除外するために使います。
- fork5(まれ):件数の少ない下位 20 件のメッセージパターンを取得します。膨大な候補の中からごくまれなものを見つけるのに役立ちます。
fork4/fork5 と全体のカテゴリ分類から異なるパターンの数を数え、ログパターンが 20 件未満になるまで繰り返し調査を続けてください。
インデックスパターン(例:logs-*、logs-*-*)、時間範囲、バケットサイズ(例:30s、5m、1h)を調整します。サンプルの LIMIT は小さく保ちます(デフォルトは 10~20、上限は 500)。サンプルブランチが完全なドキュメントではなく、要約フィールドのみを返すよう KEEP を使います。
KQL フィルターの追加
KQL("...") で結果を絞り込みます。KQL 式は ES|QL 内で 1 つの二重引用符付き文字列です。
リクエスト本文でのエスケープ: クエリは JSON 内で送信されるため、ES|QL 文字列の一部となる二重引用符はすべてエスケープする必要があります。KQL 式を囲む引用符には \" を使います。KQL 式自体に二重引用符が含まれる場合(例:message: "GET /health" のようなフレーズ)、JSON 内ではそれらを \\\" としてエスケープし、KQL パーサーがリテラルの引用符文字を受け取れるようにします。
POST /_query
{
"query": "FROM logs-* METADATA _id, _index | WHERE @timestamp >= TO_DATETIME(\"2025-03-06T10:00:00.000Z\") AND @timestamp <= TO_DATETIME(\"2025-03-06T11:00:00.000Z\") | WHERE KQL(\"service.name: checkout AND log.level: error\") | FORK (STATS count = COUNT(*) BY bucket = BUCKET(@timestamp, 1m) | SORT bucket) (STATS total = COUNT(*)) (SORT @timestamp DESC | LIMIT 10 | KEEP _id, _index, message, error.message, service.name, host.name, kubernetes.pod.name) (LIMIT 10000 | STATS COUNT(*) BY CATEGORIZE(message) | SORT `COUNT(*)` DESC | LIMIT 20) (LIMIT 10000 | STATS COUNT(*) BY CATEGORIZE(message) | SORT `COUNT(*)` ASC | LIMIT 20)"
}
NOT によるノイズの除外
既知のノイズを除外して絞り込みを進めます。リクエスト本文では、KQL 文字列を \"...\" で囲み、KQL 式の中の引用符は \\\" としてエスケープします:
"query": "... | WHERE KQL(\"NOT message: \\\"GET /health\\\" AND NOT kubernetes.namespace: \\\"kube-system\\\"\") | ..."
"query": "... | WHERE KQL(\"error.message: * AND NOT message: \\\"Known benign warning\\\"\") | ..."
ディメンションごとにグループ化したヒストグラム
トレンドを第 2 のディメンション(例:log.level、service.name)で分類し、どのレベルやエンティティが急増を引き起こしているかを確認します:
STATS count = COUNT(*) BY bucket = BUCKET(@timestamp, 1m), log.level
結果が膨大にならないよう、レスポンスに含めるグループの値は限定します(例:件数上位 N 件を返し、残りは _other としてまとめます)。
例
サービスの過去 1 時間のログ
POST /_query
{
"query": "FROM logs-* METADATA _id, _index | WHERE @timestamp >= NOW() - 1 hour AND @timestamp <= NOW() | WHERE KQL(\"service.name: api-gateway\") | SORT @timestamp DESC | LIMIT 20"
}
トレンドとサンプルを含むエラーログ
POST /_query
{
"query": "FROM logs-* METADATA _id, _index | WHERE @timestamp >= NOW() - 2 hours AND @timestamp <= NOW() | WHERE KQL(\"log.level: error\") | FORK (STATS count = COUNT(*) BY bucket = BUCKET(@timestamp, 5m) | SORT bucket) (STATS total = COUNT(*)) (SORT @timestamp DESC | LIMIT 15)"
}
イテレーティブなファネル:興味深い部分が見つかるまで NOT を重ねる
除外を 1 回行っただけで終わらせないでください。各ラウンドで、現在最も多いノイズに対する NOT を追加してから、再実行します。
ラウンド 1: KQL("service.name: advertService") → 例:ログ 55k 件。サンプルには「Returning N ads」、「WARNING:
request...」、「received ad request」が表示されます。
ラウンド 2: 最も多いノイズを除外します:
KQL("service.name: advertService AND NOT message: *Returning* AND NOT message: *WARNING*") → 再実行し、新しい合計件数
とサンプルを確認します。
ラウンド 3: 次のノイズ(例:リクエスト/キャッシュ関連の雑音)を除外します:
KQL("service.name: advertService AND NOT message: *Returning* AND NOT message: *WARNING* AND NOT message: *received ad request* AND NOT message: *Adding* AND NOT message: *Cache miss*") →
再実行します。
ラウンド 4 以降: サンプル内で依然として目立つものに対して NOT を追加し続けます(パターンを確認するには fork4/fork5 の分類を使用します)。ログパターンが 20 件未満になるまで続けます。その後に残ったものが、報告すべきシグナルです(例:「広告の取得エラー」、エンコーディングの問題)。
エスケープ:JSON 内では KQL 文字列を \"...\" で囲みます。KQL 内の引用符付きフレーズには \\\" を使用します。
ガイドライン
- ファネル:NOT を使って反復します。 範囲の広いクエリを 1 回実行しただけで結果を報告しないでください。支配的なノイズに対する NOT 句を追加し、完全なフィルター(以前の NOT をすべて維持)で再実行して、ログパターンが 20 件未満になるまで繰り返します(件数の確認には分類 fork4/fork5 を使用します)。早く止めると、シグナルではなくノイズが残ります。
- まずヒストグラムを確認: 傾向(fork1)を見て、スパイクや減少が発生した時刻を確認します。必要に応じて、さらに NOT を追加する前に、スパイクの時間範囲に絞り込みます。
- コンテキストの最小化: サンプルブランチには要約フィールドのみを残します。デフォルトの LIMIT は 10~20、上限は 500 です。各ファネル段階は次の呼び出しを決めるためのもので、コンテキストと要約に使用するのは最後に絞り込んだ結果のみです。
- リクエストボディのエスケープ:
queryの値は JSON です。ES|QL 文字列内の二重引用符をエスケープします。KQL を囲むには\"、KQL 式内の引用符(例:フレーズ値)には\\\"を使用します。 - クエリをプログラムで作成する場合、
startとendには Elasticsearch の日付演算(例:now-1h、now-15m)を使用します。 - 時間範囲に応じてバケットサイズを選びます。バケット数が約 20~50 個になるようにします(例:1h のウィンドウなら
1mまたは2m)。 - ECS フィールド名を優先します。Observability インデックステンプレートでは OTel フィールドが ECS にエイリアスされています。リソースメタデータフィールドの代替候補(コンテナー、ホスト、クラスター、名前空間、ポッド、ワークロード)については、references/log-search-reference.md を参照してください。
log.level: これによるフィルタリングやグループ化は問題ない場合もありますが、レベルが欠落していたり誤って設定されていたりすると信頼性に欠けることがよくあります。失敗の検出には、メッセージの内容またはerror.messageを優先してください。- キーワード検索: 「error」や「fail」のような単語だけを検索すると、誤った結果になることがよくあります(例:「no error」、「error code 0」)。エンティティで範囲を絞り、実際のメッセージパターンに対する NOT を使ってファネルを進めてください。
参考資料
- references/log-search-reference.md — ECS/OTel フィールドのマッピングとインデックスパターン