2016年7月30日にクラウドワークスで4回目のOSS Gateワークショップを開催しました。今回はこれまでで最多の23名の参加でした。(ビギナーは19人申し込みで実際に参加したのは11人。参加率は約58%。)
参加者の感想は以下のとおりです。(増えたら追記します。)
ここではワークショップ運営視点でまとめます。
OSS Gateワークショップはもっとよくしていけるワークショップです。そのため、毎回、前回の課題を解決することにチャレンジしています。
前回の課題はメンターとビギナーの割合が1:2(ビギナーがメンターの2倍)だと回らない、ということでした。これまでは2:1とメンターがビギナーの2倍の体制で実施していましたが、前回は割合が逆になりました。
メンターがビギナーの2倍いないと回らない体制では、今後、ビギナーが増えるに従い、メンター不足でワークショップを実施できなくなってしまいます。そのため、ビギナーがメンターの2倍でも回せる体制の確立にチャレンジしました。
そのために考案した仕組みが「サポートメンター(仮称)」です。
サポートメンターとはメンターをサポートするメンターです。過去のワークショップでメンターを経験したことがあるメンターが担当します。
サポートメンターを導入することで次のことを期待しました。
少し補足します。
メンター1人に対してビギナーが2人の体制のときの課題は、1人のビギナーをサポートしているときにもう1人のビギナーをサポートできないことです。サポートメンターを導入することにより、そのような状態をフォローする体制にしました。同時に2人のサポートが必要なときはそれほどありませんが、1人のメンターに付き数回は発生します。それをフォローします。
経験者メンターだけがビギナー2人をサポートできないとなると、十分なメンターを揃えることが難しくなります。はじめてのメンターばかりだとメンター1人に対してビギナー1人(以下)の体制にしなければいけなく、より多くのメンターが必要になるからです。はじめてのメンターでもビギナー2人をサポートできるように、対応が難しい場面はサポートメンターがフォローする体制にしました。
実際にサポートメンターを導入した結果、これら両方を達成できました。
今回はビギナー11名、メンター6名(一時的に7名)の体制でしたが、十分にビギナーをサポートできました。これはビギナーへのアンケート結果にある満足度が総じて高かったことから判断できます。
今回のメンターは次のどちらかでした。(参加していた人で気づいていた人はどのくらいいたでしょうか。)
つまり、経験者メンター数人(今回は3人)がサポートメンターになることで、これらのメンターでも十分ビギナーをサポートできたということです。未経験の人がメンターとして参加しても十分即戦力になる体制ということです。
他に、OSS GateおよびOSS Gateワークショップの広報活動としてレポーターをチャレンジしようとしていましたが、これは人員不足でチャレンジできませんでした。次回はレポーターがいるので次回チャレンジする予定です。
これまで、OSS Gateは東京でのみ活動していましたが、今後は東京以外でも活動していきます。今のところ、札幌・沖縄での活動の準備を進めています。他の地域でもぜひ活動したいというかたは須藤(kou@clear-code.com
)に連絡をください。
札幌は、@tricknotesを中心とした人たちで準備を進めています。まずは、9月24日、11月26日にOSS Gateワークショップを開催することが決まっています。9月24日は東京でも開催するので、東京・札幌で同日開催になります。(須藤は札幌にお手伝いに行くので東京のワークショップには参加しません。)
沖縄は、@yasulabと準備を進めています。RubyKaigi 2016が終わったら本格的に動き出す予定です。
来年には「北海道から沖縄まで、全国で活動しています」と言えそうです。
このように「地域」という括りだけで活動を広げていくのではなく、プログラミング言語を含む各種「オープンソースなプロダクトのコミュニティ」とも連携して活動を広げていけないかと検討しています。自分が関わっているOSSの開発に参加する人が増えるとうれしい、という方は須藤(kou@clear-code.com
)に連絡をください。「OSS開発に参加する人を増やす」取り組みであるOSS Gateといい感じに協力できるはずです。
7月30日に4回目のOSS Gateワークショップを開催しました。
今回は次のことにチャレンジしました。
これらを実現するためにサポートメンターを導入し、うまくいきました。
はじめてのメンターでも十分にビギナーをサポートできる体制になっているので、OSS開発に参加する人が増えるとうれしい人はぜひメンターとして参加してください。なお、次回は9月24日開催の予定です。8月4日時点でビギナー30名・メンター7名の申し込みがあります。メンター1人で2人のビギナーをサポートできる体制になりましたが、この比率ではさすがにサポートできないので、メンターとして参加する人を非常に募集しています。メンターの人は優先して参加できるようにするので「キャンセル待ち」になっていても気にせず申し込んでください。(毎日少しずつビギナーの申し込みがあるのであと数日もすればキャンセル待ちになりそうです。)
もちろん、過去のワークショップでメンターをした経験者メンターも非常に募集しています。サポートメンターとしてはじめてのメンターをサポートして欲しいのです。9月24日は一番慣れている須藤が札幌にお手伝いに行くので、東京は少し手薄になります。そのあたりを経験者メンターにフォローして欲しいのです。
あるプロジェクトで、一行が長いコードを扱う機会があったので、一行を短くする(改行を入れる)方法とメリット・デメリットについて考えてみました。
ここでは、長くなってしまいがちなHTMLを例として取り上げます。例えば以下のようなコードです。
<ul><li class="beast" id="cat">ねこ</li><li class="beast" id="fish">さかな</li></ul>
このように一行の文字数が多くなってしまうと、いくつかデメリットがあります。この例だと、どんな要素や属性があるのか一目ではわからなくなってしまっています。*1
これを、例えば以下のように変更してみます。
<ul>
<li class="beast" id="cat">ねこ</li>
<li class="beast" id="fish">さかな</li>
</ul>
どんな要素や属性があるのかわかりやすくなりました。*2
また、一行を短くすると差分がわかりやすくなるというメリットもあります。
例として、さかなにtitle属性を追加する場合を考えてみます。追加後のHTMLは以下のようになります。
<ul>
<li class="beast" id="cat">ねこ</li>
<li class="beast" id="fish" title="ねこの友人">さかな</li>
</ul>
diffコマンドなどで差分を取ると、以下のように表示されます。
<ul>
<li class="beast" id="cat">ねこ</li>
- <li class="beast" id="fish">さかな</li>
+ <li class="beast" id="fish" title="ねこの友人">さかな</li>
</ul>
一目ではどこが変わったのかわかりづらいですね。*3
では、もう少し改行してみます。以下の例ではどうでしょうか。
<ul>
<li class="beast"
id="cat">ねこ</li>
<li class="beast"
id="fish">さかな</li>
</ul>
属性の先頭が同じ位置に来るようにしています。
さかなにtitle属性を追加すると以下のようになります。
<ul>
<li class="beast"
id="cat">ねこ</li>
<li class="beast"
id="fish"
title="ねこの友人">さかな</li>
</ul>
title属性を追加する前後の差分は以下です。
<li class="beast"
id="cat">ねこ</li>
<li class="beast"
- id="fish">さかな</li>
+ id="fish"
+ title="ねこの友人">さかな</li>
</ul>
先ほどよりは差分が少なくなりましたが、まだ余分なところが含まれています。
差分がもっと少なくなるように改行した例は以下です。
<ul>
<li
class="beast"
id="cat"
>ねこ</li>
<li
class="beast"
id="fish"
>さかな</li>
</ul>
さかなにtitle属性を追加したコードは以下です。
<ul>
<li
class="beast"
id="cat"
>ねこ</li>
<li
class="beast"
id="fish"
title="ねこの友人"
>さかな</li>
</ul>
差分は以下の通りです。
<li
class="beast"
id="fish"
+ title="ねこの友人"
>さかな</li>
</ul>
差分が追加された箇所のみになりました。*4
どこまでやるかは好みにもよるので、導入の前にプロジェクトメンバーで意見を出し合ってみるとよいかもしれません。
みんなが納得できるようなルールができたら、コーディングスタイル(規約)として共有(公開)してみてください。
*1 一目でわかりづらい理由としては、要素同士、属性同士の距離が離れてしまっているということが挙げられます。
*2 ただし、リストでない要素の間に改行を入れると、ブラウザ上での見た目に半角スペースが表示されてしまう場合があります。CSSを当てていれば問題にならないことも多いですが、見た目が変わってしまう場合があるので注意してください。見た目が変わってしまうのを防ぐ方法としては、デプロイ時に改行やスペースを取り除く仕組み(minify)を入れておくというのが一例です。
*3 なぜかというと、変更されていない箇所まで差分になっている(色が付いている)からです。差分は行単位で表示されることが多く、一行が長いと変更されていない箇所まで差分になってしまいます。
*4 差分はわかりやすくなりましたが、grep検索する際に表示される部分が減ってしまうので、場合によっては不便かもしれません。その場合、grepのオプションに「-A」や「-B」、「-C」を指定することで、マッチした行の前後のコードを表示させることができます。
問題を調査する場合やプログラムを改良する場合、修正する場合などプログラムの挙動を把握しなければいけないことは多々あります。プログラムの挙動を把握する方法はいろいろあります。処理の流れを追う方法や特定の値に注目する方法や状態の変化を追う方法などです。
処理Aと処理Bの間でおかしくなっていることがわかっている場合は処理の流れを追う方法がよいでしょう。AからBの間の処理の流れを追っていけば問題の箇所を見つけられるからです。
特定の値がおかしくなっていることがわかっている場合はその値に注目して把握する方法がよいでしょう。その値が変わる可能性がある箇所に注目すれば問題を特定できます。
メインループ(GUIのイベントループや言語処理系の評価器のループなど)内の処理を把握する場合は状態の変化を追う方法がよいでしょう。状態を変えながら何度もループする処理では状態によって実行する内容が変わります。状態を把握することで実行する内容も把握しやすくなります。
ここでは特定の値に注目してプログラムの挙動を把握する場合にこんなコードだったら把握しやすいというコードを紹介します。これはコードを読む人視点の言い方です。コードを書く人視点で言うと、挙動を把握しやすいコードの書き方を紹介します、になります。
「特定の値に注目する」とはどういうことでしょうか。それはその値が変わるタイミングに注目するということです。
たとえば、次のようなクラスがあり、@age
の値に注目するとします。
class User
def initialize(age)
@age = age
end
def age
@age
end
def age=(new_age)
@age = new_age
end
end
@age
が変わる可能性がある箇所は以下の2箇所です。
def initialize(age)
@age = age
end
def age=(new_age)
@age = new_age
end
そのため、この2つのメソッドが呼ばれる箇所に注目します。なぜなら、ここでしか@age
は変わらないからです。ここに注目しておかしな値を設定している処理を見つけることで問題を特定できます。
users = {}
users[:user1] = User.new(-1) # 注目
users[:user2] = User.new(14) # 注目
puts(users[:user1].age)
users.each do |key, user|
if user.age == -1
user.age = 29 # 注目
end
end
この例では@age
を変更できる箇所は2箇所でしたが、もっと変更できる箇所がある場合は注目する箇所も増えます。注目する箇所が増えるということは挙動を把握することが難しくなるということです。逆に言うと、変更できる箇所が少ないと挙動を把握しやすくなるということです。
ここまではコードを読む人視点の話です。
それでは、コードを書く人視点の話に入ります。
値を変更できる箇所が最小限になっているとコードを読むときに挙動を把握しやすくなるのでした。つまり、値を変更できる箇所が最小限なコードを書くと挙動を把握しやすいコードになります。
それでは、値を変更できる箇所を少なくする方法をいくつか紹介します。
前述の例ではコンストラクターとセッターメソッドが@age
を変更できる箇所でした。
def initialize(age)
@age = age
end
def age=(new_age)
@age = new_age
end
変更できる箇所を少なくするならage=
メソッドを提供しなければよいです。
class User
def initialize(age)
@age = age
end
def age
@age
end
end
これで変更できる箇所が1つ減りました。age=
メソッドがなくなったので使い方も変わります。
users = {}
users[:user1] = User.new(-1) # 注目
users[:user2] = User.new(14) # 注目
puts(users[:user1].age)
users.each do |key, user|
if user.age == -1
# ↓userを変更するのではなく新しくインスタンスを作ることになった
users[key] = User.new(29) # 注目
end
end
セッターメソッドを無くしても注目する箇所は3箇所から減っていないのですが、コードを読む側としてはうれしいことが増えました。それは、一度作ったインスタンスでは@age
は変わらないということです。
インスタンスは1度作られるといろいろな箇所で使われるでしょうが、インスタンスが作られる箇所はそれよりは少ないことが多いです。たとえば、以下の箇所でインスタンスを使っています。
users = {}
users[:user1] = User.new(-1)
users[:user2] = User.new(14)
puts(users[:user1].age) # 使っている
users.each do |key, user|
if user.age == -1 # 使っている
users[key] = User.new(29)
end
end
あれ、使っている箇所(2箇所)より作っている箇所(3箇所)の方が多いですね。。。実際のプログラムでは使っている箇所の方が多いはずです。。。
使っている箇所が多い前提で話を進めると、多いと@age
が変わる可能性があるかを検討することが大変になります。そのため、検討する箇所が少なくなるのはコードを読む側にとってうれしいです。
セッターメソッドを無くした結果、コンストラクターでだけ値を設定するようになりました。
class User
def initialize(age)
@age = age
end
def age
@age
end
end
@age
を変更する箇所を減らすということであれば、次のようにすることもできました。
class User
def initialize
end
def age
@age
end
def age=(new_age)
@age = new_age
end
end
使うときはこうなります。
user = User.new
user.age = 29
これよりもコンストラクターで値を設定するコードの方がよい理由は次の通りです。
@age
が変更されるタイミングはインスタンス作成時だけということが明確になる。(=インスタンスを作った後に変更されない。)@age
は必ず設定されていることが明確になる。(セッターメッソドを使う場合だとインスタンスを作った後に設定を忘れると設定されない。)つまり、コードを書いた人の意図が明確になるので読むときに助かるということです。
「変更しない値はコンストラクターでだけ設定する」はコードを書いた人の意図を明確にする書き方なので、「このクラスのインスタンスは値を変更しないぞ!」という意図でコードを書いているときは積極的に活用したい書き方です。
これまでの例では、あえて値が数値になるようにしていました。これは、Rubyでは数値は変更不可能なオブジェクトだからです。文字列は変更可能なオブジェクトなので事情が変わってきます。
たとえば、次のコードはmessage
メソッド経由でも@message
の値を変更できます。
class Commit
def initialize(message)
@message = message
end
def message
@message
end
end
次のようにすると変更できます。
commit = Commit.new("Hello")
puts(commit.message) # => Hello
commit.message << " World!"
puts(commit.message) # => Hello World!
これも防ぐようなコードにするかどうかはケースバイケース(オーバースペックの場合の方が多い)ですが、たとえば、次のようすればmessage
メソッド経由では@message
の値を変更できなくなります。
class Commit
def initialize(message)
@message = message.dup
@message.freeze
end
def message
@message
end
end
commit = Commit.new("Hello")
commit.message << " World!" # 例外:can't modify frozen String (RuntimeError)
Rubyは値(オブジェクト)を変更不能にする方法はfreeze
ですが、方法は言語ごとに違います。たとえば、CやC++ではconst
を使います。
コードを書くときに「変わらない値」とわかるような書き方にすることで、読むときに挙動を把握しやすいコードになるということを紹介しました。
いくつか書き方を紹介しましたが、中でも、「変更しない値はコンストラクターでだけ設定する」書き方は書いた人の意図を明確にしやすい書き方なのでそのような場面になったら活用してください。
なお、この話題についてまとめるきっかけになったのは次のようなコードを見たことがきっかけでした。外部で作られているオブジェクトに新しくroute_key
というデータを付与したいというコードです。
route = create_output
route.instance_eval do
route.singleton_class.class_eval do
attr_accessor :route_key
end
end
route.route_key = key
このコードは最終的に次のようなコードになりました。セッターメソッドで値を設定するのではなくコンストラクターで値を設定するようになっています。
class Route
attr_reader :output
attr_reader :key
def initialize(output, key)
@output = output
@key = key
end
end
output = create_output
route = Route.new(output, key)
2016年9月1日に開催されるSpeee Cafe Meetup #02で須藤がOSS開発について話します。
主催のSpeeeさんは開発力向上のために開発者がOSSの開発に参加することを推進しています。クリアコードはそんなSpeeeさんの取り組みをサポートしています。その縁で今回話すことになりました。
OSSの開発に参加するメリットとOSS開発に参加する最初の一歩を支援する取り組みであるOSS Gateを紹介する予定です。OSSの開発に興味がある方はぜひお越しください。
OSS Gateについては以下の資料を参考にしてください。
9月8日から10日の3日間RubyKaigi 2016が開催されます。
クリアコードは例年RubyKaigiのスポンサーをしています。去年のRubyKaigi 2015のスポンサーに引き続き、RubyKaigi 2016もスポンサーになりました。今年はブースを出します。ブースではOSS Gateとクリアコード自体のことを紹介する予定です。クリアコードが気になる人は遊びに来てください。
また、須藤と沖元がスピーカーとして話します。どちらも2日目の9月9日です。須藤は10:30からのセッションで最新のバインディングの作り方について話します。沖元は14:20からのセッションでRubyリファレンスマニュアルについて話します。
ところで、8月21日にリリースされたRubyist Magazine 0054号では東京 Ruby 会議 11 運営記録がよかったです。この記事の中に事前インタビューの目的が書いてあります。4つ目の目的として「発表者に事前インタビューすることで、発表の内容を事前に考えて貰う」が挙がっています。RubyKaigi 2016では事前インタビューはありませんが、ここに発表内容を事前にまとめることで同じ目的を達成できるはずです。ということで、須藤の発表内容を紹介します。
バインディングとはRuby以外の言語(主にC言語とC++言語)で実装された機能をRubyで使うためのライブラリーです。バインディングを使うと、Rubyでその機能を実装しなくてもその機能を使えるようになるため、短時間で高品質の機能が手に入(ることがあ)ります。また、C言語での実装はRubyでの実装よりも10倍100倍速いことがざらにあるので、性能面でも有利です。画像処理・動画処理・音声処理・全文検索・機械学習・統計処理・暗号処理などは実装コスト・性能の両面からバインディングを利用して既存実装を活用するのが向いている分野です。
というような背景があり、RubyをWebだけでなく様々な分野で活用するためにはバインディングは重要です。そんなバインディングの作り方を紹介するのが須藤の話です。
以下、説明が続きますが、文章だけで図がないため関係がわかりにくいかもしれません。RubyKaigi 2016までには図も用意する予定です。
バインディングを作るために使える仕組みが拡張ライブラリーです。拡張ライブラリーはC言語で実装された機能をRubyに組み込む仕組みです。C言語で既存の機能をラップする拡張ライブラリーを作ることでバインディングになります。
拡張ライブラリーの難点の1つに「インストールするためにはCコンパイラーが必要なこと」があります。拡張ライブラリーを使うためにはコンパイルする必要があるため、Cコンパイラーがないと拡張ライブラリーを使えません。GNU/LinuxやBSD系などCコンパイラーを用意しやすいOSもあればそうでないOSもあります。たとえば、WindowsではCコンパイラーを用意することの敷居が高く、インストールが難しいです。
この問題を解決する方法としてfat gemというやり方があります。これはgemの中にビルド済みバイナリーを入れるというやり方です。こうすることにより、ユーザーの環境にCコンパイラーがなくても拡張ライブラリーを使えるようになります。fat gemはWindowsユーザー向けだけに用意すればよいです。Windows以外のよく使われている環境ではCコンパイラーを用意することは比較的簡単だからです。
拡張ライブラリーの難点はもう1つあります。バインディング対象の機能数に応じて実装コストがあがる点です。Rubyが拡張ライブラリー用に提供しているAPIは比較的使いやすいため、拡張ライブラリーとしてバインディングを作ることは技術的にそれほど難しくありません。ただし、対象機能を1つずつラップするため、対象機能が多いとその分手間がかかるのです。
この難点を解決する方法としてバインディングを生成する方法があります。拡張ライブラリーによるバインディングの実装にはある程度パターンがあるので生成することができるのです。SWIGというツールはそれを実現するためのツールです。
SWIGを使うと(多くの場合は)少ない記述でバインディング用の拡張ライブラリーのコードを生成できます。これで、少ない手間でたくさんの機能のバインディングを作ることができます。ただ、SWIGを使った場合でもいくつか難点があります。
SWIGも拡張ライブラリーを使ったバインディングなのでインストールが難しい問題があります。この問題をfat gemというやり方で解決できるのは前述のとおりです。しかし、SWIGを使った場合は別の問題もあります。インストール時にCコンパイラーとSWIGが必要になる点です。通常、SWIGはシステムにインストールされていないので別途インストールする必要があります。インストール時にSWIGを必要なくする方法があります。SWIGで生成した拡張ライブラリーのコードをgemに含める方法です。こうすることでインストール時にSWIGは必要なくなります。ただし、gemに含めたときに生成した機能しか含まれません。gemに含めたときの対象ライブラリーはバージョン1で、ユーザーがインストールしようとしたときはバージョン2になっていた、という場合、バージョン2で追加された機能は使えない、ということです。
性能面でも幾分オーバーヘッドがあります。手書きでシンプルに実装したバインディングとSWIGで自動生成したバインディングではシンプルに実装したバインディングの方が高速です。バインディングのメソッドを大量に呼び出す(何万回とか)場合は影響があるでしょう。
これまでのバインディングの作成方法ではインストールの難しさが難点としてでていました。これはC言語を使っていることが原因です。つまり、C言語を使わずにバインディングを作れればインストールが簡単になります。その方法はFFI(Foreign Function Interface)を使う方法です。なお、最近のRubyで使えるFFIの実装はすべてlibffiというライブラリーを使っています。
FFIを使うとRubyでバインディングを書くことができます。拡張ライブラリーでやっていたことをRubyで書けるようになっただけで、1つずつ機能をラップしていかなければいけない点は変わりませんが、それをCではなくRubyで書けるようになることで書きやすくなっています。
バインディングにC言語を使わなくなったことによりバインディングのインストールは確かに簡単になるのですが、バインディングが動くためにはラップ対象のライブラリーは依然として必要です。そのため、Windowsユーザーがインストールしにくいという点はそれほど解決されていません。
また、バインディングを書く言語がCからRubyになっているので手間は減ってはいますが、対象の機能がたくさんあると結局手間なのは変わりません。
なお、FFIを使うとSWIGを使ったときよりも性能が落ちます。
SWIGが拡張ライブラリーのコードを生成するように、対象ライブラリーの情報を元にFFIを使ったバインディングを生成する方法があります。それがGObject Introspectionというライブラリーを使う方法です。
GObject Introspectionを使うと実行時に対象ライブラリーの情報を使ってバインディングを作成できます。古いバージョンと新しいバージョンを同時にサポートするというSWIGではインストールの難易度が上がるために難しかったことを容易に実現できます。
対象ライブラリーをgemに含めなければインストールが難しいという点はFFIを使ったバインディングと同じです。ただし、対象ライブラリーもgemに含める実装がRuby-GNOME2プロジェクト内にあるため、これを活用することで対象ライブラリーも含んだfat gemを提供できます。
GObject IntrospectionはRubyだけで使えるライブラリーではなく、PythonやJavaScriptなど他の言語でも使えるライブラリーです。これは、対象ライブラリーがGObject Introspectionをサポートしていればいろんな言語のバインディングがすぐに用意できるということです。これまで、バインディングは各言語ごとにそれぞれがんばって作成するものでしたが、GObject Introspectionベースにすることにより他の言語のユーザーと協力してバインディングを作成することができます。
以上のように、バインディング作成コスト(自動生成ベース+他言語ユーザーと協力でコスト削減)とインストールの容易さという観点からGObject Introspectionを使ったバインディングの作り方が魅力的です。しかし、難点もあります。それは、(現時点ではまだ)遅いこととGObject Introspection対応ライブラリーは限られているという点です。
性能に関してはRubyKaigi 2016までに改善する予定です。
というような話をする予定です。
クリアコードは9月8日から10日にかけて開催されるRubyKaigi 2016にスピーカー・スポンサーとして参加します。この機会に多くの人と交流できることを期待しています。それでは、RubyKaigi 2016で会いましょう。
クリアコードではFluentdというソフトウェアの開発に参加しています。Fluentdはログ収集や収集したデータの分配・集約などを行うソフトウェアです。
Fluentdのv0.14はv0.12とある程度の後方互換性が保たれているメジャーバージョンアップです。
v0.14での新機能を使ったプラグインを作成する際にはこれまでの Fluent
以下のクラスではなく、Fluent::Plugin
以下のクラスを継承し、実装する必要が出てきました。
また、v0.14からはプラグインでよく書かれるコードをカプセル化し共通に使えるヘルパーを提供することで、よりプラグイン開発者が簡潔で良くテストされたコードを使ってプラグインが開発出来るようになる、とアナウンスされています。*1
Inputプラグインをv0.14のプラグインに移行する際には Fluent::Input
の継承を止め、Fluent::Plugin::Input
を継承するようにします。
また、Fluentdのテストを読むと、v0.14でのInputプラグインのテストにはfluent/test/driver/input.rbにある Fluent::Test::Driver::Input
クラスのテストドライバを用いるようにすると良いことがわかります。
driver#run の書き方がv0.12と比べて変わっていると言う点に注意しなければなりません。
v0.14のテストドライバでは driver#run
の終了条件が確定しない場合は例外が上がるようになっています。 *2
そのため、v0.14向けのInputプラグインのテストでは driver#run
へブロックを渡すか、 driver#end_if
で終了条件を指定することが必要です。
fluent/input
から fluent/plugin/input
へ変更するTime.now
をナノ秒に対応した現在時刻を返す Fluent::EventTime.now
に置き換える *3という点に注意してInputプラグインのv0.14への移行作業を行います。
実際に、in_object_spaceプラグインをv0.14化したプルリクエストを見てみましょう。
1.と2.は見ての通りほぼそのままなので、とくにここでは詳細に解説しません。 Inputプラグインのv0.14への移行作業は3.の作業と4.の作業が特に重い作業です。
3.の作業に該当するものは、in_object_spaceプラグインをv0.14化したプルリクエストでは、timerヘルパーの使用にあたります。 v0.14ではタイマーを用いてイベントを発生させるのに汎用的なtimerヘルパーが提供されており、v0.12の頃はCool.ioのクラスを継承したクラスを作成してこの手の処理を行うコードを書く必要がありました。v0.14ではヘルパーを使うだけになっています。
また、プラグインヘルパーは helpers
に :inject
のように使いたいプラグインヘルパー名をシンボルで渡す形式になっています。
4.の作業では、既に driver#run
にブロックが渡されていたため、driver#emits
を driver#events
に書き換える作業と、v0.14のInput Driverクラスの Fluent::Test::Driver::Input
を用い、プラグインのクラスをv0.14のテストドライバに渡す作業のみでした。v0.14でのInputプラグインのテストドライバを利用するには fluent/test/driver/input
をrequireする必要があります。
v0.14のOutputプラグインが継承するべき Fluent::Plugin::Output
クラスは実装されているメソッドによって
の3つのOutputプラグインの性質を持つようになります。
また、v0.14でのOutputプラグインのテストドライバを利用するには fluent/test/driver/output
をrequireする必要があります。
driver#run
の書き方が変更になっており、例えば
driver.run(default_tag: 'test') do
driver.feed(time, {"a"=>1})
driver.feed(time, {"a"=>2})
end
のように、driver#run
のdefault_tag:
キーワード引数にemitする時のタグを渡したり、driver#emit
ではなくdriver#feed
を用いてイベントを流し込む必要があるのに注意してください。
流し込まれたイベントはInputと同様に driver#events
で取得することができます。
まずは、1.の場合のv0.14への移行のプルリクエストを見ていきます。
この場合は #process
メソッドのみOutputプラグインが実装する必要があります。
また、driver#run
の新しい書き方に対応させました。
out_fileのv0.14化対応のプルリクエストを見ていきます。
Buffered Synchronous Outputのv0.14のプラグインは #write
メソッドと <buffer>
セクションを解釈出来るようにするか、compat_parametersプラグインヘルパーをプラグイン中で使用するようにします。
このプルリクエストではTimeSlicedOutputを継承したOutputプラグインのv0.14化対応をしています。 また、Outputプラグインからfomatterプラグインやbufferプラグインを使う事ができるため、それに関するプラグインヘルパーのconfig sectionの追加も行っています。 v0.14形式とv0.12形式のconfigは書き方が大幅に異なっています。*4
ですが、その差異を埋めるcompat_parametersプラグインヘルパーもあります。
このプラグインヘルパーの compat_parameters_convert
メソッドを使う事により、v0.12形式のconfigでもv0.14のFluentdで引き続き使う事ができます。
Buffered Asynchronous Outputのv0.14のプラグインは #try_write
メソッドと <buffer>
セクションを解釈出来るようにするか、compat_parametersプラグインヘルパーをプラグイン中で使用するようにします。
また、 prefer_delayed_commit
でtrueを返すようにします。
まだこの非同期コミットに絞ったv0.14のプラグインはこの記事の執筆時点では書かれていません。
実は、Buffered Outputプラグインはsynchronousとasynchronousの両方の機能をconfigで切り替えられるように書く事ができます。
Buffered Outputに対応したテスト用のプラグインを追加したプルリクエストを見てみます。
ここでは、Fluent::Plugin::Output
を継承したOutputプラグインを追加しています。そこで#write
、#try_write
、そして、#prefer_delayed_commit
をそれぞれ実装しています。
このプラグインではバッファのコミットを非同期にする設定を追加してはいませんが、その設定をconfigからできるようにすることでバッファを同期的または非同期的にコミットする動作をconfigで切り替えられるプラグインを書く事ができます。
また、v0.14のOutputプラグインはレコードのformatを行うためにプラグイン固有の#format
メソッドも定義しておく事が可能です。
ParserプラグインはParserプラグイン単体で使われるプラグインではなく、Input・Outputプラグインなどから使われるOwnedプラグインと呼ばれる範疇のプラグインです。
これらParserプラグインのインスタンスはv0.14ではparserヘルパーの parser_create
メソッドを用いて作成することが推奨されます。
v0.14のParserプラグインは Fluent::Plugin::Parser
クラスを継承する必要があります。
Parserプラグインが持つべきメソッドは #parse
のみです。また、v0.14対応をするには fluent/test/driver/parser
にある Fluent::Test::Driver::Parser
クラスのParserテストドライバを用いる必要があります。
v0.12では driver#parse
にてパース結果をテストする形になっていましたが、v0.14では driver#instance
によりParserプラグインのインスタンスを取り出してからパーサープラグインのインスタンスの #parse
メソッドを呼ぶような規約に変更になったので注意が必要です。
FormatterプラグインはFormatterプラグイン単体で使われるプラグインではなく、Input・Outputプラグインなどから使われるOwnedプラグインと呼ばれる範疇のプラグインです。
v0.14のFormatterプラグインは Fluent::Plugin::Formatter
クラスを継承する必要があります。
これらFormatterプラグインのインスタンスはv0.14ではformatterヘルパーの formatter_create
メソッドを用いて作成することが推奨されます。
v0.14において、Formatterプラグインが持つべきメソッドは #format
です。
また、v0.14対応をするには fluent/test/driver/formatter
にある Fluent::Test::Driver::Formatter
クラスのFormatterテストドライバを用いる必要があります。
v0.12ではテストドライバを使う事は少なかったのですが、
v0.14では driver#instance
によりFormatterプラグインのインスタンスを取り出してからパーサープラグインのインスタンスの #format
メソッドを呼ぶとformat後の値が取得し、テストするようになったので注意が必要です。
v0.14のFilterプラグインは Fluent::Plugin::Filter
クラスを継承する必要があります。
また、v0.14でのFilterプラグインのテストドライバは fluent/test/driver/filter
をrequireする必要があります。
v0.12の頃は #filter
よりも #filter_stream
の方が10%程度高速だったため高速化のために利用されていましたが、v0.14ではfilterをパイプライン化できる時はする機能がサポートされることになり、 #filter
を使うようにすることが推奨されます。 #filter_stream
は互換性のために残されますが、非推奨扱いになります。
driver#run
の書き方が変更になっており、例えば
driver.run do
driver.feed('test', time, {"a"=>1})
driver.feed('test', time, {"a"=>2})
end
のように、driver#emit
ではなくdriver#feed
を用いてイベントを流し込む必要があるのに注意してください。
流し込まれてFilterされた後のイベントは driver#filtered_records
で取得することができます。
Fluentdのv0.12向けに書かれたプラグインのv0.14対応の概要について説明しました。Fluentd向けに多くのプラグインが公開されていますが、v0.12の書き方のままで公開されているプラグインが多くあるのが現状です。 Fluentdのプラグインのv0.14化対応はやり方を把握すれば挑戦できないことはないので、v0.14らしい書き方をしてみたいFluentdのプラグイン開発者やプラグインを良くしたいユーザーの方々はv0.14のAPIを使ってみるといいのではないでしょうか。 この記事ではプラグインヘルパーについては深入りする事ができませんでした。プラグインヘルパーについてはまたの機会に説明することとします。 最後に、v0.14でこのプラグインが動いていないというIssueを上げるだけでもありがたいのでそのようなフィードバックをして頂けると幸いです。
*1 http://www.slideshare.net/tagomoris/fluentd-overview-now-and-then
*2 https://github.com/fluent/fluentd/blob/12718a218a1e78126108a573b85d4b18e8bd56d5/lib/fluent/test/driver/base.rb#L134
*3 `Fluent::Engine.now`は内部的に`Fluent::EventTime.now`を呼んでいるのでこのままでよいようです。
*4 例えば、formatterやbufferも一つのconfigセクションとして書けるようになっています。
クリアコードではOSS開発支援サービスの一環でTreasureDataさんが中心になって開発し公開しているFluentdとそのプラグインなど*1の開発を支援しています。 かれこれ一年ほどやってきたので、どのようなことをやってきたのかふりかえります。
TreasureDataさんの目的はFluentdの普及促進です。Fluentdが普及すると、TreasureDataさんのサービスを利用する人が増えるためです。
クリアコードはTreasureDataさんの目的を達成するために以下のことをやっています。
直接依頼されたことだけでなく、Fluentdの普及促進につながることを積極的にやっています。
なお、TreasureDataさんとは基本的にはIssue/PullRequestでやりとりしています。作業の優先順位やIssue/PullRequestで相談しづらいことを相談するときはSlackを使用しています。
IssueやPRにコメントを書くことによって、コミュニティが活発になります。コミュニティが活発になると次のような理由で新規ユーザーを惹き付ける要因の一つになります。よってFluentdの普及促進につながります。
コメントを書いたIssueやPRのうち特に問題の解決につながったものをリストアップしました。
これらは依頼されて引き取ったものもありますが、Fluentdが内部で利用しているライブラリもあります。 こういったプロジェクトを引き取りメンテナンスを継続することによって、既存ユーザーは安心して使い続けることができますし、新規ユーザーも安心して使い始められます。安心して使えていると既存ユーザーは他の人から意見を求められたときに「安心して使える」という情報を伝えてくれます。このようにFluentdの普及促進につながります。
既存のFluentdのままでもかなり便利ですが、以下の点をより強化することによりさらにFluentdの普及促進につながります。
依頼されたものだけでなく、特にFluentdの普及推進につながるものをリストアップしました。
--show-plugin-config
オプションを追加しました
バグを修正することにより、それが原因で使えなかったケースでもFluentdを活用できるようになり、Fluentdの普及促進につながります。また、既存ユーザーは運用で回避せずに済むため、よりFluentdを楽に運用できるようになります。このような既存ユーザーが情報共有といった形でコミュニティで活躍してくれるとコミュニティがさらに活発になりFluendの普及促進につながります。
依頼されたものではなく、自分たちで見つけたバグを修正したものをリストアップします。
QiitaやククログでFluentd関連の技術情報を発信しています。
他にも以下のことを実施しました。
secret
パラメータ対応desc
対応これらは少し遠回しなものもありますが、次の理由でFluentdの普及促進につながると考えて実施しました。
このようにクリアコードではOSSの開発支援を実施しているので、OSSの開発をしているけれど手が足りなくて思うように進められないといったことがあれば相談してみてください。 相談はフォームからメッセージを送ってください。
クリアコードではOSS開発支援サービスの一環でTreasureDataさんが中心になって開発し公開しているFluentdとそのプラグインなど*1の開発を支援しています。 Fluentd v0.14からはプラグインでよく使われる定型の処理をより簡易に扱えるようにするプラグインヘルパーが実装されました。 この記事ではv0.14の執筆時点のプラグインヘルパーの使用方法の概説を行います。また、プラグインヘルパーを解説した各節の最後には実際の使われている箇所の一例を示します。
プラグインヘルパーは以下のようにhelpersに可変長引数で指定します。
helpers :plugin_name1, :plugin_name2, ..., plugin_nameN
また、helpersは複数回呼ぶこともできます。プラグイン毎にhelpersで使いたいプラグインヘルパーを可変長引数で指定するか複数回指定するかのポリシーを決めておくとよいでしょう。
プラグインから子プロセスを起動する定型の処理をカプセル化し、より簡易に扱えるようにするためのプラグインヘルパーです。
helpers :child_process
# ...
def start
super
child_process_execute(title, command[, ...]) do |io|
end
# ...
end
# ...
end
のように使用します。一定間隔で子プロセスを起動するには
config_param :command, :string
config_param :run_interval, :time
# ...
def start
super
child_process_execute(:example_child_process, @command, interval: @run_interval, mode: [:read]) do |io|
end
# ...
end
# ...
end
のようにします。一回だけ起動する場合は
config_param :command, :string
config_param :run_interval, :time
# ...
def start
super
child_process_execute(:example_child_process, @command, immediate: true, mode: [:read]) do |io|
end
# ...
end
# ...
end
のように使用します。このプラグインヘルパーは例えばin_execプラグインで使用されています。
第一引数はシンボルである必要があります。また、この引数で渡されたシンボルはグローバルになるのでプラグイン名_使用用途としておくことが推奨されています。
v0.12のconfigのflat styleからv0.14のstructured styleへ自動的にマッピングさせるためのプラグインヘルパーです。
v0.12のconfigではbufferのconfigは以下のような形式でした。
<match **>
@type file
path /path/to/file/*.log
buffer_type file
buffer_path /path/to/buffer/*.log
time_sliced_format %Y%m%d%H # sliced per hour
buffer_chunk_limit 16m
</match>
v0.14のconfigでは以下のような形式で書く必要があります。
<match **>
@type file
path /path/to/file/*.log
<buffer>
@type file
path /path/to/buffer/*.log
time_key 60m # sliced per hour
chunk_size_limit 16m
</buffer>
</match>
compat_parametersプラグインヘルパーを使用する事でflat styleからstructured styleへ自動的にマッピングできます。 以下、その概要です。
helpers :compat_parameters
# ...
def configure(conf)
# ...
compat_parameters_convert(conf, :buffer)
super
# ...
end
のように使用します。また、このプラグインヘルパーは後述するformatterやinject、parserプラグインと一緒に使用することがあります。
その場合は、
helpers :inject, :formatter, :compat_parameters
# ...
def configure(conf)
# ...
compat_parameters_convert(conf, :inject, :formatter)
# ...
end
のように、conpat_parametersを通して変換したいプラグインのtypeを指定します。 例えばfilter_stdoutプラグインがこのプラグインヘルパーを使用しています。
v0.14のFluentdの Fluent::Plugin::Output
クラスはデフォルトで emit
が使用できなくなりました。
v0.14のOutputプラグインでは Engine.emit
を直接呼べなくなるので router.emit
を行いたい場合はこのプラグインヘルパーを使う必要があります。
使い方は
helpers :event_emitter
とするのみです。
v0.12のOutputプラグインの時と同じように router.emit
を呼べるようになります。
このプラグインヘルパーは例えばout_relabelプラグインヘルパーで使用されています。
event_loopヘルパーは Coolio::Loop
クラスをカプセル化し、より手軽に扱えるようにしたプラグインヘルパーです。
helpers :event_loop
# ...
def start
# ...
@handler = ...
event_loop_attach(@handler)
# ...
end
のようにして使います。 @handler
の中身は Fluent::SocketUtil::UdpHandler
や Coolio::TCPServer
などのインスタンスを入れることになります。
このプラグインヘルパーは後述のtimerプラグインヘルパーの内部で使われているため、暗黙的に使われていることが多いです。
in_syslogプラグインは明示的にこのプラグインヘルパーを使っています。
formatterを使うにはv0.12では Fluent::Plugin.new_formatter
を呼び、Formatterのインスタンスを作成する一連の処理を書く必要がありました。
この処理ををカプセル化し、より手軽にformatterをプラグイン中で使用するためのプラグインヘルパーです。
このプラグインヘルパーは
helpers :formatter
DEFAULT_FORMAT_TYPE = 'out_file'
# ...
def configure(conf)
# ...
super
@formatter = formatter_create(usage: 'example_format', conf: conf.elements('format').first, default_type: DEFAULT_FORMAT_TYPE)
# ...
end
def format(tag, time, record)
# ...
@formatter.format(tag, time, record)
# ...
end
のようにして使います。
また、実際のFluentdのプラグインでv0.12形式のconfigも扱う必要がある場合はcompat_parametersプラグインと一緒に以下のようにして使います。
helpers :compat_parameters, :formatter
DEFAULT_FORMAT_TYPE = 'out_file'
# ...
def configure(conf)
compat_parameters_convert(conf, :formatter)
super
@formatter = formatter_create(usage: 'example_format', conf: conf.elements('format').first, default_type: DEFAULT_FORMAT_TYPE)
# ...
end
def format(tag, time, record)
# ...
@formatter.format(tag, time, record)
# ...
end
formatterプラグインヘルパーは多くのプラグインで使用されています。compat_parametersプラグインヘルパーと組み合わせて使っている例として、filter_stdoutプラグインがあります。
また、Fluent::Plugin::Output
クラスを継承している場合、formatter#format
はプラグイン中の #format
を実装している場合、このメソッドがバッファを書き出す前に呼ばれるようになります。
#format
にレコードのフォーマット操作のみの役割を持たせる場合、テストもしやすくなるのでおすすめです。
第一引数は文字列である必要があります。また、この引数で渡された文字列はグローバルになるのでプラグイン名_使用用途としておくことが推奨されています。
injectプラグインヘルパーはホスト名、タグ、時刻をレコードへ挿入するためのプラグインヘルパーです。 このプラグインヘルパーは
helpers :inject
# ...
def format(tag, time, record)
record = inject_values_to_record(tag, time, record)
# ...
end
のようにして使用します。基本的にrecordにアクセス出来る箇所であればどこでも使えますが、テストコードの関係上#format
メソッドの中で利用するのが良いでしょう。
このプラグインヘルパーはv0.12の SetTagKeyMixin
や SetTimeKeyMixin
や hostname
プレースホルダーの置き換えを狙ったものです。
このプラグインヘルパーは例えば、out_stdoutプラグインで使用されています。
3rdパーティ製のプラグインでもこのプラグインヘルパーの使いどころはかなりあるはずなので、使えそうな箇所を探してみてください。
formatterを使うにはv0.12では Fluent::Plugin.new_parser
を呼び、Parserのインスタンスを作成する一連の処理を書く必要がありました。
この処理ををカプセル化し、より手軽にParserをプラグイン中で使用するためのプラグインヘルパーです。
このプラグインヘルパーは
helpers :parser
DEFAULT_PARSER_TYPE = 'syslog'
# ...
def configure(conf)
# ...
super
@parser = parser_create(usage: 'example_parse', type: DEFAULT_PARSER_TYPE, conf: conf)
# ...
end
def do_something(text)
# ...
@parser.parse(text) do {|time, record|
# ...
}
# ...
end
のようにして使われます。 このプラグインヘルパーは使われ方が若干特殊ですがin_syslogプラグインで使われています。
第一引数は文字列である必要があります。また、この引数で渡された文字列はグローバルになるのでプラグイン名_使用用途としておくことが推奨されています。
retry_stateプラグインヘルパーはOutputプラグイン等でリトライ処理を行う際の決まったコードをカプセル化し、より簡易に利用出来るようにする機能を提供します。 このプラグインヘルパーはOutputプラグインのリトライ処理を切り出したもので、組み込み以外のプラグインでも組み込みのプラグインと同じようなリトライのロジックをより簡易に扱えるようになります。
helpers :retry_state
# ...
config_section :buffer, param_name: :buffer_config, init: true, required: false, multi: false, final: true do
config_param :retry_timeout, :time, default: 72 * 60 * 60
config_param :retry_type, :enum, list: [:exponential_backoff, :periodic], default: :exponential_backoff
config_param :retry_wait, :time, default: 1
end
# ...
def start
super
@retry_state = retry_state_create(
:example_retries, @buffer_config.retry_type, @buffer_config.retry_wait, @buffer_config.retry_timeout
)
# ...
end
のようにして使います。このプラグインヘルパーはOutputプラグインの継承元クラスを実装しているoutput.rbで使用されています。
v0.14ではプラグインの状態をKey-Value形式で保存するのに用いるStorageプラグインが新たに導入されました。 storageプラグインヘルパーはv0.14で新たに導入されたこのプラグインを使用する際の決まったコードをカプセル化し、より簡易に利用出来るようにする機能を提供します。
このプラグインヘルパーは
helpers :storage
DEFAULT_STORAGE_TYPE = 'local'
#...
def configure(conf)
super
@storage = storage_create(usage: 'example_storing_value', conf: config, default_type: DEFAULT_STORAGE_TYPE)
end
def start
super
@storage.put(:example_value, 0) unless @storage.get(:example_value)
# ...
end
def do_something
@sutorage.update(:example_value){|v| v + 1 }
end
のようにして使用します。 このプラグインヘルパーはin_dummyプラグインのsuspendオプションの実装に使用しました。
第一引数は文字列である必要があります。また、この引数で渡された文字列はグローバルになるのでプラグイン名_使用用途としておくことが推奨されています。
threadプラグインヘルパーはプラグインで新たにThreadを立てる必要がある際の決まったコードをカプセル化し、より簡易に利用できるようにする機能を提供します。
このプラグインヘルパーは
helpers :thread
# ...
def start(conf)
super
thread_create(:example_usage, &method(:run))
# ...
end
def run
# executed on other thread
end
# And thread lifecycle is managed by thread plugin helper automatically.
のようにして利用します。 thread_create
のブロックは必須です。このブロックが別スレッドで実行されます。
このプラグインはevent_loopプラグインヘルパーの中で使用され、また、timerプラグインがevent_loopプラグインヘルパーに依存しているため、知らず知らずの内に使っている事が多いです。
明示的に使用している例としてはin_dummyプラグインがあります。
第一引数はシンボルである必要があります。また、この引数で渡されたシンボルはグローバルになるのでプラグイン名_使用用途としておくことが推奨されています。
timerプラグインヘルパーはプラグインで新たに高精度なタイマーを実装する必要がある際の決まったコードをカプセル化し、より簡易に利用できるようにする機能を提供します。
このプラグインヘルパーは
helpers :timer
# ...
config_param :emit_interval, :time, default: 60
#...
def start(conf)
super
timer_execute(:example_timer, @emit_interval, &method(:on_timer))
# ...
end
def on_timer
# periodically execution block by timer
end
# And timer lifecycle is managed by timer plugin helper automatically.
のようにして利用します。 timer_execute
のブロックは必須です。このブロックが別スレッドで実行されます。
また、一回のみの実行で良い場合は repeat: false
をtimer_executeに渡します。
timer_execute(:example_timer, @emit_interval, repeat: false) do
# one-shot timer execution block
end
in_gc_statプラグインではこのプラグインヘルパーを使っています。
第一引数はシンボルである必要があります。また、この引数で渡されたシンボルはグローバルになるのでプラグイン名_使用用途としておくことが推奨されています。
執筆時点でのプラグインヘルパーの使い方と使われている箇所について解説しました。 v0.14ではプラグインでよく使われる定型の処理についてより簡易に扱えるようにプラグインヘルパーが実装されました。 プラグインでより高度なことをするのにためらっていたプラグイン開発者はv0.14向けのプラグインではこれらのプラグインヘルパーを使い、よりよいプラグインを目指されてみてはいかがでしょうか?
*1 fluent-plugin-xxxやfluent-logger-xxxが多い