たごもりすメモ

コードとかその他の話とか。

fluentdのためのプラグインをイチから書く手順

(2012/02/21追記: bundle gem して作成する手順をこっちに書いた http://d.hatena.ne.jp/tagomoris/20120221/1329815126 )

fluentdがいい感じでパフォーマンスにも問題ない状況になってきたように見えるので、よっしゃいっちょプラグインでも書くか! と思ったもののリポジトリgithubに作ったはいいがコード書いてテストしてgemとしてリリースするまでには様々にめんどくさいことがあり gem とか作ったことない自分*1には摩訶不思議なあれやこれやが広がっていてコード書くところに辿りつくまでが長過ぎるというか、端的に言ってあちこちに散在する情報を集めるのに必要な時間とともにやる気がとめどなく流出していってもうだめだという気分になる。

というような主旨のtweetをしてみたもののどうにかなるわけでもないので、試行錯誤しながらここにメモっとく。こうしたほうがいいよ! という話があったらぜひ教えてほしい。

……と思ったら、俺はどうも根本から間違っていたようだ。

ruby のGemパッケージを作る方法.その2 - それマグで!

これをベースに方法を組み立て直してみる。自分の手元で fluent-plugin-hoop プラグインを作っているので、その手順そのまま。とりあえずBufferedOutputプラグインを作るのでその手順になってることに注意。

jewelerを使う準備とディレクトリツリー、リポジトリの作成

まず jewelerとgemcutterを入れる。また最近はどうも常識っぽいので、まだ入ってなければ bundle もインストールしておこう。

gem install jeweler
gem install gemcutter
gem install bundle

それからディレクトリツリーの初期状態、およびgithubリポジトリをまとめて作る。githubの設定が行われていない場合には警告が出るのでその通りのコマンドを叩いてからリトライすればいい。githubAPI Keyはgithubのアカウント設定ページにある。

 $ jeweler --create-repo fluent-plugin-hoop
 	create	.gitignore
 	create	Rakefile
 	create	Gemfile
 	create	LICENSE.txt
 	create	README.rdoc
 	create	.document
 	create	lib
 	create	lib/fluent-plugin-hoop.rb
 	create	test
 	create	test/helper.rb
 	create	test/test_fluent-plugin-hoop.rb

これで必要なファイルひととおり。らしい。この時点でgithubへのpushも自動的に行われている。

ただしこれだと fluentd のプラグイン用のディレクトリ構成になっていないので、以下のようにコマンドを叩いて再配置する。

mkdir -p lib/fluent/plugin
git mv lib/fluent-plugin-hoop.rb lib/fluent/plugin/out_hoop.rb
mkdir -p test/plugin
git mv test/test_fluent-plugin-hoop.rb test/plugin/test_out_hoop.rb
git commit -m 'replace for fluentd plugin style' -a

またこれにあわせて test/helper.rb の中身も1行だけ書き換える。

@@ -12,7 +12,7 @@ require 'shoulda'
 
 $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
 $LOAD_PATH.unshift(File.dirname(__FILE__))
-require 'fluent-plugin-hoop'
+require 'fluent/plugin/out_hoop'
 
 class Test::Unit::TestCase
 end
fluentを持ってくる

fluentdのコードは大変頻繁にアップデートされているので、プラグインを書く上では常に最新版を参照できるようにしておくのがよろしい。ということで git submodule を使ってプラグインのツリー下にもってくる。

git submodule add git://github.com/fluent/fluentd.git vendor/fluentd
git commit -m 'add fluentd as submodule' -a

あとの作業のためにここでgemspecも作っておこう。なおrvmを使っている場合 vendor/fluentd に行くとそこにある .rvmrc を使うかと聞かれるが、別にいらないので No にしていい。

cd vendor/fluentd
rake gemspec
cd ../..
Gemfile や rake -T および bundle install

ここでおもむろに rake -T してみると、なんか足りないとか言われる。

$ rake -T
(in /Users/tagomoris/Documents/fluent-plugin-hoop)
Could not find gem 'shoulda (>= 0)' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems

Gemfileをのぞいてみると shoulda とかいうのがある。これか。ついでにGemfileにfluentdを足しておこう。

source "http://rubygems.org"
# Add dependencies required to use your gem here.
# Example:
#   gem "activesupport", ">= 2.3.5"

# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
  gem "shoulda", ">= 0"
  gem "bundler", "~> 1.0.0"
  gem "jeweler", "~> 1.6.4"
  gem "rcov", ">= 0"
end

gem "fluentd", :path => 'vendor/fluentd' if RUBY_VERSION >= "1.9.2"

これで bundle install する。

$ bundle install
Using rake (0.9.2.2) 
Using bundler (1.0.18) 
Using iobuffer (1.0.0) 
Using cool.io (1.0.0) 
Using http_parser.rb (0.5.2) 
Using json (1.6.1) 
Using msgpack (0.4.6) 
Using yajl-ruby (1.0.0) 
Using fluentd (0.10.6) from source at vendor/fluentd 
Using git (1.2.5) 
Using jeweler (1.6.4) 
Using rcov (0.9.11) 
Using shoulda (2.11.3) 
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.

Gemfileをcommitしておく。

git commit -m 'add fluentd' Gemfile
AUTHORS を作り LICENSE.txt と .gitignore を更新し VERSION を作る

とりあえず自分の名前とメールアドレスを入れて連絡くらいつくようにしておこう。

echo 'TAGOMORI Satoshi <tagomoris _at_ gmail.com>' > ./AUTHORS
git add AUTHORS

あとLICENSE.txtは修正BSDライセンスで作られているようだ。まあこのままでもいいっちゃいいんだけど fluentd が Apache License v2 なのでそれにあわせておく。まあてきとうにコピペするなりするとよろしい。

それと .gitignore を編集しておく。デフォルトで色々なエディタ対策の記述があるが、それに加えて vendor ディレクトリに入ったものをcommitしないように、などもやっておく。

$ cat .gitignore 
# rcov generated
coverage

# rdoc generated
rdoc

# yard generated
doc
.yardoc

# bundler
.bundle

# jeweler generated
pkg

# For MacOS
.DS_Store

# For TextMate, emacs, vim
*.tmproj
tmtags
*~
\#*
.\#*
*.swp

# not to lock gems version, and for bundler
Gemfile.lock
vendor

とりあえずここでVERSIONファイルも作っとこう!

$ rake version:write
Updated version: 0.0.0
$ ls
AUTHORS      Gemfile.lock README.rdoc  VERSION      test
Gemfile      LICENSE.txt  Rakefile     lib          vendor
$ cat VERSION 
0.0.0

ここまでやってcommitする。ディスクがクラッシュすると泣いちゃうのでpushもしておこう。

git commit -m 'update for this plugin' -a
git push -u origin master
プラグインの骨組みをつくる

このままおもむろに rake とか実行するとなんか言われる。

$ rake
WARNING: 'require 'rake/rdoctask'' is deprecated.  Please use 'require 'rdoc/task' (in RDoc 2.4.2+)' instead.
    at /Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rdoctask.rb
/Users/tagomoris/.rvm/rubies/ruby-1.9.2-p290/bin/ruby -I"lib:lib:test" -I"/Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib" "/Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rake_test_loader.rb" "test/**/test_*.rb" 
Loaded suite /Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rake_test_loader
Started
F
Finished in 0.001525 seconds.

  1) Failure:
