絶対忘れるのでFluentdの設定内容とその意味をまとめました


業務でロギング機構を作ったのですが、しばらく経ったら設定内容の意味を忘れることが目に見えているので先にまとめておきます。
よければ参考にしてください。
パートごとに抜粋しているので、設定ファイル全体は記事の一番最後を見てください。
# ログの取得

``` fluent.conf
<source>
  @type  forward
  port  24224
</source>
```
#### `forward`
TCP経由でログ情報を受け取るinputプラグイン
ついでにハートビートによる死活監視も行っているらしい

https://docs.fluentd.org/input/forward

# ログの整形

``` fluent.conf
<filter **>
  @type grep
  <exclude>
    key log
    pattern /^$/
  </exclude>
</filter>

<filter rails>
  @type parser
  key_name log
  reserve_data true
  <parse>
    @type regexp
    expression /^\[(?<@timestamp>[^\]]*)\] \[(?<log_level>[^\]]*)\] \[(?<request_id>[^\]]*)\] (?<log>.*)$/
    time_format %Y-%m-%d %H:%M:%S.%L%z
  </parse>
</filter>

<filter nginx>
  @type parser
  key_name log
  reserve_data true
  <parse>
    @type nginx
  </parse>
</filter>
```
#### `<filter **>`
すべてのタグが対象
#### `<exclude>`
grepしてパターンマッチしたイベントを除外する
#### `key log`
`log`というフィールドにgrepをかける
#### `pattern`
grepのパターン（今回は空文字にマッチするように指定）
Railsのリクエスト処理完了後、なぜか空文字のログが最後に付いてくるのでfilterプラグインで除去している
空のログはノイズになるので、他のコンテナから送られきた場合も想定して`<filter **>`で全てのタグにマッチするようにしている

#### `<filter xxx>,`
`xxx`というtagだけが対象

#### `<@type>`
`regexp` `expression`に独自に正規表現を定義してパースする
`nginx` デフォルトのNginxのログ用のパーサーを使う

#### `key_name log`
`log` フィールドの値をパースする
#### `reserve_data true`
`true`にしないとその他に元からあったフィールドが消えるので注意
Fluentdが自動で設定してくれている`container_name`のフィールドも消えてしまうので、Elasticsearchでどのコンテナのログかわからなくなって困る

#### `expression`
正規表現のパターン
railsのログは
`[2022-12-01T01:23:45.678+0900] [INFO] [030388f7-1d2c-416f-bfb8-50f3d8349155] log_message`
というフォーマットを想定
Elasticsearchはスペース入りのタインプスタンプフォーマットはサポートしていないので、`T`は必要な子
正規表現で時刻、ログレベル、リクエストID、ログ内容を抜き、それぞれ`@timestamp`, `log_level`, `request_id`, `log`　フィールドとして設定
元からある`log`フィールドは上書き

https://docs.fluentd.org/filter/parser

# Elasticsearch, S3への送信
``` fluent.conf
<match **>
  @type copy

  <store>
    @type elasticsearch
    host elasticsearch
    port 9200
    user "#{ENV['ELASTIC_USER']}"
    password "#{ENV['ELASTIC_PASSWORD']}"
    scheme https
    ca_file path/to/ca_file.crt
    logstash_format true
    include_timestamp false
    logstash_prefix fluentd
    logstash_prefix_separator _
    logstash_dateformat %Y%m%d
    log_es_400_reason true

    <buffer>
      @type file
      flush_mode interval
      flush_interval 10s
      path /var/log/td-agent/elasticsearch
      chunk_limit_size 512m
      flush_at_shutdown true
    </buffer>
  </store>

  <store>
    @type s3
    aws_key_id "#{ENV['AWS_ACCESS_KEY_ID']}"
    aws_sec_key "#{ENV['AWS_SECRET_ACCESS_KEY']}"
    s3_bucket log-bucket
    s3_region "#{ENV['AWS_REGION']}"
    path logs
    time_slice_format %Y%m%d_%H%M%S
    s3_object_key_format %{path}%{time_slice}.%{file_extension}
    check_object false

    <buffer time>
      @type file
      flush_mode lazy
      path /var/log/td-agent/s3/logs.*
      timekey 1d # 1 day
      timekey_wait 2m
      chunk_limit_size 5096m
      flush_at_shutdown true
    </buffer>
  </store>
</match>
```
#### `@type copy`
Elasticsearch, S3の2つにログを保存するため、copyプラグインを使うことで送信先を複数指定できる

#### `scheme https`, `ca_file path/to/ca_file.crt`
HTTPSでElasticsearchと通信するための設定
ElasticSearch7.x系と8.5.0でインストール手順のリファレンス内容が結構変わってたので、エラー出たら公式参照

https://www.elastic.co/guide/en/elasticsearch/reference/8.5/docker.html

https://github.com/uken/fluent-plugin-elasticsearch

