無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

複数行からなるログを解析するために、EmbulkのparserプラグインをRubyで開発する話(準備編)

最近、仕事の関係で、1個のログが1行〜複数行からなる特殊なログを解析する必要があり、Rubyでパーサを書く機会がありました。

しかし、そういえばこのパース処理ってEmbulkを使えばより簡単に作れて、かつ機能追加(パース結果をデータベースに入れるとか)が可能になるんじゃないか、と思い、parserプラグインRubyで開発する方法を調べてみました。

ちなみに、Fluentdのin_tailプラグインのmultilineで頑張って分析することも考えたのですが、先日のTreasure Data Tech Talkの懇親会で古橋さんに相談したところ、「パーサの処理が複雑なら、Embulkのparserプラグインを自分で書いたほうが楽ですよ」とアドバイスを頂いたので、今回はEmbulkで行くことにしました。

Embulkのインストール

私は、上記の動作環境で試しました。EmbulkのGitHubにあるQuick Start通りにインストールして、特に引っかかるところはありませんでした。

サンプルログの解析

同じくEmbulkのGitHubにあるTrying Examples通りにコマンドを実行すると、サンプルログを解析するための設定ファイルが生成されます。

embulk example ./try1
embulk guess   ./try1/example.yml -o config.yml
embulk preview config.yml
embulk run     config.yml

上記の手順通りに進めると、以下の様なconfig.ymlが生成されます。今回、独自のパーサを作るにあたっても設定ファイルは必要なので、このファイルをベースに作ります。

in:
  type: file
  path_prefix: /Users/myoshiz/devel/embulk-plugins/try1/csv/sample_
  decoders:
  - {type: gzip}
  parser:
    charset: UTF-8
    newline: CRLF
    type: csv
    delimiter: ','
    quote: '"'
    escape: ''
    skip_header_lines: 1
    columns:
    - {name: id, type: long}
    - {name: account, type: long}
    - {name: time, type: timestamp, format: '%Y-%m-%d %H:%M:%S'}
    - {name: purchase, type: timestamp, format: '%Y%m%d'}
    - {name: comment, type: string}
exec: {}
out: {type: stdout}

独自のパーサを作る場合、parserパラメータ内のtypeは自分のパーサ名を指定するために必要、charsetとnewlineも後述するLineDecoderを使うなら必要、その他のパラメータはCSVパーサ固有のパラメータなので不要になります。

parserプラグインのひな形(Ruby用)の生成

embulkコマンドにはnewというパラメータがあり、色々なプラグインのひな形を作成できます。new以降の引数を与えないと、以下のように選べるカテゴリーが確認できるのですが、Rubyのほうはまだ未実装の部分が多いみたいですね。

happyturn% embulk new
2015-03-15 15:22:57.591 +0900: Embulk v0.5.2
Usage: new <category> <name>
categories:
    ruby-input                 Ruby record input plugin    (like "mysql")
    ruby-output                Ruby record output plugin   (like "mysql")
    ruby-filter                Ruby record filter plugin   (like "add-hostname")
    #ruby-file-input           Ruby file input plugin      (like "ftp")          # not implemented yet [#21]
    #ruby-file-output          Ruby file output plugin     (like "ftp")          # not implemented yet [#22]
    ruby-parser                Ruby file parser plugin     (like "csv")
    ruby-formatter             Ruby file formatter plugin  (like "csv")
    #ruby-decoder              Ruby file decoder plugin    (like "gzip")         # not implemented yet [#31]
    #ruby-encoder              Ruby file encoder plugin    (like "gzip")         # not implemented yet [#32]
    java-input                 Java record input plugin    (like "mysql")
    java-output                Java record output plugin   (like "mysql")
    java-filter                Java record filter plugin   (like "add-hostname")
    java-file-input            Java file input plugin      (like "ftp")
    java-file-output           Java file output plugin     (like "ftp")
    java-parser                Java file parser plugin     (like "csv")
    java-formatter             Java file formatter plugin  (like "csv")
    java-decoder               Java file decoder plugin    (like "gzip")
    java-encoder               Java file encoder plugin    (like "gzip")

examples:
    new ruby-output hbase
    new ruby-filter int-to-string

今回はRubyでパーサを書きたいので、次のようにコマンドを実行します。今回は、プラグインの名前を仮にmultiline-log-sampleとします。

happyturn% pwd
/Users/myoshiz/devel/embulk-plugins
appyturn% embulk new ruby-parser multiline-log-sample
2015-03-15 15:47:58.832 +0900: Embulk v0.5.2
Creating embulk-parser-multiline-log-sample/
  Creating embulk-parser-multiline-log-sample/README.md
  Creating embulk-parser-multiline-log-sample/LICENSE.txt
  Creating embulk-parser-multiline-log-sample/.gitignore
  Creating embulk-parser-multiline-log-sample/Rakefile
  Creating embulk-parser-multiline-log-sample/Gemfile
  Creating embulk-parser-multiline-log-sample/embulk-parser-multiline-log-sample.gemspec
  Creating embulk-parser-multiline-log-sample/lib/embulk/parser/multiline-log-sample.rb
  Creating embulk-parser-multiline-log-sample/lib/embulk/guess/multiline-log-sample.rb

parserプラグインのひな形(Ruby用)の動作確認

これが正しい方法かは分からないのですが、私がやってみて楽だった方法を紹介します。

