LoginSignup
2
1

More than 5 years have passed since last update.

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

Posted at

はじめに

monologから出力したログをfluentdで集約するを書いていて、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は下記のようなログを出力します。

[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"}

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

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

正規表現

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

  • contextextraは、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の使い方

下記のコマンドでプラグインを追加します。

$ 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をコピーしてきて、ちょっと正規表現変えればいいと思っていました。
で、Apache2Parser見ると、下記のようになっているわけです。

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に、その名前を見つけることができます。

module Fluent
  module Plugin
    class Parser < Base
      include OwnedByMixin

      class ParserError < StandardError; end

      # 省略

      class TimeParser
        def initialize(time_format)      

ここでもTextParserは出てきません。
最終的に、DevNullParserで始めるFluentd パーサプラグイン入門 - Qiitaに、掲載されていたコードを見て、真似してうまく行きました。

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

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

参考URL

一度、公開したらバージョンアップするしかない

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

参考URL

最終的なコード(v0.1.5)

最終的に下記のようなコードになりました。

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になるので、多少書けないといけないのかなと、最近はモヤモヤしております。

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1