Laravelでログ出力と併せてメールで通知する

#はじめに
バグを完全に防ぐことは難しいですが、問題が起こった場合には迅速に対処できるようにしておきたいものです。
CloudWatch LogsやZabbixなどでログ監視できていると安心ですが、実際には導入が困難な場合もあるでしょう。

そこで、Laravelにてログ出力と併せてメールで通知する機能を設け、簡易的なログ監視を行いたいと思います。
この場合サーバー自体がダウンしているとどうにもならないですが、プログラムのバグであればすぐに気づくことができるでしょう。

なお、LaravelはデフォルトでSlack通知に対応しているので、Slackを利用されている方はそちらの方が便利かもしれません。

Laravelのバージョンは5.7にて確認しています。

#Handler.phpでメール通知
Laravelのドキュメントには下記のようにあります。
> Laravelの例外はすべて、App\Exceptions\Handlerクラスで処理されます。
> このクラスはreportとrender二つのメソッドで構成されています。両メソッドの詳細を見ていきましょう。reportメソッドは例外をログするか、BugSnagやSentryのような外部サービスに送信するために使います。

[Laravel ドキュメント](https://readouble.com/laravel/5.7/ja/errors.html "Laravel 5.7 エラー処理")

つまり、下記のreportメソッド内にメール送付するロジックを記述すれば実現できるということです。
さっそく記述してみました。

```php:App\Exceptions\Handler.php
public function report(Exception $exception)
{
    $error['message'] = $exception->getMessage();
    $error['code']    = $exception->getCode();
    $error['file']    = $exception->getFile();
    $error['line']    = $exception->getLine();
    $error['url']     = url()->current();

    Mail::send(['text' => 'emails.exception'], ["e" => $error], function (Message $message) {
        $message
            ->to(config('mail.to.address'))
            ->from(config('mail.from.address'))
            ->subject('【'.config('app.name').'】['.ENV('APP_ENV').'] サーバーエラー発生の連絡');
    });

    parent::report($exception);
}
```
```php:resources\views\emails\exception.blade.php
<?php 
$action = (\Route::getCurrentRoute()) ? \Route::getCurrentRoute()->getActionName() : "n/a";
?>
----------------------------------------------------------------------
このメールは{{ config('app.name') }}から自動で配信しています。
----------------------------------------------------------------------

{{ config('app.name') }}サーバーでエラーが発生しました。

======================================================================
[Message]
{{ $e['message'] }}
======================================================================
 [Action] {{ $action }}
 [URL] {{ $e['url'] ?? '' }}
 [File] {{ $e['file'] }}
 [Line] {{ $e['line'] }}
 [Code] {{ $e['code'] }}
======================================================================
```

うまく通知できましたが、しかしこれではすべての例外を通知してしまいます。
404などの400系エラーやバリエーションエラー、未認証でのログイン画面リダイレクトまで通知されてしまうと、肝心のサーバーエラーが起こった際に通知が埋もれてしまう危険性があります。

#Handler.phpでメール通知（改）
というわけで通知したいエラーをある程度限定したいと思います。
再びLaravelのドキュメントを見返すと、$dontReportプロパティを利用する事で特定の例外をログ対象から外すことができるとあります。
> 例外ハンドラの$dontReportプロパティは、ログしない例外のタイプの配列で構成します。たとえば、404エラー例外と同様に、他のタイプの例外もログしたくない場合です。

今回は認証関連の例外とバリデーションチェック関連の例外を除外してみました。
また上記に加え、400系エラーの除外と、本番環境とステージング環境のみ通知を行う判定を加えたのが下記です。

```php:App\Exceptions\Handler.php
protected $dontReport = [
    \Illuminate\Auth\AuthenticationException::class,
    \Illuminate\Auth\Access\AuthorizationException::class,
    \Illuminate\Validation\ValidationException::class,
];

public function report(Exception $exception)
{
    $status = $this->isHttpException($exception) ? $exception->getStatusCode() : 500;

    if ($exception instanceof \Exception && $this->shouldReport($exception))
    {
        if (\App::environment(['staging', 'production']))
        {
            $status = $this->isHttpException($exception) ? $exception->getStatusCode() : 500;

            if ($status >= 500)
            {
                $error['message'] = $exception->getMessage();
                $error['status']  = $status;
                $error['code']    = $exception->getCode();
                $error['file']    = $exception->getFile();
                $error['line']    = $exception->getLine();
                $error['url']     = url()->current();

                Mail::send(['text' => 'emails.exception'], ["e" => $error], function (Message $message) {
                    $message
                        ->to(config('mail.to.address'))
                        ->from(config('mail.from.address'))
                        ->subject('【'.config('app.name').'】['.ENV('APP_ENV').'] サーバーエラー発生の連絡');
                });
            }
        }
    }

    parent::report($exception);
}
```
```php:resources\views\emails\exception.blade.php
<?php 
$action = (\Route::getCurrentRoute()) ? \Route::getCurrentRoute()->getActionName() : "n/a";
?>
----------------------------------------------------------------------
このメールは{{ config('app.name') }}から自動で配信しています。
----------------------------------------------------------------------

{{ config('app.name') }}サーバーでエラーが発生しました。

======================================================================
[Message]
{{ $e['message'] }}
======================================================================
 [Action] {{ $action }}
 [URL] {{ $e['url'] ?? '' }}
 [File] {{ $e['file'] }}
 [Line] {{ $e['line'] }}
 [Code] {{ $e['code'] }}
 [Status] {{ $e['status'] }}
======================================================================
```

#おわりに
アプリケーションの安定稼働に少しでも寄与できれば幸いです。