test: FluentPluginHoop should probably rename this file and start testing for real. (TestFluentPluginHoop) [/Users/tagomoris/Documents/fluent-plugin-hoop/test/plugin/test_out_hoop.rb:5]:
hey buddy, you should probably rename this file and start testing for real

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

Test run options: --seed 34610
rake aborted!
Command failed with status (1): [/Users/tagomoris/.rvm/rubies/ruby-1.9.2-p2...]

Tasks: TOP => test
(See full trace by running task with --trace)

とりあえず Rakefile の require 'rake/rdoctask' と書いてあるところを require 'rdoc/task' に変えて Gemfile に gem "rdoc" と追加する。
rake すると、正常になっているようだ。

$ rake
/Users/tagomoris/.rvm/rubies/ruby-1.9.2-p290/bin/ruby -I"lib:lib:test" -I"/Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib" "/Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rake_test_loader.rb" "test/**/test_*.rb" 
Loaded suite /Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rake_test_loader
Started
F
Finished in 0.001340 seconds.

  1) Failure:
test: FluentPluginHoop should probably rename this file and start testing for real. (TestFluentPluginHoop) [/Users/tagomoris/Documents/fluent-plugin-hoop/test/plugin/test_out_hoop.rb:5]:
hey buddy, you should probably rename this file and start testing for real

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

