😗

Sentryで始めるエラー監視

2022/04/24に公開

はじめに

そもそもSentryとはエラーの可視化、監視ツールです。ダッシュボード上でエラー発生時のスタックトレースや、リクエストデータなどを確認することができます。

こんな感じでエラーが可視化されます。


マスク多くて申し訳ないですが、Issue画面です

パフォーマンス監視ツールとしての機能も持ちますが、今回の導入目的からは逸れるので言及しません。

以下で紹介するSentryの機能を利用するためには、有償プランを契約する必要があります。監視ツールを利用していない場合や、自前でエラー通知だけ行っていると、有償ツールの利用に抵抗があるかもしれません。しっかりと使いこなせばコスト以上に十分なリターンがあると感じているため、本記事により、Sentryの利用体験が少しでも向上すれば幸いです。※特にSentryの回し者ではありません。

導入目的

エラーの発生をSlack経由で検知し、その時の状況を分かりやすい形で確認できること、そしてエラーへの対応ワークフローを定めることが目的です。
また、エラー通知地獄に陥り、重要なエラーを見逃したり、エラー対応がおざなりになってしまうというような経験があるかもしれません。これに対し、必要十分なエラー通知を設定できるという点も重要なポイントです。
Sentryの利用体験をより良いものにするために、簡単なガイドラインも設けます。

用語

  • Projects...エラーイベントのトラッキング対象となる個別のアプリケーション単位です。1つのサービスが複数の異なる言語やフレームワーク、リポジトリなどで管理されている場合、それぞれを別のプロジェクトとして用意することが推奨されています。
  • Issues...同じエラーイベントをグルーピングしたものです。発生タイミングが異なるエラーであっても、同じエラーであれば、それが1つのIssueとして扱われます。「同じエラー」の定義はエラーイベントのfingerprintが一致しているかどうからしく、この辺りはSentry側がうまく扱っているようです(カスタマイズも可能)。異なるIssueと分類されたエラーイベントを手動で同じIssueにまとめることも可能です。
  • Quota...プランの課金体系に関係する月間のエラーイベント発生数。

設定方法

SDKのインストールはLaravelアプリケーションを前提としますが、他アプリケーションへの導入についてもドキュメントは充実しているようです。

https://docs.sentry.io/platforms/
また、SDKのインストール方法を本記事の主眼としている訳ではないため、Laravel以外のアプリケーションでも十分参考になる内容かと思います。

前提

Sentryへのアカウント登録は予め行ってください。登録時にステップ通りに進めるとLaravelアプリケーション用のプロジェクトが1つ作成されるはずです。
Sentryにはいくつかプランがありますが、ユーザーが複数追加でき、Slackなどの外部サービスとの連携も行えるTeamプラン以上の利用をおすすめします。

SDKインストール

composer require sentry/sentry-laravel

PHP実装

app/Exceptions/Handler.php
public function register()
{
    $this->reportable(function (Throwable $e) {
        if (app()->bound('sentry')) {
            app('sentry')->captureException($e);
        }
    });
}

Sentryエラーハンドラの登録。

config/sentry.php
<?php

use Sentry\ExceptionDataBag;

