fluent-plugin-monologをrubygems.orgに公開しました


## はじめに
[monologから出力したログをfluentdで集約する](http://qiita.com/imunew/items/632616b59820272a5447)を書いていて、monolog用のparserを公開してもいいのでは、という気持ちになり、頑張って公開してみました。
https://rubygems.org/gems/fluent-plugin-monolog
公開に際して、ハマったところなどありましたので、新たに投稿しておきます。

## monologから出力されるログのフォーマット
monologのデフォルトのフォーマッターは、LineFormatterです。

```Monolog/Handler/FormattableHandlerTrait.php

trait FormattableHandlerTrait
{
	/** 中略 **/

    /**
     * Gets the default formatter.
     *
     * @return FormatterInterface
     */
    protected function getDefaultFormatter(): FormatterInterface
    {
        return new LineFormatter();
    }
}
```

LineFormatterは下記のようなログを出力します。

```txt
[2016-07-08 16:12:42] myapp.ERROR: Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException: No route found for "GET /path/to/execute": Method Not Allowed (Allow: POST) (uncaught exception) at /apps/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/EventListener/RouterListener.php line 148 {"exception":"[object] (Symfony\\Component\\HttpKernel\\Exception\\MethodNotAllowedHttpException(code: 0): No route found for \"GET /path/to/execute\": Method Not Allowed (Allow: POST) at /apps/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/EventListener/RouterListener.php:148, Symfony\\Component\\Routing\\Exception\\MethodNotAllowedException(code: 0):  at /apps/vendor/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcher.php:101)"} {"url":"/path/to/execute","ip":"192.168.xx.xx","http_method":"GET","server":"*.example.com","referrer":null,"server_ip":"192.168.xx.xx","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"}
```

まとめると、以下のようなフォーマットになります。

```txt
[{$timestamp}] {$channel}.{$level} ${message} ${context} ${extra}
```

## 正規表現
元々パースされることを考えられているフォーマットではないと思うので、なかなかパターンを絞りにくいのですが、下記の特徴があるので、何とか正規表現でパースできています。

- `context`と`extra`は、array => json_encodeされる
- PHPのjson_encodeの仕様で、空のarray()は`[]`になる（`{}`ではない）

```
/^\[(?<time>[\d\-]+ [\d\:]+)\] (?<channel>.+)\.(?<level>(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY))\: (?<message>[^\{\}]*) (?<context>(\{.+\})|(\[.*\])) (?<extra>(\{.+\})|(\[.*\]))\s*$/
```

※パースできないパターンを見つけたら、issuesください。

## fluent-plugin-monologの使い方
下記のコマンドでプラグインを追加します。

```bash
$ td-agent-gem install fluent-plugin-monolog
```

`in_tail`のformatに`monolog`を指定します。

```
<source>
  @type tail
  path /path/to/example.log
  format monolog
  pos_file /path/to/example.log.pos
  tag example.stream
</source>
```

## rubygems.orgに公開する際にハマったこと
正直、本筋ではないのですが、rubygems.orgに公開した後で、数時間ハマってしまったので、書き記しておきます。
ちなみに、私はruby初心者なので、常識的なことさえ知らない可能性がありますので、生暖かい目で見ていただけると幸いです。

### `rake test`は通るけど、`td-agent-gem install`で動かない

#### 結論、`TextParser`のnested classとしてパーサーを定義しないといけなかった
元々、[lib/fluent/plugin/parser_apache2.rb](https://github.com/fluent/fluentd/blob/master/lib/fluent/plugin/parser_apache2.rb)をコピーしてきて、ちょっと正規表現変えればいいと思っていました。
で、Apache2Parser見ると、下記のようになっているわけです。

```ruby
require 'fluent/plugin/parser'

module Fluent
  module Plugin
    class Apache2Parser < Parser
      Plugin.register_parser('apache2', self)

      REGEXP = /^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$/
      TIME_FORMAT = "%d/%b/%Y:%H:%M:%S %z"

      def initialize
        super
        @time_parser = TimeParser.new(TIME_FORMAT)
        @mutex = Mutex.new
      end
```

このコードを丸っと、持ってきて、tagomorisさんのはてブとか見てプラグイン作って、`rake test`も通るんですが、`TimeParser.new`のところでどうしてもエラーになってしまいます。

では、このTimeParserはどこで定義されているかというと、[lib/fluent/plugin/parser.rb](https://github.com/fluent/fluentd/blob/master/lib/fluent/plugin/parser.rb)に、その名前を見つけることができます。

```ruby
module Fluent
  module Plugin
    class Parser < Base
      include OwnedByMixin

      class ParserError < StandardError; end
      
      # 省略
      
      class TimeParser
        def initialize(time_format)      
```

ここでもTextParserは出てきません。
最終的に、[DevNullParserで始めるFluentd パーサプラグイン入門 - Qiita](http://qiita.com/kiyoto/items/9903718147adde4d6c32)に、掲載されていたコードを見て、真似してうまく行きました。

```ruby
module Fluent
  class TextParser
    #パーサープラグインの名前
    class DevNullParser < Parser
      # このプラグインをパーサプラグインとして登録する
      Plugin.register_parser('dev_null', self)
```

うーん、Parserプラグインを継承しているから、ネストクラスのTimeParserにもそのままアクセスできるイメージでいたのですが、正直、未だによく分かっていません。

#### 参考URL
- [fluentdのためのプラグインをイチから書く手順(bundler版) - たごもりすメモ](http://tagomoris.hatenablog.com/entry/20120221/1329815126)
- [DevNullParserで始めるFluentd パーサプラグイン入門 - Qiita](http://qiita.com/kiyoto/items/9903718147adde4d6c32)

### 一度、公開したらバージョンアップするしかない
pluginとして、インストールしたときにうまく動かなかったので、何度も`rake release`しなければいけなかったのですが、とにかくバージョンをあげていかないことには、リリースできません。
言ってみれば、セマンティック・バージョニングを強制されることになります。
セマンティック・バージョニングに抵抗はないのですが、ローカルでの再現に手間取ったのと、前述のTimeParserがnewできない問題があったので、パッチバージョンを何度も上げる結果になってしまいました。

#### 参考URL
- [セマンティック・バージョニングと、Gemfileのバージョン指定方法 - Gemfileでよく見る`~>`を使いこなす - Qiita](http://qiita.com/awakia/items/5745938c192ca1139c63)

## 最終的なコード（v0.1.5）
最終的に下記のようなコードになりました。

```ruby
require 'json'

module Fluent
  class TextParser
    class MonologParser < Parser
      Fluent::Plugin.register_parser('monolog', self)

      REGEXP = /^\[(?<time>[\d\-]+ [\d\:]+)\] (?<channel>.+)\.(?<level>(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY))\: (?<message>[^\{\}]*) (?<context>(\{.+\})|(\[.*\])) (?<extra>(\{.+\})|(\[.*\]))\s*$/
      TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

      def initialize
        super
        @time_parser = TimeParser.new(TIME_FORMAT)
        @mutex = Mutex.new
      end

      def patterns
        {'format' => REGEXP, 'time_format' => TIME_FORMAT}
      end

      def parse(text)
        m = REGEXP.match(text)
        unless m
          yield nil, nil
          return
        end

        time = m['time']
        time = @mutex.synchronize { @time_parser.parse(time) }

        channel = m['channel']
        level = m['level']
        message = m['message']
        context = JSON.parse(m['context'])
        extra = JSON.parse(m['extra'])

        record = {
            "channel" => channel,
            "level" => level,
            "message" => message,
            "context" => context,
            "extra" => extra
        }
        record["time"] = m['time'] if @keep_time_key

        yield time, record
      end
    end
  end
end
```

## おわりに
packagistデビューする前に、RubyGemsデビューすることになりました。
Webアプリケーション作るのには、PHPはかなり書きやすくて気に入ってはいますが、ツール系はPHPはほとんどなくて、今回のようにツール系のプラグインを書こうとすると、RubyとかPythonになるので、多少書けないといけないのかなと、最近はモヤモヤしております。