はじめまして、インフラエンジニアの末廣です。
この記事は TECHSCORE Advent Calendar 2014、15日目の記事です。
fluentd は今や広く使われているログコレクターであり、もはや説明の必要もないでしょう。
弊社でもログ集約・ログ解析の前処理・イベント検知などに利用しています。
この記事では fluentd (td-agent 2.1.2, fluentd-0.10.57) による生ログ集約を題材に、Parser plugin や Formatter plugin によるカスタマイズについて説明します。
※ 12月14日に v0.12 がリリースされて Parser plugin と Formatter plugin の新 API が利用できるようになったので、最後に新 API についても触れます。
生ログの転送
かつて、素の fluentd ではログを生のまま転送することはできませんでした。
当時は、fluent-agent-lite と file-alternative plugin を組み合わせたり、生ログをそのまま扱うプラグインを自作したりしていました。
v0.10.39 で in_tail などの format パラメータに none が追加されました。これにより、ログの行をそのまま "message" key の値として取り込むことができるようになりました。また、v0.10.50 で out_file などの出力 formatter に SingleValue が使えるようになり、特定の key に対応する値のみを出力できるようになりました。これらを組み合わせると、素の fluentd で生ログを転送することができます。
たとえば、 次のように in_tail と out_file を設定すると Apache の access_log を生ログのまま転送することができます。
1 2 3 4 5 6 7 |
<source> type tail format none path /var/log/httpd/access_log pos_file /var/log/td-agent/access_log tag apache.access </source> |
1 2 3 4 5 |
<match apache.access> type file format SingleValue path /var/log/access_log </match> |
複数ホストの生ログの集約
複数のホストからなるサービスでは、複数ホストのログを1つのファイルに集約したくなることがあります。このとき、ログの各行の先頭にホスト名を入れたくなるのですが、素の fluentd ではできません。そこで、v0.10.46 で導入された Parser plugin と v0.10.49 導入された Formatter plugin を利用します。カスタムの Parser plugin で生ログとホスト名をレコードに挿入し、カスタムの Formatter plugin で生ログの先頭にホスト名を付与して出力します。
Parser Plugin
Parser plugin は TextParser クラスの内部クラスとして実装します。
下記の NoneWithHostnameParser は、生ログとホスト名(デフォルトは hostname -s で得られる値)をレコードに格納する Parser です。NoneParser をお手本にして、ホスト名をレコードに挿入するコードを追加しています。register_template で この Parser を none_with_hostname として登録しています。
/etc/td-agent/plugin/parser_none_with_hostname.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
module Fluent class TextParser class NoneWithHostnameParser include Configurable config_param :message_key, :string, :default => 'message' config_param :hostname, :string, :default => `hostname -s`.chomp attr_accessor :estimate_current_event def initialize super @estimate_current_event = true end def configure(conf) super end def call(text) record = {} record[@message_key] = text record['hostname'] = @hostname time = @estimate_current_event ? Engine.now : nil if block_given? yield time, record else return time, record end end end register_template("none_with_hostname", Proc.new { NoneWithHostnameParser.new }) end end |
in_tail の設定で format に none_with_hostname を指定すると、生ログとホスト名がレコードに格納されます。
1 2 3 4 5 6 7 |
<source> type tail format none_with_hostname path /var/log/httpd/access_log pos_file /var/log/td-agent/access_log tag apache.access </source> |
Formatter Plugin
Formatter plugin は TextFormatter モジュールの内部クラスとして実装します。
下記の HostnameAndMessageFormatter は、SingleValueFormatter とほぼ同じですが、出力行の先頭にホスト名を挿入する Formatter です。register_template で この Formatter を hostname_and_message として登録しています。
/etc/td-agent/plugin/formatter_hostname_and_message.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
module Fluent module TextFormatter class HostnameAndMessageFormatter include Configurable config_param :message_key, :string, :default =>; 'message' config_param :add_newline, :bool, :default =>; true def configure(conf) super end def format(tag, time, record) text = "#{record['hostname']} #{record[@message_key].to_s}" text << "\n" if @add_newline end end register_template("hostname_and_message", Proc.new { HostnameAndMessageFormatter.new }) end end |
out_file の設定で format に hostname_and_message を指定するとログの各行の先頭にホスト名がつくようになります。
1 2 3 4 5 |
<match apache.access> type file format hostname_and_message path /var/log/access_log </match> |
これらの plugin を組み合わせることにより、複数ホストの生ログをいい感じに集約しています。
おまけ:fluentd v0.12
ここまで書いたところで、12月14日に v0.12 がリリースされたことに気づきました。このリリースで Parser および Formatter plugin に新しい API が追加され、plugin をより簡潔に記述できるようになりました。v0.10.58 でも同様の記述ができるようになるようです。
新API の変更点は次の通りです。
- ベースとなる Parser および Formatter クラスが定義されたので、これらを継承する形で plugin を実装する。
- plugin の登録は、他の plugin と同様に Plugin.register_parser および Plugin.register_formatter で行えるようになった。
- Parser の実際の処理を実装するメソッドが call から parse に変更された。
新 API を利用すると上記の plugin はそれぞれ次のように記述できます。
/etc/td-agent/plugin/parser_none_with_hostname.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
module Fluent class TextParser class NoneWithHostnameParser < Parser Plugin.register_parser('none_with_hostname', self) config_param :message_key, :string, :default => 'message' config_param :hostname, :string, :default => `hostname -s`.chomp def configure(conf) super end def parse(text) record = {} record[@message_key] = text record['hostname'] = @hostname time = @estimate_current_event ? Engine.now : nil if block_given? yield time, record else return time, record end end end end end |
/etc/td-agent/plugin/formatter_hostname_and_message.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
module Fluent module TextFormatter class HostnameAndMessageFormatter > Formatter Plugin.register_formatter('hostname_and_message', self) config_param :message_key, :string, :default => 'message' config_param :add_newline, :bool, :default => true def configure(conf) super end def format(tag, time, record) text = "#{record['hostname']} #{record[@message_key].to_s}" text << "\n" if @add_newline end end end end |
以前と比べると分かりやすくなりましたね。