return [

    'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')),

    // capture release as git sha
    // 'release' => trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')),
    'release' => env('SENTRY_VCS_RELEASE_NAME', 'UNKNOWN'),

    // When left empty or `null` the Laravel environment will be used
    'environment' => env('SENTRY_ENVIRONMENT'),

    'breadcrumbs' => [
        // Capture Laravel logs in breadcrumbs
        'logs' => true,

        // Capture SQL queries in breadcrumbs
        'sql_queries' => true,

        // Capture bindings on SQL queries logged in breadcrumbs
        'sql_bindings' => false,

        // Capture queue job information in breadcrumbs
        'queue_info' => true,

        // Capture command information in breadcrumbs
        'command_info' => true,
    ],

    'tracing' => [
        // Trace queue jobs as their own transactions
        'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', false),

        // Capture queue jobs as spans when executed on the sync driver
        'queue_jobs' => true,

        // Capture SQL queries as spans
        'sql_queries' => true,

        // Try to find out where the SQL query originated from and add it to the query spans
        'sql_origin' => true,

        // Capture views as spans
        'views' => true,

        // Indicates if the tracing integrations supplied by Sentry should be loaded
        'default_integrations' => true,
    ],

    // @see: https://docs.sentry.io/platforms/php/configuration/options/#send-default-pii
    'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),

    'traces_sample_rate' => (float)(env('SENTRY_TRACES_SAMPLE_RATE', 0.0)),

    'controllers_base_namespace' => env('SENTRY_CONTROLLERS_BASE_NAMESPACE', 'App\\Http\\Controllers'),

    'sample_rate' => (float)(env('SENTRY_SAMPLE_RATE', 1.0)),

    // 'max_breadcrumbs' => 50,

    'before_send' => function (\Sentry\Event $event): ?\Sentry\Event {
        if (null !== ($stacktrace = $event->getStacktrace())) {
            foreach ($stacktrace->getFrames() as $frame) {
                $frame->setVars([]);
            }
        }

        foreach ($event->getExceptions() as $exception) {
            /** @var ExceptionDataBag $exception */
            if (!($stacktrace = $exception->getStacktrace())) {
                continue;
            }

            foreach ($stacktrace->getFrames() as $frame) {
                $frame->setVars([]);
            }
        }

        return $event;
    },
];
補足
  • breadcrumbs...エラー発生時の形跡です。形跡とされるイベントはカスタムで実装することも可能ですが、基本的なものはSDK側がうまくやってくれます(SQLやログなど)。
  • tracing...パフォーマンスモニタリングにあたるものです。'traces_sample_rate'の値がデフォルトでは0のため、このままにしておけばパフォーマンスモニタリングはされないことになります。
.env
SENTRY_LARAVEL_DSN=https://.....

Sentryアカウント登録後に表示されるプロジェクトのDSN。プロジェクトの設定からも確認できます。

app/Http/Middleware/AddSentryUserContext.php
<?php

namespace App\Http\Middleware;

use Closure;
use Sentry\State\Scope;

