AWS SDK for Rubyを使ってCloudWatch Logs Insightsの分析内容を取得する

# はじめに
CloudWatchのログの内容が膨大なときはCloudWatch Logs Insightsを使って必要なログのみを取得し分析していたのですが、AWS SDK for Rubyを使ってCSV形式で出力する機会があったのでその方法についてまとめてみました。

CloudWatch Logs Insightsをあまり使ったことがない方、CloudWatchのログデータをCSV形式で取得する方法について興味がある方のお役に立つかと思います！

# CloudWatch Logs Insightsとは？
独自のクエリ構文を使いCloudWatch Logsのログデータをインタラクティブに検索、分析できるAWSが提供するサービスの一つです。

:::note info
CloudWatch Logsとは？
CloudWatchの機能の一つで、AWSのリソースやアプリケーションから生成されるログデータを収集、監視、保存する
:::

CloudWatchのサイドバーから「ログのインサイト」に進むと操作画面に移ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1241122/dc9ba8dd-d6b7-4515-c4fe-3f20b154b502.png)

ざっくりですが操作手順は以下の通りです。
1. 検索、分析を行うロググループを選択 
1. クエリ構文を作成し実行
1. 実行したクエリの内容に応じて取得したクエリが表示される

基本的な操作方法やクエリ構文については本記事では割愛するので、下記の記事を参考にしてみてください。

https://qiita.com/suuu/items/8387df88f134348f22c7

https://dev.classmethod.jp/articles/how-to-cloudwatch-logs-insights/

# AWS SDK for Rubyとは
Ruby言語を使用してAWSのリソースやサービスをプログラム的に操作するためのツールキットです。
AWS SDK for Ruby のバージョン3からは サービス固有のgem (例: aws-sdk-s3、aws-sdk-dynamodb など) にモジュール化されたので、必要なSDKのみを個別のgemで選択できるようになりました。

https://aws.amazon.com/jp/sdk-for-ruby/

# 事前準備

Ruby on Railsを使ってCloudWatch Logs Insightsの分析内容をCSV形式で取得するスクリプトを作りますが、その前に必要な準備します。

開発環境: Mac M2

## 認証情報の設定
AWS SDKを使ってAWSサービスに接続するためには認証情報をあらかじめ指定しておく必要があります。

1. AWS IAMコンソールからIAMユーザを作成し、アクセスキーIDとシークレットアクセスキーを取得
1. ローカル環境で`~/.aws/credentials`を作成し、取得した認証情報を設定したのちに保存
```bash
[default]
aws_access_key_id='取得したアクセスキーID'
aws_secret_access_key='シークレットアクセスキー'
aws_session_token='セッショントークン(一時的なアクセス権限をとする場合に必要)'
```

認証情報の設定について：

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/signup-create-iam-user.html#setup-temp-creds

## SDKのインストール

cloudwatch用のgemをGmefileに追加し、bundle installを実行
```
gem 'aws-sdk-cloudwatchlogs'
```

## RailsコンソールでCloudWatchに接続しログを取得する
Railsコンソールを使って実際にログを取得してみることで、どうすればログを取得できるかを見てみます。

### CloudWatchへの接続

はじめに先述で設定した認証情報を使ってCloudWatchに接続してみましょう。

```ruby
client = Aws::CloudWatchLogs::Client.new(region: 'ap-northeast-1')
```

`Aws::CloudWatchLogs::Client.new`はCloudWatch Logsの操作をサポートするクライアントです。呼び出される際に、`~/.aws/credentials`から認証情報を自動で読み込みます。`region`でAWSリージョンを指定する必要があります。

### query_idを取得

接続がうまくいったら、StartQueryを使用してCloudWatch Logs Insightsのクエリを指定し、query_idを取得できるか確認しましょう。

```ruby
query_response = client.start_query({
  log_group_name: '/log_group_path',
  start_time: (Time.zone.now - 3600).to_i * 1000,
  end_time: Time.zone.now.to_i * 1000,
  query_string: 'fields @message | limit 20'
})
```

`start_query`は、CloudWatch Logsのログデータに対して指定したクエリを実行し、ログデータの検索を実行します。

リクエストパラメータについては以下の通りです。


- `log_group_name`
クエリを実行するロググループを指定

- `start_time`
クエリする時間範囲の開始時刻

- `end_time`
クエリする時間範囲の終了時刻

- `query_string`
使用するクエリ文字列
ここで指定したクエリが実際に実行される

start_queryについて:

https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_StartQuery.html


実行後、`query_id`が取得できれば成功です

```ruby
query_response
=> <struct Aws::CloudWatchLogs::Types::StartQueryResponse query_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx">
```

### ログを取得

取得した`query_id`を使ってログを取得します。

```ruby
response = client.get_query_results(query_id: query_response[:query_id])
```

