業務でロギング機構を作ったのですが、しばらく経ったら設定内容の意味を忘れることが目に見えているので先にまとめておきます。
よければ参考にしてください。
パートごとに抜粋しているので、設定ファイル全体は記事の一番最後を見てください。
ログの取得
<source>
@type forward
port 24224
</source>
forward
TCP経由でログ情報を受け取るinputプラグイン
ついでにハートビートによる死活監視も行っているらしい
ログの整形
<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
フィールドは上書き
Elasticsearch, S3への送信
<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でインストール手順のリファレンス内容が結構変わってたので、エラー出たら公式参照
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.
<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を付す等がよいと思われる
プラグインのソースでは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で制御するのが推奨されている
上記のようなタグで制御しているとワーニングが出る
エラーイベントの制御
<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はバージョンによって結構仕様が変わるので、この記事も遅かれ早かれ陳腐化します
公式ドキュメントを参考にするのが結局一番早い場合があるので、まずはそちらを確認しましょう
設定内容全体
<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>