CloudWatch Logs Insightsで始める簡単!!ログ集計

GA technologies SREチームの永冶です。 社内でログを集計した際にCloudWatch Logs Insightsが便利だと感じたので早速ブログにしました。 これからCloudWatch Logs Insightsを使用する方の助けになればと思います。

CloudWatch Logs Insightsとは

CloudWatch Logsのログデータを分析する機能です。
専用のクエリ言語が用意されており、ログを簡単に集計することができます。

公式のドキュメントはこちらです。CloudWatch Logs Insights でログデータを分析する

まずはコンソールで触ってみましょう

CloudWatch Logs Insightsがどんなものなのかは実際に触ってみるのが一番です。
AWSコンソールでClloudWatchから左のナビゲーションバーの「インサイト」をクリックするとCloudWatch Logs Insightsの画面になります。

f:id:t_nagaya:20190411003134j:plain ここでやるべきことは、下記の4点のみです。

  1. ロググループを選択します。
  2. クエリを書きます。
  3. 検索する期間を選択します(CloudWatch Logsと同様に期間を選択できます)。
  4. 「クエリの実行」を押します。

新しいクエリ言語を覚えないといけないのかと思うかもしれませんが、
コンソール上には「クエリのヘルプ」があり、コマンドの使い方などを確認しながらクエリを書くことができます。 また、クエリのヘルプでコマンドを選択し追加すると、コマンドのサンプルと共にクエリに反映されます。 このサンプルを参考にしながら書けば、すぐに要件に合うクエリを書くことができるはずです。

f:id:t_nagaya:20190411011854j:plain
クエリのヘルプにより簡単にクエリを書くことができます

サンプルとして下記のクエリを解説します。

fields @timestamp, @message
| sort @timestamp desc
| filter @message like /(POST|GET) \/api\/v1/
  • fieldsで取得する項目を指定します。SQLのSELECTのイメージです。ログのメッセージと、タイムスタンプを取得します。
  • sortは指定した項目でソートします。SQLのORDER BYのイメージです。サンプルではタイムスタンプの降順に表示します。
  • filterは絞り込みの条件を指定します。SQLのWHERE のイメージです。サンプルでは正規表現/(POST|GET) \/api\/v1/にマッチしたログのみに絞り込みます。

詳しいクエリ構文についてはドキュメントを参照ください。CloudWatch Logs Insights クエリ構文

SDKを用いてクエリの結果を取得しましょう

コンソールでCloudWatch Logs Insightsに親しんだら、CLIで実行してみたくなりますよね。 そもそもコンソールだとCloudWatch Logsの検索期間を指定するのがやりづらいので、CLIで日時指定できた方が楽できます。

前提

今回は下記のような実行環境でAWS SDK for Rubyを使用してスクリプトを書きました。

  • Mac OS Mojave 10.14
  • Ruby 2.6.2
  • AWS SDK for Ruby | CloudWatch Logs Gem v1.17.0 *1
  • jq 1.6 (JSONを整形しないなら必要ありません)*2

ロジック

Aws::CloudWatchLogs::Clientには、CloudWatch Logs Insightsを利用するためのメソッドがいくつか用意されています。今回使用するのは下記の2つです。

処理の手順

  1. start_queryで対象のロググループ、検索期間、クエリを指定し、CloudWatch Logs Insightsのクエリを開始します。
  2. get_query_resultsでクエリ実行の進捗状況を確認します。
  3. まだ作業途中であれば、数秒後に再度問い合わせを行います。
  4. クエリ実行が完了のステータスになったら取得したデータを表示します。

ハマりポイント

私がハマったポイントを書いておきます。

  • クエリ実行が完了したことを確認しないと、結果が返ってこなかったり、不完全な集計結果になる可能性があります。
  • コンソールで書いたSQLをそのまま流用すればCLIでも問題なく実行できますが、Rubyでは\がエスケープされエラーになっていました。
    • サンプルに記載した通り、バックスラッシュを使用する場合は\\にしましょう。

Rubyで書いたスクリプト

サンプルとしてPOST /api/v1GET /api/v1がログに出力された回数を1時間ごとに計測するスクリプトを用意しました。

require 'aws-sdk-cloudwatchlogs'
require 'json'

# 別途AWSアクセスキー、シークレットキーを環境変数で指定しておく必要があります
# export AWS_ACCESS_KEY=xxxx
# export AWS_SECRET_ACCESS_KEY=xxxx

client = Aws::CloudWatchLogs::Client.new(
  region: ENV.fetch('AWS_REGION', 'ap-northeast-1'),
)

# クエリの構築
# 「POST /api/v1」、「GET /api/v1」がログに出力された回数を1時間ごとに計測します
query_string = <<TEXT
fields @timestamp, @message
| sort @timestamp desc
| filter @message like /(POST|GET) \\/api\\/v1/
| stats count(*) by bin(1h)
TEXT

# 期間の設定
# 集計開始日時
start_time = Time.new(2019, 3, 1, 0, 0, 0, '+09:00')
# 集計終了日時
end_time = Time.new(2019, 3, 31, 0, 0, 0, '+09:00')

# クエリ実行を開始します
resp = client.start_query({
                            log_group_name: "hoge", # ロググループ名を指定します
                            start_time: start_time.to_i,
                            end_time: end_time.to_i,
                            query_string: query_string
                          })
resp2 = client.get_query_results({query_id: resp.query_id.to_s})

# クエリが実行中でなくなるまで問い合わせします
while ['Scheduled', 'Running'].include?(resp2.status)
  sleep 5
  resp2 = client.get_query_results({query_id: resp.query_id.to_s})
end

# 結果を出力します
# result[i] #<struct Aws::CloudWatchLogs::Types::ResultField field="..", value="..">
puts JSON.dump(resp2.results.map {|result| [result[0].value, result[1].value] })

スクリプトをsample.rbといった名前で保存し、 下記コマンドを実行するとJSONが出力されます。

ruby sample.rb

jqで整形してもいいかもしれません。

ruby sample.rb | jq .

下記のように出力されます。

[
  [
    "2019-03-01 17:00:00.000",
    "3"
  ],
  [
    "2019-03-01 16:00:00.000",
    "2"
  ],
  [
    "2019-03-01 15:00:00.000",
    "3"
  ]
]

例えば、 ["2019-03-01 15:00:00.000","3"]は2019/03/01 15:00~15:59(UTC)の間に3件文字列が検出できたことを表します。

最後に

CloudWatch Logs Insightsで簡単にログを分析し集計できました。 学習コストかからないようにコンソールのUIはしっかりしていると思うので、 是非まだCloudWatch Logs Insightsを使用していない方は試してみてください!

*1:「gem install aws-sdk-cloudwatchlogs」でインストールしましょう

*2:Macであればhomebrewでjqをインストールすることができます