You've already forked Atomcms-edit
Initial commit
This commit is contained in:
Executable
+267
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Services\AlertService;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
#[\Override]
|
||||
protected $levels = [
|
||||
//
|
||||
];
|
||||
|
||||
#[\Override]
|
||||
protected $dontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
#[\Override]
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
private const string CACHE_KEY_ERROR_COUNT = 'error_count_';
|
||||
|
||||
private const int ERROR_COUNT_DURATION = 300;
|
||||
|
||||
private const array AUTO_RECOVERABLE_ERRORS = [
|
||||
'ViteManifestNotFoundException',
|
||||
'filemtime',
|
||||
'stat failed for',
|
||||
'No application encryption key has been specified',
|
||||
'file does not exist',
|
||||
'view [',
|
||||
'Target class [view] does not exist',
|
||||
'BindingResolutionException',
|
||||
];
|
||||
|
||||
private const int AUTO_RECOVER_COOLDOWN = 60;
|
||||
|
||||
#[\Override]
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
$this->attemptAutoRecovery($e);
|
||||
$this->handleExceptionAlert($e);
|
||||
});
|
||||
|
||||
$this->renderable(function (Throwable $e) {
|
||||
$recovered = $this->attemptAutoRecovery($e);
|
||||
if ($recovered) {
|
||||
return redirect()->to(request()->url());
|
||||
}
|
||||
|
||||
$this->handleExceptionAlert($e);
|
||||
});
|
||||
}
|
||||
|
||||
private function attemptAutoRecovery(Throwable $e): bool
|
||||
{
|
||||
$message = $e->getMessage() ?: '';
|
||||
$exceptionClass = $e::class;
|
||||
$isRecoverable = array_any(self::AUTO_RECOVERABLE_ERRORS, fn ($pattern) => str_contains($exceptionClass, (string) $pattern) || str_contains($message, (string) $pattern));
|
||||
|
||||
if (! $isRecoverable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheKey = 'auto_recovery_cooldown';
|
||||
if (Cache::has($cacheKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Cache::put($cacheKey, true, self::AUTO_RECOVER_COOLDOWN);
|
||||
|
||||
Log::warning('Attempting auto-recovery from error', [
|
||||
'exception' => $exceptionClass,
|
||||
'message' => $message,
|
||||
]);
|
||||
|
||||
try {
|
||||
Artisan::call('view:clear');
|
||||
Artisan::call('cache:clear');
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('route:clear');
|
||||
|
||||
Artisan::call('config:cache');
|
||||
Artisan::call('view:cache');
|
||||
|
||||
if (str_contains($exceptionClass, 'ViteManifestNotFoundException') || str_contains($message, 'Vite manifest')) {
|
||||
$this->rebuildViteManifest();
|
||||
}
|
||||
|
||||
if (function_exists('opcache_reset')) {
|
||||
@opcache_reset();
|
||||
}
|
||||
|
||||
Log::info('Auto-recovery completed successfully');
|
||||
|
||||
return true;
|
||||
} catch (Throwable $recoveryError) {
|
||||
Log::error('Auto-recovery failed', [
|
||||
'original_error' => $message,
|
||||
'recovery_error' => $recoveryError->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function rebuildViteManifest(): void
|
||||
{
|
||||
$manifestPath = public_path('build/manifest.json');
|
||||
|
||||
if (! file_exists($manifestPath)) {
|
||||
Log::warning('Vite manifest missing, attempting rebuild');
|
||||
|
||||
$result = Process::timeout(120)->run('npm run build');
|
||||
|
||||
if ($result->successful()) {
|
||||
Log::info('Vite manifest rebuilt successfully');
|
||||
|
||||
if (file_exists('/var/www/atomcms/public/build')) {
|
||||
Process::run('chown -R www-data:www-data /var/www/atomcms/public/build');
|
||||
Process::run('chmod -R 775 /var/www/atomcms/public/build');
|
||||
}
|
||||
} else {
|
||||
Log::error('Vite manifest rebuild failed', [
|
||||
'output' => $result->output(),
|
||||
'error' => $result->errorOutput(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function handleExceptionAlert(Throwable $e): void
|
||||
{
|
||||
if (! $this->shouldAlertException($e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$alertService = app(AlertService::class);
|
||||
|
||||
$errorMessage = $e->getMessage() ?: $e::class;
|
||||
|
||||
$alertService->sendCriticalError($errorMessage, $e);
|
||||
|
||||
$this->trackErrorRate();
|
||||
|
||||
Log::channel('emergency')->error('Critical exception reported via alert', [
|
||||
'exception' => $e::class,
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
} catch (\Exception $alertException) {
|
||||
Log::error('Failed to send exception alert: ' . $alertException->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldAlertException(Throwable $e): bool
|
||||
{
|
||||
if (! app()->isBooted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! (bool) setting('alert_errors_enabled', true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$criticalExceptions = [
|
||||
QueryException::class,
|
||||
RconConnectionException::class,
|
||||
];
|
||||
|
||||
foreach ($criticalExceptions as $criticalException) {
|
||||
if ($e instanceof $criticalException) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isHighErrorRate()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$minSeverity = setting('alert_min_severity', 'error');
|
||||
|
||||
return $this->isSeverityHighEnough($e, $minSeverity);
|
||||
}
|
||||
|
||||
private function trackErrorRate(): void
|
||||
{
|
||||
if (! app()->isBooted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = self::CACHE_KEY_ERROR_COUNT . now()->format('YmdHi');
|
||||
|
||||
$count = Cache::get($key, 0);
|
||||
Cache::put($key, $count + 1, self::ERROR_COUNT_DURATION);
|
||||
}
|
||||
|
||||
private function isHighErrorRate(): bool
|
||||
{
|
||||
if (! app()->isBooted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$threshold = (int) setting('alert_error_threshold', 10);
|
||||
$key = self::CACHE_KEY_ERROR_COUNT . now()->format('YmdHi');
|
||||
|
||||
$count = Cache::get($key, 0);
|
||||
|
||||
return $count >= $threshold;
|
||||
}
|
||||
|
||||
private function isSeverityHighEnough(Throwable $e, string $minSeverity): bool
|
||||
{
|
||||
$severityLevel = [
|
||||
'info' => 0,
|
||||
'warning' => 1,
|
||||
'error' => 2,
|
||||
'critical' => 3,
|
||||
];
|
||||
|
||||
$exceptionSeverity = $this->determineExceptionSeverity($e);
|
||||
$minLevel = $severityLevel[$minSeverity] ?? 2;
|
||||
|
||||
return $exceptionSeverity >= $minLevel;
|
||||
}
|
||||
|
||||
private function determineExceptionSeverity(Throwable $e): int
|
||||
{
|
||||
if ($e instanceof \Error) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if ($e instanceof ValidationException) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($e instanceof AuthenticationException) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($e instanceof AuthorizationException) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user