はじめに

猫好きエンジニアの福田です。
動物病院に通院するときはいつもタクシーなのですが、家の近くにHonda EveryGoというカーシェアができました。このHonda EveryGoは「月額費用が無料」で「ペット乗車OK」という愛猫家にとってありがたいカーシェアです。タクシー往復代よりもリーズナブルに利用できるはずなので次回通院時に利用してみようと思います。

さて、ログの調査で CloudWatch Logs インサイトを利用することがありますが、たまにしか利用しないため書き方を調べながらクエリを書いています。
その都度調べるのが面倒でしたので、備忘として本記事にまとめておきたいと思います。
(あくまでも個人的によく利用するものを整理していますので、足りない情報はあると思います。必要に応じて随時追加や修正、間違いの訂正などしていきます。)

公式ドキュメント

本記事では個人的によく利用するものしか整理していません。
これ以外にも様々な関数や構文がありますので、以下に公式ドキュメントへのリンクを貼っておきます。

CloudWatch Logs Insights クエリ構文
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html

ブール、比較、数値、日時、その他の関数
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_QuerySyntax-operations-functions.html

クエリ例

この記事では、JSON形式のログにフォーカスしてクエリ例をご紹介します。

filter

条件に一致するもののみフィルタできます。

// ログサンプル
{ "user": "john.doe", "request_url": "https://user.example.com/me", "status_code": 200, "response_time": 1500 }
# 比較
filter status_code = 200
filter status_code != 404
filter response_time > 1000
filter response_time < 500
filter response_time >= 1000
filter response_time <= 500

# 論理
filter status_code = 200 and response_time < 1000
filter status_code = 500 or status_code = 404
filter not status_code = 200
filter not (200 <= status_code and status_code < 300 and ispresent(status_code)

# in / not in
filter status_code in [200, 404, 500]
filter status_code not in [200, 404, 500]

# 正規表現
filter message like /^\[API*/
filter message =~ /^\[API*/
filter not (message =~ /^\[API*/)  # 含まないもの

# 含む
filter message like "[API"
filter message =~ "[API"
filter not (message =~ "[API")  # 含まないもの

# フィールドが存在
filter ispresent(status_code)

ログがネストしている場合はドットでアクセスできます。

// ログサンプル
{ "user": { "id": "john.doe"}, "request": { "url": "https://user.example.com/me"}, "response": { "status": 200, "response_time": 1500 } }
filter user.id = "john.doe"
filter response.status != 200

parse

文字列内の特定箇所をフィールドとして抽出でき、抽出したものはその後のstatsやdisplay等で利用することができます。

// ログサンプル
{ "message": "[API] GET https://example.com/cats -> 200, process_time: 0.1000s" }
{ "message": "[API] POST https://example.com/cats -> 200, process_time: 0.9000s" }

一部抽出

# 文字列ver
parse message "process_time: *s" as process_time

# 正規表現ver
parse message /process_time: (?<process_time>.*?)s$/

複数抽出

# 文字列ver
parse message "[API] * *-> *, process_time: *s" as method, url, status_code, process_time

# 正規表現ver
parse message /^\[API\] (?<method>\S+) (?<url>.*?)-> (?<status_code>\d{3}), process_time: (?<process_time>.*?)s$/

# parse文 複数ver(一度に取得ではないが、複数のparase文でも取得可能)
parse message "process_time: *s" as process_time
| parse message "-> *," as status_code

stats

平均値や合計、件数など集計するためのものです。

// ログサンプル
{ "request_url": "https://user.example.com/me", "status_code": 200, "response_time": 1500 }
{ "request_url": "https://user.example.com/me", "status_code": 200, "response_time": 2000 }
{ "request_url": "https://user.example.com/users/1", "status_code": 401, "response_time": 300 }
{ "request_url": "https://user.example.com/users/2", "status_code": 401, "response_time": 200 }

ステータスコードごとに平均、パーセンタイル(95)、最小、最大値を集計

stats avg(response_time), pct(response_time, 95), min(response_time), max(response_time), count(*) by status_code
| sort status_code asc
status_code avg(response_time) pct(response_time, 95) min(response_time) max(response_time) count(*)
200 1750 2000 1500 2000 2
401 250 300 200 300 2

日次で、処理件数の合計と処理時間(msからsに変換)を集計

bin(1d)が集計単位。1dを1wや1m, 1h, 1sなどに変更することで1時間ごとに修正などに変更できます。

{ "job_name": "RecieveSQSMessageJob", "process_count": 4, "process_time": 2100 }
{ "job_name": "RecieveSQSMessageJob", "process_count": 9, "process_time": 5900 }
{ "job_name": "RecieveSQSMessageJob", "process_count": 1, "process_time": 1800 }
stats sum(process_count), avg(process_time/1000) by bin(1d)
bin(1d) sum(process_count) avg(process_time/1000)
2024-05-21T09:00:00.000+09:00 14 3.2667

複数フィールドでグルーピングし集計

ジョブ名でグルーピング後、その中のステータスコードの数とprocess_timeを集計しています。

// ログサンプル
// Cat
{ "job_name": "FindCatJob", "message": "[API] GET https://cat.example.com/cats/1 -> 200, process_time: 0.50s" }
{ "job_name": "FindCatJob", "message": "[API] GET https://cat.example.com/cats/2 -> 200, process_time: 0.50s" }
{ "job_name": "UpdateCatJob", "message": "[API] PUT https://cat.example.com/cats/2 -> 409, process_time: 0.90s" }
// Dog
{ "job_name": "FindDogJob", "message": "[API] GET https://dog.example.com/dogs/1 -> 200, process_time: 0.30s" }
{ "job_name": "FindDogJob", "message": "[API] GET https://dog.example.com/cats/9999 -> 404, process_time: 0.50s" }
{ "job_name": "DeleteDogJob", "message": "[API] DELETE https://dog.example.com/cats/1 -> 200, process_time: 0.60s" }
parse message "-> *," as status_code
| parse message "process_time: *s" as process_time
| filter message like /^\[API*/
| stats count(*), avg(process_time) by job_name, status_code
| sort job_name asc, status_code asc
job_name status_code count(*) avg(process_time)
DeleteDogJob 200 1 0.6
FindCatJob 200 2 0.5
FindDogJob 200 1 0.3
FindDogJob 404 1 0.5
UpdateCatJob 409 1 0.9

終わりに

ここまで読んでいただき、ありがとうございました!
CloudWatch Logs インサイトでよく使う関数や構文の具体例をざっくりとお伝えしました。
うまくクエリを書ければ一度のクエリで必要な情報が取得できたり、調査などの時間短縮にもつながると思います。
もし CloudWatch Logs インサイト利用時に「これってどうやるんだっけ?」と思ったときには、ぜひこのブログを思い出して参考にしていただけたら幸いです。