稼働中のLaravelアプリケーションにSentryを導入した話

2023/04/30に公開

稼働中のLaravelアプリケーションにSentryを導入した話を書きます。
Sentryはフロントエンドアプリケーションに使うもの、というイメージをなぜか持っていて、NuxtやNextのアプリケーションにはどんどん入れていたのですが、今回初めてバックエンドにも導入してみました。

対象読者

  • Laravelアプリケーションを開発している方
  • Sentryを使ってみたい方
  • Laravelアプリケーションで、エラー監視・アラートの方法に満足していない方

導入背景

Sentryを導入する前にどういったエラー監視の方法を採用していて、どういった課題があり、Sentryの導入を決めたかという背景について簡単に説明します。

Sentry導入以前は、CloudWatch Logsでエラーログを収集 + Lambdaによって検知してSlack通知をすることでエラー監視していました。
具体的には、CloudWatch Logsに流れたLaravelの各種ログ(Fargate上のECSタスクから標準出力に流れた文字列)内に「ERROR」という文字が含まれていることをトリガーとしてLambda関数を起動します。その後はLambda関数内部で多少の条件分岐を通過したのち、Slackのエラー監視チャンネルにPOSTされます。大変お手軽な実現手段です。

ログ内に「ERROR」という文字が含まれる条件は主に2つあります。

  • 実行中のコード内で例外が発生し、捕捉されなかった場合
  • コード中でLog::error()関数が実行された場合

そのため、例外が発生した場合はもちろんですが、たとえば処理上はtry-catchしたけど社内のエンジニア向けに異常を通知したいときに気軽に実現できる手段として、Log::errorで実装するという手段が採用されていました(※今思うとこの手抜き指針を採用するにしても、腐敗防止層としてLog::errorをするだけの専用クラスでも別途切っておけばよかったかもしれません)。

この仕組みのメリットは以下のようなものが挙げられます。

  • Laravelアプリケーションの開発時にはエラー監視のことを考えなくていい(≒Laravelにエラー監視の仕組みが依存しない)
  • 原始的な仕組みなので、学習コストが低い(実装工数も低いし、誰かに仕組みを共有するのも簡単)

デメリットもありまして、以下のようなものが挙げられます。

  • エラーが起こったらその分Slack通知されるので、障害発生時などに大量にSlack通知されて見にくい
  • エラー内容が全部Slackに流れるため、蓄積されず検索性が損なわれたり、どのエラーが何回起きているかなど統計情報が見れない
  • 上記と関連するが、エラー内容がStack Trace + メッセージというシンプルな情報で流れるため、見るエンジニアに前提知識を結構必要とする
  • (厳密には)ERRORという文字が含まれているからといってエラーとは限らないので正確ではない(ただし、これはその気になればJSON形式でログを流しているのでCloudWatchのクエリを書き直せば厳密にできますし、そもそもサービスを2年以上運営してERRORという文字列がエラー以外で込められたことがなかったので問題になりませんでした)

こういった背景があり、Sentryへの移行を検討し、実施しました。

Sentry for Laravelを使うメリット・デメリット

Sentryそのものについては、すでに色々な技術記事等が出ていると思うので概要を知りたい方はそちらを参照してください。

ここでは、前節で説明したメリット・デメリットのうち、メリットを維持しつつ、デメリットを解消することがSentryにできるのか、という観点で概要を示します。

まず、メリットについてですが、「Laravelアプリケーションの開発時にはエラー監視のことを考えなくていい」というメリットはSentryでもそのまま享受できます。
以下に示すように、SentryにはLaravel用のパッケージが有り導入も簡単なのですが、日頃実装するときにSentryにエラーを投げなくてはいけない、といった意識をする必要はないようにできています。

https://docs.sentry.io/platforms/php/guides/laravel/