#### `logstash_format true` 〜 `logstash_dateformat %Y%m%d`
このあたりはElasticsearchのインデックス名のフォーマットの設定
最終的に、`fluentd_20221201`のようなフォーマットになる

#### `include_timestamp false`
filterセクションにて、パースしたログを元に`@timestamp`のフィールドを設定しているため、`false`にして上書きされないようにしている
ログのタイムスタンプはアプリケーション側で付与された時刻にしたいので、Fluentd側では付与しないこと
アプリケーションからFluentdにログを送信する際に何らかの事情で遅延した場合、Fluentd側でタイムスタンプを振っていると、アプリケーションがログを出力した時刻とズレが発生してしまう

#### `log_es_400_reason true`
`false`だとElasticSearchでエラーが発生してもログに出力されない場合があるので`true`

#### `<buffer>`, `<buffer time>`
bufferプラグインでログの書き込みを即時に行うのではなく、一時的にファイルに溜め込んでからまとめて書き込むようにし負荷を低減している
メモリに書き込むようにもできるが、耐久性の観点から基本的にはファイルが公式から推奨されている
> For the usual workload, the file buffer plugin is recommended. It is more durable for the general use-cases.

https://docs.fluentd.org/configuration/buffer-section

`<buffer>`の引数に何も指定しなければ、すべてのイベントが一つのバッファファイルに書き込まれる
S3の方は`time`を指定しているので、`timekey`で指定した単位でバッファファイルが分けられる
いずれの場合でも、書き込み後はバッファファイルは自動的に削除される

#### `flush_mode lazy`, `timekey 1d`, `timekey_wait 2m`
1日単位でバッファファイルを分けて送信先へ書き込み
0時の時点で即座には書き込まず、2分待ってから書き込み（デフォルトだと10分）
1日単位でログローテーションしているイメージ
`<buffer>`の引数に`time`を指定すれば、`flush_mode`はデフォルトで`lazy`になる

#### `flush_mode interval`, `flush_interval 10s`
10秒間隔でバッファから送信先へ書き込み
`<buffer>`の引数に何も指定しなければ、`flush_mode`はデフォルトで`interval`になる

#### `chunk_limit_size`
チャンクというイベントの集合ごとにバッファファイルが作られるので、その最大サイズの指定
このサイズを超えた量のイベントが書き込まれると、バッファオーバーフローのエラーが発生する
その際は後述の`<label @ERROR>`にイベントが送信される

#### `flush_at_shutdown true`
ファイルにバッファリングしているとはいえ、コンテナが消えたり再作成されるとそのファイル自体が消えてしまうため、シャットダウン時にバッファの内容を送信するようにしている

#### `path logs`
パスの指定には`/`をつけないこと

#### `time_slice_format %Y%m%d_%H%M%S`
オブジェクト名の日付日時フォーマット
`20221120_0123`の形式

#### `s3_object_key_format`
S3に保存されるパス、オブジェクト名の指定
`path logs`, `time_slice_format %Y%m%d`を指定しているため、以下のように展開される
`%{path}/%{time_slice}.%{file_extension}`
↓
`logs/20221201_0123.gz`
#### `check_object false`
オブジェクト名を設定するときに、S3のバケットに現在存在しているオブジェクト一覧をAPI経由で取得しないように指定
S3のオブジェクト一覧の取得にはわずかながら課金が発生するので、`false`推奨
デフォルトでは`true`になっているが、なぜこんなことをしているかと言うと、オブジェクト名が重複しないように日付の後ろに連番を付しているから
そのため、`false`にする場合は、オブジェクト名が一意になるように`s3_object_key_format`を設定しないといけない
今回は連番の代わりに秒までのタイムスタンプを付しているが、不安ならチャンクIDを付す等がよいと思われる

https://github.com/fluent/fluent-plugin-s3/blob/master/lib/fluent/plugin/out_s3.rb

プラグインのソースでは`time_slice`, `hms_slice`を組み合わせて秒までのタイムスタンプを取っているが、`time_slice`はJST、`hms_slice`はUTCになる現象が発生したので、`time_slice_format`で秒まで指定した方がいらぬバグを踏まずにいいかと

# Fluentd自体のログ
```
<label @FLUENT_LOG>
  <match **>
    @type stdout
  </match>
</label>
```

#### `<label @FLUENT_LOG>`
`@FLUENT_LOG`はFluentd自体のログに付されるラベル
とりあえず標準出力に出すだけ

`fluent.**`のようなtagではなく、labelで制御するのが推奨されている
上記のようなタグで制御しているとワーニングが出る

https://docs.fluentd.org/deployment/logging

# エラーイベントの制御
```
<label @ERROR>
  <match **>
    @type s3
    aws_key_id "#{ENV['AWS_ACCESS_KEY_ID']}"
    aws_sec_key "#{ENV['AWS_SECRET_ACCESS_KEY']}"
    s3_bucket log-bucket
    s3_region "#{ENV['AWS_REGION']}"
    path logging-errors
    time_slice_format %Y%m%d_%H%M%S
    s3_object_key_format %{path}/%{time_slice}.%{file_extension}
    check_object false

    <buffer>
      @type file
      path /var/log/td-agent/s3/logging-errors.*
      timekey 1d # 1 day
      timekey_wait 2m
      chunk_limit_size 2048m
      flush_at_shutdown true
    </buffer>
  </match>
</label>
```

