CloudWatch Logsのフィルターで複雑な検索条件を指定して欲しいイベントだけ取得する

下図のような流れでアプリケーションログをSlackへ通知する仕組みを構築しています。
![app_err_log-slack_cloudwatch.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/02f49cba-6e5d-c5a6-b39e-56e64e71eeec.jpeg)

この仕組み上で通知するログの種類をフィルタリングする場合はLambdaトリガー設定時のフィルタかLambda関数内になるかと思います。
しかし、不必要な通知をLambda関数内でフィルタリング処理を行う場合、Lambda関数自体を動作させる必要がある為、料金が多く発生してしまいます。
これを避ける為、Lambdaトリガー（やCloudWatch Logs メトリクスフィルター）のフィルターパターンを駆使してフィルタリングを行う方法を記します。

# フィルターとパターンの構文について
フィルターは以下３種類に分類できます。※公式名称ではありません。
本稿では「テキストベースフィルター」及び「スペース区切りフィルター」について取り扱います。

- テキストベースフィルター　　※全文一致
- スペース区切りフィルター　　※OR パターンマッチング
- JSONフィルター

## 出来ない事
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html)に記載が無かったりして困った点は下記の通りです。

- フィルターパターンに正規表現が扱えない。

> Q: 正規表現をログデータに使用できますか?
> Amazon CloudWatch のメトリクスフィルタでは、正規表現はサポートされていません。
> [よくある質問 - Amazon CloudWatch | AWS](https://aws.amazon.com/jp/cloudwatch/faqs/)より

- テキストベースフィルターにて、ORパターンマッチングと語句の除外を同構文中で表現したくてもORパターンマッチングが無視される。（もしくは構文エラーとなる）

※スペース区切りフィルターではOR演算子「||」や除外（否定）演算子「!=」等が同構文中で併用できます。

> CloudWatch Logs におけるフィルタパターンで OR パターンマッチングをご使用頂く場合、「?」を付与した語句と「-」を付与した語句とをフィルタパターン内で併用致しますと、現状では「?」が付与された語句が無視されるという動作となる
> AWSサポート回答より※2019/07/26時点

- スペース区切りフィルターにて括弧に対する除外が出来ない。（構文エラーとなる）

エラー例：`[A && !(B || C)]`
※ただし、以下を用いて対応可能です。
改善例：`[A && (!B && !C)]`

> ORで複数除外条件を記載したい場合
> `!(P || Q) = !P && !Q`
> ANDで複数除外条件を記載したい場合
> `!(P && Q) = !P || !Q`
> [ド・モルガンの法則](https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%BB%E3%83%A2%E3%83%AB%E3%82%AC%E3%83%B3%E3%81%AE%E6%B3%95%E5%89%87)より

## 出来る事
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html#extract-log-event-values)に記載の通りスペース区切りログイベントにてカンマで区切った任意の文字列を全て角括弧[]で囲むと一致する位置（順番）のフィールドのフィルターパターンとなります。
逆に、角括弧に囲まれたカンマで区切られていない任意の文字列はフィールド全体に対するフィルターパターンとする事ができます。
> CloudWatch Logs のフィルターにつきましては、JSON ログイベントもしくはスペース区切りログイベントから値を取得することができます。
> 今回のお客様のフィルター構文の場合、スペース区切りログフィルターとなりますが、構文の中に , (カンマ) を入れず、区切らないことによりログイベントの全体をひとつのフィールドとして認識させているものとお考えいただければと存じます。
> AWSサポート回答より※2019/08/07時点

例えば以下のような３件のログイベントが登録されているとします。

```text:ログ例
127.0.0.1 - frank [10/Oct/2000:13:25:15 -0700] "GET /apache_pb.gif HTTP/1.0" 200 1534
127.0.0.1 - frank [10/Oct/2000:13:35:22 -0700] "GET /apache_pb.gif HTTP/1.0" 500 5324
127.0.0.1 - frank [10/Oct/2000:13:50:35 -0700] "GET /apache_pb.gif HTTP/1.0" 200 4355
```
ログ例に対して以下のフィルターを行うと１行目と３行目が一致します。

```text:フィルター例1
[ip, user, username, timestamp, request, status_code = 200, bytes]
```
次に、以下のフィルターを行った場合、２行目（status_codeの50）と３行目（timestamp列の50）が一致します。

```text:フィルター例2
[hoge = *50*]
```

# フィルターパターン作成

上項ログ例は綺麗に列が揃っているスペース区切りログイベントでしたのでスペース区切りフィルターが綺麗に適用できますが、今回は都合上、以下のようなアプリケーションエラーのスタックトレースが1つのイベントとして登録されてしまいました。
このような場合は単純なスペース区切りフィルターの作成が困難なのでフィールド全体に対するスペース区切りフィルターを使用します。

```text:アプリケーションエラーログ例
org.apache.catalina.connector.ClientAbortException: java.io.IOException: Connection reset by peer
    ～（中略）～
    at org.apache.coyote.ajp.AjpProcessor.service(AjpProcessor.java:214)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:39)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:412)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1388)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:426)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:479)
```

ここで、上項の出来る事と出来ない事を掛け合わせる事で、AまたはBの中から、CまたはDまたはEを除外しつつ、FかつGである場合も除外するといったフィルタリングが可能になります。
`[(A || B) && (!C && !D && !E) && (!F || !G)]`

例えば、以下のようなフィルターパターンでは、
1. `Exception`もしくは`exception`がメッセージに含まれる全てのイベントの中から、
2. `hogehugaController.java`か`yokuErrorDeruClass.java`か`テスト`というメッセージが含まれるイベントを除外し、
3. `ClientAbortException`かつ`Connection reset by peer`が含まれるイベントも除外した後に残ったイベントだけがマッチするようになります。

```text:フィルターパターン例
[(str = "*Exception*" || str = "*exception*")
 && (str != "*hogehugaService.java*" && str != "*yokuErrorDeruClass.java*" && str != "*テスト*")
 && (str != "*ClientAbortException*" || str != "*Connection reset by peer*")]
```

# 確認
AWS CloudWatchコンソール上から ロググループ（ログ） >  [フィルター対象のロググループ] > [フィルター対象のログストリーム] を選択します。
作成した構文を下図赤枠の「イベントをフィルター」項目に入力し、項目右側の検索する日付範囲を指定するとLambdaトリガーのフィルタ設定時と同様の結果が得られます。
![2020-08-27_17h33_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/9f421678-891d-1e15-4adc-949bc99ba52b.png)

以下旧インターフェイスの場合
![CloudWatch Logsフィルター.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/9a7252f4-4c8e-72a0-6040-15876c2cfecd.jpeg)

# まとめ
フィルターパターンを駆使して不必要なログを除外する事で料金や通知の量を減らして運用コストを下げましょう！

#参考
[フィルターとパターンの構文 - Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html)