最近、弊社でも Slack が使われはじめている。 弊社のアラート通知は今まで IRC (とメール)にだけ流していたが、Slack にも流せるように Ikachan 互換 API を持ち、IRC, Slack 両方に流せるサーバとしてDaioikachan というものを作った。

要件

以下のような要件を満たす

  • Ikachan 互換 API
  • 複数バックエンド対応
    • Slack だけではなく、IRCにも流す(移行期間中用。いずれは Slack に完全移行したい)
  • ルーティング機能
    • どのチャンネルをどのSlackチームに流すかルーティング設定できる
  • プラグイン機構
    • IRC, Slack 以外のバックエンドにも対応出来る(せっかくなので)

特に、弊社では草の根活動的に、Slack 利用が広まったという背景もあり、チーム毎に別の Slack チーム(ドメイン) を使っていることがある。 そのため、「このチャンネルは、あのSlackチームのあのチャンネル」という風にルーティングできる機能が欲しかった。

設計

以下のような設計にした

  • Ruby 1プロセスで捌く。ミドルウェアに依存しない。
    • アプリの特徴として IRC, Slack といった外部リソースに対して I/O ブロックされるが、CPU リソースを多く使うようなものではないので1プロセスで十分そう
  • バックエンドへの post は非同期に捌く
    • Web API は、enqueue だけして即座にレスポンスを返す
    • IRC, Slack といったバックエンドへの書き込みは、非同期に処理する(non-blocking I/O or スレッド)
    • 投稿制限があるので sleep をいれてゆっくり処理する
  • データの validation は(そんなに)やらない
    • バックエンドへの post が失敗した場合にログに warn を吐いて、ログを別途監視して対応
  • 設定ファイルでルーティングを記述できる

使い方

README にも書いているけど、いちおう書いておくと、こんなかんじ

$ gem install daioikachan
$ daioikachan -g daioikachan.conf # サンプル設定生成
$ vim .env
IRC_SERVER=XX.XX.XX.XX
SLACK_API_TOKEN=XXX-XXXXX-XXXXXX-XXXXX
$ daioikachan -c daioikachan.conf # 起動

で、あとは

$ curl -d "channel=#channel&message=test message" http://localhost:4979/notice

とかするだけ。デフォルト daioikachan.conf は IRC と Slack 両方に投げるけど、Slack だけに投げたかったら設定ファイルをいじってIRC側を消してもらえればいい。

daioikachan.conf

IRCと2つのSlackチームに流すサンプルはこんなかんじになる。あれ、なんか Fluentd っぽい。

<source>
  type daioikachan
  bind 0.0.0.0
  port 4979
  min_threads 0
  max_threads 4
  backlog 1024
  @label @raw
</source>

<label @raw>
  <match **>
    type copy
    <store>
      type stdout
    </store>
    <store>
      type relabel
      @label @slack
    </store>
    <store>
      type relabel
      @label @irc
    </store>
  </match>
</label>

<label @irc>
  <match **>
    type irc
    host "#{ENV['IRC_SERVER']}"
    port 6667
    nick daioikachan
    user daioikachan
    real daioikachan
    command %s
    command_keys command
    channel %s
    channel_keys channel
    message %s
    out_keys message
    send_interval 2s # IRC would return Excess Flood, so sleep
  </match>
</label>

<label @slack>
  # #team1_warn => team1.slack.com#general
  <match *.team1_warn>
    type slack
    token "#{ENV['TEAM1_TOKEN']}"
    username daioikachan
    channel general
    color good
    icon_emoji :ghost:
    flush_interval 2s # slack API has limit as a post / sec
  </match>
  # other channels => team2.slack.com#${channel}
  <match **>
    type slack
    token "#{ENV['TEAM2_TOKEN']}"
    username daioikachan
    channel %s
    channel_keys channel
    color good
    icon_emoji :ghost:
    flush_interval 2s # slack API has limit as a post / sec
  </match>
</label>

daioikachan の実装

勘の良い人は気づいたかもしれないが、daioikachan は Fluentd の仕組みに乗っかって実装されている。

  1. 非同期処理は BufferedOutput プラグインという仕組みがすでにある
  2. プラグイン機構がすでにある
  3. tag や label といった設定ファイルでのルーティング機能がすでにある
  4. IRC, Slack プラグインもすでにあるし、
  5. 他にも hipchat、twitter、mail、 twilio プラグインなどもすでにある

あとは Ikachan 互換の HTTP を受ける口だけあれば良い、ということで、in_daioikachan というプラグインを書いた。 プラグイン内部で http サーバ puma を動かしつつ、生 rack なコードを書いて実現している。

in_daioikachan にこんなかんじで post すると、

curl -d "channel=#channel&message=test message" http://localhost:4979/notice

内部的には以下のようなメッセージが emit されるので、

notice.channel {"command":"notice","channel":"channel","message":"test message"}

それを output プラグインで処理すれば良いというわけだ。ちなみに、パラメタを増やすと、emit メッセージのフィールドも増えるので、out_mail 用に subject を増やすとかもできるかもしれない。

fluent-plugin-irc, fluent-plugin-slack

この2つは機能拡張、性能検証もしっかりやって、daioikachan にデフォルトで bundle している。

まだ、マージされていないものがあるので、その辺はあとで更新しますが、お使いいただける状態です。

まとめ

複数のバックエンド(IRC, Slack, etc)に対応する Ikachan 互換サーバ、Daioikachan を書いた。 バックエンドを増やしたい時には、Fluentd のプラグインとして書けば良いので、既存の資産に乗っかれて大変便利。

どうぞご利用ください。