Fluent Bit の複数行フィルターを使って、ECS on Fargate のタスクから出る複数行のログを1レコードに連結する。

記事タイトルとURLをコピーする

こんにちは。
技術課の山本です。

ECS on Fargate のタスクから出る1つのログが、複数行になるときの話です。
Cloud Watch Logs に出力すると、1行ごとに1レコード出来てしまいます。
ログ監視をしていると、検知した部分の1レコード(=ログの1行のみ)を拾って通知が来てしまいます。
そのため、通知内容を見てもエラーの内容がよくわからないことがあります。

以下ブログ記事の「困っていること」と内容は同じです。

blog.serverworks.co.jp

今回は解決したので「困っていたこと」になりました。

★2023年3月3日追記
もう少し運用負荷の低い方法を見つけましたので、後編のブログ記事をあげています。
こちらもご参照ください。

blog.serverworks.co.jp

困っていたこと

エラー例:

java.nio.file.NoSuchFileException: from.txt
    at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
    at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
    at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116)
    at java.base/sun.nio.fs.UnixCopyFile.copy(UnixCopyFile.java:548)
    at java.base/sun.nio.fs.UnixFileSystemProvider.copy(UnixFileSystemProvider.java:258)
    at java.base/java.nio.file.Files.copy(Files.java:1295)
    at FileCopy.main(FileCopy.java:1)

awslogs ログドライバーをデフォルトの設定にしていると、以下のように出力されます。
1つのログが複数行の場合にも、1行ごとに1レコードできます。

本当は1レコードに1つのログを結合できると嬉しいです。

Fluent Bit の複数行フィルターを使って、ECS on Fargate のタスクから出る複数行のログを1レコードに連結する。

Fluent Bit の複数行フィルターについては、下の公式ドキュメントに記述があります

複数行フィルターは、もともと 1 つのコンテキストに属していても、複数のレコードまたはログ行に分割されたログメッセージを連結するのに役立ちます。

複数行またはスタックトレースログメッセージを連結する - Amazon Elastic Container Service

1つのログが複数行の場合にも、1レコードに1つのログを結合できそうです。
Fluent Bit のドキュメントをみると、複数行フィルターは以下の言語でのみ使用可能です。

  • go
  • python
  • ruby
  • java (Google Cloud Platform Java stacktrace format)

docs.fluentbit.io

java については、「java (Google Cloud Platform Java stacktrace format) 」とあり、GCPのドキュメントに記載のあるフォーマットで出るようです。
参考:ログでエラーをフォーマットする  |  Error Reporting  |  Google Cloud

前提条件としては、Fluent Bit のコンテナをタスク定義内に追加して、サイドカーコンテナにする必要があります。

前提となる IAM ロール(タスクロール)の準備

まず、以下の IAM ポリシーを作成します

  • 名前

    • ecs-policy-for-firelens
      • 任意の名前で大丈夫です。
  • 内容

    • 以下スニペット
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": [
            "logs:CreateLogStream",
            "logs:CreateLogGroup",
            "logs:PutLogEvents"
        ],
        "Resource": "*"
    }]
}

次に、IAM ロールを作成します。
信頼されたエンティティは以下にします。

  • AWS のサービス
    • Elastic Container Service
      • Elastic Container Service Task

IAM ロールには、上で作成した IAM ポリシー (ecs-policy-for-firelens) を付与し、任意の名前で保存します。
私の環境では、名前を ecs-task-role-for-firelens としました。

Fluent Bit コンテナのイメージ作成

"FluentBitDockerImage" フォルダを作り、フォルダ内に以下のように2ファイルを作成します。

── FluentBitDockerImage
   ├── Dockerfile
   ├── extra.conf

Dockerfile の内容

FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:latest
ADD extra.conf /extra.conf

2023/02/09 追記 安定版を使うなら以下です。
参考:Fluent Bit イメージに AWS を使用する

FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:stable
ADD extra.conf /extra.conf

extra.conf の内容 (javaの Built-in multiline parser を使うため、multiline.parser を java にしています。)

[FILTER]
    name                  multiline
    match                 *
    multiline.key_content log
    multiline.parser      java

(extra.conf の書き方は、必要に応じて Multiline - Fluent Bit: Official Manual を参照ください。初心者の私は深追いすると辛そうだったので、multiline.parser を java にするのみに留めました。)

手順は割愛しますが、ECR にリポジトリを作成し、pushします。
私の環境では "fluent-bit-multiline-image" というリポジトリ名にしました。

以下は、イメージ名:タグ名を以下にして pushする例です。

  • fluent-bit-multiline-image:1

(<AWSアカウント番号>は置き換えてください。)

# FluentBitDockerImage フォルダ内で実行

# ECR にログイン(東京リージョン)
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <AWSアカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com

# ローカルで Docker build
docker build -t fluent-bit-multiline-image:1 .

# タグ付与
docker tag fluent-bit-multiline-image:1 <AWSアカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com/fluent-bit-multiline-image:1

