CloudWatch Logs Insightsは、CloudWatch Logsに溜まったログをその場でクエリして集計できるログ分析機能です。使い方そのものは難しくありませんが、実務で最初につまずくのは構文ではなく「請求額」と「取得件数の上限」です。課金はスキャンした非圧縮データ量に対して発生するため、圧縮されて保存されている容量の感覚で期間を広げると、想定外の請求が立ちます。この記事では東京リージョンの実単価、limitの既定値と上限、そして2024年に加わったOpenSearch PPL・SQLを含む3つのクエリ言語とスケジュールクエリまでを、AWS公式の数値に沿って整理します。
まとめ
先に結論だけ押さえておきます。
- 課金はスキャンした非圧縮データ1GBあたり0.0076USD(東京リージョン)。アカウントあたり毎月5GBまでは無料利用枠。
- スキャン単価より取り込み単価のほうが100倍高い(東京リージョン、Standardログクラスのカスタムログで0.76USD/GB)。コスト削減はまず取り込み量から。
- 取得件数は
limitを省略すると最大10,000件、明示すれば100,000件まで。それ以上はstatsで集計するかAPI経由に切り替える。 count_ifという関数は存在しない。条件付きカウントはcase()でラベルを作ってグループ化する。- 期間の絞り込みを
@timestampと日付文字列の比較で書いても効かない。時間範囲セレクタかtoMillis()を使う。 - クエリ言語はLogs Insights QL・OpenSearch PPL・OpenSearch SQLの3種類。定期実行は2025年11月からネイティブ機能で組める。
それぞれの根拠と、実際に書くクエリを以下で見ていきます。
CloudWatch Logs Insightsの課金構造と東京リージョンの実単価
GSCの実クエリを見ると、この機能について最も多く調べられているのは使い方ではなく料金です。そして料金の誤解はほぼ一点、「何に対して課金されるのか」に集中しています。
スキャン量の数え方|保存容量ではなく非圧縮の取り込みバイト
AWS公式は「CloudWatch Logs Insightsのクエリは、クエリ言語にかかわらず、スキャンされた非圧縮ログデータ量に基づいて課金される」と明記しています。ここが実務上の落とし穴です。CloudWatch Logsは保存時にログを圧縮するため、コンソールに表示される保存容量とスキャン対象量は一致しません。しかも圧縮率はログの内容に依存し、AWSは具体的な比率を公表していません。保存容量からスキャン量を逆算することはできない、と考えてください。
課金の分母になるのは、選択したロググループ群に含まれる、選択した時間範囲ぶんの非圧縮バイトの合計です。ヒットした件数ではありません。limit 20を付けても、絞り込み条件がインデックスに乗らない限りスキャン量は減らない、という点は必ず押さえてください。同じロググループを同じ期間で対象にする限り、クエリの書き方を変えてもスキャン量は基本的に変わりません。
取り込み・保存・スキャンの単価差(東京リージョン)
AWSのPrice List APIからap-northeast-1の単価を取得すると、次のようになります。取り込みと保存はStandardログクラスのカスタムログの場合の値です。
| 課金対象 | 東京リージョン単価 | 無料利用枠 |
|---|---|---|
| ログの取り込み(Standard) | 0.76 USD/GB | 月5GB |
| ログの取り込み(Infrequent Access) | 0.38 USD/GB | 同上 |
| ログの保存 | 0.033 USD/GB-月 | 月5GB |
| Logs Insightsのスキャン | 0.0076 USD/GB | 月5GB |
スキャン単価は取り込み単価のちょうど100分の1で、100GBをスキャンして初めて1GB取り込んだのと同じ金額になる計算です。つまり「Logs Insightsは高い」という言い方は正確ではありません。正しくは「取り込みが高く、スキャンは安いが青天井に積める」。コストを本気で下げたいなら、クエリの書き方を磨く前に、そもそも何を取り込んでいるかを削るほうが桁で効きます。ログ量を送信前に絞り込む工程についてはFluent Bitとは?仕組み・Fluentd比較・AWSログ運用を実務解説【2026年v5対応】で扱っています。保存側も同じで、保持期間が「失効しない」のまま放置されていれば保存料は永久に積み上がります。保持期間はロググループ単位で設定できるので、実際に遡って調査する期間まで縮めるだけで下がります。なお無料利用枠はリージョンごとではなくアカウント単位で、複数リージョンで使っても5GBが増えるわけではありません。
そのうえで、スキャン側の請求が跳ねる典型は人の操作ミスではなくダッシュボードです。Logs Insightsのウィジェットは更新のたびに新しいクエリを実行するため、高頻度で自動更新するダッシュボードに重いクエリを置くと、誰も見ていない時間帯も課金が続きます。公式もベストプラクティスとして、コンソールを閉じる前に実行中のクエリをキャンセルすること、ダッシュボードの更新頻度を上げすぎないことを挙げています。
スキャン量削減の二択|フィールドインデックスとInfrequent Accessログクラス
取り込み量を削れない事情があるとき、次の手はフィールドインデックスかログクラスの変更です。ただしこの二つは併用できません。理由は後述します。
フィールドインデックスは、よく検索するフィールドにインデックスを張り、そのフィールドを含まないログイベントの処理をスキップさせる仕組みです。効くのはfilter フィールド名 = 値とfilter フィールド名 IN [...]の形だけで、公式は「likeを使うクエリはインデックスを使わず、選択したロググループのログイベントを常に全件スキャンする」と明言しています。速度と料金の両面で、likeから=へ書き換える価値があるということです。制約も具体的で、対象はJSONとサービスログのみ、インデックスはポリシー作成後に取り込まれたログにしか付かず、各ログイベントは取り込みから30日間インデックスされた状態を保ちます。フィールド名は大文字小文字を区別するため、RequestIdのインデックスはrequestIdにマッチしません。@logStreamやtraceIdなどはStandardログクラスの既定インデックスとして最初から使え、クォータにも算入されません。
もう一方のInfrequent Accessログクラスは、取り込み単価だけが半額になります。ここは誤解が多い箇所で、公式は「課金面でStandardとInfrequent Accessが異なるのは取り込みコストのみ。保存料金とCloudWatch Logs Insightsの料金はどちらのログクラスでも同じ」と明記しています。保存やスキャンは安くなりません。
そして値引きの代償は小さくありません。IAログクラスではフィールドインデックスそのものが使えず、メトリクスフィルタ、サブスクリプションフィルタ、Live Tail、異常検出、前期間との比較、自然言語クエリ生成、GetLogEventsとFilterLogEventsのAPIも非対応です。Logs Insightsのクエリ自体は大半のコマンドが使え、PPL・SQL・スケジュールクエリ・S3エクスポートも動くため、「取っておくが常時監視には使わない」ログには合います。逆に、アラートを飛ばしたり他サービスへ転送したりする経路に乗っているログをIAへ置くと、監視の手段を先に失います。ログクラスは作成後に変更できないため、ロググループを作る時点で確定させてください。
CloudWatch・CloudWatch Logs・Logs Insightsの守備範囲の違い
「CloudWatchとCloudWatch Logsの違い」は実データでも上位の疑問ですが、三者は競合ではなく層が違います。Amazon CloudWatchはメトリクス・アラーム・ダッシュボードを束ねる監視サービスの総称、CloudWatch Logsはその中でログの収集・保存・保持期間管理を担うサブサービス、CloudWatch Logs Insightsは溜まったログを検索・集計するクエリ機能です。料金体系も別で、ログの取り込みと保存はCloudWatch Logs側、スキャンはLogs Insights側で課金されます。
使い分けの判断基準はシンプルです。「異常を継続的に検知して通知したい」なら、メトリクスとアラーム、あるいはメトリクスフィルタでログから数値を切り出す側の仕事になります。Logs Insightsはクエリを人が投げて答えが返る対話型の機能なので、常時監視の土台には向きません。障害が起きた後に原因を掘る、あるいは定期レポートを作る用途に置くのが適切です。Lambdaを対象にした具体的な監視設計はAWS LambdaをCloudWatchで監視する方法|標準メトリクス・ログ・アラーム・Lambda Insightsにまとめています。
Logs Insightsを開く場所はCloudWatchコンソールの左メニュー「ログ」配下にある「ログのインサイト」で、対象のロググループと時間範囲を選んでからクエリを書きます。日本語表示では「Logs Insights」ではなく「ログのインサイト」と表記されるため、名前で探して見つからない場合はこの表記を手がかりにしてください。
なお、Logs Insightsで検索できるのは2018年11月5日以降にCloudWatch Logsへ送られたログだけで、ロググループの作成時刻より前のタイムスタンプを持つログイベントにはアクセスできません。クエリは60分でタイムアウトし、実行結果は7日間参照できます。
取得件数の上限と1万件を超えるログの扱い
「10000件以上」で流入があるとおり、件数の壁は実務で必ず当たります。ここは仕様が更新されている箇所なので、古い記事の数字をそのまま信じないでください。
limitの既定値10,000件と指定上限100,000件
公式の記述は明快です。limitを省略した場合、クエリは最大10,000件のログイベントを返します。そしてlimitには100,000までの値を指定できます。したがって「1万件が上限」は誤りで、1万件は指定しなかったときの既定値です。
fields @timestamp, @message
| sort @timestamp desc
| limit 100000
件数よりコストを優先したい場面ではlimit anyが使えます。通常のlimitが時間範囲のデータを全てスキャンしてから上位N件を返すのに対し、limit any NはN件見つかった時点で走査を打ち切ります。スキャン量が減る代わりに、結果がどの時間帯から拾われたかも順序も保証されません。「エラーが出ているかどうかだけ知りたい」というサンプリング用途に限って使ってください。
fields @message
| filter @message like "OutOfMemory"
| limit any 20
100,000件を超える結果が必要なときの経路
上限を引き上げる設定は用意されていないので、要件のほうを変える必要があります。まず疑うべきは、生ログを全件持ってくる必要が本当にあるのかという点です。件数を数える、上位を出す、時系列で推移を見るのが目的ならstatsで集計すれば結果行数は激減し、スキャン量も結果サイズも問題になりません。statsコマンドは1クエリに最大10個まで置け(上限はログクラスに依存します)、集計結果をさらに集計できます。
それでも生ログが要るなら、StartQueryとGetQueryResultsのAPIで時間範囲を分割して回す形になります。ネットワークの制約でコンソールのLogs Insightsを開けない環境でも、APIなら実行できます。長期のデータを継続的に扱うのであれば、そもそもCloudWatch Logsのエクスポート機能でS3へ出し、分析基盤側で処理するほうが筋がいい設計です。Logs Insightsを大量データの一括抽出パイプラインとして使うのは、料金面でも上限面でも無理があります。
実務で使うクエリ構文
ここから先は、実際に検索されている構文の疑問に個別に答えます。なお以下の例で使うstatusCodeのようなフィールドは、JSON形式のログであれば自動検出されます。プレーンテキストのログではparse @message "Status Code: *;" as statusCodeのようにparseで切り出してからでないと参照できません。
filterにおけるin・like・正規表現の使い分け
inは集合への所属を判定します。公式の説明どおり、比較されるのは完全一致の文字列であり、部分一致には使えません。部分一致はlikeか正規表現演算子=~の担当で、likeには単一または二重引用符で囲む文字列と、スラッシュで囲む正規表現パターンの両方を渡せます。大文字小文字を無視したいときはパターンの先頭に(?i)を置きます。
fields @timestamp, @message, httpMethod
| filter httpMethod in ["POST", "PUT", "DELETE"]
| filter @message like /(?i)exception/
| sort @timestamp desc
| limit 50
前述のとおりフィールドインデックスが効くのは=とINだけなので、まず=やINで母集団を切り、そのうえでlikeを重ねる順序で書いてください。
count_ifの不在と条件付きカウントの代替構文
count_ifを探して辿り着く方が一定数いますが、Logs Insights QLの集計関数一覧にこの名前の関数はありません。用意されているのはcount()、countDistinct()、sum()、avg()、min()、max()、pct()、stddev()、variance()、values()、topk()の11個です。
条件付きカウントは、case()で条件ごとのラベルを作り、そのラベルでグループ化する形が公式の用法に沿っています。case()のような汎用関数はfieldsコマンドとfilterコマンドで使うと明記されているためです。「group by」で調べている方が探しているのもこの形です。
fields case(statusCode >= 500, "5xx",
statusCode >= 400, "4xx",
"other") as statusClass
| stats count(*) as cnt by statusClass, bin(1h) as t
| sort t asc
case()が扱える分岐は最大10までです。文字列が含まれるかどうかを数えるだけなら、1か0を返すstrcontains()をsum()に直接渡すほうが短く書けます。
fields @timestamp, @message
| stats sum(strcontains(@message, "ERROR")) as errorCount,
count(*) as total by bin(1h) as t
| sort t asc
どちらの例もbin(1h)にas tで別名を付けている点が重要です。公式は「statsコマンドの後に参照するフィールドは、そのstatsコマンドで定義されていなければならない」と定めており、stats ... by bin(1h)の後にsort @timestampと書いても@timestampは残っていないため成立しません。上位N件だけが欲しい場合はtopk(3, statusCode)のように書けます。kには1から10000までを指定できますが、この関数はbyや他の集計関数と併用できません。
日付文字列の比較が効かない理由と正しい期間指定
これは旧来の記事に誤りが広く残っている箇所です。公式ドキュメントは「現在、CloudWatch Logs Insightsは人が読める形式のタイムスタンプによるログの絞り込みをサポートしていません」と明記しています。つまり次の書き方は、一見動いているように見えても意図した絞り込みになりません。
# 効かない書き方
fields @timestamp, @message
| filter @timestamp >= '2026-08-01T00:00:00Z'
and @timestamp <= '2026-08-31T23:59:59Z'
期間はコンソールの時間範囲セレクタ、またはAPIのstartTimeとendTimeで指定するのが基本です。そのうえでクエリ内部でさらに相対的に絞りたいときは、エポックミリ秒に変換して比較します。now()はクエリ処理開始時刻をエポック秒で返すので、1000倍してミリ秒に揃えます。
# 直近2時間だけを対象にする
fields @timestamp, @message
| filter toMillis(@timestamp) >= (now() * 1000 - 7200000)
| sort @timestamp desc
ただし、時間範囲セレクタを広く取ったままクエリ内で絞る書き方は、絞り込み前の全範囲がスキャン対象になるため課金上は不利です。料金を意識するなら、範囲はセレクタ側で最初から狭めてください。
3つのクエリ言語の選び分け
CloudWatch Logs Insightsは現在、性格の異なる3つのクエリ言語をサポートしています。
| 言語 | 得意な領域 | 同時実行数 |
|---|---|---|
| Logs Insights QL | 絞り込み・時系列集計 | 100 |
| OpenSearch PPL | 多段のデータ変換・加工 | PPLとSQLで計15 |
| OpenSearch SQL | SQL資産の流用・BI的な集計 | PPLとSQLで計15 |
ログを絞って数えるだけなら、QLが最短で書けます。QL側もこの数年で大きく拡張され、join・lookup・subqueries・countFrequent・sessionizeといったコマンドが加わっているため、ロググループをまたいだ結合や入れ子のクエリもQLの範囲で書けるようになりました。したがってSQLを選ぶ理由は「JOINができるから」ではありません。実際の判断基準は、チームにSQLの読み書きが定着していて、SELECT・GROUP BY・HAVINGの語彙のほうが速く正確に書けるかどうかです。PPLは複数段の変換を挟む加工処理向けで、パイプライン型の処理に慣れたチームなら読みやすさで有利になります。
ただしPPLとSQLは同時実行の枠が合わせて15と少なく、QLでしか使えない機能(前期間との比較を行うdiff、インデックスを使うfilterIndex)もあります。既定はQL、必要な場面だけ切り替えるのが現実的です。SQLとPPLでは、@messageのように英数字以外を含むフィールド名をバッククォートで囲む必要がある点にも注意してください。
加えて、自然言語でやりたいことを書くとクエリを生成し、行ごとの説明まで返す機能も利用できます(Standardログクラスのみ)。クエリ言語を覚え切っていないメンバーが調査に加わる場面では、この生成機能を下書きに使い、生成されたクエリを人がレビューする運用が現実的です。
スケジュールされたクエリによる定期実行
「定期実行したい」という要件は、長らくEventBridgeとLambdaでStartQueryを叩く自作の仕組みで満たすしかありませんでした。2025年11月にネイティブのスケジュールクエリがサポートされ、この自作は基本的に不要になっています。
スケジュールクエリは、指定したスケジュールでクエリを自動実行し、結果を配信先へ届ける機能です。配信先は3種類あり、選択がそのまま用途を決めます。S3は結果をJSONで長期保管し、データレイクや監査証跡に回す経路。EventBridgeイベントバスは結果をイベントとして流し、Lambdaや通知へつなぐ経路で、イベント中のqueryIdから結果を取得できる期間は30日です。ルックアップテーブルは実行のたびに内容を全置換で更新し、他のクエリがlookupコマンドで参照する参照データを最新に保つ経路で、許可リストや資産一覧をログから生成し続ける用途がこれに当たります。EventBridge側の課金設計はAmazon EventBridgeの料金|無料になる条件と64KB課金の落とし穴で整理しています。
スケジュールはcron(minute hour day-of-month month day-of-week year)形式で指定します。設計上の注意点は二つです。第一に、スケジュールクエリは常にUTCで実行されます。日本時間の朝9時にレポートを受け取りたいならcron(0 0 * * ? *)(0時UTC)と指定する必要があり、ローカルタイムの感覚で書くと9時間ずれます。第二に、IAMロールが2本必要です。クエリを実行するロール(logs:StartQuery、logs:StopQuery、logs:GetQueryResults、logs:DescribeLogGroups)と結果を配信先へ書き込むロールが分かれており、どちらもlogs.amazonaws.comが引き受けられる信頼ポリシーを要します。信頼関係の設定漏れは権限エラーで失敗する典型パターンです。
スケジュール実行でも課金は通常のクエリと同じくスキャン量に対して発生します。毎時実行のクエリが1回500GBをスキャンするなら、月あたりのスキャンは約360TB、東京リージョンの単価で約2,700USDです。定期実行を組む前に、1回あたりのスキャン量と頻度の掛け算を必ず見積もってください。
よくある質問
CloudWatch Logs Insightsとは何ですか?
CloudWatch Logsに保存されたログを、専用のクエリ言語で検索・集計できる対話型のログ分析機能です。コンソールでは「ログのインサイト」と表示されます。ロググループと時間範囲を選んでクエリを実行すると、絞り込み結果や集計値、時系列のグラフが得られます。2018年11月5日以降に送られたログが対象です。
CloudWatch Logs Insightsの料金はいくらですか?
東京リージョンでスキャンした非圧縮データ1GBあたり0.0076USD、アカウントあたり毎月5GBまでは無料枠です。別途、取り込み(Standardログクラスのカスタムログで0.76USD/GB)と保存(0.033USD/GB-月)がCloudWatch Logs側で課金されます。単価は変動するため見積もり時はAWS公式の料金ページで確認してください。
CloudWatch LogsとCloudWatch Logs Insightsの違いは何ですか?
ログを送る先がCloudWatch Logs、送ったログに問いを投げるのがLogs Insightsです。課金も取り込み・保存とスキャンで別建てになっています。
1万件を超えるログを取得するにはどうすればよいですか?
limitを明示すれば100,000件まで指定できます。10,000件はlimitを省略したときの既定値です。100,000件を超える場合は上限を引き上げられないため、statsでの集計、StartQueryとGetQueryResultsのAPIによる時間分割、S3エクスポートのいずれかに切り替えます。
count_if関数は使えますか?
Logs Insights QLの集計関数一覧にcount_ifはありません。条件付きカウントはfields case(条件, "ラベル", ...) as 列名でラベル列を作り、stats count(*) by 列名でグループ化します。文字列の有無を数えるだけなら、1か0を返すstrcontains()をsum()に渡す形も使えます。
期間を指定してログを検索するにはどうすればよいですか?
公式に「人が読める形式のタイムスタンプによる絞り込みは非対応」と明記されているため、filter @timestamp >= '2026-08-01T00:00:00Z'のような日付文字列との比較は使えません。期間はコンソールの時間範囲セレクタ、またはAPIのstartTimeとendTimeで指定します。クエリ内で相対的に絞る場合はfilter toMillis(@timestamp) >= (now() * 1000 - 7200000)のようにエポックミリ秒で比較してください。