CloudWatch Logs を AWS CLI で見るときの基本は、流れを追うなら aws logs tail、期間と条件で探すなら aws logs filter-log-events、集計するなら aws logs start-query(Logs Insights)の3つです。 管理画面を開かなくても、ターミナルからログの確認・検索・集計まで行えます。

この記事では、3つのコマンドに加えてロググループの一覧や保持期間の設定までを1本にまとめ、オプション早見表と実行した出力を載せます。フィルターパターンの構文と Logs Insights のクエリ例は、コピーして使える一覧にしました。

確認環境:Ubuntu 24.04、AWS CLI 2.37.9、Python 3.11.15、boto3 1.43.103、jq 1.7。AWS の API をローカルで再現するツール(moto 5.2.3)で実行しています。掲載するコマンドは、実際の AWS でそのまま使える形(接続先の指定なし)にしてあります。

CloudWatch Logs CLIの早見表

やりたいこと コマンド
直近10分のログを見る aws logs tail /app/orders
流れ続けるログを追う aws logs tail /app/orders --follow
1時間前から見る aws logs tail /app/orders --since 1h
ERROR を含む行だけ見る aws logs tail /app/orders --since 1h --filter-pattern ERROR
時刻と本文だけの短い表示にする aws logs tail /app/orders --format short
特定のストリームだけ見る aws logs tail /app/orders --log-stream-names web-2
期間を指定して検索する aws logs filter-log-events --log-group-name G --start-time MS --end-time MS --filter-pattern P
ロググループの一覧を見る aws logs describe-log-groups --log-group-name-prefix /app
ストリームの一覧を見る aws logs describe-log-streams --log-group-name G
保持期間を30日にする aws logs put-retention-policy --log-group-name G --retention-in-days 30
Logs Insights のクエリを投げる aws logs start-query --log-group-name G --start-time SEC --end-time SEC --query-string Q
クエリの結果を受け取る aws logs get-query-results --query-id ID

時刻の単位は、filter-log-events がミリ秒、start-query が秒です。ここを取り違えると結果が空になります(後半で実際の出力を示します)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CloudWatch LogsのAWS CLI tail・検索・クエリ例」

この記事で使うサンプルログ

ロググループ /app/orders に、ストリーム web-1 と web-2 を作り、JSON形式のログ8件と、JSONでない1件(Traceback)を入れてあります。

aws logs create-log-group --log-group-name /app/orders
aws logs create-log-stream --log-group-name /app/orders --log-stream-name web-1
aws logs create-log-stream --log-group-name /app/orders --log-stream-name web-2

3つとも、成功すると何も表示しません。ログの投入は put-log-events で行い、イベントは「ミリ秒エポックの timestamp と message」の配列で渡します。

aws logs put-log-events --log-group-name /app/orders --log-stream-name web-1 \
  --log-events "[{\"timestamp\": $(date +%s%3N), \"message\": \"{\\\"level\\\": \\\"INFO\\\", \\\"msg\\\": \\\"order created\\\", \\\"orderId\\\": 1007}\"}]"

logs tail:ログを追いかける

aws logs tail は AWS CLI バージョン2のコマンドです。手元の AWS CLI 1.46.1 で aws logs tail help を実行すると使い方のエラーになり、バージョン2(2.37.9)では動きました。aws --version で 2 系であることを確かめてから使います。

オプション早見表

オプション 既定値 意味
(位置引数) ― ロググループ名
--since 10m いつから表示するか。相対時間(s 秒、m 分、h 時間、d 日、w 週)か ISO 8601 の時刻
--follow オフ 新しいログを待ち受けて表示し続ける。終了は Ctrl+C
--format detailed detailed(ミリ秒とタイムゾーンつきの時刻+ストリーム名+本文)、short(短い時刻+本文)、json(本文がJSONなら整形表示)
--filter-pattern なし(全件) フィルターパターン。filter-log-events と同じ構文
--log-stream-names なし 対象のストリーム名を並べる
--log-stream-name-prefix なし ストリーム名の前方一致。--log-stream-names とは同時に指定できない

