読者です 読者をやめる 読者になる 読者になる

たごもりすメモ

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

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

fluent

前に自分で書いた fluentdのためのプラグインをイチから書く手順 - tagomorisのメモ置き場 はたいへん重宝していたのだが、書いたすこし後になって実は現在すでに bundle gem コマンドを使うやりかたが良さそうだということがわかってしまったがばたばたしてて移行してなかった。
で、またひとつプラグインを書くことにしたのでついでに bundle を使った手順をざっくりまとめておく。以下のエントリをたいへん参考にさせてもらった。

T-POINTを取得するスクリプトをGistから移動, Bundlerを使ったGem作成メモ (自分用) - ただのにっき(2012-02-18)

準備とディレクトリツリーの作成

bundler は必要なので、なにはなくとも入れておこう。

gem install bundler

そしてプラグイン用ディレクトリツリーを作成する。今回は DataCounterOutput というプラグインを作ることにして、パッケージ名は fluent-plugin-datacounter にする。

$ bundle gem fluent-plugin-datacounter
      create  fluent-plugin-datacounter/Gemfile
      create  fluent-plugin-datacounter/Rakefile
      create  fluent-plugin-datacounter/.gitignore
      create  fluent-plugin-datacounter/fluent-plugin-datacounter.gemspec
      create  fluent-plugin-datacounter/lib/fluent-plugin-datacounter.rb
      create  fluent-plugin-datacounter/lib/fluent-plugin-datacounter/version.rb
Initializating git repo in /Users/tagomoris/Documents/fluent-plugin-datacounter

ファイル配置が fluentd plugin ぽくないので修正するためにコマンドを叩く。あとバージョン管理に version.rb を使うのは rake build とかしたとき色々面倒だったので削除。バージョン番号は gemspec に直接書くことにする。

$ cd fluent-plugin-datacounter/
$ mkdir -p lib/fluent/plugin
$ mv lib/fluent-plugin-datacounter.rb lib/fluent/plugin/out_datacounter.rb
$ rm lib/fluent-plugin-datacounter/version.rb
$ rmdir lib/fluent-plugin-datacounter
$ mkdir -p test/plugin
$ touch test/plugin/test_out_datacounter.rb

out_datacounter本体のモジュール名だけとりいそぎ更新。requireのパスとモジュール名だけ書き換えてある(あとでちゃんと直す)。

module Fluent
  class DataCounterOutput < Fluent::BufferedOutput
      # Your code goes here...
  end
end

これにあわせて fluent-plugin-flowcounter.gemspec を更新。versionやhomepage, summary, descriptionを適当に変更する。また version.rb をrequireしている行を削除する。

# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)

Gem::Specification.new do |s|
  s.name        = "fluent-plugin-datacounter"
  s.version     = "0.0.1"
  s.authors     = ["TAGOMORI Satoshi"]
  s.email       = ["tagomoris@gmail.com"]
  s.homepage    = "https://github.com/tagomoris/fluent-plugin-datacounter"
  s.summary     = %q{Output filter plugin to count messages that matches specified conditions}
  s.description = %q{Output filter plugin to count messages that matches specified conditions}

  s.rubyforge_project = "fluent-plugin-datacounter"
# ...
  s.add_development_dependency "rake"
  s.add_runtime_dependency "fluentd"
end

基本的にはこれでいいはず。gemspecに依存モジュールが書いてあり Gemfile はgemspecを読み込んでるので bundle install すれば必要なモジュール(ここではrake, fluentdとその依存モジュールだけ)はインストールされる。

なお前は vendor/fluentd にsubmoduleしてたけど、最近はさすがにもうそんなことしなくてもいいなーと思ってて、割とさっくりそのままリリース版のgemからfluentdを入れてる。ので、そのへんは省略。

ついでにライセンスをどうするか決めて LICENSE.txt でも用意して書いておく。fluentd が Apache License v2.0 なので、あわせておくならそのへんの文面を突っ込んでおけばいい。
また .gitignore に最低限のリストがあるけど、エディタのバックアップファイルなどは必要に応じて追記しておく。自分はjewelerが作っていたものから適当にコピーしてきてこんな感じ。