`get_query_results`は、指定したクエリの結果を返します。

get_query_resultsについて: 

https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_GetQueryResults.html


返却された結果からログが確認できたら成功です。

```ruby
response[:results]
=>
[
   [#<struct Aws::CloudWatchLogs::Types::ResultField field="@message", value="I, [2024-01-01T13:56:10.298884 #1]  INFO -- : [xxx-xxx-xxx-xxx-xxx] Started GET \"/_health\" for 10.0.10.196 at 2024-01-01 13:56:10 +0900\n">,
     #<struct Aws::CloudWatchLogs::Types::ResultField field="@ptr", value="">],
    [#<struct Aws::CloudWatchLogs::Types::ResultField field="@message", value="I, [2024-01-01T13:56:09.749941 #1]  INFO -- : [xxx-xxx-xxx-xxx-xxx]  Started GET \"/_health\" for 10.0.11.161 at 2024-01-01 13:56:09 +0900\n">,
     #<struct Aws::CloudWatchLogs::Types::ResultField field="@ptr", value="">],
    [#<struct Aws::CloudWatchLogs::Types::ResultField field="@message", value="I, [2024-01-01T13:56:09.576021 #1]  INFO -- : [xxx-xxx-xxx-xxx-xxx]  Started GET \"/_health\" for 10.0.12.74 at 2024-01-01 13:56:09 +0900\n">,
     #<struct Aws::CloudWatchLogs::Types::ResultField field="@ptr", value="">],
    [#<struct Aws::CloudWatchLogs::Types::ResultField field="@message", value="I, [2024-01-01T13:55:40.268645 #1]  INFO -- : [xxx-xxx-xxx-xxx-xxx]  Started GET \"/_health\" for 10.0.10.196 at 2024-01-01 13:55:40 +0900\n">,
     #<struct Aws::CloudWatchLogs::Types::ResultField field="@ptr", value="">]
]
```


# ログをCSV形式で出力するスクリプトを作成

記事のアクセス数順で、記事IDとアクセス数をCSV形式で出力するサービスクラスを作成します。

### コード
``` ruby
class Aws::CloudwatchLogsService
  require 'aws-sdk-cloudwatchlogs'

  LOGGER = ActiveSupport::Logger.new($stdout)

  def call
    client = Aws::CloudWatchLogs::Client.new(region: 'ap-northeast-1')
    
    columns = ['articlue_id', 'accessCount']
    query_string =  'filter @message like /articlues\/.*\?include_comments/
                    | parse @message "articlues/*?" as articlue_id
                    | stats count(articlue_id) as accessCount by articlue_id
                    | sort accessCount desc
                    | limit 10000'

    params = {
      log_group_name: '/articlue',
      start_time: Time.zone.now.beginning_of_day.to_i * 1000,
      end_time: Time.zone.now.end_of_day.to_i * 1000,
      query_string: query_string
    }

    query_response = client.start_query(params)
    response = nil

    # 再試行回数は20回まで
    20.times do
      sleep 1
      response = client.get_query_results(query_id: query_response[:query_id])
      break if response[:status] != 'Running'
    end

    unless response[:status] == 'Complete'
      logger.warning("Fetch Data Failed. Status -> #{response[:status]}")
    end

    generate_csv_data(response, columns)
  end

  #
  # CSV形式に書き出すデータを生成
  #
  def generate_csv_data(data, columns)
    result = []
    data[:results].each do |row|
      record = []
      columns.each do |column|
        value = get_value(row, column)
        record << value
      end
      result << record
    end
    result
  end

  def get_value(record, key)
    result = record.select { |x| x[:field] == key }
    result.empty? ? '' : result[0][:value]
  end
end
```

### 実行結果

コンソールで実行してみると以下のような実行結果が得られます。

```ruby
Aws::CloudwatchLogsService.new.call
=>
[["3820", "283"],
 ["2840", "243"],
 ["3912", "234"],
 ["1022", "210"],
 ["2192", "189"],
 ["1792", "172"],
 ["2832", "150"],
 ["3172", "142"],
 ["1047", "140"],
 ...
 ["3953", "1"],
 ["2947", "1"],
 ["2292", "1"]]
```

# まとめ

最後のスクリプトに関しては、アプリの設計、仕様や元々のログの形式の説明を無視して載せているので分かりにくい部分があるかと思いますが、ログの取得から出力までの流れに着目してもらえれば幸いです。
CloudWatch Logsで取得したログをCSVとして出力できれば、他のデータ分析ツールに流用したり、アプリケーション上ではログの生成、取得が難しくても、別々で出力した同士をマージして一つのデータして管理できたりと、分析の幅が広がると思います。
今までCloudWatch Logs Insightsを使ったことがなかった方は、ぜひ使ってみてください！