この表は、インストールした AWS CLI 2.37.9 のコマンド定義(ヘルプ文)に基づいています。ヘルプには、相対時間で 5h30m のように複数の単位を組み合わせる書き方には対応していないこと、ログの表示順は厳密な時刻順とは限らないことも書かれています。

既定は「直近10分」

aws logs tail /app/orders
2026-10-05T06:09:33.534000+00:00 web-2 {"level": "INFO", "msg": "order canceled", "orderId": 1001, "ms": 25, "path": "/orders"}
2026-10-05T06:12:33.534000+00:00 web-1 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1005, "ms": 3000, "path": "/stock"}
2026-10-05T06:13:33.534000+00:00 web-2 Traceback (most recent call last): ValueError: bad quantity

9件入れたうち、直近10分の3件だけが出ました。何も表示されないときは、まず --since を広げてみます。 時刻は UTC(+00:00)で表示されます。日本時間はここに9時間を足した時刻です。

--since:さかのぼる範囲を決める

aws logs tail /app/orders --since 20m
2026-10-05T05:59:33.534000+00:00 web-2 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1006, "ms": 3001, "path": "/stock"}
2026-10-05T06:04:33.534000+00:00 web-1 {"level": "INFO", "msg": "order created", "orderId": 1004, "ms": 38, "path": "/orders"}
2026-10-05T06:09:33.534000+00:00 web-2 {"level": "INFO", "msg": "order canceled", "orderId": 1001, "ms": 25, "path": "/orders"}
2026-10-05T06:12:33.534000+00:00 web-1 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1005, "ms": 3000, "path": "/stock"}
2026-10-05T06:13:33.534000+00:00 web-2 Traceback (most recent call last): ValueError: bad quantity

絶対時刻で指定するときは ISO 8601 で書きます。

aws logs tail /app/orders --since 2026-10-05T06:00:00Z --format short
2026-10-05T06:04:33 {"level": "INFO", "msg": "order created", "orderId": 1004, "ms": 38, "path": "/orders"}
2026-10-05T06:09:33 {"level": "INFO", "msg": "order canceled", "orderId": 1001, "ms": 25, "path": "/orders"}
2026-10-05T06:12:33 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1005, "ms": 3000, "path": "/stock"}
2026-10-05T06:13:33 Traceback (most recent call last): ValueError: bad quantity

--format:3つの表示形式

--format short は、上のとおり秒までの時刻と本文だけになります。ストリーム名が長いロググループで横幅を節約したいときに向いています。

--format json は、本文がJSONのイベントを整形して表示します。

aws logs tail /app/orders --since 1h --format json
2026-10-05T05:24:33.534000+00:00 web-1 
{
    "level": "INFO",
    "msg": "order created",
    "orderId": 1001,
    "ms": 42,
    "path": "/orders"
}
2026-10-05T05:29:33.534000+00:00 web-2 
{
    "level": "INFO",
    "msg": "member joined",
    "memberId": 501,
    "ms": 20,
    "path": "/members"
}
…(省略)…

--filter-pattern:条件に合う行だけ表示する

aws logs tail /app/orders --since 1h --filter-pattern ERROR
2026-10-05T05:44:33.534000+00:00 web-1 {"level": "ERROR", "msg": "stock not enough", "orderId": 1003, "ms": 15, "path": "/orders"}
2026-10-05T05:59:33.534000+00:00 web-2 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1006, "ms": 3001, "path": "/stock"}
2026-10-05T06:12:33.534000+00:00 web-1 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1005, "ms": 3000, "path": "/stock"}

空白を含む語句は、シェルの引用符の内側に二重引用符を書いて渡します。

aws logs tail /app/orders --since 2h --filter-pattern '"order created"' --format short
2026-10-05T05:24:33 {"level": "INFO", "msg": "order created", "orderId": 1001, "ms": 42, "path": "/orders"}
2026-10-05T06:04:33 {"level": "INFO", "msg": "order created", "orderId": 1004, "ms": 38, "path": "/orders"}

--log-stream-names:ストリームを絞る