*.gem
.bundle
Gemfile.lock
pkg/*
# For TextMate, emacs, vim
*.tmproj
tmtags
*~
\#*
.\#*
*.swp

ここまでやったらとりあえず全部 commit してpushする。(pushする前にgithub側でリポジトリを作っておくこと。)

$ git add lib/fluent
$ git add test
$ git commit -m 'init tree' -a
$ git remote add origin git@github.com:tagomoris/fluent-plugin-datacounter.git
$ git push -u origin master
プラグインの骨組みをつくる

ともあれプラグインとして最低限の体裁を整えてテストが走るようにしたい。こんな感じにしておく。まず out_datacounter.rb 本体の方。

class Fluent::DataCounterOutput < Fluent::BufferedOutput
  Fluent::Plugin.register_output('datacounter', self)

  # 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

あとテスト。まず test/helper.rb を作っておく。ここに書いた fluent/test の読み込みがないと fluentd のTestDriverなどが読み込まれず、テストが走らない。またこれから書くプラグインもちゃんと書いておく。またテスト走行時に fluentd のログがコンソールに出力されないように logger をゴニョゴニョする。(VERBOSE=1 rake test すれば出てくるようにもする。)

require 'rubygems'
require 'bundler'
begin
  Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
  $stderr.puts e.message
  $stderr.puts "Run `bundle install` to install missing gems"
  exit e.status_code
end
require 'test/unit'

$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'fluent/test'
unless ENV.has_key?('VERBOSE')
  nulllogger = Object.new
  nulllogger.instance_eval {|obj|
    def method_missing(method, *args)
      # pass
    end
  }
  $log = nulllogger
end

require 'fluent/plugin/out_datacounter'

class Test::Unit::TestCase
end

で、テストの骨組みを test/plugin/test_out_datacounter.rb に。create_driver に引数を追加し BufferedOutputTestDriver.new の引数にプラグインクラスの他にタグを渡すようにしている。これでテスト内で 'test' 以外のタグを扱いたくなっても対応できる。

require 'helper'

class DataCounterOutputTest < Test::Unit::TestCase
  def setup
    Fluent::Test.setup
  end

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

  def create_driver(conf = CONFIG, tag='test')
    Fluent::Test::BufferedOutputTestDriver.new(Fluent::DataCounterOutput, tag).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
  end
end

Rakefileにテスト用の記述を足す。

diff --git a/Rakefile b/Rakefile
index 2995527..f0f33ef 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1 +1,11 @@
 require "bundler/gem_tasks"
+
+require 'rake/testtask'
+Rake::TestTask.new(:test) do |test|
+  test.libs << 'lib' << 'test'
+  test.pattern = 'test/**/test_*.rb'
+  test.verbose = true
+end
+
+task :default => :test

ここまでできたらテストが走るはず。試してみよう。

$ 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" 
/Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/fluentd-0.10.12/lib/fluent/plugin.rb:156: warning: already initialized constant Plugin
2012-02-21 18:17:36 +0900: registered output plugin 'datacounter'
Loaded suite /Users/tagomoris/.rvm/gems/ruby-1.9.2-p290/gems/rake-0.9.2.2/lib/rake/rake_test_loader
Started
2012-02-21 18:17:36 +0900: registered buffer plugin 'file'
2012-02-21 18:17:36 +0900: registered buffer plugin 'memory'
2012-02-21 18:17:36 +0900: registered input plugin 'exec'
2012-02-21 18:17:36 +0900: registered input plugin 'forward'
2012-02-21 18:17:36 +0900: registered input plugin 'http'
2012-02-21 18:17:36 +0900: registered input plugin 'tcp'
2012-02-21 18:17:36 +0900: registered input plugin 'unix'
2012-02-21 18:17:36 +0900: registered input plugin 'syslog'
2012-02-21 18:17:36 +0900: registered input plugin 'tail'
2012-02-21 18:17:36 +0900: registered output plugin 'copy'
2012-02-21 18:17:36 +0900: registered output plugin 'exec'
2012-02-21 18:17:36 +0900: registered output plugin 'exec_filter'
2012-02-21 18:17:36 +0900: registered output plugin 'file'
2012-02-21 18:17:36 +0900: registered output plugin 'forward'
2012-02-21 18:17:36 +0900: registered output plugin 'null'
2012-02-21 18:17:36 +0900: registered output plugin 'roundrobin'
2012-02-21 18:17:36 +0900: registered output plugin 'stdout'
2012-02-21 18:17:36 +0900: registered output plugin 'tcp'
2012-02-21 18:17:36 +0900: registered output plugin 'unix'
2012-02-21 18:17:36 +0900: registered output plugin 'test'
...
Finished in 0.059279 seconds.

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

Test run options: --seed 34276

やったやった! とりあえずもういちど commit & push しとく。

$ git add test/helper.rb
$ git commit -m 'write skeleton of plugin and its tests' -a
開発、テスト、リリース

開発とテストは、まあなんだ、頑張れ。開発中にこのリポジトリからgemを作りたくなったら rake build して、それをインストールする場合は rake install すればいい(らしい)。
リリースは gemspec に書いてあるバージョンを更新し、rubygems.org にアカウントがあるならそのまま rake release で終わり。

おつかれさまでした!