まず、embulk bundleコマンドで、開発用のEmbulk動作環境を作ります。ここでは、開発用のディレクトリを、ひな形を作ったのと同じディレクトリ /Users/myoshiz/devel/embulk-plugins の直下に作成しました。

happyturn% pwd
/Users/myoshiz/devel/embulk-plugins
happyturn% embulk bundle ./embulk_bundle
2015-03-15 11:56:36.230 +0900: Embulk v0.5.2
Initializing ./embulk_bundle...
  Creating ./embulk_bundle/.bundle/config
  Creating ./embulk_bundle/embulk/input/example.rb
  Creating ./embulk_bundle/embulk/output/example.rb
  Creating ./embulk_bundle/embulk/filter/example.rb
  Creating ./embulk_bundle/Gemfile
Fetching: bundler-1.8.5.gem (100%)
Successfully installed bundler-1.8.5
1 gem installed
The Gemfile specifies no dependencies
Resolving dependencies...
Bundle complete! 0 Gemfile dependencies, 1 gem now installed.
Bundled gems are installed into ..

次に、ここで生成したディレクトリに、先ほど作成したひな形をコピーします。parserディレクトリは自動生成されないのですが、embulk_bundle/embulk/parserディレクトリを作り、ここにmultiline-log-sample.rbをコピーすれば動作します。

その上で、embulkコマンドの実行時に-bオプションでこのembulk_bundleディレクトリを指定すると、開発環境のEmbulkが使われます。例えば、次のようにembulk previewを実行できます。

happyturn% embulk preview config.yml -b ./embulk_bundle

ただし、この時点では、config.ymlのなかで、parserのtypeにmultiline-log-sampleを指定しても、読み込んだバッファの数だけダミーの分析結果(["col1", 2, 3.0])が返されるだけです。

2015-03-17追記

twitterで@hiroysatoさんに教えていただいたのですが、embulk_bundleディレクトリ以下にmultiline-log-sample.rbにコピーする必要はないみたいです。embulk previewコマンドの-Iオプションで、ロードパスにプラグインのlibディレクトリを追加するだけで実行できました(参考:How to develop embulk parser plugin)。

happyturn% embulk preview -I embulk-parser-multiline-log-sample/lib config.yml

embulk previewコマンドのヘルプはこんな感じ。

happyturn% embulk preview --help
2015-03-17 08:45:47.741 +0900: Embulk v0.5.2
Usage: preview <config.yml>
    -b, --bundle BUNDLE_DIR          Path to a Gemfile directory
    -l, --log-level LEVEL            Log level (fatal, error, warn, info, or trace)
    -I, --load-path PATH             Add ruby script directory path ($LOAD_PATH)
    -C, --classpath PATH             Add java classpath separated by : (CLASSPATH)
    -G, --vertical                   Use vertical output format

parserプラグインの開発

今回想定するログ

今回書くparserプラグインでは、以下の様なログを想定します。一応お断りしておくと、私が仕事で出くわしたログとは形式を変えて記載しています。

今回は、基本的には生成時刻とエラーレベルとエラーメッセージが出力されるものの、ERRORレベルのときにはスタックトレースが表示される場合もある、というログを想定します。また、スタックトレースについては無視するのではなく、その直前のERRORログの一部としてパースしたい、というニーズがあるものとします。

Dummy multi-line log

行単位のログ読み込み

自動生成されたMultilineLogSampleParserPluginクラスは、以下のrunメソッドでログを読み込み、その結果をpage_builderに書き込みます。

      def run(file_input)
        while file = file_input.next_file
          file.each do |buffer|
            # parsering code
            record = ["col1", 2, 3.0]
            page_builder.add(record)
          end
        end
        page_builder.finish
      end

で、あとはこの file.each do |buffer| 以降の処理を書き換えるだけ、と思ったのですが……このbufferには行の区切りを無視してログが書き込まれます。例えば、"2015-03-14 20:16:45, 183"という時刻の"2015-03-"までが前のバッファ、"14 20:16:45, 183"が次のバッファに分かれるといったことが普通に起こってしまいます。この結合を自前で行わなければいけないとしたら、ちょっと面倒ですよね……。

で、私はここで行き詰まってしまったのですが、LineDecoderというユーティリティを使えばこの結合を自動化出来るよ、と古橋さん(@frsyuki)にアドバイスしてもらいました。RubyのコードからこのLineDecoderを呼び出すやり方には(Javaで書かれたLineDecoderをJRubyから呼ぶために)少し癖があるのですが、古橋さんからサンプルコードも頂いて、なんとかLineDecoderを使うことができました。

以下は、LineDecoderを使って行単位でログを読み込み、行全体を1個のカラムに格納して返すパーサの例です。古橋さんからもらったサンプルコードと内容はほぼ同じですが、こちらはembulk newで自動生成されたコードも、比較のためにコメントアウトして残しています。

LineDecoder Usage Sample

ちなみに、今回行き詰まったときにコードを読んで確認しましたが、config.ymlに書かれたcharsetとnewlineはきちんとLineDecoder.javaに渡されます。

複数行からなるログのパース

ここまで来れば、あとは1行ずつログを読み込んでパースするrubyのプログラムを書くのと、何も変わらないかと思います。

次回は実際に、上記のログのパーサをparserプラグインとして書いてみて、そのコード量や、Embulkでパースすることによるメリットなどを整理してみます。とりあえず、パース結果をPostgreSQLサーバに登録できるようにするところが当面の目標かなと思ってます。

参考文献