aws logs tail /app/orders --since 1h --log-stream-names web-2 --format short
2026-10-05T05:29:33 {"level": "INFO", "msg": "member joined", "memberId": 501, "ms": 20, "path": "/members"}
2026-10-05T05:59:33 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1006, "ms": 3001, "path": "/stock"}
2026-10-05T06:09:33 {"level": "INFO", "msg": "order canceled", "orderId": 1001, "ms": 25, "path": "/orders"}
2026-10-05T06:13:33 Traceback (most recent call last): ValueError: bad quantity

フィルターパターンと組み合わせることもできます。

aws logs tail /app/orders --since 2h --filter-pattern 'timeout stock' --log-stream-names web-1
2026-10-05T06:12:33.534000+00:00 web-1 {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1005, "ms": 3000, "path": "/stock"}

--follow:新しいログを待ち受ける

--follow を付けると、コマンドは終了せずに新しいログを表示し続けます。次の例は、待ち受けている間に別のターミナルから2件のログを入れたときの表示です。

aws logs tail /app/orders --follow --since 1m --format short
2026-10-05T06:17:01 {"level": "INFO", "msg": "order created", "orderId": 1007}
2026-10-05T06:17:05 {"level": "ERROR", "msg": "stock not enough", "orderId": 1008}

終了するには Ctrl+C を押します。--follow と --filter-pattern ERROR を組み合わせると、デプロイ直後にエラーだけを見張る使い方ができます。

filter-log-events:期間と条件で検索する

filter-log-events は、ロググループ全体(複数ストリーム)を対象に、期間とパターンで検索するコマンドです。--start-time と --end-time は「ミリ秒のエポック時刻」で渡します。

aws logs filter-log-events --log-group-name /app/orders --filter-pattern ERROR \
  --start-time $(date -d '3 hours ago' +%s%3N) --end-time $(date +%s%3N) \
  --query 'events[].[timestamp,logStreamName,message]' --output text | sort
1791179073534   web-1   {"level": "ERROR", "msg": "stock not enough", "orderId": 1003, "ms": 15, "path": "/orders"}
1791179973534   web-2   {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1006, "ms": 3001, "path": "/stock"}
1791180753534   web-1   {"level": "ERROR", "msg": "timeout calling stock api", "orderId": 1005, "ms": 3000, "path": "/stock"}
1791181025773   web-2   {"level": "ERROR", "msg": "stock not enough", "orderId": 1008}

--query(JMESPath)で時刻・ストリーム名・本文だけを取り出し、sort で時刻順にそろえています。--query を付けない場合、1件のイベントは次のキーを持つJSONで返ります。

キー 中身
logStreamName イベントが入っているストリーム名
timestamp イベントの時刻(ミリ秒エポック)
message ログの本文
ingestionTime CloudWatch Logs が受け取った時刻(ミリ秒エポック)
eventId イベントのID

tail と filter-log-events の使い分け

観点 logs tail filter-log-events
時刻の指定 --since 1h のような相対時間、ISO 8601 ミリ秒エポックで開始と終了を指定
終了時刻の指定 なし(今まで) --end-time で指定できる
出力 人が読みやすい1行表示 JSON(--query や jq で加工しやすい)
待ち受け --follow でできる 1回の検索で終わる
向いている場面 いま起きていることを見る 過去の決まった時間帯を調べる、スクリプトで処理する

ミリ秒エポックの作り方

作りたい時刻 コマンド 結果
今(ミリ秒) date +%s%3N 例:1791180910847
30分前(ミリ秒) date -d '30 minutes ago' +%s%3N 例:1791179110845
日本時間の 2026-10-05 15:00:00 date -d '2026-10-05 15:00:00 JST' +%s%3N 1791180000000
UTC の 2026-10-05 06:00:00 date -u -d '2026-10-05T06:00:00Z' +%s%3N 1791180000000
今(秒。start-query 用) date +%s 例:1791180935

%s が秒、%3N がミリ秒の3桁です(GNU date の書き方)。日本時間の15時とUTCの6時は同じ瞬間なので、同じ値になりました。Python では次のように作れます。

from datetime import datetime, timezone, timedelta

