Laravel 4 から Slack へメッセージを投げる

この記事は Laravel Advent Calendar 2014 の 24日目のエントリーです。

はじめに

11月から、仕事で Laravel 4 と 話題の Slack を使うようになりました。

ChatOps の第一歩として、 Slack の Integrations の機能を使って、メンバーの Commit / pullreq を #github channel に垂れ流すようにしました。これがとっても便利で、他のエンジニアの方が何をしているか可視化されることで、業務効率も上がりました。なにより、単純に楽しいということが大事ですよね。

こうなってくると、プロダクトのアラートメールなども一元的に Slack で受け取れるようにしたいな…と思ってきてしまいました。

Slack API の Incoming WebHooks

Slack は API を提供しており、自由に外部サービスからメッセージを投げることができたりします。Incoming WebHooks を登録すると、token 込みの WebHook URL を取得できるので、それを使って、まずは試しに自分の Team の #general channel にメッセージを投げてみましょう。

curl -X POST --data-urlencode 'payload={"channel": "#general", "username": "webhookbot", "text": "This is posted to #general and comes from a bot named webhookbot.", "icon_emoji": ":ghost:"}' (WebhookURL)
image

こんな感じでメッセージが投げられたでしょうか。

Laravel 4 から メッセージを投げてみる

PHP から Slack へメッセージを投げるプラグインは、このページを見る限りでもずいぶんたくさんあります。https://api.slack.com/community

この中でも、Laravel 4 で一番使いやすそうな maknz / slack を使わせてもらうことにしました。

インストールは README を見れば簡単にできます。試しに Laravel から送信してみるための artisan command を書いてみました。Webhook URL は、別途 Config から取得するようにしています。

<?php
use IlluminateConsoleCommand;
use SymfonyComponentConsoleInputInputOption;

class ProjectTestSlackCommand extends Command
{
    protected $name = 'project:testslack';
    protected $description = 'Slack へ post できるか疎通確認をする';

    public function __construct()
    {
        parent::__construct();
    }

    public function fire()
    {
        $message = $this->option('message');
        $config = [];

        if (! empty($this->option('username'))) {
            $config['username'] = $this->option('username');
        }

        if (! empty($this->option('channel'))) {
            $config['channel'] = $this->option('channel');
        }

        if (! empty($this->option('icon'))) {
            $config['icon'] = $this->option('icon');
        }

        $slack = App::make(
            'MaknzSlackClient',
            [
                Config::get('slack.endpoint'),
                $config
            ]
        );

        $slack->send($message);
    }

    protected function getOptions()
    {
        return [
            [
                'message',
                '',
                InputOption::VALUE_REQUIRED,
                'post する message'
            ],
            [
                'username',
                '',
                InputOption::VALUE_OPTIONAL,
                'post する bot の名前'
            ],
            [
                'icon',
                '',
                InputOption::VALUE_OPTIONAL,
                'post する bot の icon(URL でも、 Slack の絵文字でも OK)'
            ],
            [
                'channel',
                '',
                InputOption::VALUE_OPTIONAL,
                '投稿先(例:#general @ryo.shibayama)'
            ]
        ];
    }
}

README を見ればわかると思いますが、以下のように chaining して書くこともできます。

$client->from('Username')->to('@regan')->send('A message');

アラートメールの代替手段としての Slack

Slack の API は、1 request / sec までのリクエストしか受け付けてません。厳密にコントロールされているかは分かりませんが、大量に送るのは考えものです。

今回は、アラートメールの代替手段として考えていたので、デプロイ直後などにアラートが飛びまくる可能性があることも念のため考慮しました。

以下のような感じで、 SlackHelper クラスを作ってしまい、Slack へのメッセージングはここで管理するようなイメージです。連投を防ぐ手段としては、Slack にメッセージを投げるたびに /tmp/postslack という空ファイルを touch して、その最終更新日時を見ることで、メッセージを投げていいか判定しています。

<?php
use CarbonCarbon;

class SlackHelper
{

    private $file;

    public function __construct()
    {
        $this->file = Config::get('slack.filePath');
    }

    public function sendAlertToDeveloper($exceptionAsString)
    {
        $message = "*Error (" . App::environment() . ")*";
        $message .= "n";
        $message .= "```" . $exceptionAsString . "```";

        try {
            if ($this->can()) {
                Slack::from(Config::get('slack.displayName.alert'))
                    ->to('#alert')
                    ->withIcon(Config::get('slack.icon.alert'))
                    ->send($message);
                $this->touch();
            }
        } catch (Exception $e) {
        }
    }

    /**
     * Slack に post 可能か判定
     *
     * @return bool
     */
    private function can()
    {
        if (! File::exists($this->file)) {
            return true;
        }

        if (Carbon::now()->subSeconds(Config::get('slack.intervalSec'))->timestamp > File::lastModified($this->file)) {
            return true;
        }

        return false;
    }

    /**
     * Slack に最後に post した日時を記録する
     *
     * @return void
     */
    private function touch()
    {
        touch($this->file);
    }
}

エラーハンドリングは、 Laravel のドキュメントにある通り、ログリスナーを登録する形で行っています。http://laravel.com/docs/4.2/errors#logging

Slack 最高

これで無事にアラートが Slack に投げられるようになりました。アラート以外にも他の通知系でメールを使用している部分も少しずつ Slack に寄せたいなと思ったりしています。

余談

この package ですが、 json_encode の UTF-8 問題で、最初は日本語文字列が含まれている場合 false が返ってきてしまう不具合があり送信できなかったのですが、 issue 登録したら、すぐに対応してくれました!

さいごに

明日はついに最終日ですね!@localdisk さんのポエムに期待しつつ、みなさん良いクリスマスを!