コネヒト開発者ブログ

コネヒト開発者ブログ

CakePHP3用のSentryプラグインをオープンソース化したのでご紹介させてくださいね

こんにちは。サーバーサイドやっております(@o0h_)です。

https://cdn.mamari.jp/authorized/amana_5ab65cf0-f200-4d16-a7b6-0017ac120004.jpg

マイホームヒーロー、どんどん盛り上がっていきますね・・!この後どうなるでしょう。

マイホームヒーロー

マイホームヒーロー

コネヒトでは自由なソフトェアへの貢献活動に取り組んでいるメンバーも多いのですが、年末くらいから社内でもソフトウェアの開発を進めるなどしておりました。
そして、やっとこさ「最低限公開できる形にはなったかな〜」というとこまで進んだ次第です。
ということで、本日はそんなOSSのご紹介をしたいと思います。

CakePHP3用の、Sentryプラグインです。
packagist.org

What’s cake-sentry

いろいろなプロジェクトでエラートラッキングにSentryを活用しています。 sentry.io

Sentryは無料でも使えて、通知やIssue管理も柔軟に行える便利なサービスです!*1
そしてこれは「Sentryへのログの送出をサポートしよう」というプラグインです。
「Cake3っぽく書いて」「とにかくすぐ、簡単に動いて」「柔軟な設定もサポートする」といった実装を目指しました。

Sentry自体は以前から利用していたため、CakePHP3を用いたプロジェクトの立ち上げに際しても継続して利用すると判断していました。しかしながら、「プロダクトに投入できそうなインテグレーションがないね」という話になり、独自に実装を進めることになります。*2

その後、他にもCakePHP3ベースのプロジェクトが立ち上がっていく中で、

  • 複数のレポジトリに手軽に横展開したいし
  • 当初より良さそうな設計も見えてきたし
  • ポータビリティを担保するためにプラグイン化しよう
  • どうせ作るなら公開しちゃえば

という機運が高まっていきます。そうして生まれたのが当プラグインです*3
まずはプライベートレポジトリで開発を進め、実際に本番投入も経て、公開用に少し体裁を整えてからOSS化されました。

composerからインストール

インストールは、もちろんcomposerコマンド1発で可能です

$ composer require connehito/cake-sentry

プラグインをアプリケーションに入れる

cakeのコマンドを用いて、プラグインをロードさせるようにします。bootstrapオプションは必須ではないですが、プラグイン自体のbootstrapによってロガーの設定やミドルウェア(エラーハンドラ)の設定が自動的に行われます。
実行される内容については実際のソース(bootstrap.php)を御覧ください。

$ bin/cake plugin load Connehito/CakeSentry --bootstrap

/app/config/bootstrap.php modified

bootstrap中に、次のような1行が付け足されました。

Plugin::load('Connehito/CakeSentry', ['bootstrap' => true]);

これでインストールは完了です。

Sentry DSNの設定

続いて、Sentryの接続情報の設定です。 今の時点では、アクセスをすると次のようなエラーが発生します。 f:id:o0h:20180324224206p:plain

これはSentryの接続情報(アカウント情報)が渡されていないためです。
ということで、確認して来ます。

DSNの確認

プロジェクトの作成直後に言語・フレームワーク(今回はPHP)を選択した後に出てくる画面で確認できます。 f:id:o0h:20180324224229p:plain

もしくは、Settingsから確認可能です。 f:id:o0h:20180324224242p:plain

Sentry DSNの設定

このプラグインでは、 Cofigure クラス経由で設定値を読み込むようになっています。Sentry.dsn というキーに、先ほど確認したDSNの値を設定してください。
例えば以下のようになります。

$ tail -13 config/app.php
     * - 'cache' - Use the Cache class to save sessions.
     *
     * To define a custom session handler, save it at src/Network/Session/<name>.php.
     * Make sure the class implements PHP's `SessionHandlerInterface` and set
     * Session.handler to <name>
     *
     * To use database sessions, load the SQL file located at config/schema/sessions.sql
     */
    'Session' => [
        'defaults' => 'php',
    ],
    'Sentry' => [
        'dsn' => 'https://abcabcabc:xxxxxxxx@sentry.io/000000',
    ],
];

これで基礎的な設定が完了しました(!) あなたのCakePHP3プロジェクトは、もう動作可能です。

使ってみる(ベーシック編)

実際に、どのような情報が送られるのかを見てみましょう。
AppController内でエラーを発生させてみます。