JST = timezone(timedelta(hours=9))
ms = int(datetime(2026, 10, 5, 15, 0, 0, tzinfo=JST).timestamp() * 1000)
print(ms)
print(datetime.fromtimestamp(ms / 1000, tz=JST).isoformat())
1791180000000
2026-10-05T15:00:00+09:00

出力に出てくるミリ秒エポックを読める時刻に戻すときは、下3桁を除いて date -d @秒 に渡します。

TZ=Asia/Tokyo date -d @1791181025 '+%F %T %Z'
2026-10-05 15:17:05 JST

フィルターパターン構文の一覧

--filter-pattern に書ける構文を、公式ドキュメント「Filter pattern syntax」の例に沿って整理します。パターンはすべて大文字小文字を区別します。

語句で探す(形式を問わないログ)

パターン 意味
ERROR ERROR を含む
ERROR ARGUMENTS 両方を含む(AND)
?ERROR ?ARGUMENTS どちらかを含む(OR)
"INTERNAL SERVER ERROR" この語句をそのまま含む(空白や記号を含む語句は二重引用符で囲む)
ERROR -ARGUMENTS ERROR を含み、ARGUMENTS を含まない(除外)
" " すべてのイベント
%AUTHORIZED% 正規表現(% で囲む)

公式ドキュメントには、? を付けた語は、ほかの語(含める語・除外する語)と混ぜると無視されるという注意があります。OR 条件はそれだけで書きます。

このうち、単語1つ・複数語(AND)・二重引用符の語句の3つは、この記事のサンプルログで次の結果になりました。

実行したパターン 一致した件数 一致した内容
ERROR 3件 level が ERROR の3件
timeout stock 2件 timeout と stock の両方を含む2件
"order created" 2件 msg が order created の2件
error(小文字) 0件 大文字小文字が違うため一致しない

JSONログの項目で探す

ログがJSONなら、波かっこの中に $.キー名 で条件を書けます。

パターン 意味
{ $.level = "ERROR" } level が ERROR
{ $.errorCode = 400 } 数値と等しい
{ $.errorCode != 500 } 数値と等しくない
{ $.latency < 50 }、{ $.bandwidth > 75 } 数値の大小(<=、>= も使える)
{ $.sourceIPAddress != 123.123.* } ワイルドカード(*)つきの不一致
{ $.arrayKey[0] = "value" } 配列の要素(0始まり)
{ $.eventType = %Trail% } 項目の値に正規表現
{ ($.user.id = 1) && ($.users[0].email = "John.Doe@example.com") } AND
{ $.user.email = "John.Stiles@example.com" || $.coordinates[0][1] = "nonmatch" } OR
{ $.SomeObject IS NULL } 値が null
{ $.SomeOtherObject NOT EXISTS } 項目が存在しない
{ $.['cluster.name'] = "c" } キー名にピリオドを含むとき

1行目はこの記事のサンプルログに合わせた書き方、2行目以降は公式ドキュメントに載っている例です。シェルから渡すときは、$ が展開されないように全体を単一引用符で囲みます。

aws logs filter-log-events --log-group-name /app/orders --filter-pattern '{ $.level = "ERROR" }'

スペース区切りのログを項目で探す

アクセスログのように空白で区切られたログは、角かっこの中に項目名を順に並べて条件を書きます。角かっこ [] や二重引用符で囲まれた部分は、1つの項目として扱われます。

パターン 意味
[ip, user, username, timestamp, request, status_code = 4*, bytes] 6番目の項目が 4 で始まる
[..., request =*.html*, status_code = 4*, bytes] ... で前の項目を省略する
[ip, user, username, timestamp, request, status_code = 404 || status_code = 410, bytes] 404 または 410
[ip, user, username, timestamp, request, status_code >= 400, bytes] 数値の比較(>、<、>=、<=、!=)
[w1=ERROR, w2] 先頭の語が ERROR
[w1!=ERROR && w1!=WARNING, w2] 先頭の語が ERROR でも WARNING でもない

ロググループとストリームを調べる

describe-log-groups