Test run options: --seed 37798
rake aborted!
Command failed with status (1): [/Users/tagomoris/.rvm/rubies/ruby-1.9.2-p2...]

Tasks: TOP => default => test
(See full trace by running task with --trace)

さて、書く対象のプラグインモジュールの体裁くらいは整えないとTDDもできない。ので、書く対象のプラグインをつくる。正確に言うと以下のページからモジュール名だけ変えて lib/fluent/plugin/out_hoge.rb にコピペする。
http://fluentd.org/doc/devel.html
これはBufferedOutputプラグインの例ね。こんなかんじかなー。公式の例だとMessagePackがoptionalみたいな感じになってるけど、普通にMessagePack使う形で書けばいいと思う。JSONにしたほうがいいケースってあるのかな。JSONをそのまま出力したいケースか。あんのかそんなの。あるのかな。

class Fluent::HoopOutput < Fluent::BufferedOutput
  Fluent::Plugin.register_output('hoop', self)

  include Fluent::SetTagKeyMixin
  config_set_default :include_tag_key, false

  include Fluent::SetTimeKeyMixin
  config_set_default :include_time_key, true

  # config_param :hoge, :string, :default => 'hoge'

  def initialize
    super
    # require 'hogepos'
  end

  def configure(conf)
    super
    # @path = conf['path']
  end

  def start
    super
    # init
  end

  def shutdown
    super
    # destroy
  end

  def format(tag, time, record)
    [tag, time, record].to_msgpack
  end

  def write(chunk)
    records = []
    chunk.msgpack_each { |record|
      # records << record
    }
    # write records
  end
end

続けてテストの骨組も作ってしまう。ドキュメント……には残念ながらないので fluentd 本家のテストコードから枠組みをぱくってきて以下のコードを test/plugin/test_out_hoge.rb にコピペする。プラグイン用のテストドライバのコード例はないと忘れそうなのでコメントアウトしてある部分は残しておきたい。(自分は。)

require 'helper'
# require 'time'

class HoopOutputTest < Test::Unit::TestCase
  # TMP_DIR = File.dirname(__FILE__) + "/../tmp"

  def setup
    Fluent::Test.setup
    # FileUtils.rm_rf(TMP_DIR)
    # FileUtils.mkdir_p(TMP_DIR)
  end

  CONFIG = %[
  ]
  # CONFIG = %[
  #   path #{TMP_DIR}/out_file_test
  #   compress gz
  #   utc
  # ]

  def create_driver(conf = CONFIG)
    Fluent::Test::BufferedOutputTestDriver.new(Fluent::HoopOutput).configure(conf)
  end

  def test_configure
    #### set configurations
    # d = create_driver %[
    #   path test_path
    #   compress gz
    # ]
    #### check configurations
    # assert_equal 'test_path', d.instance.path
    # assert_equal :gz, d.instance.compress
  end

  def test_format
    d = create_driver

    # time = Time.parse("2011-01-02 13:14:15 UTC").to_i
    # d.emit({"a"=>1}, time)
    # d.emit({"a"=>2}, time)

    # d.expect_format %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n]
    # d.expect_format %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n]

    # d.run
  end

  def test_write
    d = create_driver

    # time = Time.parse("2011-01-02 13:14:15 UTC").to_i
    # d.emit({"a"=>1}, time)
    # d.emit({"a"=>2}, time)

    # ### FileOutput#write returns path
    # path = d.run
    # expect_path = "#{TMP_DIR}/out_file_test._0.log.gz"
    # assert_equal expect_path, path

    # data = Zlib::GzipReader.open(expect_path) {|f| f.read }
    # assert_equal %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n] +
    #                 %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n],
    #              data
  end
end

また test/helper.rb にも必要な記述を追加する。

@@ -12,6 +12,7 @@ require 'shoulda'
 
 $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
 $LOAD_PATH.unshift(File.dirname(__FILE__))
+require 'fluent/test'
 require 'fluent/plugin/out_hoop'
 
 class Test::Unit::TestCase