--- a/src/Controller/AppController.php
+++ b/src/Controller/AppController.php
@@ -50,5 +50,6 @@ class AppController extends Controller
          */
         //$this->loadComponent('Security');
         //$this->loadComponent('Csrf');
+        trigger_error('なにかエラーが!!', E_USER_WARNING);
     }
 }

トップページにアクセスをすると、Sentryにエラーが飛んできました。

f:id:o0h:20180324232111p:plain

「発生日時」「エラーメッセージ」「エラーレベル」「発生箇所(file, line, code)」「スタックトレース」及び「URLやヘッダーなどのリクエスト情報」といった、最低限必要な情報+αは含まれています。
駆け出しのプロジェクトにおいては、この時点で既に活用できるレベルかも知れません。

続いて例外を投げた場合はどうでしょうか?

--- a/src/Controller/PagesController.php
+++ b/src/Controller/PagesController.php
@@ -44,6 +44,10 @@ class PagesController extends AppController
         if (!$count) {
             return $this->redirect('/');
         }
+        throw new \Cake\Network\Exception\BadRequestException(
+            '悪いリクエストだねぇ!',
+            499
+        );
         if (in_array('..', $path, true) || in_array('.', $path, true)) {
             throw new ForbiddenException();
         }

f:id:o0h:20180324232241p:plain

先程の情報に加えて、「例外名」「例外メッセージ」及び「例外が投げたメソッドの引数」が情報として引き渡されています。(PagesController::display()$pathとして、home が入ってきました)

このように、「インストール & ひとつだけ設定値を入れる」ですぐにSentryを導入だ!!を実現できています。

もっと快適に使う

ここからは、より具体的な実用に向けての設定について紹介します。

記録をしない例外を指定する

「これは別にSentryに送らなくていいや」という指定を、例外クラス名の列挙により行うことが可能です。Webサービスを運用していると、「これは本質的な問題でないから対応するのに力を注がなくて良い」あるいは「必要な部分に必要な力を注ぎたい」と思うのは常だと思うので、ログやトラッキングを綺麗にする事は大事ですよね。

例えば、誰しも1度は見掛けた事があるであろう /wp-login.php へのアクセス・・・!これは「存在しないURL」として、デフォルトのCakePHP的には「MissingControllerException」を返します。*4

f:id:o0h:20180324224925p:plain 嫌なもんですね。

本プラグインのSentryへのログ送出はCakePHPのLog機構を用いて行われているので、FileやDatabaseなどの通常のLogEngineと同様の方法で「ログに吐き出すのをスキップするタイプ」を設定することが可能です。

以下のような記述を加えます。

--- a/config/app.php
+++ b/config/app.php
@@ -152,6 +152,9 @@ return [
     'Error' => [
         'errorLevel' => E_ALL,
         'exceptionRenderer' => 'Cake\Error\ExceptionRenderer',
+        'skipLog' => [
+            \Cake\Routing\Exception\MissingControllerException::class,
+        ],
         'log' => true,
         'trace' => true,
     ],

これだけでMissingControllerException は無視されるようになりました。
この辺りも、例によってbook上に翻訳済み説明がありますので、詳しくはそちらを御覧ください。
エラーと例外の処理 - 3.5

コンテキストの追加

Sentryでは、Context という形でデバッグやトラブルシュートに役立つ様々な情報を付与していくことができます。

Context – Sentry Documentation

本プラグインではContextの付与もサポートしています。
CakePHPの イベントシステム を利用し、エラーハンドラーやロギングシステムそのものに手を入れないままでのユーザーによる自由な設定の注入を可能としました。
サンプルコードを交えて見ていきましょう。

user_contextの追加

現状では、Request情報をcontextとして渡すようになっています(踏み込んで言うと、Request経由でSession情報も取得することが可能です)。

例えば、まずイベント購読用のクラスを実装して

diff --git a/src/Error/SentryErrorContext.php b/src/Error/SentryErrorContext.php
new file mode 100644
index 0000000..6203d7f
--- /dev/null
+++ b/src/Error/SentryErrorContext.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Error;
+
+use Cake\Event\Event;
+use Cake\Event\EventListenerInterface;
+
+class SentryErrorContext implements EventListenerInterface
+    {
+    public function implementedEvents()
+    {
+        return [
+            'CakeSentry.Client.beforeCapture' => 'setContext',
+        ];
+    }
+
+    public function setContext(Event $event)
+    {
+        $request = $event->getSubject()->getRequest();
+        $session = $request->getSession();
+        $raven = $event->getSubject()->getRaven();
+        $raven->user_context([
+                'id' => $session->read('Auth.User.id'),
+                'username' => $session->read('Auth.User.name'),
+            ]);
+    }
+}

それをbootstrap.phpからグローバルに登録させます。

diff --git a/config/bootstrap.php b/config/bootstrap.php
index 97f92a6..c5f1ce2 100644
--- a/config/bootstrap.php
+++ b/config/bootstrap.php
@@ -216,3 +216,5 @@ if (Configure::read('debug')) {
 }

 Plugin::load('Connehito/CakeSentry', ['bootstrap' => true]);
+
+\Cake\Event\EventManager::instance()->on(new \App\Error\SentryErrorContext());

こうすることで、user情報が入るようになりました。*5

f:id:o0h:20180324225219p:plain

user情報が扱えるようになると、エラー(Issue)ごとに遭遇したユーザー数がカウントされるようになって便利です。影響範囲が見えやすくなります。デフォルトではIPやSession IDを利用してSentryがよしなに判定してくれはするものの、やはり明示的にアプリケーション側からロジックを持って通達してあげた方が正確でしょう。*6

さらに多くの情報を追加

Environmentやtagのセットも、とても簡単に行うことができます。

1つは setEnvironment()tags_context() といったRaven_Client*7の公開APIを利用することです。 これらはプラグイン側の独自実装ではなく、Sentry PHP SDKのAPIを用いて行うものになるので、詳しくは公式のドキュメントを参照してください
Usage – Sentry Documentation

もう1つは「セットしたい情報をreturnする」方法です。
次の例では、 extra情報をこの方法で渡しています。

diff --git a/src/Error/SentryErrorContext.php b/src/Error/SentryErrorContext.php
index 6203d7f..d60062b 100644
--- a/src/Error/SentryErrorContext.php
+++ b/src/Error/SentryErrorContext.php
@@ -23,5 +23,15 @@ class SentryErrorContext implements EventListenerInterface
                 'id' => $session->read('Auth.User.id'),
                 'username' => $session->read('Auth.User.name'),
             ]);
