【AWS】CloudWatch Logsで本番障害を追う|ログクエリ(Logs Insights)の実践例
はじめに
本番で障害が出たとき、まず CloudWatch Logs のコンソールでログストリームを開き、エラー文字列を目で追っていませんか。ログ量が多いとスクロールが追いつかず、「いつから増えたか」「どのリクエスト ID か」を掴むまでに時間がかかります。
CloudWatch Logs Insights は、ロググループに対してクエリを実行し、条件に合うイベントの抽出や集計を行える機能です。障害調査では、次のような問いに答えやすくなります。
- 直近 30 分で
ERRORを含むログは何件か - 例外メッセージの出現が時間帯でどう増えたか
- Lambda の遅い実行(REPORT 行)はどれか
- JSON ログから
requestIdやstatusを取り出して絞れるか
この記事では、Logs Insights の専用クエリ言語(Logs Insights QL)を前提に、コンソールでの実行手順と、本番障害で使いやすいクエリ例を整理します。コンソールには OpenSearch 系の PPL / SQL もありますが、本記事では扱いません。
前提: CloudWatch Logs にアプリや Lambda のログが入っていること。検証環境は AWS マネジメントコンソール上の操作を想定しています。IAM では、対象ロググループの閲覧と Logs Insights のクエリ実行が必要です。例として logs:DescribeLogGroups、logs:StartQuery、logs:GetQueryResults があります。一覧にロググループが出ない、またはクエリ実行が Denied になるときは、権限とリージョンを先に確認してください。
背景:Logs Insights で何ができるか
ロググループとクエリの関係
CloudWatch Logs では、関連するログを ロググループ にまとめ、その中の ログストリーム にイベントが時系列で並びます。Logs Insights は、選んだロググループ(複数可)と時間範囲に対してクエリを走らせ、結果を表形式で返します。
料金は、返ってきた件数ではなく スキャンした非圧縮ログデータの量 に基づきます(Analyzing log data with CloudWatch Logs Insights)。広い時間範囲や巨大なロググループを選ぶほど、調査コストと待ち時間が増えやすい点を先に押さえておきます。単価はリージョンや時期で変わるため、Amazon CloudWatch Pricing を確認してください。
自動検出フィールド(@ 付き)
Logs Insights は、多くのログ形式についてフィールドを自動検出し、@ で始まる名前を付けます(Supported logs and discovered fields)。Standard ログクラスのロググループでは、少なくとも次のシステムフィールドが付きます。
| フィールド | 意味の目安 |
|---|---|
@timestamp | イベントのタイムスタンプ |
@message | ログ本文 |
@logStream | 所属するログストリーム名 |
@log | ロググループの識別情報(account-id:log-group-name 形式) |
@ingestionTime | CloudWatch Logs が受け取った時刻 |
Lambda の標準ログでは、加えて @requestId、@type、@duration、@memorySize、@maxMemoryUsed などが検出されます。アプリ独自のプレーンテキストでは、まず @message を filter や parse で扱う流れが一般的です。JSON 形式ではネストしたキーもフィールドとして検出されやすいです。
クエリの基本形
コマンドはパイプ | でつなぎます。コメントは # です(CloudWatch Logs Insights language query syntax)。障害調査でよく使うコマンドは次の表です。
| コマンド | 役割 |
|---|---|
fields | 表示するフィールドを選ぶ。計算や別名も可能 |
filter | 条件に合うイベントだけ残す |
sort | 並び替え(asc / desc) |
limit | 返す件数の上限 |
stats | 件数・平均などの集計 |
parse | @message などからフィールドを切り出す |
dedup | 指定フィールドの重複を除く |
公式ドキュメントでは、過剰な課金を避けるために次が推奨されています。
- 必要なロググループだけを選ぶ
- 時間範囲をできるだけ狭くする
- コンソールを閉じる前に実行中クエリをキャンセルする(閉じても完了まで動き続ける)
実装と検証:コンソールでクエリを走らせる
1. Logs Insights を開く
- 障害が起きた リージョン を選ぶ
- CloudWatch コンソールで Logs → Logs Insights を開く
- 対象の ロググループ を選択する(障害の疑いがあるアプリ / Lambda から)
- 右上の 時間範囲 を障害時刻付近に絞る(まずは 15〜60 分など短めから。時間を半分にするとスキャン対象も減りやすい)
- クエリエディタにクエリを貼り、クエリの実行 を押す
結果タブで行を選び、前後のログ(Surrounding logs / 前後のログ)を見る機能もあります。原因候補の 1 行が見つかったあとに、前後の文脈を確認する用途向けです。
2. まず「直近のログ」を出す
何も絞れないときは、公式サンプルと同様に最新イベントを確認します。
fields @timestamp, @message
| sort @timestamp desc
| limit 25
ここでの sql はエディタ表示用の識別子です。構文は Logs Insights QL です。
実装時の注意点:
limitは表示件数の上限であり、課金対象のスキャン量を直接ゼロにはしない。時間範囲とロググループの選定が主である- ロググループ作成より前のタイムスタンプを持つイベントは、Logs Insights から参照できないとドキュメントにある
- 2018 年 11 月 5 日より前に送信されたログデータは、Logs Insights の検索対象外である(Analyzing log data)
実践例:本番障害でよく使うクエリ
ここからは、冒頭の問いに対応するクエリ例です。公式の Sample queries をベースにしています。ログ本文の文言は環境に合わせて置き換えてください。
例1:ERROR / Exception を含む行を探す
fields @timestamp, @message, @logStream
| filter @message like /(?i)(error|exception)/
| sort @timestamp desc
| limit 50
like に正規表現を渡し、(?i) で大文字小文字を無視する書き方です(filter)。まず件数と時刻の分布を見てから、メッセージを具体化するとノイズが減ります。
例2:例外の発生件数を 1 時間ごとに集計する
件数が多い時間帯(ピーク)を探すときの定番です。
filter @message like /Exception/
| stats count(*) as exceptionCount by bin(1h)
| sort exceptionCount desc
bin(1h) は時間バケットです。sort exceptionCount desc は件数の多いバケット順です。時系列で眺めたいときは sort bin(1h) asc に変えます。障害が短い時間に集中しているなら bin(5m) や bin(1m) も検討してください。
例3:Lambda の遅い実行(REPORT)を探す
Lambda のロググループ向けです。非 Lambda のアプリログでは例1・2・4を先に使ってください。Lambda は実行終了時に @type = "REPORT" の行を出します。@duration はミリ秒です。
fields @timestamp, @requestId, @message, @logStream
| filter @type = "REPORT" and @duration > 1000
| sort @timestamp desc
| dedup @requestId
| limit 20
1 秒超の実行を探し、リトライなどで同じ @requestId が重複する場合は dedup で抑えます。しきい値の 1000 はアプリの SLA に合わせて変更してください。
メモリの余裕を見る場合は、公式の REPORT 集計例が参考になります。
filter @type = "REPORT"
| stats max(@memorySize / 1000 / 1000) as provisionedMemoryMB,
avg(@maxMemoryUsed / 1000 / 1000) as avgMemoryUsedMB,
max(@maxMemoryUsed / 1000 / 1000) as maxMemoryUsedMB
例4:JSON ログからフィールドを取り出して絞る
アプリが JSON 1 行ログを出している場合、検出済みフィールドを使えることがあります。例として、次のようなログを想定します。
{"level":"error","requestId":"abc-123","status":500,"msg":"payment failed"}
クエリ前に、コンソールの検出フィールド一覧で level や status が見えることを確認してください。見えない場合は、後述の parse か例1の @message like に切り替えます。
fields @timestamp, level, requestId, status, msg
| filter level = "error" and status >= 500
| sort @timestamp desc
| limit 50
フィールド名にドットなどが含まれる場合は、バッククォートで囲むとよいです。規則の詳細は Fields that contain special characters 周辺を参照してください。
プレーンテキストから切り出すときは parse を使います。パターンは自分の @message 1 行に合わせて * の位置を調整してください。
fields @timestamp, @message
| parse @message "requestId=* status=* *" as requestId, status, rest
| filter status = "500"
| sort @timestamp desc
| limit 50
parse は glob と正規表現の両方に対応します(parse の例)。
例5:特定リクエストの流れを追う
アラートやクライアントから requestId が分かっている場合です。本文に ID が埋もれているなら like、JSON や Lambda でフィールドとして検出されているなら等価比較が向いています。
# 本文検索
fields @timestamp, @message, @logStream
| filter @message like /abc-123/
| sort @timestamp asc
| limit 100
# 検出フィールドがある場合(Lambda なら @requestId も使える)
fields @timestamp, @message, requestId
| filter requestId = "abc-123"
| sort @timestamp asc
| limit 100
複数マイクロサービスにログが分かれているときは、関連するロググループをまとめて選択し、同じ ID で横断検索します。スキャン量は増えるため、時間範囲は障害時刻に寄せてください。
調査を速くする運用上のコツ
| コツ | 理由 |
|---|---|
| 時間範囲を障害時刻±αに絞る | スキャン量と待ち時間を抑える |
まず filter で件数を見る | 全文を眺める前に規模感を掴む |
stats でピーク時刻を特定する | いつ増えたかが分かると原因仮説が立てやすい |
| クエリを保存する | 同じ調査手順を次回に再利用できる |
大規模ログを繰り返し調べる段階では、フィールドインデックスも検討できます。今すぐ必須ではなく、まずは時間範囲の絞り込みで十分なことが多いです。詳細は Create field indexes を参照してください。filter の like 条件はインデックスの恩恵を受けない、とドキュメントにあります。
よくある失敗例:時間範囲を広げすぎてクエリが重い
障害対応中に「念のため過去 7 日」で複数の巨大ロググループを選び、次のようなクエリだけを繰り返し実行するケースです。
fields @timestamp, @message
| filter @message like /error/
| sort @timestamp desc
| limit 20
limit 20 でも、選択した時間範囲のログはスキャン対象になり得ます。結果が 20 行でも、スキャン量に応じた課金と待ち時間が発生します。コンソールを閉じても実行中クエリは完了まで動き続けるため、不要ならキャンセルしてください。
対処:
- まず障害通知時刻の前後 30〜60 分に絞る
- ロググループは疑わしいものから 1〜2 個に限定する
- ピークが見えたら、そのバケットだけ時間をさらに狭めて詳細を見る
- 調査が終わったら実行中クエリを確認し、残っていればキャンセルする
もう1つの失敗は、JSON と思って status で絞ったのに結果が空になるケースです。実際の @message がプレーンテキストだと、検出フィールド status は存在しません。そのときは例1のように @message like /.../ か、parse で切り出してから絞ります。
まとめ
- Logs Insights は、ロググループと時間範囲に対して Logs Insights QL で抽出・集計できる
- 課金は結果件数ではなくスキャンしたログ量ベース。時間範囲とロググループの選定が調査コストに直結する
- 障害調査では、まず絞る(
filter)→ ピークを見る(stats)→ 必要なら切り出す(parse)の順が使いやすい
次に試すこと
- 検証用ロググループ(意図的に ERROR を出したサンプルでも可)で、例1の ERROR 検索を 15 分の時間範囲で実行する
- 例2で例外件数のピークを出し、その前後だけ時間範囲を狭めて再実行する
- Lambda を使っているなら、例3で
@durationのしきい値を自分の SLA に合わせて変える - よく使うクエリを Logs Insights の保存クエリに登録する
- 料金感を見たい場合は Amazon CloudWatch Pricing で Logs Insights のスキャン課金を確認する





