Cloudwatch Logs Insights の parse 関数の使い方

最近プロダクション環境でFARGATE on ECSを導入しました。この場合ログはCloudwatch Logsに送られることになります。
ログから簡単なデータ集計をしたい場合に昨年発表されたCloudwatch Logs Insightsが使えるのではないかと思い試してみたところ、非常に便利だった、かつちょっとparse関数に手間取ったので備忘録がてら残しておきます。

## Cloudwatch Logs Insightsとは

(詳細は公式ドキュメントに任せて、便利なところを)
Cloudwatch LogsのデータをWebコンソール上から確認する場合、これまでは`Log Group`内の`Log Stream`を指定しなければフィルタがかけられませんでした。
FARGATEで複数コンテナが動く場合、コンテナ1つ1つに対して`Log Stream`が作られるため、APIコンテナが4つ稼働していた場合、4つのログに対して同時にログのフィルタをかけることが(Webコンソール単体では)できませんでした。

Insightsを使うことで、`Log Stream`をまたいで`クエリ`を発行できるようになり、前述の問題点が解消されます。
さらに、構造化(=JSON)ログを出力している場合はSQLライクな集計も簡単にできます。また、構造化されていないログを出している場合でも、今回説明するparse関数を使うことである程度集計を行うことができます。

## やりたいこと

Go製のメッセージ配信APIサーバがあり、あるIDに対して何個のメッセージを配信したかをログに出しています。(通知に相当するものであり、量が多くなるためにデータベースには記録していません)

```
[GIN] 2019/04/14 - 18:35:55 | 200 | 387.381544ms | 10.0.0.167 | POST /api/v1/message/
2019/04/14 18:35:55.044306: [I] message(s) sent to 2 address(es) for id 01234567
2019/04/14 18:59:56.772482: [I] message(s) sent to 1 address(es) for id 98765432
[GIN] 2019/04/14 - 18:59:56 | 200 | 49.884µs | 10.0.1.163 | GET /
```

(フレームワークのログとアプリケーションのログの形式が違うのはいつか直したいところです)

2行目と3行目のログを抽出し、特定期間のidと送信したメッセージの数を集計したいです。

## できたこと

まず始めに完成したクエリと、実行した画面サンプルを表示します。

```javascript
fields @message
| filter @message =~ "message(s) sent to"
| parse @message /sent to (?<nummsgs>\d+) address\(es\) for id (?<cid>[a-zA-Z0-9]+)/
| stats sum(nummsgs) by cid
```

画面サンプルはちょっとだけ正規表現が違ったりするのでぼかしています。上のものがそのまま入ってるとお考え下さい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90487/e1d1745d-03af-d548-39b6-e217d1412ad0.png)

ご覧のように、SQLライクな感じで集計ができています。メインでないので省きますが期間はクエリを書くボックスの右上から指定できています(もちろんクエリ内で指定する方法もある、はず)

## やってること

### 1行目 `fileds @message`

fields関数を使い、Logsからもってくる`フィールド`を指定します。 [サポートされるログと検出されるフィールド \- Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html) にある表では「その他のログタイプ」に相当します。今回欲しいのはログのテキストだけなので `@message` を指定します

### 2行目 `filter @message =~ "message(s) sent to"`

filter関数を使い、 欲しいデータのみにそぎ落としています。 `=~` は `like` でも大丈夫ですし、正規表現で指定することも可能です


### 3行目 `parse @message /sent to (?<nummsgs>\d+) address\(es\) for id (?<cid>[a-zA-Z0-9]+)/`

[使い方や注意点は後述](#parse関数の使い方、注意点)しますが、parse関数を用いてログからデータを作っています。

### 4行目 `stats sum(nummsgs) by cid`

SQL(どちらかというとElasticsearchに近いですかね？)のようにグルーピングと集計を行っています。
少し補足ですが、実行イメージ画像の棒グラフの下に「34 records matched」となっており、結果は24recordsなので、ちゃんとグルーピングされていることもわかります。

## parse関数の使い方、注意点

本題です。
[CloudWatch Logs Insights クエリ構文 \- Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html) にparse関数の使い方は書いてあるのですが、非常に簡単なサンプルがかいてあるだけで、`*`が使えることしかわかりません。

ですが、英語版のドキュメントやStack Overflowを覗くと以下のようにもかけることがわかりました。

- 正規表現の名前付きグループ`(?<名前>正規表現)`を使って次のクエリに渡せる

クエリを読めばわかるのでこれ以上の説明はないのですが、他にもはまったことがあるので記載します

- 新たに作成するfieldに`@id`は使えない

何かとバッティングしているのでしょう。 `cid`の部分を初めは`@id`にしていたのですが、クエリが失敗するエラーが出ました。


## おまけ データのcsv出力

クエリした結果をCSVで出力するのに5分くらい探し回ったので置いておきます。アクションの中にあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90487/d9335e34-d70c-3ee3-dccb-81d5bf670959.png)


## 終わりに

月1の報告のためにこの集計がしたかったのですが、「ログ全部S3にもってって手元にDLしてから集計するか～」とか思っていたのですが、これで十分でした。

料金については以下のような感じなので、データ量が多い場合は注意が必要ではありますが、コンソール右上で期間が予め指定されており、普通にクエリを打つだけでは全走査は走らないので安心です。

> 料金はクエリごとに走査したログデータの量に対してかかります。US East (N. Virginia)の場合、$0.005/GB で、他のリージョンも同様の料金です。

[新機能 – Amazon CloudWatch Logs Insights – 高速でインタラクティブなログ分析 \| Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/new-amazon-cloudwatch-logs-insights-fast-interactive-log-analytics/)

Cloudwatch LogsからElasticsearchに流し込む方法も用意されていますが、簡単なものだとこれで十分ですね！ｽﾊﾞﾗｼｲ！