Files
ismysiteup.online/app/Jobs/CheckHealthEndpoint.php
T

256 lines
8.7 KiB
PHP
Raw Normal View History

2026-01-17 16:42:43 +02:00
<?php
namespace App\Jobs;
use App\Models\HealthCheck;
use App\Notifications\HealthCheckAlert;
use App\Notifications\PerformanceAlert;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CheckHealthEndpoint implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 3;
/**
* The number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 60;
/**
* Create a new job instance.
*/
public function __construct(
public HealthCheck $healthCheck
) {}
/**
* Execute the job.
*/
public function handle(): void
{
try {
$client = new Client([
'timeout' => 10,
'connect_timeout' => 5,
'allow_redirects' => [
'max' => 3,
'track_redirects' => true,
],
'http_errors' => false,
]);
$url = $this->healthCheck->full_url;
$startTime = microtime(true);
Log::info('CheckHealthEndpoint: Starting check', [
'health_check_id' => $this->healthCheck->id,
'url' => $url,
]);
$response = $client->get($url, [
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'IsMySiteUp Health Monitor/1.0',
],
]);
$endTime = microtime(true);
$responseTime = (int) round(($endTime - $startTime) * 1000);
$statusCode = $response->getStatusCode();
$body = (string) $response->getBody();
// Try to decode JSON
$responseData = null;
$isValidJson = false;
try {
$responseData = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
$isValidJson = true;
} catch (\JsonException $e) {
Log::warning('CheckHealthEndpoint: Invalid JSON response', [
'health_check_id' => $this->healthCheck->id,
'error' => $e->getMessage(),
'body_preview' => substr($body, 0, 500),
]);
}
// Determine if endpoint is healthy
$isHealthy = $this->determineHealth($statusCode, $isValidJson, $responseData);
$wasHealthy = $this->healthCheck->is_healthy;
// Update health check record
$this->healthCheck->update([
'last_checked_at' => now(),
'is_healthy' => $isHealthy,
'response_data' => $responseData,
'response_time_ms' => $responseTime,
'status_code' => $statusCode,
]);
Log::info('CheckHealthEndpoint: Check completed', [
'health_check_id' => $this->healthCheck->id,
'is_healthy' => $isHealthy,
'status_code' => $statusCode,
'response_time_ms' => $responseTime,
]);
// Send notification if health status changed from healthy to unhealthy
if ($wasHealthy === true && $isHealthy === false) {
$this->healthCheck->site->user->notify(new HealthCheckAlert(
$this->healthCheck,
'unhealthy',
$statusCode,
$responseTime
));
Log::warning('CheckHealthEndpoint: Unhealthy alert sent', [
'health_check_id' => $this->healthCheck->id,
'status_code' => $statusCode,
]);
}
// Optionally send notification when health is restored
if ($wasHealthy === false && $isHealthy === true) {
$this->healthCheck->site->user->notify(new HealthCheckAlert(
$this->healthCheck,
'healthy',
$statusCode,
$responseTime
));
Log::info('CheckHealthEndpoint: Healthy alert sent', [
'health_check_id' => $this->healthCheck->id,
]);
}
// Check performance threshold and send alert if exceeded
if ($this->healthCheck->performance_threshold && $responseTime > $this->healthCheck->performance_threshold) {
$this->healthCheck->site->user->notify(new PerformanceAlert(
resourceType: 'Health Check',
resourceName: $this->healthCheck->endpoint,
responseTime: $responseTime,
threshold: $this->healthCheck->performance_threshold,
url: $this->healthCheck->full_url,
resourceId: $this->healthCheck->site_id
));
Log::warning('CheckHealthEndpoint: Performance threshold exceeded', [
'health_check_id' => $this->healthCheck->id,
'response_time_ms' => $responseTime,
'threshold_ms' => $this->healthCheck->performance_threshold,
]);
}
} catch (ConnectException $e) {
// Connection timeout or DNS failure
Log::error('CheckHealthEndpoint: Connection failed', [
'health_check_id' => $this->healthCheck->id,
'error' => $e->getMessage(),
]);
$this->healthCheck->update([
'last_checked_at' => now(),
'is_healthy' => false,
'response_data' => ['error' => 'Connection failed: ' . $e->getMessage()],
'response_time_ms' => null,
'status_code' => null,
]);
} catch (RequestException $e) {
// HTTP error occurred
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : null;
Log::error('CheckHealthEndpoint: Request failed', [
'health_check_id' => $this->healthCheck->id,
'status_code' => $statusCode,
'error' => $e->getMessage(),
]);
$this->healthCheck->update([
'last_checked_at' => now(),
'is_healthy' => false,
'response_data' => ['error' => 'Request failed: ' . $e->getMessage()],
'response_time_ms' => null,
'status_code' => $statusCode,
]);
} catch (\Exception $e) {
// Any other exception
Log::error('CheckHealthEndpoint: Unexpected error', [
'health_check_id' => $this->healthCheck->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->healthCheck->update([
'last_checked_at' => now(),
'is_healthy' => false,
'response_data' => ['error' => 'Unexpected error: ' . $e->getMessage()],
'response_time_ms' => null,
'status_code' => null,
]);
throw $e;
}
}
/**
* Determine if the endpoint is healthy based on response.
*/
protected function determineHealth(int $statusCode, bool $isValidJson, ?array $responseData): bool
{
// Check if status code is successful (2xx)
if ($statusCode < 200 || $statusCode >= 300) {
return false;
}
// Check if response is valid JSON
if (!$isValidJson) {
return false;
}
// Validate expected JSON keys if configured
if (!empty($this->healthCheck->expected_json_keys)) {
if (!$this->healthCheck->validateResponseKeys($responseData ?? [])) {
Log::warning('CheckHealthEndpoint: Missing expected JSON keys', [
'health_check_id' => $this->healthCheck->id,
'expected_keys' => $this->healthCheck->expected_json_keys,
'response_keys' => array_keys($responseData ?? []),
]);
return false;
}
}
return true;
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::error('CheckHealthEndpoint: Job failed after all retries', [
'health_check_id' => $this->healthCheck->id,
'exception' => $exception->getMessage(),
]);
}
}