aws logs describe-log-groups --log-group-name-prefix /app/orders
{
    "logGroups": [
        {
            "logGroupName": "/app/orders",
            "creationTime": 1791180871237,
            "retentionInDays": 30,
            "metricFilterCount": 0,
            "arn": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/app/orders:*",
            "storedBytes": 908,
            "logGroupArn": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/app/orders"
        }
    ]
}

(保持期間を30日に設定したあとの出力です。設定していないロググループには retentionInDays のキーがありません。)

名前と保持期間だけを表にすると、設定もれを見つけやすくなります。

aws logs describe-log-groups --log-group-name-prefix /app \
  --query 'logGroups[].[logGroupName,retentionInDays]' --output table
-------------------------
|   DescribeLogGroups   |
+--------------+--------+
|  /app/access |  None  |
|  /app/orders |  30    |
+--------------+--------+

describe-log-streams

aws logs describe-log-streams --log-group-name /app/orders \
  --query 'logStreams[].[logStreamName,lastEventTimestamp]' --output table
----------------------------
|    DescribeLogStreams    |
+--------+-----------------+
|  web-1 |  1791181021327  |
|  web-2 |  1791181025773  |
+--------+-----------------+

いちばん最近書き込まれたストリームの名前は、次のように取れます。

aws logs describe-log-streams --log-group-name /app/orders \
  --order-by LastEventTime --descending --limit 1 \
  --query 'logStreams[0].logStreamName' --output text
web-2

保持期間を設定する:put-retention-policy

保持期間を設定するコマンド名は put-retention-policy で、日数を --retention-in-days で渡します(put-retention-in-days というコマンドはありません)。

aws logs put-retention-policy --log-group-name /app/orders --retention-in-days 30

成功すると何も表示しません。結果は上の describe-log-groups の表で確認できます。

公式のAPIリファレンスによると、指定できる日数は決まった値だけです。

1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653

同じリファレンスには、保持期間を過ぎたログがすぐに消えるわけではなく、実際に削除されるまで通常72時間ほどかかることも書かれています。無期限に戻すときは aws logs delete-retention-policy --log-group-name G を使います。

Logs Insights をCLIから使う:start-query と get-query-results

集計や並べ替えをしたいときは Logs Insights を使います。CLI では「クエリを投げる」と「結果を受け取る」の2段階です。

aws logs start-query --log-group-name /app/orders \
  --start-time $(date -d '3 hours ago' +%s) --end-time $(date +%s) \
  --query-string 'fields @message | sort @timestamp desc | limit 1'
{
    "queryId": "4c297c85-029d-11c0-b04c-8a38189cad75"
}

start-query の時刻は「秒」のエポックです(APIリファレンスに、1970年1月1日からの秒数と書かれています)。filter-log-events のミリ秒と単位が違うので、+%s と +%s%3N を使い分けます。

返ってきた queryId を get-query-results に渡します。応答の status が Complete になれば結果がそろっています。APIリファレンスによると、status の値は Scheduled、Running、Complete、Failed、Cancelled、Timeout、Unknown で、Running や Scheduled の間は途中までの結果が返ります。

完了を待って結果を表示するスクリプト

insights.sh として保存します。

#!/bin/bash
# 使い方: ./insights.sh ロググループ名 'クエリ' [さかのぼる時間(例: 3 hours)]
GROUP=$1; QUERY=$2; RANGE=${3:-1 hour}
QUERY_ID=$(aws logs start-query --log-group-name "$GROUP" \
  --start-time "$(date -d "$RANGE ago" +%s)" --end-time "$(date +%s)" \
  --query-string "$QUERY" --query queryId --output text) || exit 1
while true; do
  STATUS=$(aws logs get-query-results --query-id "$QUERY_ID" --query status --output text)
  case "$STATUS" in
    Complete) break ;;
    Failed|Cancelled|Timeout) echo "query $STATUS" >&2; exit 1 ;;
  esac
  sleep 1
done
aws logs get-query-results --query-id "$QUERY_ID" --output json \
  | jq -c '.results[] | map({(.field): .value}) | add | del(.["@ptr"])'
