Fluentdのレコードが全てString型になるアレな挙動を改善するpull-reqを出した話
Apache/Nginxのアクセスログやローカルファイルから、Fluentdのin_tail機能を使ってログを収集しているケースはあると思います。この時、元々は123
といった数値や123.45
といったfloat型だったものが、全てString型になっていること、ご存じでしょうか。
それをそのままTreasureData(Hive)やmongoDBなどで数値比較を行いたい時には、正規表現での比較を行うという奇妙な対処が必要です。
※ もちろんJSON形式でファイルに書き出したり、直接Fluentdに転送している場合には問題になりませんが、それの話は棚に上げます。
そういった時の対処方法は用意されており、fluent-plugin-typecast
やfluent-plugin-mongo-typed
を使うことで、カラムを指定した上で、明示的に型変換を行えます。
しかしこれが面倒なのです。構造化ログフォーマットなのに、カラムの構造が変わる度に追従する必要があり、スキーマレスのメリットがデメリットになってしまっています。
という訳で、欲しかったint型とfloat型の自動変換オプションをparser.rb
へ追加しました。
その他のarray, bool, timeへの型変換が必要であれば、既存のプラグインに任せる考えです。
差分コード
該当のプルリクエストから確認できます。
add auto_type_convert option for parser.rb by y-ken · Pull Request #151 · fluent/fluentd
https://github.com/fluent/fluentd/pull/151
利用例
このプルリクエストが取り込まれたら、次のようにauto_type_convert yes
というオプションを渡すことで、自動変換を有効化できます。
<source> type tail format ltsv time_format %d/%b/%Y:%H:%M:%S %z path /var/log/httpd/access_log tag debug.apache.access auto_type_convert yes </source>
適用前/適用後のアクセスログは、status, size, response_timeに違いを確認できます。
-debug.apache.access {"domain":"127.0.0.1","host":"127.0.0.1","server":"127.0.0.1","ident":"-","user":"-","method":"GET","path":"/","protocol":"HTTP/1.1","status":"404","size":"198","referer":"-","agent":"Mozilla","response_time":"398","cookie":"-","set_cookie":"-"} +debug.apache.access {"domain":"127.0.0.1","host":"127.0.0.1","server":"127.0.0.1","ident":"-","user":"-","method":"GET","path":"/","protocol":"HTTP/1.1","status":404,"size":198,"referer":"-","agent":"Mozilla","response_time":398,"cookie":"-","set_cookie":"-"}
型変換のパフォーマンス
以下の実装はベンチマークを行い、最速となった前者の型変換の実装を利用しています。
この型変換については別ブログ記事にてお話ししようと思います。
より速い実装があるならば知りたいので、是非教えて下さい。
# usage irb(main):001:0> require './convert.rb' => true irb(main):002:0> record = {"int" => "123", "float" => "12.34", "str" => "13498734hoge", "msg" => "/path/to"} => {"int"=>"123", "float"=>"12.34", "str"=>"13498734hoge", "msg"=>"/path/to"} irb(main):003:0> convert_type(record) => {"int"=>123, "float"=>12.34, "str"=>"13498734hoge", "msg"=>"/path/to"}
def convert_type(record) record.each do |key,value| if value == (int_value = value.to_i).to_s record[key] = int_value elsif value == (float_value = value.to_f).to_s record[key] = float_value end end return record end
def convert_type(record) record.each do |key,value| if (int_value = Integer(value) rescue false) record[key] = int_value elsif (float_value = Float(value) rescue false) record[key] = float_value end end return record end
追記:ベンチマークの取り方を間違えており、結果が異なっていたため差し替えております。
あとがき
ここからはフィクションですが、これが取り込まれたら、ログ出力形式が知らぬうちに変わって、変更するまでの期間のログが解析できないという洒落にならないトラブルにも遭遇しなくなりますね!
取りこぼし期間を特定してログの再取り込みをしたり、ログフォーマット変更に目を光らせる必要も無くなります。むしろ何もしなくて良い。
そう、プログラマは怠惰であるべきなのです。(`・ω・´)キリッ