# ECR に push
docker push <AWSアカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com/fluent-bit-multiline-image:1

push 出来ました。

タスク定義の作成

頑張ってみたのですが、AWSマネジメントコンソールからは作成できませんでした。
aws cli でタスク定義を作成します。
タスク定義への設定内容を json ファイルを作成し記載します。
<AWSアカウント番号>、<タスクロール名>、<Javaコンテナ名:タグ名>は置き換えてください。
また、以下の値は適切な値に変えていただいて大丈夫です。

  • family
    • タスク定義名
  • log_group_name
    • ロググループ名
  • log_stream_prefix
    • ログストリームのプレフィックス
  • cpu
    • 割り当てCPU
  • memory
    • 割り当てメモリ
    • Fluent Bit コンテナの memoryReservation が 50 なので、その分は確保ください

json 中にあるコンテナ定義("containerDefinitions" セクション)のうち、1つ目は Fluent Bit コンテナ、2つ目は 複数行のログを出力するJavaコンテナです。

multiline-task-definition.json

{
    "family": "firelens-example-multiline",
    "taskRoleArn": "arn:aws:iam::<AWSアカウント番号>:role/<タスクロール名>",
    "executionRoleArn": "arn:aws:iam::<AWSアカウント番号>:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "essential": true,
            "image": "<AWSアカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com/fluent-bit-multiline-image:1",
            "name": "log_router",
            "firelensConfiguration": {
                "type": "fluentbit",
                "options": {
                    "config-file-type": "file",
                    "config-file-value": "/extra.conf"
                }
            },
            "memoryReservation": 50
        },
        {
            "essential": true,
            "image": "<AWSアカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com/<Javaコンテナ名:タグ名>",
            "name": "app",
            "logConfiguration": {
                "logDriver": "awsfirelens",
                "options": {
                    "Name": "cloudwatch_logs",
                    "region": "ap-northeast-1",
                    "log_group_name": "multiline-test/application",
                    "auto_create_group": "true",
                    "log_stream_prefix": "multiline-"
                }
            },
            "memoryReservation": 100
        }
    ],
    "requiresCompatibilities": ["FARGATE"],
    "networkMode": "awsvpc",
    "cpu": "256",
    "memory": "512"
}

以下のコマンドでタスク定義を作成します。

aws ecs register-task-definition --cli-input-json file://multiline-task-definition.json --region ap-northeast-1

タスク定義ができました。

結果

上で作成したタスク定義を使って、タスクを実行してみると、以下のような出力になりました。

全部で1行のログを1レコードで出している箇所

{
    "log": "Welcome to Amazon Corretto!",
    "container_id": "xxxx-xxxx",
    "container_name": "app",
    "source": "stdout",
    "ecs_cluster": "test1",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:<AWSアカウント番号>:task/test1/yyyyyyyy",
    "ecs_task_definition": "firelens-example-multiline:2"
}

複数行のログを1レコードに結合している箇所

{
    "container_name": "app",
    "source": "stderr",
    "log": "java.nio.file.NoSuchFileException: from.txt\n\tat java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)\n\tat java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)\n\tat java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116)\n\tat java.base/sun.nio.fs.UnixCopyFile.copy(UnixCopyFile.java:548)\n\tat java.base/sun.nio.fs.UnixFileSystemProvider.copy(UnixFileSystemProvider.java:258)\n\tat java.base/java.nio.file.Files.copy(Files.java:1295)\n\tat FileCopy.main(FileCopy.java:1)",
    "container_id": "xxxx-xxxx",
    "ecs_cluster": "test1",
    "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:<AWSアカウント番号>:task/test1/yyyyyyyy",
    "ecs_task_definition": "firelens-example-multiline:2"
}

ログ本文は改行が「\n」、タブが「\t」という形で出力されており、結合していました。
java (Google Cloud Platform Java stacktrace format) ということですね。 また、タスク定義名や、タスクの ARN 、 コンテナID などもわかるようになっています。

Javaコンテナの Dockerfile

検証に私用したJavaコンテナの Dockerfile は以下の記事と同じです。

ECS on Fargate で awslogs ログドライバーの "awslogs-multiline-pattern" オプションを使ってみる。 - サーバーワークスエンジニアブログ

まとめ

Fluent Bit の複数行フィルターを使うと、特定の言語であれば、複数行のログを1レコードに連結できました。

余談

今年の初登山は1泊2日の甲斐駒ヶ岳でした。
標高 2700m地点でキャンプしたら、夜の気温がマイナス17度でした。
準備万端だったので、安全に暖かく寝れました。
冷えた分、朝焼けが美しかったです。
本当は仙丈ヶ岳まで縦走したかったのですが、時間と天候により断念しました。
またリベンジしようと、体力を鍛える日々です。

山本 哲也 (記事一覧)

カスタマーサクセス部のエンジニア(一応)

好きなサービス:ECS、ALB

趣味:トレラン、登山(たまに)