chmod +x insights.sh
./insights.sh /app/orders 'fields @message | sort @timestamp desc | limit 3' '3 hours'
{"@message":"{\"level\": \"ERROR\", \"msg\": \"stock not enough\", \"orderId\": 1008}"}
{"@message":"{\"level\": \"INFO\", \"msg\": \"order created\", \"orderId\": 1007}"}
{"@message":"Traceback (most recent call last): ValueError: bad quantity"}

get-query-results の results は、1件ごとに {"field": 名前, "value": 値} の配列になっています。スクリプトの jq は、それを1件1オブジェクトにまとめ、@ptr(ログレコードを指す識別子)を取り除いています。

Logs Insights クエリ例の一覧

--query-string に渡すクエリの例です。公式ドキュメントの構文(fields、filter、stats、parse、sort、limit、dedup)に沿って、この記事のJSONログ(level、msg、ms、path、orderId)の項目名で書いています。JSONログの項目は、公式ドキュメントのとおり自動で検出され、項目名のまま使えます。

最新のログを見る

fields @timestamp, @message | sort @timestamp desc | limit 25

エラーだけを見る

fields @timestamp, @logStream, msg, orderId
| filter level = "ERROR"
| sort @timestamp desc
| limit 50

本文に特定の文字列を含むものを探す(含まないものを探す)

fields @timestamp, @message | filter @message like /Exception/ | sort @timestamp desc
fields @message | filter @message not like /Exception/

エラー件数を数える

filter level = "ERROR" | stats count(*) as errorCount

時間別に集計する

filter level = "ERROR"
| stats count(*) as errorCount by bin(1h)
| sort errorCount desc

bin() の単位は 5m、1h、1d などです。

項目ごとに件数を数えて上位N件を出す

stats count(*) by path
| sort count(*) desc
| limit 10

平均・最大・パーセンタイルを出す

stats avg(ms), max(ms), pct(ms, 95) by path

遅い処理を上位から並べる

fields @timestamp, path, ms, orderId
| filter ms > 1000
| sort ms desc
| limit 20

parse で本文から値を取り出す

JSONでないログは、parse で値を取り出してから集計します。* が取り出す場所で、as のあとに付けたい名前を並べます(公式ドキュメントの例)。

parse @message "user=*, method:*, latency := *" as @user, @method, @latency
| stats avg(@latency) by @method, @user

正規表現の名前つきグループでも書けます(公式ドキュメントの例)。

parse @message /(?<NetworkInterface>eni-.*?) /
| display NetworkInterface, @message

項目の値ごとに最新の1件だけを残す

fields @timestamp, server, severity, message
| sort @timestamp desc
| dedup server, severity

自動で使える項目

項目 中身
@message ログの本文そのまま
@timestamp イベントの時刻
@ingestionTime CloudWatch Logs が受け取った時刻
@logStream ストリーム名
@log ロググループの識別子(アカウントID:ロググループ名)

Lambda のログでは @requestId、@duration、@billedDuration、@maxMemoryUsed、@memorySize なども自動で使えます。JSONログから取り出される項目は最大200個です。

集計関数 意味
count(*)、count(項目) 件数
countDistinct(項目) 重複を除いた件数
avg、min、max、sum 平均、最小、最大、合計
pct(項目, 95) パーセンタイル
earliest(項目)、latest(項目) 最初の値、最後の値

boto3 の paginator で検索する

filter_log_events は1回の呼び出しで返る件数に上限があるため(APIリファレンスでは1ページあたり最大1MBまたは10,000件)、paginator で最後のページまでたどるのが確実です。APIリファレンスには「ページが空や少なめでも、nextToken があれば続きがある」とも書かれており、paginator はこの判定を任せられます。

logs_search.py として保存し、python3 logs_search.py で実行します。

import json
import time
from datetime import datetime, timedelta, timezone

import boto3

logs = boto3.client("logs", region_name="ap-northeast-1")
JST = timezone(timedelta(hours=9))

# 直近3時間をミリ秒エポックで指定する
end = datetime.now(timezone.utc)
start = end - timedelta(hours=3)
start_ms = int(start.timestamp() * 1000)
end_ms = int(end.timestamp() * 1000)