しかし、「原始的な仕組みなので、学習コストが低い」のほうのメリットについては、Sentryについて少々詳しくなる必要があるので、もとの原始的な仕組みほど学習コストが低くないかもしれません。とはいえ、もとの仕組みもLambdaやCloudWatch Logsについて知らないといけないという意味では、似たようなものかと思います。
今回は、以下のように社内でエンジニアメンバー向けに勉強会を開催することで、基礎知識の共有を図りました。

CleanShot 2023-04-30 at 14 29 38

続いてデメリットの解消についてです。

まずは「障害発生時などに大量にSlack通知されて見にくい」というデメリットの解消について解説します。
SentryはエラーをまずIssuesという一覧に収集した上で、その中で規定の条件を満たしたIssueのみをAlertすることができます。そして、Alertは色々な向き先を選べるのですが、その中にSlack通知もあります。SentryではAlertするための条件を調整することで、障害発生時などで短期間に大量のIssueが記録されても、SlackへのAlert自体はN回に抑えるということが可能です。

続いて「統計情報が見れない」というデメリットの解消についてです。Sentryでは、収集したIssueを一覧で見ることができるのはもちろん、Issueの発生源となったUser Idで絞り込んだり、期間を好きに変えて検索できるなど、エラーの検索機能も充実しています。

最後に「見るエンジニアに前提知識を結構必要とする」というデメリットについては、SentryのIssueの画面はとても見やすく、エラーの内容とUser IdやAPIなどがわかりやすく確認できるため、Stack Traceのみから情報を読み取るように要求するよりも相当ハードルが低くなっています。

ちなみに、SentryはTeam Planに加入しています。無料プランだと相当制限があるため、企業でのサービスではTeam Planに加入することがおすすめです。そのため逆に言うと、費用面では元々の原始的な仕組みのほうが優れています。

Sentry for Laravelの導入

ここまで解説したところで、続いて導入方法について説明します。

基本的にはこちらのガイダンスに則ればOKです。

https://docs.sentry.io/platforms/php/guides/laravel/

実はSentry for Laravelの導入方法にはおおまかに言って2つの方針があります。Exceptions/Handler.phpを使う方法と、Laravel Log Channelを使う方法です。
前者は、Handler.phpまで捕捉されずに上がってきたエラーをSentryに投げるコードを実装しておく方針です。
普通はこちらの方針で問題ないかと思いますが、今回の導入には不適切と判断しました。というのも、もともとのエラー監視ではLog Levelが「ERROR」だったログは全部Slack通知するという方針でやっており、例外ではなく単なるLog::errorもSlack通知されることを期待して運用されていたからです。もしLog::errorを直接使わずにエラーログ用のモジュールを切ってそれを使うように実装していれば、そのモジュール内部でSentry::captureExceptionを呼ぶだけでよかったのですが・・・後悔しても仕方ないので、2つ目の方法であるLaravel Log Channelを採用しました。

Log Channelを使うことで、Log::errorで出力されたログもSentryへの通知対象となります。というか、最終的に吐き出されたログのLevelを見て判断するようになる感じです(軽く本体のソースを追ったのですが、カスタムログチャネルというLaravelの機能を使ってそうでした。ただ、Laravel側にその解説をしているドキュメントが見当たらなかったので、一旦それ以上追うのは断念しました)。なので、実質的にExceptions/Handler.phpよりカバー範囲が広い仕組みになっています。Log Channelを使った方法は以下に解説されています。

https://docs.sentry.io/platforms/php/guides/laravel/usage/#log-channels

本記事では、上記のドキュメントと若干違う実装をした部分や、補足になるような内容を説明します。

まずは、SentryのScopeを使うためのSentryContextというMiddlewareを実装します。SentryのScopeは、エラーに紐付けたいUser Idなどの情報をグローバルに設定しておくことができ、そのScope設定後に捕捉されたエラーにはScopeの内容が紐づいて保存されるという機能です。これにより、エラーをSentryに投げるときに都度都度User Idを指定せずとも、User Idを保存できるようになります。

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Sentry\State\Scope;

