windyakinってなんて読む

うぃんぢゃきんではない

シンプルに Fluentd にログ転送ができる RubyGem "awesome_fluent_logger" をつくった

f:id:windyakin:20210124141456p:plain

Rails など Ruby のログ出力を Fluentd に転送するための RubyGem "awesome_fluent_logger" をつくって公開したので、それについて紹介する。

余談だが、 Awesome というのは完全な主観でしかないので我ながらなかなかひどい名前だと思うのだが、それに代わるいい名前も思いつかなかったという経緯があり、このような名前になったことを言い訳しておく。

この RubyGem の目的

ログを Fluentd に転送するための RubyGem は有名所に act-fluent-logger-rails というものがあるのだが、その名前に "rails" というワードが入っている通り、実装コードそのものがかなり Rails ありきになっているため、 Rails への導入は容易という点で優れているのだが、いくつか困る点もあった。若干蛇足になるが具体的に困った例を挙げよう。

  • コードが Rails に強く依存しているために Rails のアップデートの度にこの RubyGem の更新も足並みをあわせる必要がある
  • 少し難しいことをさせようとすると小回りが効きづらいという問題があった
    • Lograge などをつかってアクセスログも同時に出力する際に logger の使いまわしがしづらい
    • ログをファイルにも同時に出力したかったが ActiveSupport::Logger.broadcast への受け渡しがうまくできなかった

なので今回つくったこの RubyGem は以下の利点をもたせることに重点を置いた。

  • できるだけ Ruby 標準ライブラリである Logger クラスの実装に寄り添ったものにする
    • 継承はするがあくまで機能追加ではなく Fluentd の事情を回収する実装のみにする
  • pure-Ruby なプログラムでも使えるほどシンプルにする
  • Fluentd の tag を活用して Fluentd 側でログを捌きやすくする
    • できる限り多様なログも転送できるようにする

具体的な使い方

使い方は至極シンプルで、以下が最小限の動作コードになる。

require 'awesome_fluent_logger'
logger = AwesomeFluentLogger.new(fluent: { host: 'localhost', port: 24224 })
logger.info('information logging')

すると Fluentd 側は以下のように出力が行われる。

2021-01-23 13:28:46.000000000 +0000 info: {"severity":"INFO","time":"2021-01-23 13:28:46.336397 +0000","progname":null,"message":"information logging"}

この Logger を Rails で使いたい場合には、以下のような設定ファイルなどから config.logger に AwesomeFluentLogger のインスタンスを差し込めばよい。

  • config/application.rb
  • config/environments/{RAILS_ENV}.rb

またこのとき ActiveSupport::TaggedLogging との併用も可能である。

logger = AwesomeFluentLogger.new(fluent: { host: 'localhost', port: 24224 })
config.logger = ActiveSupport::TaggedLogging.new(logger)

改めて言うまでもないが、もし 12-Factor に則りたい場合は接続先情報である host や port の受け渡しに環境変数を用いるとよいだろう。

初期化引数の fluent について

初期化引数の fluent で渡しているパラメータはほぼそのまま Fluent::Logger::FluentLogger へ丸渡しされるようになっている。

なので UNIX Socket でログを転送していてもそのまま指定することができる。

logger = AwesomeFluentLogger.new(fluent: { socket_path: '/tmp/fluent.sock' })

また、なんらかの事情により AwesomeFluentLogger を2箇所以上で呼び出す場合、コネクション数を減らすために他所でインスタンス化した Fluent::Logger::FluentLogger クラスをそのまま渡すこともできる。

fluent_instance = Fluent::Logger::FluentLogger.new(nil, host: 'localhost', port: 24224)
logger = AwesomeFluentLogger.new(fluent: fluent_instance)

また AwesomeFluentLogger#logger から Fluent::Logger::FluentLogger のインスタンスを取り出すこともできたりと、とにかく融通が効くようになっている。

タグの扱いについて

Fluentd 側での出力の際、先頭に表示されるタグの部分が info となっているのは、何も指定されていない場合にログレベル(Serverity)がタグとして入るようにしているからである。

例えばいくつかのログを1つの Fluentd で捌くために、タグを使って内部で振り分けをしたい場合がある。そんなとき初期化時のパラメータである fluenttag_prefix を指定することで、その値をタグの先頭に付けることができる。

logger = AwesomeFluentLogger.new(fluent: { tag_prefix: 'kanan', host: 'localhost', port: 24224 })
logger.info('ご機嫌いかがかなん?')
2021-01-23 13:28:46.000000000 +0000 kanan.info: {"severity":"INFO", ...

さらに Ruby 標準ライブラリである Logger クラス標準の progname を使うことで、 Rails の標準ログと Lograge のアクセスログといった二種類のログの出し分けも容易にできる。

rails_logger = AwesomeFluentLogger.new(
  fluent: { tag_prefix: 'application', host: 'localhost', port: 24224 },
  progname: 'rails'
)
config.logger = ActiveSupport::TaggedLogging.new(rails_logger)

:

config.lograge.logger = AwesomeFluentLogger.new(
  fluent: config.logger.logger,
  progname: 'request'
)
2021-01-23 13:28:46.000000000 +0000 application.rails.info: {"severity":"INFO", ...
2021-01-23 13:28:48.000000000 +0000 application.request.info: {"severity":"INFO", ...

このようにタグ欄を正しく活用できることで Fluentd 側の設定である filter を書くときに重宝する。

既存の Formatter の使いまわしもできる

標準の Fluent::Logger::FluentLogger の使いづらさはログとして渡されるものは必ず Hash でなければならないという点で、もしすでに Formatter 側でJSONの文字列に変換してしまっているような場合にこれが足かせになってしまう。特にオリジナルの実装が入った Formatter がすでにある場合はその載せ替えが大変なのだが、 AwesomeFluentLogger であれば文字列が来た場合に一旦 Hash に仮詰めしてくれるようになっている。

logger = AwesomeFluentLogger.new(fluent: { host: 'localhost', port: 24224 })
logger.formatter = Logger::Formatter.new
logger.info('hogehoge')
2021-01-24 04:51:07.000000000 +0000 info: {"data":"I, [2021-01-24T04:53:09.379625 #209]  INFO -- : hogehoge\n"}

上記の例は極端な例として Logger::Formatter を使っているので少し無理があるものの、例えばこれが JSON の文字列だった場合などは Fluentd の perser filter を使えばパースして展開させることが可能になるはずだ。

長くなってしまったが

README にも同等のことを書いたつもりなのですが、正直英語で書くと思った文章がかけてるか不安になり、日本語で書けるこちらのエントリーのほうが充実してしまった。

まだ出来たばっかりなので、あまり多くのパターンを試してないのですが、動かないパターンの報告やよりよくできるポイントがあれば Issue ・ Pull Request などお待ちしております。