# 1. filter_log_events を paginator で最後のページまでたどる
paginator = logs.get_paginator("filter_log_events")
pages = paginator.paginate(
    logGroupName="/app/orders",
    startTime=start_ms,
    endTime=end_ms,
    filterPattern="ERROR",
    PaginationConfig={"PageSize": 2},   # ページ送りを確かめるため小さくしている
)
events = []
for page_no, page in enumerate(pages, 1):
    print(f"page{page_no}: {len(page['events'])} events")
    events.extend(page["events"])

for e in sorted(events, key=lambda e: e["timestamp"]):   # 時刻順に並べる
    ts = datetime.fromtimestamp(e["timestamp"] / 1000, tz=JST).strftime("%H:%M:%S")
    body = json.loads(e["message"])
    print(f"{ts} {e['logStreamName']} orderId={body['orderId']} {body['msg']}")
print("total:", len(events))

# 2. ロググループの一覧も paginator で
for page in logs.get_paginator("describe_log_groups").paginate(logGroupNamePrefix="/app"):
    for g in page["logGroups"]:
        print(g["logGroupName"], g.get("retentionInDays", "無期限"))

# 3. Logs Insights:start_query → 状態が Complete になるまで待つ(時刻は秒)
query_id = logs.start_query(
    logGroupName="/app/orders",
    startTime=int(start.timestamp()),
    endTime=int(end.timestamp()),
    queryString="fields @logStream, @message | sort @timestamp desc | limit 2",
)["queryId"]
while True:
    res = logs.get_query_results(queryId=query_id)
    if res["status"] in ("Complete", "Failed", "Cancelled", "Timeout"):
        break
    time.sleep(1)
print("status:", res["status"])
for row in res["results"]:
    record = {c["field"]: c["value"] for c in row if c["field"] != "@ptr"}
    print(record["@logStream"], "|", record["@message"])

# 4. 無いロググループは ResourceNotFoundException
try:
    logs.filter_log_events(logGroupName="/app/nothing")
except logs.exceptions.ResourceNotFoundException as e:
    print("ResourceNotFoundException:", e.response["Error"]["Message"])
page1: 2 events
page2: 2 events
14:44:33 web-1 orderId=1003 stock not enough
14:59:33 web-2 orderId=1006 timeout calling stock api
15:12:33 web-1 orderId=1005 timeout calling stock api
15:17:05 web-2 orderId=1008 stock not enough
total: 4
/app/orders 30
status: Complete
web-2 | {"level": "ERROR", "msg": "stock not enough", "orderId": 1008}
web-1 | {"level": "INFO", "msg": "order created", "orderId": 1007}
ResourceNotFoundException: The specified log group does not exist.

PageSize を2にしたので、ERROR の4件が2ページに分かれて返りました。boto3 でも、filter_log_events はミリ秒、start_query は秒です。

結果が空になる原因とエラーの読み方

結果が空になるとき

原因 確かめ方・直し方
tail の既定は直近10分 --since 1h のように範囲を広げる
filter-log-events に秒のエポックを渡している date +%s%3N でミリ秒にする
パターンの大文字小文字が違う error と ERROR は別。ログの表記に合わせる
空白を含む語句を二重引用符で囲んでいない '"order created"' のように書く(囲まないと「両方の語を含む」AND になる)
? の語をほかの語と混ぜている OR はそれだけで書く
リージョンが違う --region を付けるか、aws configure get region で確かめる
ストリームの指定が合っていない describe-log-streams で名前を確かめる

秒とミリ秒の取り違えは、エラーにならずに0件が返るので気づきにくい原因です。同じ3時間の範囲で試した結果が次のとおりです。

# 秒で渡した場合
aws logs filter-log-events --log-group-name /app/orders \
  --start-time $(date -d '3 hours ago' +%s) --end-time $(date +%s) --query 'length(events)'
# ミリ秒で渡した場合
aws logs filter-log-events --log-group-name /app/orders \
  --start-time $(date -d '3 hours ago' +%s%3N) --end-time $(date +%s%3N) --query 'length(events)'
0
11