これでいいんじゃね? と満を持して rake test やったら通りました! やったね!*2

$ rake test
/Users/tagomoris/.rvm/rubies/ruby-1.9.2-p290/bin/ruby -I"lib:lib:test" -I"/Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib" "/Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rake_test_loader.rb" "test/**/test_*.rb" 
2011-11-18 16:39:49 +0900: registered output plugin 'hoop'
Loaded suite /Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rake_test_loader
Started
2011-11-18 16:39:49 +0900: registered buffer plugin 'file'
2011-11-18 16:39:49 +0900: registered buffer plugin 'memory'
2011-11-18 16:39:49 +0900: registered input plugin 'exec'
2011-11-18 16:39:49 +0900: registered input plugin 'forward'
2011-11-18 16:39:49 +0900: registered input plugin 'http'
2011-11-18 16:39:49 +0900: registered input plugin 'tcp'
2011-11-18 16:39:49 +0900: registered input plugin 'unix'
2011-11-18 16:39:49 +0900: registered input plugin 'syslog'
2011-11-18 16:39:49 +0900: registered input plugin 'tail'
2011-11-18 16:39:49 +0900: registered output plugin 'copy'
2011-11-18 16:39:49 +0900: registered output plugin 'exec'
2011-11-18 16:39:49 +0900: registered output plugin 'exec_filter'
2011-11-18 16:39:49 +0900: registered output plugin 'file'
2011-11-18 16:39:49 +0900: registered output plugin 'forward'
2011-11-18 16:39:49 +0900: registered output plugin 'null'
2011-11-18 16:39:49 +0900: registered output plugin 'roundrobin'
2011-11-18 16:39:49 +0900: registered output plugin 'stdout'
2011-11-18 16:39:49 +0900: registered output plugin 'tcp'
2011-11-18 16:39:49 +0900: registered output plugin 'unix'
2011-11-18 16:39:49 +0900: registered output plugin 'test'
...
Finished in 0.108051 seconds.

3 tests, 0 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 4553

これでいいんだけど、テストを繰り返してるとfluentdの出すログがちょっとうざい感じなので、以下のようにして抑制する。

diff --git a/test/helper.rb b/test/helper.rb
index bc0a6c3..ffeccdf 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -12,7 +12,19 @@ require 'shoulda'
 
 $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
 $LOAD_PATH.unshift(File.dirname(__FILE__))
+require 'fluent/test'
 require 'fluent/plugin/out_test_counter'
 
+if ENV['FLUENT_TEST_DEBUG'] == 'TRUE'
+  nulllogger = Object.new
+  nulllogger.instance_eval {|obj|
+    def method_missing(method, *args)
+      # pass
+    end
+  }
+  $log = nulllogger
+end
+
+
 class Test::Unit::TestCase
 end

diff --git a/Rakefile b/Rakefile
index 3f33bf8..212228c 100644
--- a/Rakefile
+++ b/Rakefile
@@ -27,6 +27,9 @@ Jeweler::RubygemsDotOrgTasks.new
 
 require 'rake/testtask'
 Rake::TestTask.new(:test) do |test|
+  unless ENV['DEBUG']
+    ENV['FLUENT_TEST_DEBUG'] = 'TRUE'
+  end
   test.libs << 'lib' << 'test'
   test.pattern = 'test/**/test_*.rb'
   test.verbose = true

こうしとくと普通のテスト実行時にはfluentdのログが出てこない。プラグインのロードまわりなどで怪しいからログ見たいなーというときはオプション引数を与えれば出てくる。

$ rake test DEBUG=true

やったね!これで準備は完了なのでまた commit & push しておく。

git commit -m 'plugin initialized' -a
git push
あとは実処理を書いてリリース

ここまで来たらあとはテスト書いてコード書いてテスト書いてコード書いて、完成したらリリース、のはず。
まあリリースにまたひと悶着ありそうだが、それはコード書けてから心配すればいいのかなーと思う。しばらく先の話。

さあ書くぞー!

*1:というかCPANとかもないしなにもない。言語(のパッケージリポジトリ)ごとに秘伝があり割と頻繁にアップデートされていてわけがわからない。

*2:もちろん最初はうまくいかなくて中略という状態だったが、上にコピペしてあるコードなら大丈夫、のはず!