DevNullParserで始めるFluentd パーサプラグイン入門

#whoami

Fluentd Advent Calendar三日目は、Fluentdコミッタの[@kiyototamura](https://twitter.com/kiyototamura)がお届けします。トレジャーデータという会社で、日本国外でのFluentdの啓蒙活動を主にやっております。おそらく日本ではもっとも知名度の低いコミッタです。

追記：[@repeatedly先生のフィードバック](https://twitter.com/repeatedly/status/539838427428966400)を受けて、コードがシンプルになりました。

2014/01/07 更新: v0.10.58、v0.12.0以降で新APIが生えた[[1]](https://github.com/fluent/fluentd/pull/499)[[2]](https://github.com/fluent/fluentd/pull/497)のでそちらを使うようにサンプルコードを更新

#で、パーサプラグインって何よ

一言で言うと、インプットプラグインの`format`パラメータを拡張できる機能です。インプットに入ってくるデータは必ずしも構造化されていないため、ログをJSONとして持つ設計のFluentdでは、インプットプラグインがパースをする必要があります。この役割を担うのがパーサプラグインです。プラグイン化されたのはv0.10.46ですが、内部実装的には、最初から存在していて、例えばtailプラグインで

```
<source>
  type tail
  format json
  path /path/to/file
  tag  aoi.yu
</source>
```

と書くとJSONパーサが走りますし、tcpプラグインで

```
<source>
  type tcp
  format none
  tag akai.yu
</source>
```

と書くとNoneパーサ（まったくパースせず、一行そのまま"message"というキーにぶち込むパーサです）が走ります。

これらの実装に関しては`/lib/fluent/parser.rb`を読むと幸せになれるかもしれません。

ちなみにデフォルトでは以下のパーサが入っています：

1. `format apache_error`: Apacheエラーログパーサ
2. `format apache2`: Apache2アクセスログパーサ。`apache`ってのもあるが違いがわからず…
3. `format syslog`: いわゆるsyslogをよしなにパース。
4. `format json`: インプットをそのままJSONとみなしてパース。
5. `format tsv`: いわゆるtsv。keysを指定することでJSONに変換。
6. `format ltsv`:　みんな大好きltsv。
7. `format csv`: みんな大嫌いcsv。やはりkeysを指定。
8. `format nginx`: nginxのアクセスログをパース。
9. `format none`: 何もパースしないでインプットをそのままmessageフィールドにぶっこむ。`message_key`パラメータで他のフィールドに入れられる。
10. `format multiline`: 複数行のログをパースするのに便利。（追記：[in_tail専用です](https://twitter.com/repeatedly/status/539838001442848768))

そしてもちろん`format /^(?<foo>\w+)$/`みたいに名前キャプチャを使った正規表現を書くことで、キャプチャの名前がキーとなったJSONが吐き出されます。これは正規表現パーサと呼ばれ、apache_errorパーサやnginxパーサはこれで実装されています。

これだけデフォルトで入っていると、自分でパーサを書く必要なぞないように思えます。必要なら[Fluentular](http://fluentular.herokuapp.com)を使ってコツコツと正規表現を作ればいいし。

#しかし世の中甘くない

ところがどっこい、デフォルトのパーサではパースできないフォーマットもあります。例えばこんなやつ：

>key1=val1|key2=val2|key3=val3...

これそのものは正規文法で表現できるのですが（＝正規表現でマッチできる）、keyの数が任意数あるため、名前キャプチャを使った正規表現では書けません。

また、世の中には正規文法ではないフォーマットも存在しないとは限りません。そうなると、デフォのパーサ群ではお手上げ、ということになります。

#パーサプラグインの書き方

ということで、パーサプラグインの登場です。パーサプラグインの実装方法は以下の通りです：

1. `parser_`で始まるファイル名で、プラグインファイルを作る。
2. 以下のひな形を作る。ちなみにこのパーサーは、何もパースせずにデータを葬りさってくれるので、DevNullParserと名付けてあります。
    
    ```rb
    module Fluent
      class TextParser
        #パーサープラグインの名前
        class DevNullParser < Parser
          # このプラグインをパーサプラグインとして登録する
          Plugin.register_parser('dev_null', self)
          def initialize
            super
            # いろいろとここで初期化
          end
          def configure(conf={})
            super
            # 設定ファイルによるものはここで
          end
          def parse(text)
            # これが肝となるメソッドで、textをパースする。
            # parser.parse(text) {|time, record| ... } みたいに使う。
            yield Engine.now, {}
          end
        end
      end
    end
    ```
    
3. Fluentdのpluginディレクトリの中に`parser_dev_null.rb`（と名付けました）を配置。
4. `format`パラメータのあるinputプラグインで使う。例えばこんな風に
    
    ```
    <source>
      type tcp
      format dev_null
      port 13337
      tag nothing
    </source>
    <match nothing>
      type stdout
    </match>
    ```

実際に使ってみましょう。私の場合はこんな感じです。

```
vagrant@precise64:~$ cat t.conf
<source>
  type tcp
  format dev_null
  port 13337
  tag nothing
</source>
<match nothing>
  type stdout
</match>
vagrant@precise64:~$ ls my_plugins/
parser_dev_null.rb
vagrant@precise64:~$ fluentd -c t.conf -p my_plugins/
2014-12-01 02:46:36 +0000 [info]: starting fluentd-0.10.57
2014-12-01 02:46:36 +0000 [info]: reading config file path="t.conf"
2014-12-01 02:46:37 +0000 [info]: gem 'fluent-plugin-record-reformer' version '0.4.0'
2014-12-01 02:46:37 +0000 [info]: gem 'fluentd' version '0.10.57'
2014-12-01 02:46:37 +0000 [info]: using configuration file: <ROOT>
  <source>
    type tcp
    format dev_null
    port 13337
    tag nothing
  </source>
  <match nothing>
    type stdout
  </match>
</ROOT>
2014-12-01 02:46:37 +0000 [info]: adding source type="tcp"
2014-12-01 02:46:37 +0000 [info]: adding match pattern="nothing" type="stdout"
```

そして実際にデータを送ってみると…

```
vagrant@precise64:~$ echo 'this is a loooooooooooooooooooooooooooooooooooong message' | nc localhost 13337
```

な、なんと！

```
2014-12-01 02:46:43 +0000 nothing: {}
```

きちんと全く何もパースせず空のイベントを吐き出してきました。これぞdevnullismの鑑。ちなみにもっとまともな例は、[こちらの記事をご参考に。](http://qiita.com/toru-takahashi/items/b475aa4747e0e360f454)

#こんなメタなこともできるよ：Parse Me Maybe

inputプラグインに流れ込んでくる文字列をパースするためにあるパーサプラグインですが、メタな挙動をさせることもできます。その一例として、MaybeParserを実装してみたいと思います。

いわゆる非構造テキストログをFluentdで集める際ににありがちな話として、

1. よしなに正規表現を書いて各行をパースしたつもりが
2. あとでログ見て、予想だにしない行が結構あってFluentdがエラー投げてた

なんてことがあります。エラー処理をきちんとしていないとログの取りこぼしなどもしてしまうことも。

MaybeParserはこれを解決します。挙動としては、

1. 既存パーサでパースできなかったら
2. データをパースせずに`format none`と同じ挙動で吐き出す。

というものです。

では実装を見てみましょう。MaybeParserという名前は、某モナドの影響を受けています。

```rb
module Fluent
  class TextParser
    #パーサープラグインの名前
    class MaybeParser < Parser
      Plugin.register_parser('maybe', self)

      config_param :parser, :string
      # フォールバックするNoneParserでのキーの名前。
      config_param :fallback_message_field_key, :string, :default => nil

      def initialize
        super
        # いろいろとここで初期化
      end

      def configure(conf={})
        super
        conf['format'] = conf.delete('parser') # parserフィールドがformatの値なので、書き換えてパーサを初期化
        @parser = TextParser.new
        @parser.configure(conf)
        @fallback_parser = NoneParser.new
        @fallback_parser.configure("message_key" => @fallback_message_field_key)
      end

      def parse(text, &block)
        # パースに失敗したらfallback_parserを呼ぶだけ
        @parser.parse(text) do |time, record|
          if time.nil? or record.nil?
            @fallback_parser.call(text, &block)
          else
            yield time, record
          end
        end
      end
    end
  end
end
```

さて、これを実行するとこんな感じになります。

```
vagrant@precise64:~$ cat t.conf
<source>
  type tcp
  format maybe
  parser json
  port 13337
  tag something
</source>

<match something>
  type stdout
</match>
vagrant@precise64:~$ ls my_plugins/
parser_devnull.rb  parser_maybe.rb
vagrant@precise64:~$ fluentd -c t.conf -p my_plugins/
2014-12-01 03:30:59 +0000 [info]: starting fluentd-0.10.57
2014-12-01 03:30:59 +0000 [info]: reading config file path="t.conf"
2014-12-01 03:30:59 +0000 [info]: gem 'fluent-plugin-record-reformer' version '0.4.0'
2014-12-01 03:30:59 +0000 [info]: gem 'fluentd' version '0.10.57'
2014-12-01 03:30:59 +0000 [info]: using configuration file: <ROOT>
  <source>
    type tcp
    format maybe
    parser json
    port 13337
    tag something
  </source>
  <match something>
    type stdout
  </match>
</ROOT>
2014-12-01 03:30:59 +0000 [info]: adding source type="tcp"
2014-12-01 03:30:59 +0000 [info]: adding match pattern="something" type="stdout"
```

まず普通のJSONを送ると…

```
vagrant@precise64:~$ echo '{"hello":"world"}' | nc localhost 13337
```

以下の様に、JSONParserで、ちゃんとパースされました。

```
2014-12-01 03:31:44 +0000 something: {"hello":"world"}
```

今度はJSONじゃないものを送ると…

```
vagrant@precise64:~$ echo 'this is not JSON' | nc localhost 13337
```

NoneParserで処理されました！

```
2014-12-01 03:33:49 +0000 something: {"message":"this is not JSON"}
```

ここでは実装のシンプルさからNoneParserをデフォルトとしましたが、もう少しコードを書けば、任意のパーサにフォールバック…みたいなこともできます。

#最後に宣伝

今年は海外でもFluentdの認知度があがり、[Googleさんの各種クラウドサービスにも導入され](http://qiita.com/kazunori279/items/8554cb8eccef05d2d4e5)、それ相応に流行ってきたかなという感じです。2015年はさらにFluentdをデファクトスタンダードにしていきたいと考えていますので、世界中を飛び回ってFluentdを流行らせる仕事をやってみたい方、Fluentdと関連プロジェクトをどっぷり開発したいという方は、[一言ご連絡](mailto:kiyoto@treasuredata.com)ください。