FireLens(fluentbit)を使ってFargateのJSONログをパースして出力する

## 目的
- ECSコンテナ（Fargate）から出力されるアプリケーションログをS3へ出力したい。
- アプリケーションログはJSON形式なので、FireLens(fluentbit)でパースをしたい。

## やってみた

システム構成は下記のイメージですが、FargateやS3の構築の方法や設定ついては省略しています。

![FireLends.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28783/147617c9-f369-77d8-6119-3ea33c6a7425.png)

ここでは、JSON形式のログをパースして出力する方法と、設定値による出力の違いを記載します。

なお、fluentbitの設定方法は2パターンあります。さらにカスタム設定ファイルを読み込む方法も2パターンあります。
1. Fargateのタスク定義のオプションに設定する
2. カスタム設定ファイルに設定する
    ⅰ. S3に配置したカスタム設定ファイルを読み込む
    ⅱ. コンテナイメージビルド時にカスタム設定ファイルをイメージにコピーする

今回は「**ⅰ. S3に配置したカスタム設定ファイルを読み込む**」パターンで検証しています。

参考手順）

https://dev.classmethod.jp/articles/external-firelens-config-for-fargate/

https://zenn.dev/nh8939/articles/ca3cba603f7045

### JSON形式のログをパースして出力する

アプリケーションコンテナから出力されるJSONを以下として、どのように出力されるか見てみます。
```json
{"key1":"value1", "key2":"value2", "key3":"value3", "key4":"value4", "key5":"value5"}
```

まず、パースをしないで出力した例です。`log`にパースされていない状態で出力されています。
```json
{
	"date": "2023-12-28T05:44:21.333117Z",
	"container_name": "side-car",
	"source": "stdout",
	"log": "{\"key1\":\"value1\", \"key2\":\"value2\", \"key3\":\"value3\", \"key4\":\"value4\", \"key5\":\"value5\"}",
	"container_id": "20fd58957fe14e6ba5ac7d5a5d9c54a8-159383053",
	"ecs_cluster": "test_ecs_cluster",
	"ecs_task_arn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/test_ecs_cluster/20fd58957fe14e6ba5ac7d5a5d9c54a8",
	"ecs_task_definition": "test_ecs_task_definition:9"
}
```

JSONをパースしたい場合は、fluentbitのコンテナイメージにパーサー設定ファイルが用意されているため、`[SERVICE]`で読み込みます。`[FILTER]`にパースする設定を記載します。

```conf
[SERVICE]
    Parsers_File /fluent-bit/parsers/parsers.conf

[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser json
    Preserve_Key False
    Reserve_Data False
```

### [FILTER]の設定値について

| キー | 設定値 | 説明 |
|:-----------|------------|------------|
| Name       | parser        | フィルタープラグインの名前。今回は [parser](https://docs.fluentbit.io/manual/pipeline/filters/parser) というプラグインを指定。|
| Match     | *      | 受信レコードのタグに対してマッチするパターン。大文字と小文字を区別し、ワイルドカードとしてスター (*) 文字をサポートします。       |
| Key_Name       | log        | 解析するレコードのフィールド名を指定します。         |
| Parser         | json          | フィールドを解釈するパーサーを指定する。事前に用意されたjsonというパーサー。自作したパーサーも設定可。|
| Preserve_Key    | False     | 解析された結果に元のKey_Nameフィールドを残す。false の場合、フィールドは削除されます。  デフォルトはfalse       |
| Reserve_Data       | False       | パースされた結果に他のすべてのオリジナル・フィールドを残す。falseの場合、他のすべてのオリジナルフィールドは削除されます。デフォルトはfalse     |


参考）FILTER

https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/classic-mode/configuration-file#config_filter

参考）parser

https://docs.fluentbit.io/manual/pipeline/filters/parser

### Preserve_Key と Reserve_Data の挙動

設定値のうち、`Preserve_Key`と`Reserve_Data`の設定による違いがよくわからなかったので、違いを比べてみます。

- どちらもFalse（デフォルトと同じ）

パースされた`log`のデータと`date`が出力されます。

```json
{
	"date": "2023-12-28T03:18:42.950209Z",
	"key1": "value1",
	"key2": "value2",
	"key3": "value3",
	"key4": "value4",
	"key5": "value5"
}
```

- どちらもOnにした場合

`container_id`や`container_name`、パースされていないままの`log`もそのまま出力されています。

```json
{
	"date": "2023-12-28T05:08:21.979872Z",
	"key1": "value1",
	"key2": "value2",
	"key3": "value3",
	"key4": "value4",
	"key5": "value5",
	"container_name": "side-car",
	"source": "stdout",
	"log": "{\"key1\":\"value1\", \"key2\":\"value2\", \"key3\":\"value3\", \"key4\":\"value4\", \"key5\":\"value5\"}",
	"container_id": "edc63f4367784d408944b56f4967546a-159383053",
	"ecs_cluster": "test_ecs_cluster",
	"ecs_task_arn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/test_ecs_cluster/edc63f4367784d408944b56f4967546a",
	"ecs_task_definition": "test_ecs_task_definition:9"
}
```

- `Preserve_Key`だけOnの場合の出力結果

パースされていない`log`のデータが出力されると思ったんですが出力されずでした。どちらもfalseの時と同じ結果。（もしかしたら設定方法が良くなかったかも）
```json
{
	"date": "2023-12-28T02:59:34.892455Z",
	"key1": "value1",
	"key2": "value2",
	"key3": "value3",
	"key4": "value4",
	"key5": "value5"
}
```

- `Reserve_Data`だけOnの場合の出力結果

`container_id`や`container_name`などが追加されて出力されています。

```json
{
	"date": "2023-12-28T02:49:00.524241Z",
	"key1": "value1",
	"key2": "value2",
	"key3": "value3",
	"key4": "value4",
	"key5": "value5",
	"container_id": "75cfcc2b4f914ea09103bd2c26c566e8-159383053",
	"container_name": "side-car",
	"source": "stdout",
	"ecs_cluster": "test_ecs_cluster",
	"ecs_task_arn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/test_ecs_cluster/75cfcc2b4f914ea09103bd2c26c566e8",
	"ecs_task_definition": "test_ecs_task_definition:9"
}
```

## まとめ

- JSONパーサーは用意されているので、それを使うことができる
- パースされた結果、トップレベルのオブジェクトとして出力される
    - パース前だとlogの下に出力されているが、トップレベルで出力される
    - logの下に出力して欲しいケースもありそう
- parseの設定値について
    - Preserve_Key
        - デフォルトはFalse
        - もともとのlogもパースされてない状態でそのまま出力される
    - Reserve_Data
        - デフォルトはFalse
        - Trueにすると`container_id`や`container_name`など ECS 関連のメタデータが出力できる。



### その他参考

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/firelens-example-taskdefs.html#firelens-example-parsing

https://github.com/aws-samples/amazon-ecs-firelens-examples/tree/mainline/examples/fluent-bit/parse-json