【Laravel】ミニマルなエラー通知を独自ログ機能で実装する

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、私は過去に(ほぼ鳴かず飛ばずの)個人サイトをつくってきましたが、その開発をしている中でいくつか「うーん…」となってしまった経験がありました。

そして、その中には今回テーマ「エラー通知」があります。

どういうことかと言うと、

「エラーログをいちいちチェックするとめんどうだよね😂」

だから、メールやslackで通知しよう❗というのはよくあることだと思うのですが、いかんせん個人開発なので通知が来ても、

「いや、本業が忙しくて個人開発まで手が回らない…😫」

となってしまい放置。

でも、エラーは何度も発生するので同じ内容の通知を繰り返し受け取った結果、うんざりしてモチベーションが下がる…という悪循環が発生していました。(私は心の弱い人間です、はい 💦)

そこで❗

今回はこの状態を回避すべく以下のような機能をつくってみることにしました。

  • 同じ内容のエラーは通知しない
  • ただし、1か月以上前の場合は再送信する

つまり、「ミニマル(必要最低限)のエラー通知」ができる機能です。

ぜひ何かの参考になりましたら嬉しいです。😄✨

「臨機応変になるよう、
椅子の高さ、曲順、音量を
毎回変えてピアノ練習してます❗」

開発環境: Laravel 9.x、PHP 8.1

モデル&マイグレーションをつくる

まずは、「このエラーは送信すべきかどうか❓」がチェックできるようデータベースにログを保存できるようにします。

以下のコマンドを実行してください。

php artisan make:model ErrorLog -m

すると、モデルとマイグレーションの2ファイルが作成されるので中身を以下のようにします。

database/migrations/****_**_**_******_create_error_logs_table.php

// 省略

public function up()
{
    Schema::create('error_logs', function (Blueprint $table) {
        $table->id();
        $table->string('file'); // 👈 ファイルパス
        $table->string('line'); // 👈 行
        $table->string('message'); // 👈 エラーメッセージ
        $table->timestamps();
    });
}

今回はシンプルに「どこで・何か起こったか❓」がわかるような構成にしています。(お好みで URL や パラメータを入れてもいいですね)

では、マイグレーションを実行してテーブルを作成します。
以下のコマンドを実行してください。

php artisan migrate

すると実際のテーブルはこうなりました。

独自のログ・チャンネルをつくる

次に、独自の「ログ・チャンネル」をつくります。

ログ・チャンネルとは簡単に言うと「ログのためのグループ」(例:コムドット)だと考えてください。

そして、このチャンネルの中で「ハンドラー」(例: やまとくん)が以下のような処理を行うことになります。

  • ファイルに書き込む
  • slack へ送信する
  • Syslog で送信する

つまり、グループには1つだけでなく複数のハンドラーを設置することができます。

そして、今回は「ミニマルな通知」ということで「MinimalNotification」チャンネルを作って実装してみます。

では、以下のファイルを作成してください。

app/Logging/MinimalNotificationLogger.php

<?php

namespace App\Logging;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;

class MinimalNotificationLogger
{
    public function __invoke(array $config)
    {
        $name = 'minimal_notification'; // 👈 チャンネル名
        $logger = new Logger($name);
        $logger->pushHandler(new MinimalNotificationHandler());

        return $logger;
    }
}

この中では、次の項目でつくることになる独自ハンドラー「MinimalNotificationHandler」が実行されるようにしています。

なお、ログ・チャンネルは作っただけでは有効にはなりませんので、以下のようにLaravel側へ登録しておいてください。

config/logging.php

// 省略

return [

    // 省略

    'channels' => [

        // 省略

        'minimal_notification' => [
            'driver' => 'custom',
            'via' => App\Logging\MinimalNotificationLogger::class,
        ],

    ],

];

また、初期状態ではログチャンネルはstackになっているので、合わせて.envも変更しておきましょう。

.env

LOG_CHANNEL=minimal_notification

※ ただし、開発中はこの設定だとメンドウなので、これを変更するのは本番環境の.envだけの方がいいでしょう。

これでチャンネルの作成は完了です❗

独自のログ・ハンドラーをつくる

では、グループに対して各メンバーとなる「ハンドラー」をつくっていきましょう。

以下のファイルを作成してください。

app/Logging/MinimalNotificationHandler.php

<?php

namespace App\Logging;

use App\Mail\ErrorOccurred;
use App\Models\ErrorLog;
use Illuminate\Support\Facades\Mail;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Handler\StreamHandler;

class MinimalNotificationHandler extends AbstractProcessingHandler
{
    const SKIP_SECONDS = 2592000; // 30 days

    protected function write(array $record): void
    {
        // Exception を取得
        $e = data_get($record, 'context.exception');

        if($e) {

            $file = $e->getFile();
            $line = $e->getLine();
            $message = $e->getMessage();
            $now = now();

            $exists = ErrorLog::query()
                ->where('file', $file)
                ->where('line', $line)
                ->where('message', $message)
                ->where('created_at', '>', $now->subSeconds(self::SKIP_SECONDS))
                ->exists();

            if($exists === false) { // 一定期間に同じエラーがない場合(メール送信)

                // DB に保存
                $error_log = new ErrorLog();
                $error_log->file = $file;
                $error_log->line = $line;
                $error_log->message = $message;
                $error_log->save();

                // 念のため、ログをファイルにも書き込む
                $path = storage_path('logs/minimal_notification.log');
                $stream = new StreamHandler($path);
                $stream->write($record);

                // メールで通知する
                $admin_email_address = 'admin@example.com'; // 送信先のメールアドレス
                Mail::to($admin_email_address)->send(new ErrorOccurred($error_log));

            }
        }
    }
}

この中でやっているのは以下のとおりです。

  1. ファイルパス、行、メッセージを使ってメッセージ送信すべきかをチェック
  2. 送信すべき場合はメールで通知
  3. さらに、念の為テキストのログも残します

なお、少しイレギュラーなのはStreamHandlerを使っている部分です。

これは、ログ管理パッケージMonoLogに用意されている基本的なハンドラーなのですが、これを手動で起動してテキストログに書き込んでいるわけです。(自分で実装しなくていい部分をショートカットしているわけですね👍)

メール送信部分をつくる

では、先ほどのハンドラーでセットしたメール送信部分をつくっていきましょう。

以下のコマンドを実行してください。

php artisan make:mail ErrorOccurred

すると、Mailableファイルが作成されるので中身を以下のようにします。

app/Mail/ErrorOccurred.php

<?php

namespace App\Mail;

use App\Models\ErrorLog;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class ErrorOccurred extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(private ErrorLog $error_log)
    {}

    /**
     * Get the message envelope.
     *
     * @return \Illuminate\Mail\Mailables\Envelope
     */
    public function envelope()
    {
        return new Envelope(
            subject: 'エラーが発生しました',
        );
    }

    /**
     * Get the message content definition.
     *
     * @return \Illuminate\Mail\Mailables\Content
     */
    public function content()
    {
        return new Content(
            view: 'emails.error_occurred',
            with: [
                'error_log' => $this->error_log,
            ],
        );
    }

    // 省略

}