+        $raven->setEnvironment(env('STAGE') ?? 'local');
+        $raven->tags_context([
+            'app_ver' => $request->getHeaderLine('App-Version') ?: 0.9,
+        ]);
+
+        return [
+            'extra' => [
+                'foo' => 'bar',
+            ],
+        ];
     }
 }

こうすることで、呼び出し元にextra.foo が渡ることになり、それがそのままRaven_Clientに引き渡されることになります。

  • tagのセット(app_ver)
    f:id:o0h:20180324225604p:plain
  • Environmentのセット(local)
    f:id:o0h:20180324225615p:plain
  • extraのセット(foo:bar)
    f:id:o0h:20180324225627p:plain

以上が、 connehito/cake-sentryのご紹介となります。

今後

先のエントリーでも触れましたが、CakePHP3.6にてPlugin周りの改修が行われます。*8
「作る側としてはどう付き合うべきかな?」について、じっくりと判断していきたいです。

また、Sentry SDKは2.0というブランチが活動中であり、大きく作りが変わるかも知れません。(やっとモダンぽくなる気がする・・!)
こちらはリリースされたら是非対応したいな〜とは考えています。

GitHub - getsentry/sentry-php at 2.0

最後に

効果的に情報収集を行えると、デバッグやトラブルシューティングが何倍も効率化されていくと思います。
そのため、チームやプロジェクトによって「ログに残す情報を柔軟に取り扱える」というのは重要な点だと考えていました。それが当プラグインを作る上で意識したことです。
他方で、「ほとんど何もしなくても動く」というレベルもキープできているのではないかと思います。

CakePHP3を扱っているチームがありましたら、ご利用いただけたら幸いです。
もちろん、使い方の提案や実装・機能の要望などはgithub上でいつでもお待ちしております!

*1:本記事ではサービス自体には触れませんが、ぜひ調べたり試したりしてみてください!

*2:独自実装と言っても、Sentryの公式PHP SDKは存在するので、それをラップして組み込む形です

*3:ただし、CakePHP3.5以上とPHP7.0以上を要することにご注意ください

*4:具体的に言えば、Routingがfallback()処理に入った上でurlのパースには成功、しかしながらmapされた:controllerに該当するクラスがない・・という状態

*5:今回はサンプルのアプリケーションを動かしているので、直接AuthComponent::setUser()でユーザーをセットしました

*6:特にIPベースとなった場合、直接のリクエスト元としてロードバランサーのIPを用いて「ユーザー数」が出されたりもするので、実用的な数字が上がってきません

*7:Sentryの公式SDK

*8:http://tech.connehito.com/entry/akephp-36-beta1