Y-Ken Studio

新しもの好きのデータエンジニアが四方山話をお届けします。

Fluentdのレコードが全てString型になるアレな挙動を改善するpull-reqを出した話

Apache/Nginxのアクセスログやローカルファイルから、Fluentdのin_tail機能を使ってログを収集しているケースはあると思います。この時、元々は123といった数値や123.45といったfloat型だったものが、全てString型になっていること、ご存じでしょうか。
それをそのままTreasureData(Hive)やmongoDBなどで数値比較を行いたい時には、正規表現での比較を行うという奇妙な対処が必要です。

※ もちろんJSON形式でファイルに書き出したり、直接Fluentdに転送している場合には問題になりませんが、それの話は棚に上げます。

そういった時の対処方法は用意されており、fluent-plugin-typecastfluent-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

追記:ベンチマークの取り方を間違えており、結果が異なっていたため差し替えております。

あとがき

ここからはフィクションですが、これが取り込まれたら、ログ出力形式が知らぬうちに変わって、変更するまでの期間のログが解析できないという洒落にならないトラブルにも遭遇しなくなりますね!
取りこぼし期間を特定してログの再取り込みをしたり、ログフォーマット変更に目を光らせる必要も無くなります。むしろ何もしなくて良い。

そう、プログラマは怠惰であるべきなのです。(`・ω・´)キリッ