67 lines
2.3 KiB
PHP
67 lines
2.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\HealthCheck;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Runs an uptime probe NATIVELY (Laravel HTTP client / a PHP TCP stream) — no SSH, no shell, so the
|
|
* target is never interpolated into a command. An HTTP probe reports only up/down + status code +
|
|
* latency (never the response body), so it can't exfiltrate an internal endpoint's content. Redirects
|
|
* are disabled so a 30x can't bounce the probe to an unseen target.
|
|
*/
|
|
class HealthService
|
|
{
|
|
/**
|
|
* @return array{ok:bool, latency:?int, detail:string}
|
|
*/
|
|
public function probe(HealthCheck $check, int $timeout = 5): array
|
|
{
|
|
return $check->type === 'tcp'
|
|
? $this->tcp((string) $check->target, (int) $check->port, $timeout)
|
|
: $this->http((string) $check->target, $timeout);
|
|
}
|
|
|
|
private function http(string $url, int $timeout): array
|
|
{
|
|
$start = microtime(true);
|
|
try {
|
|
$res = Http::timeout($timeout)->withoutRedirecting()->get($url);
|
|
$latency = (int) round((microtime(true) - $start) * 1000);
|
|
$status = $res->status();
|
|
|
|
// 2xx/3xx = up; a 4xx/5xx means the endpoint answered but is unhealthy.
|
|
return ['ok' => $status >= 200 && $status < 400, 'latency' => $latency, 'detail' => 'HTTP '.$status];
|
|
} catch (Throwable $e) {
|
|
return ['ok' => false, 'latency' => null, 'detail' => $this->reason($e->getMessage())];
|
|
}
|
|
}
|
|
|
|
private function tcp(string $host, int $port, int $timeout): array
|
|
{
|
|
$start = microtime(true);
|
|
$errno = 0;
|
|
$errstr = '';
|
|
$sock = @stream_socket_client('tcp://'.$host.':'.$port, $errno, $errstr, $timeout);
|
|
|
|
if (! $sock) {
|
|
return ['ok' => false, 'latency' => null, 'detail' => $errstr !== '' ? $errstr : 'connection failed'];
|
|
}
|
|
|
|
fclose($sock);
|
|
$latency = (int) round((microtime(true) - $start) * 1000);
|
|
|
|
return ['ok' => true, 'latency' => $latency, 'detail' => 'TCP '.$port.' open'];
|
|
}
|
|
|
|
/** Keep the failure reason short + on one line for the compact status row. */
|
|
private function reason(string $msg): string
|
|
{
|
|
$msg = trim(preg_split('/\R/', $msg)[0] ?? $msg);
|
|
|
|
return mb_strlen($msg) > 80 ? mb_substr($msg, 0, 80).'…' : $msg;
|
|
}
|
|
}
|