complete story 8.10 with qa test
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
/**
|
||||
* Marker interface for exceptions that should trigger admin notification.
|
||||
* Implement this interface on custom exceptions that require admin attention.
|
||||
*/
|
||||
interface ShouldNotifyAdmin {}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Queue\Events\JobFailed;
|
||||
|
||||
class QueueFailureNotification extends Notification
|
||||
{
|
||||
public function __construct(
|
||||
public JobFailed $event
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$jobName = $this->event->job->resolveName();
|
||||
$environment = app()->environment();
|
||||
|
||||
return (new MailMessage)
|
||||
->subject("[URGENT] [{$environment}] Queue Job Failed: {$jobName} - Libra Law Firm")
|
||||
->greeting('Queue Failure Alert')
|
||||
->line('A queued job has failed.')
|
||||
->line("**Job:** {$jobName}")
|
||||
->line('**Queue:** '.$this->event->job->getQueue())
|
||||
->line('**Error:** '.$this->event->exception->getMessage())
|
||||
->line('**Time:** '.now()->format('Y-m-d H:i:s'))
|
||||
->line('**Attempts:** '.$this->event->job->attempts())
|
||||
->line('Please check the failed_jobs table and application logs for details.')
|
||||
->salutation('— Libra System Monitor');
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'queue_failure',
|
||||
'job' => $this->event->job->resolveName(),
|
||||
'queue' => $this->event->job->getQueue(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Throwable;
|
||||
|
||||
class SystemErrorNotification extends Notification
|
||||
{
|
||||
public function __construct(
|
||||
public Throwable $exception
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$environment = app()->environment();
|
||||
$errorType = class_basename($this->exception);
|
||||
|
||||
return (new MailMessage)
|
||||
->subject("[URGENT] [{$environment}] System Error: {$errorType} - Libra Law Firm")
|
||||
->greeting('System Alert')
|
||||
->line('A critical error occurred on the platform.')
|
||||
->line("**Error Type:** {$errorType}")
|
||||
->line('**Message:** '.$this->exception->getMessage())
|
||||
->line('**File:** '.$this->exception->getFile().':'.$this->exception->getLine())
|
||||
->line('**Time:** '.now()->format('Y-m-d H:i:s'))
|
||||
->line("**Environment:** {$environment}")
|
||||
->line('Please check the application logs for full stack trace and details.')
|
||||
->salutation('— Libra System Monitor');
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'system_error',
|
||||
'error_type' => class_basename($this->exception),
|
||||
'message' => $this->exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,14 @@ namespace App\Providers;
|
||||
use App\Listeners\LogFailedLoginAttempt;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\TimelineUpdate;
|
||||
use App\Notifications\QueueFailureNotification;
|
||||
use App\Observers\ConsultationObserver;
|
||||
use App\Observers\TimelineUpdateObserver;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Illuminate\Queue\Events\JobFailed;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -30,5 +34,20 @@ class AppServiceProvider extends ServiceProvider
|
||||
|
||||
Consultation::observe(ConsultationObserver::class);
|
||||
TimelineUpdate::observe(TimelineUpdateObserver::class);
|
||||
|
||||
Queue::failing(function (JobFailed $event) {
|
||||
if (! config('libra.notifications.system_notifications_enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adminEmail = config('libra.notifications.admin_email');
|
||||
|
||||
if (empty($adminEmail)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::route('mail', $adminEmail)
|
||||
->notifyNow(new QueueFailureNotification($event));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Contracts\ShouldNotifyAdmin;
|
||||
use App\Notifications\SystemErrorNotification;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Throwable;
|
||||
|
||||
class AdminNotificationService
|
||||
{
|
||||
public function notifyError(Throwable $exception): void
|
||||
{
|
||||
if (! config('libra.notifications.system_notifications_enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adminEmail = config('libra.notifications.admin_email');
|
||||
|
||||
if (empty($adminEmail)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$errorKey = $this->getErrorKey($exception);
|
||||
$throttleMinutes = config('libra.notifications.throttle_minutes', 15);
|
||||
|
||||
if (Cache::has($errorKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cache::put($errorKey, true, now()->addMinutes($throttleMinutes));
|
||||
|
||||
Notification::route('mail', $adminEmail)
|
||||
->notifyNow(new SystemErrorNotification($exception));
|
||||
}
|
||||
|
||||
protected function getErrorKey(Throwable $exception): string
|
||||
{
|
||||
return 'admin_notified:'.md5(get_class($exception).$exception->getMessage());
|
||||
}
|
||||
|
||||
public function shouldNotifyAdmin(Throwable $exception): bool
|
||||
{
|
||||
if ($exception instanceof ShouldNotifyAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$excludedTypes = [
|
||||
\Illuminate\Validation\ValidationException::class,
|
||||
\Illuminate\Auth\AuthenticationException::class,
|
||||
\Illuminate\Auth\Access\AuthorizationException::class,
|
||||
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
|
||||
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
|
||||
\Illuminate\Session\TokenMismatchException::class,
|
||||
];
|
||||
|
||||
foreach ($excludedTypes as $type) {
|
||||
if ($exception instanceof $type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($exception instanceof \Illuminate\Database\QueryException) {
|
||||
return $this->isCriticalDatabaseError($exception);
|
||||
}
|
||||
|
||||
if ($exception instanceof \Symfony\Component\Mailer\Exception\TransportExceptionInterface) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function isCriticalDatabaseError(\Illuminate\Database\QueryException $e): bool
|
||||
{
|
||||
$criticalCodes = ['2002', '1045', '1049'];
|
||||
|
||||
return in_array($e->getCode(), $criticalCodes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user