秒の値をミリ秒として読むと1970年1月の時刻になるため、その範囲にはログがなく0件になります。

エラー文の一覧

エラー 表示されたメッセージ 原因と直し方
ResourceNotFoundException(tail、filter-log-events、describe-log-streams、start-query、delete-log-group) The specified log group does not exist. ロググループ名の打ち間違い(先頭の / の有無を含む)、またはリージョン違い。describe-log-groups で確かめる
ResourceAlreadyExistsException(create-log-group) The specified log group already exists 同じ名前がすでにある。セットアップ用のスクリプトでは、このエラーを「作成済み」として扱う
使い方のエラー(AWS CLI 1 系で logs tail) usage: aws [options] … logs tail はバージョン2のコマンド。aws --version で確かめる

実際の表示は次の形です。

aws: [ERROR]: An error occurred (ResourceNotFoundException) when calling the FilterLogEvents operation: The specified log group does not exist.

先頭の / を付け忘れて app/orders と指定した場合も、同じ ResourceNotFoundException になりました。

このほか、APIリファレンスには、start-query のクエリ文の構文に誤りがあると MalformedQueryException、filter-log-events で logStreamNames と logStreamNamePrefix を同時に指定すると InvalidParameterException が返ると書かれています。

よくある質問

Q. aws logs tail を実行しても何も表示されません。 A. 既定では直近10分のログだけを表示します。--since 1h や --since 1d で範囲を広げてください。それでも出ないときは、ロググループ名とリージョン、--filter-pattern の大文字小文字を確かめます。

Q. tail と filter-log-events と Logs Insights はどう使い分けますか? A. いま流れているログを見るなら tail --follow、過去の決まった時間帯から条件に合う行を取り出すなら filter-log-events、件数や平均などの集計・並べ替えをしたいなら Logs Insights(start-query)です。

Q. JSONログの特定の項目で絞り込めますか? A. フィルターパターンなら { $.level = "ERROR" }、Logs Insights なら filter level = "ERROR" と書きます。シェルから渡すときは、$ が展開されないように単一引用符で囲みます。

Q. start-query の結果がすぐに返ってきません。 A. start-query は queryId だけを返します。get-query-results の status が Complete になるまで、少し間隔をあけて繰り返し確認します。この記事の insights.sh はその待ち合わせを行います。

まとめ

  • 流れを追うなら logs tail。既定は直近10分で、--since・--follow・--format・--filter-pattern を組み合わせる
  • 過去を探すなら filter-log-events。時刻は ミリ秒エポック(date +%s%3N)で渡す
  • 集計するなら start-query と get-query-results。こちらの時刻は 秒(date +%s)
  • フィルターパターンは 大文字小文字を区別。語句は二重引用符、JSONは { $.キー = "値" }、スペース区切りは [項目, ...]
  • 保持期間のコマンドは put-retention-policy --retention-in-days。指定できる日数は決まった値だけ
  • boto3 では paginator で最後のページまでたどる
  • 結果が空のときは 期間・単位・大文字小文字・リージョン の順に確かめる

参考・出典

  • Amazon CloudWatch Logs ユーザーガイド「Filter pattern syntax for metric filters, subscription filters, filter log events, and Live Tail」 https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
  • Amazon CloudWatch Logs ユーザーガイド「Sample queries」 https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax-examples.html
  • Amazon CloudWatch Logs ユーザーガイド「parse」 https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax-Parse.html
  • Amazon CloudWatch Logs ユーザーガイド「stats」 https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax-Stats.html
  • Amazon CloudWatch Logs ユーザーガイド「Supported logs and discovered fields」 https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html
  • Amazon CloudWatch Logs API リファレンス「FilterLogEvents」 https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_FilterLogEvents.html
  • Amazon CloudWatch Logs API リファレンス「StartQuery」 https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_StartQuery.html
  • Amazon CloudWatch Logs API リファレンス「GetQueryResults」 https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_GetQueryResults.html
  • Amazon CloudWatch Logs API リファレンス「PutRetentionPolicy」 https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutRetentionPolicy.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CloudWatch LogsのAWS CLI tail・検索・クエリ例」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?