ここも基本的なメール送信となっています。(少し前に形式が変わりましたね)

そして、メール文面のテンプレートをつくります。
以下のファイルを作成してください。

resources/views/emails/error_occurred.blade.php

エラーが発生しました。<br><br>

ファイル: {{ $error_log->file }}<br>
行: {{ $error_log->line }}<br>
メッセージ: {{ $error_log->message }}<br>
ページ URL: {{ request()->fullUrl() }}<br><br>

あせらずエラー処理しよう &#x2728;&#x1F60A;&#x1F44D;

これで作業はすべて完了です❗
お疲れ様でした😄

テストしてみる

では、実際にテストしてみましょう❗
まずは、ルートにわざとエラーが出るコードを追加して実行してみましょう。

※ なお、メール受信は mailcatcher をつかっています。

routes/web.php

// 省略

Route::get('minimal_notification', function(){

   echo $test; // 👈 変数が存在してないのでエラーになる

});

どうなるでしょうか・・・・・・

はい❗
うまくエラーの内容が送信されてきました。

ちなみに、DBの中身はこうなっています。

そして、テキストログはこうです。

[2023-02-04T01:38:16.365208+09:00] minimal_notification.ERROR: Undefined variable $test {"exception":"[object] (ErrorException(code: 0): Undefined variable $test at /var/www/html/l9x-ssr/routes/web.php:313)"} []

では、続いて同じページをリロードして、メールが送信されてこないかをチェックしてみましょう。

すると・・・・・・

はい❗

メールは送信されて来ず、DBにもデータは追加されていません。(これは1ヶ月スキップされることになります)

では最後に、例外処理ではどうなるか見てみましょう。

routes/web.php

// 省略

Route::get('minimal_notification', function(){

   throw new \Exception('何かの例外が発生しました!'); // 👈 例外を実行する

});

うまくいくでしょうか・・・・・・

はい❗
こちらもうまく通知されてきました。

すべて成功です😄✨

企業様へのご提案

今回は「必要最小限の通知」にしていますが、独自ログはアイデアしだいで無限に変更することができます。

例えば、以下のようなものも履歴に残すことができます。

  • ログインしているユーザーは誰だったのか?
  • IP アドレスの取得
  • アクセスされた URL やパラメータの取得
  • セッションやクッキーの状態
  • 注文しようとしていた商品は何だったのか?

などなど

もしこういった内容をご準備されたい場合はぜひお問い合わせからご相談ください。お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravelで独自ログを使い、エラー通知をする機能を実装してみました。

ウェブサービスでいうとこの間記事にもしたBugSnapがとても便利でしたが、それほど無料枠があるわけではないので、個人開発の場合だとこういった工夫をすることで使いやすくなるんじゃないでしょうか。

📝 参考ページ: Laravel & Bugsnag でエラー検知&監視をする

ぜひ皆さんも個人開発する際の参考にしてみてくださいね。

ではでは〜❗

「バーチャルな
キーボード&モニターがあれば、
外出先でもバグ対応できるのになぁ…」

このエントリーをはてなブックマークに追加       follow us in feedly