class SentryContext
{
    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param Closure                  $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (auth()->check() && app()->bound('sentry')) {
            \Sentry\configureScope(function (Scope $scope): void {
                $scope->setUser([
                    'id' => auth()->user()->id,
                ]);
            });
        }

        return $next($request);
    }
}

実装したMiddlewareを、src/app/Http/Kernel.phpに反映します。最初はどこに追加してもいいと思っていましたが、\Illuminate\Session\Middleware\AuthenticateSession::classの上に記述してしまうと、ログインユーザーが設定される前に動いてしまいScopeが正しく設定されないので注意してください。

SentryのサンプルコードではユーザーのemailもScopeにしていますが、意味がないですし、無駄にユーザーのメールアドレスを漏洩させることになるのでやめました。

src/config/logging.phpで、ログのチャンネルにsentryを指定します。

        'stack' => [
            'driver' => 'stack',
+            'channels' => ['stderr', 'sentry'],
            'ignore_exceptions' => false,
        ],
+        'sentry' => [
+            'driver' => 'sentry',
+            'level' => env('LOG_LEVEL', 'error'),
+            'bubble' => true,
+        ],

この辺の実装は既存のloggingの実装によると思うので参考までにお願いします。

また、src/config/sentry.phpの設定ですが、デフォルトだと正直デバッグには要らないと思う情報まで流れてしまうので、適宜falseにしました。

最後に、DSNを環境変数に設定します。AWS Fargateを使っている場合ですが、SENTRY_LARAVEL_DSN環境変数をステージング以降のFargateにて、Parameter Storeから取得するように設定してタスク定義を更新し、再デプロイを走らせました。
ちなみにローカルでSentryが動くと面倒なので、ローカルではSENTRY_LARAVEL_DSN=nullに設定しています。ステージング以降では、environmentを分ければ混ざることは無いので、SENTRY_LARAVEL_DSNを設定しています。

アラートの設定

IssueがSentryに記録されることを確認したら、最後にAlertを設定します。Sentryの管理画面の左のサイドバーから、Alertを選択し、「Create Alert」を押してアラートの作成を始めます。

  1. Select an environment and project → Productionを選びます
  2. Set conditions → WHENの部分がポイントです。「A new issue is created」だけにしていると、本当に未知のエラーが起きたときしかアラートしてくれません。個人的には、エラーが起きたのはわかっても、すぐに原因がわからなかったり、わかっても一旦放置、という判断をすることもあるので、そこに対して高頻度で起きているならより問題が大きいと認知するためにも、過去のエラーでも再度起きたら通知したいと思っています(現状それでも全然通知がうるさくない、というのもありますが)。そのため、「The issue is seen more than 1 times in one hour」というオプションも選択しました。
  3. Set action interval → ここで設定した期間内では同一のエラーでもアラートを連投しないようです。なのでここは60minutesにとりあえず設定しました。
  4. Preview → ここで、現在設定しているAlert条件のPreviewを確認できるので、条件が合っているかいないかを判断します

CleanShot 2023-04-30 at 15 07 26

まとめ

以上で、SentryのLaravelへの導入と、アラートの設定まで完了しました。設定後3週間以上経過しましたが、現状、エラー通知が連投されないことと、管理画面上からエラーのコンテキストがわかりやすいことで、非常に体験がよくなっています。

また、感想としては、フロントエンドでは随分前からSentryを導入していたため、勝手に先入観でSentryはフロントエンドのためのもの、と思っていたのですが、今回調べてみるとLaravel向けのモジュールも全然便利に使えるので、もっと早くやればよかったなと思いました(なまじCloudWatch Logs + Lambdaの仕組みをサクッと昔作ってしまったので、愛着が湧いていたというのも正直ありそうですが笑)。

エラー監視はサービスの規模や状況に応じて必要となる要件が異なるので、今回の対応内容がいつも正しいとは限らないですが、同様の課題を抱えている方などに参考になれば幸いです。

マナリンク Tech Blog

Discussion