#### `<label @ERROR>`
Fluentdで発生したエラーイベントに付されるラベル
パースエラーや、バッファオーバーフローのようなエラーが起こった際に元のイベントの情報に`@ERROR`のラベルが付される
とりあえずエラーログ専用のバッファとバケットを用意してS3に格納する形（設定はほぼ同じ）
ここでエラー処理を行った後、`relabel`して再エミットするなりどこかしらに保存するなりすればエラーが起きてもログが欠損することはない
エラーの際は、`@FLUENT_LOG`にどんなエラーが起こったかというデバッグ情報が渡ってきて、`@ERROR`にはエラーが起こった元のイベントがそのまま渡ってくる

`@FLUENT_LOG` (エラーのデバッグ情報）
```
2022-11-21 16:53:17.780796000 +0900 fluent.warn: {"error":"#<Fluent::Plugin::Parser::ParserError: pattern not matched with data 'StandardError'>","location":null,"tag":"rails","time":1669017197,"message":"send an error event to @ERROR: error_class=Fluent::Plugin::Parser::ParserError error=\"pattern not matched with data 'StandardError'\" location=nil tag=\"rails\" time=1669017197"}
```

`@ERROR` (エラー元イベントそのまま）
```
`2022-11-21T16:53:17+09:00 rails {"container_id":"e62322ce694ee13ed9a74a664ff622805f51b81eb970dea6f7550a6a41f12bf9","container_name":"/rails","source":"stdout","log":"StandardError"}`
```

# さいごに
Fluentdはバージョンによって結構仕様が変わるので、この記事も遅かれ早かれ陳腐化します
公式ドキュメントを参考にするのが結局一番早い場合があるので、まずはそちらを確認しましょう

# 設定内容全体

``` fluent.conf
<source>
  @type  forward
  port  24224
</source>

<filter **>
  @type grep
  <exclude>
    key log
    pattern /^$/
  </exclude>
</filter>

<filter rails>
  @type parser
  key_name log
  reserve_data true
  <parse>
    @type regexp
    expression /^\[(?<@timestamp>[^\]]*)\] \[(?<log_level>[^\]]*)\] \[(?<request_id>[^\]]*)\] (?<log>.*)$/
    time_format %Y-%m-%d %H:%M:%S.%L%z
  </parse>
</filter>

<filter nginx>
  @type parser
  key_name log
  reserve_data true
  <parse>
    @type nginx
  </parse>
</filter>

<match **>
  @type copy

  <store>
    @type elasticsearch
    host elasticsearch
    port 9200
    user "#{ENV['ELASTIC_USER']}"
    password "#{ENV['ELASTIC_PASSWORD']}"
    scheme https
    ca_file path/to/ca_file.crt
    logstash_format true
    include_timestamp false
    logstash_prefix fluentd
    logstash_prefix_separator _
    logstash_dateformat %Y%m%d
    log_es_400_reason true

    <buffer>
      @type file
      flush_mode interval
      flush_interval 10s
      path /var/log/td-agent/elasticsearch
      chunk_limit_size 512m
      flush_at_shutdown true
    </buffer>
  </store>

  <store>
    @type s3
    aws_key_id "#{ENV['AWS_ACCESS_KEY_ID']}"
    aws_sec_key "#{ENV['AWS_SECRET_ACCESS_KEY']}"
    s3_bucket log-bucket
    s3_region "#{ENV['AWS_REGION']}"
    path logs
    time_slice_format %Y%m%d_%H%M%S
    s3_object_key_format %{path}%{time_slice}.%{file_extension}
    check_object false

    <buffer time>
      @type file
      flush_mode lazy
      path /var/log/td-agent/s3/logs.*
      timekey 1d # 1 day
      timekey_wait 2m
      chunk_limit_size 5096m
      flush_at_shutdown true
    </buffer>
  </store>
</match>

<label @FLUENT_LOG>
  <match **>
    @type stdout
  </match>
</label>

<label @ERROR>
  <match **>
    @type s3
    aws_key_id "#{ENV['AWS_ACCESS_KEY_ID']}"
    aws_sec_key "#{ENV['AWS_SECRET_ACCESS_KEY']}"
    s3_bucket log-bucket
    s3_region "#{ENV['AWS_REGION']}"
    path logging-errors
    time_slice_format %Y%m%d_%H%M%S
    s3_object_key_format %{path}/%{time_slice}.%{file_extension}
    check_object false

    <buffer>
      @type file
      path /var/log/td-agent/s3/logging-errors.*
      timekey 1d # 1 day
      timekey_wait 2m
      chunk_limit_size 2048m
      flush_at_shutdown true
    </buffer>
  </match>
</label>
```