256 lines
8.7 KiB
PHP
256 lines
8.7 KiB
PHP
<?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(),
|
|
]);
|
|
}
|
|
}
|