class AddSentryUserContext
{
    /**
     * @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);
    }
}
app/Http/Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    protected $middlewareGroups = [
        'web' => [
            ...
            \App\Http\Middleware\AddSentryUserContext::class,
        ],
    ];
}

認証済みユーザーの場合、Sentryにユーザー識別子を追加で送信しています。idの他に、usernameemailip_addressの指定が可能です。ここではアプリケーションのユーザーIDを直接指定していますが、ドキュメントではusernameのほうが良いとされています。未認証ユーザーの場合はip_addressを利用するとかも良さそうです。詳細はドキュメントを参照ください。
※書きながら思いましたが、別にこれはMiddlewareではなく前述のconfig.sentry.before_sendでも良かった気がします...

Sentry設定

通知

必要十分な通知をSlackで受け取るための設定を行います。
https://docs.sentry.io/product/alerts/best-practices/

通知の概念や設定は少しややこしく感じたので、先に簡単な補足をしておきます。※あくまでも理解を助けることを目的としているので、構造などは正確ではない部分があるかもしれません。

  • Alerts...エラーイベントの発生をトリガーとするアラート(≒通知)です。このアラートを行うためにはAlert Rulesを作成し、いつ、どんなタイミングでそのアラートが発生し、それをどこに通知するのかを定義する必要があります。単なる通知ではなく、エラーイベントが発生したときのアラートであることを意識する必要があります。通知=Alertsで行うと勘違いしたことで少し苦しみました...
  • Workflow Notifications...(Sentry上での)ユーザーの行動や、Issueの状態変更をトリガーとする通知です。トリガー条件はあらかじめSentry側が定めており、IssueがResolve状態(もしくはその逆)に変更された場合や、Issueの担当者として指定された場合などがそれにあたります。通知の他の分類としてはDeployQuotaWeekly Reportsもありますが、ここでは言及しません。また、通知対象は該当Issueを購読しているユーザーとなります。Issueが購読状態になる条件はドキュメントを参照ください。

上記を踏まえ、AlertsをSlackのプロジェクト専用チャンネルに通知し、Workflow NotificationsをSlackの個人チャンネルに通知するよう設定を行います。

Slack連携
  1. Sentry上でSlack統合をインストールし、ワークスペースを追加。
    Organization Settings > Integrations

    Slackの権限確認画面が出るので、確認してください。
  2. SlackワークスペースにSentryAppをインストール。
  3. SlackにAlerts用チャンネルを作成。
    ブライベート、パブリックどちらでも構いませんが、プライベートチャンネルの場合は、チャンネルにSentryAppを追加してください。
  4. SentryAppのメッセージにWorkflow Notificationsを連携。※ここで連携するものは正確にはPersonal Notificationであり、その中にWorkflow Notificationsが含まれるイメージです。
    Slackで以下コマンドを実行。
/sentry link

SentryAppメッセージ上ではなく、個人用の専用チャンネルを別途作成して、とかでも良いかと思います。

Alerts作成
新規Issue発生時 or 一定期間内でのIssue発生数に基づく通知

同じエラーイベントの発生が大量に通知されないために、通知条件を新規Issue発生時に限定します。加えて、同じエラーであっても短期間に大量に発生した場合はそれを通知します。※大量の通知が都度行われる訳ではなく、大量にエラーが発生したことを(↓の例では1時間に100件以上)通知します。
Alerts > Create Alert


上記ルールはあくまでも一例になります。状況に合わせてカスタマイズしてください。
https://docs.sentry.io/product/alerts/create-alerts/issue-alert-config/

ガイドライン

Issueのトリアージ

検知したエラーへの対応ルールのようなものです。

1.Issue担当者設定

  1. SlackAlerts用チャンネルにIssue通知が届く。
  2. 1次対応者が該当通知にリアクション(👀とか?)をつけ、確認中であることをマークする。
    ※1次対応者は予め固定しておくか、曜日ごとに割り当てるとかが良いと思います。
  3. Issue内容を簡単に確認し、担当者(ASSIGNEE)をアサインする。

    ※アサインされるとWorkflow Notificationsの通知が飛びます。
  4. 1のAlerts通知にアサイン済み(もしくは解決済みとか)のリアクションをつける。

2.Issue状態更新

  1. アサインされた担当者は該当Issueを確認し、簡単に調査を行う。
  2. すぐに解決できる内容であれば対応を行い、Resolveをマークする。

    Resolve後に再度エラーが発生すると、改めてIssueがUnresolvedとなります(=Regressions)。
  3. すぐに解決できないが、対応が必要な内容であればMark Reviewedをマークする。

    ※その後、実際に対応を行ったタイミングでIssueをResolve状態に変更します。

https://docs.sentry.io/product/issues/states-triage/#mark-reviewed

  1. 対応が不要だと判断した場合は、Ignoreをマークする。

https://docs.sentry.io/product/alerts/best-practices/#state-change-alerts

Issueの定期確認

週次や日次でIssuesページを確認します。これは前述の1次対応者に関係なく、チームに所属する全メンバーが行います。
優先して確認すべきはFor Reviewタブで、未確認Issueが0件になるよう努めます。具体的には、担当者未設定のIssueがあればその日の1次対応者が担当のアサインを行い、自分が担当のIssueがあれば、必要な対応を行います。
All Unresolvedのタブも確認し、放置が続いている場合はIgnoreの対応を取るなどします。

異なるIssueを1つにまとめる

Sentryがそれぞれ異なるエラーと判断したIssueであっても、手動で1つのイベントにまとめることが可能です。もし、該当するものがあれば対応しておきましょう。

その他

スパイク保護

短期間にエラーイベントが大量発生し、Quotaを圧迫することを防ぐための保護機能です。詳細はドキュメントを参照ください。

https://docs.sentry.io/product/accounts/quotas/#spike-protection

Quota上限超過への対応

イベント予約や、従量課金などにより、プランのQuota上限を超えたイベントの捕捉が可能となります。詳細は設定画面(Organization Settings > Subscription > Manage Subscription)や、ドキュメントを確認ください。

https://docs.sentry.io/product/accounts/pricing/

機密データのスクラブ

機密データをSentry上に保存することを防ぐための方法が提供されています。また、SDKがそれらしきデータを予めスクラブしてくれていたりもします。

https://docs.sentry.io/product/data-management-settings/scrubbing/

https://docs.sentry.io/product/data-management-settings/scrubbing/server-side-scrubbing/

コードリポジトリとの連携

GitHubなどのコードリポジトリと連携することで諸々便利な機能があります。例)コミットやプルリクでSentryのIssueを自動でResolveに変更する。コミット情報から、Issueに対して適切な担当者の提案を行う。

以上です。参考になれば幸いです。

Discussion