
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、私は過去に(ほぼ鳴かず飛ばずの)個人サイトをつくってきましたが、その開発をしている中でいくつか「うーん…」となってしまった経験がありました。
そして、その中には今回テーマ「エラー通知」があります。
どういうことかと言うと、
「エラーログをいちいちチェックするとめんどうだよね」
だから、メールや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));
}
}
}
}
この中でやっているのは以下のとおりです。
- ファイルパス、行、メッセージを使ってメッセージ送信すべきかをチェック
- 送信すべき場合はメールで通知
- さらに、念の為テキストのログも残します
なお、少しイレギュラーなのは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>
あせらずエラー処理しよう ✨😊👍
これで作業はすべて完了です
お疲れ様でした
テストしてみる
では、実際にテストしてみましょう
まずは、ルートにわざとエラーが出るコードを追加して実行してみましょう。
※ なお、メール受信は 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 やパラメータの取得
- セッションやクッキーの状態
- 注文しようとしていた商品は何だったのか?
などなど
もしこういった内容をご準備されたい場合はぜひお問い合わせからご相談ください。お待ちしております。
おわりに
ということで、今回はLaravel
で独自ログを使い、エラー通知をする機能を実装してみました。
ウェブサービスでいうとこの間記事にもしたBugSnap
がとても便利でしたが、それほど無料枠があるわけではないので、個人開発の場合だとこういった工夫をすることで使いやすくなるんじゃないでしょうか。
参考ページ: Laravel & Bugsnag でエラー検知&監視をする
ぜひ皆さんも個人開発する際の参考にしてみてくださいね。
ではでは〜
「バーチャルな
キーボード&モニターがあれば、
外出先でもバグ対応できるのになぁ…」