(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の設定が行われていない場合には警告が出るのでその通りのコマンドを叩いてからリトライすればいい。githubのAPI 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
あとは実処理を書いてリリース
ここまで来たらあとはテスト書いてコード書いてテスト書いてコード書いて、完成したらリリース、のはず。
まあリリースにまたひと悶着ありそうだが、それはコード書けてから心配すればいいのかなーと思う。しばらく先の話。
さあ書くぞー!