最近、仕事の関係で、1個のログが1行〜複数行からなる特殊なログを解析する必要があり、Rubyでパーサを書く機会がありました。
しかし、そういえばこのパース処理ってEmbulkを使えばより簡単に作れて、かつ機能追加(パース結果をデータベースに入れるとか)が可能になるんじゃないか、と思い、parserプラグインをRubyで開発する方法を調べてみました。
ちなみに、Fluentdのin_tailプラグインのmultilineで頑張って分析することも考えたのですが、先日のTreasure Data Tech Talkの懇親会で古橋さんに相談したところ、「パーサの処理が複雑なら、Embulkのparserプラグインを自分で書いたほうが楽ですよ」とアドバイスを頂いたので、今回はEmbulkで行くことにしました。
Embulkのインストール
- 動作環境
- MacBook Pro (Retina, 15-inch, Mid 2014)
- OS X Yosemite version 10.10.2
- Embulk v0.5.2
私は、上記の動作環境で試しました。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ログの一部としてパースしたい、というニーズがあるものとします。
行単位のログ読み込み
自動生成された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で自動生成されたコードも、比較のためにコメントアウトして残しています。
ちなみに、今回行き詰まったときにコードを読んで確認しましたが、config.ymlに書かれたcharsetとnewlineはきちんとLineDecoder.javaに渡されます。
複数行からなるログのパース
ここまで来れば、あとは1行ずつログを読み込んでパースするrubyのプログラムを書くのと、何も変わらないかと思います。
次回は実際に、上記のログのパーサをparserプラグインとして書いてみて、そのコード量や、Embulkでパースすることによるメリットなどを整理してみます。とりあえず、パース結果をPostgreSQLサーバに登録できるようにするところが当面の目標かなと思ってます。
参考文献
- embulk/embulk
- EmbulkのGitHubページ。
- tail Input Plugin | Fluentd
- multilineの設定例あり。すべてを正規表現で書かなければいけないので、ログの形式が複雑な場合は確かに大変そう。
- Fluentdのバッチ版Embulk(エンバルク)のまとめ - Qiita
- hiroysatoさんによるEmbulk関連ページの一覧。行き詰まってたときにtwitterでコメント頂きました。ありがとうございます。
- Apache Log Parser Plugin
- Java で Embulk Output Plugin を書く - Qiita
- Treasure Dataの西澤さんの記事。Javaでの開発で、かつParser pluginではありませんが、開発手順の参考になりました。
- Embulk-plugin-inputの作り方 - Qiita
- 開発手順、embulk bundleについてはこちらも参考になりました。