chore: complete update
This commit is contained in:
233
src/Framework/Database/HealthCheck/ConnectionHealthChecker.php
Normal file
233
src/Framework/Database/HealthCheck/ConnectionHealthChecker.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\HealthCheck;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Exception\DatabaseException;
|
||||
|
||||
final readonly class ConnectionHealthChecker
|
||||
{
|
||||
public function __construct(
|
||||
private int $timeoutSeconds = 5,
|
||||
private array $customQueries = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Führt einen einfachen Health Check aus
|
||||
*/
|
||||
public function checkHealth(ConnectionInterface $connection): HealthCheckResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
// Einfacher SELECT zum Testen der Verbindung
|
||||
$result = $connection->queryScalar('SELECT 1');
|
||||
|
||||
$responseTime = (microtime(true) - $startTime) * 1000; // in Millisekunden
|
||||
|
||||
if ($result != 1) {
|
||||
return HealthCheckResult::unhealthy(
|
||||
$responseTime,
|
||||
'Unexpected result from health check query',
|
||||
null,
|
||||
['expected' => 1, 'actual' => $result]
|
||||
);
|
||||
}
|
||||
|
||||
return HealthCheckResult::healthy($responseTime, 'Connection is healthy');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$responseTime = (microtime(true) - $startTime) * 1000;
|
||||
return HealthCheckResult::error($e, $responseTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen Health Check mit Timeout aus
|
||||
*/
|
||||
public function checkHealthWithTimeout(ConnectionInterface $connection): HealthCheckResult
|
||||
{
|
||||
$startTime = time();
|
||||
|
||||
while ((time() - $startTime) < $this->timeoutSeconds) {
|
||||
$result = $this->checkHealth($connection);
|
||||
|
||||
if ($result->isHealthy) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Kurz warten vor nächstem Versuch
|
||||
usleep(100000); // 100ms
|
||||
}
|
||||
|
||||
return HealthCheckResult::timeout($this->timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt erweiterte Health Checks aus
|
||||
*/
|
||||
public function checkDetailedHealth(ConnectionInterface $connection): HealthCheckResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$additionalData = [];
|
||||
|
||||
try {
|
||||
// 1. Basis Health Check
|
||||
$basicResult = $this->checkHealth($connection);
|
||||
if (!$basicResult->isHealthy) {
|
||||
return $basicResult;
|
||||
}
|
||||
|
||||
$additionalData['basic_check'] = $basicResult->toArray();
|
||||
|
||||
// 2. PDO-Status prüfen
|
||||
$pdoStatus = $this->checkPdoStatus($connection);
|
||||
$additionalData['pdo_status'] = $pdoStatus;
|
||||
|
||||
// 3. Custom Queries ausführen
|
||||
if (!empty($this->customQueries)) {
|
||||
$customResults = $this->executeCustomQueries($connection);
|
||||
$additionalData['custom_queries'] = $customResults;
|
||||
}
|
||||
|
||||
// 4. Connection Attributes prüfen
|
||||
$attributes = $this->getConnectionAttributes($connection);
|
||||
$additionalData['connection_attributes'] = $attributes;
|
||||
|
||||
$responseTime = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
return HealthCheckResult::healthy(
|
||||
$responseTime,
|
||||
'Detailed health check passed',
|
||||
$additionalData
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$responseTime = (microtime(true) - $startTime) * 1000;
|
||||
return HealthCheckResult::error($e, $responseTime)->withAdditionalData('partial_data', $additionalData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob die Connection noch lebendig ist (schneller Check)
|
||||
*/
|
||||
public function checkConnectionAlive(ConnectionInterface $connection): bool
|
||||
{
|
||||
try {
|
||||
$pdo = $connection->getPdo();
|
||||
|
||||
// Prüfe ob die PDO-Verbindung noch aktiv ist
|
||||
$stmt = $pdo->query('SELECT 1');
|
||||
return $stmt !== false;
|
||||
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft den PDO-Status
|
||||
*/
|
||||
private function checkPdoStatus(ConnectionInterface $connection): array
|
||||
{
|
||||
try {
|
||||
$pdo = $connection->getPdo();
|
||||
|
||||
return [
|
||||
'connection_status' => $pdo->getAttribute(\PDO::ATTR_CONNECTION_STATUS),
|
||||
'server_info' => $pdo->getAttribute(\PDO::ATTR_SERVER_INFO),
|
||||
'driver_name' => $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME),
|
||||
'client_version' => $pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION),
|
||||
'server_version' => $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION),
|
||||
'in_transaction' => $connection->inTransaction(),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt benutzerdefinierte Queries aus
|
||||
*/
|
||||
private function executeCustomQueries(ConnectionInterface $connection): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($this->customQueries as $name => $query) {
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
$result = $connection->queryScalar($query);
|
||||
$responseTime = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
$results[$name] = [
|
||||
'success' => true,
|
||||
'result' => $result,
|
||||
'response_time_ms' => round($responseTime, 2),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$results[$name] = [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'response_time_ms' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt Connection-Attribute
|
||||
*/
|
||||
private function getConnectionAttributes(ConnectionInterface $connection): array
|
||||
{
|
||||
try {
|
||||
$pdo = $connection->getPdo();
|
||||
|
||||
$attributes = [];
|
||||
$attributesToCheck = [
|
||||
'AUTOCOMMIT' => \PDO::ATTR_AUTOCOMMIT,
|
||||
'ERRMODE' => \PDO::ATTR_ERRMODE,
|
||||
'CASE' => \PDO::ATTR_CASE,
|
||||
'NULL_TO_STRING' => \PDO::ATTR_NULL_TO_STRING,
|
||||
'STRINGIFY_FETCHES' => \PDO::ATTR_STRINGIFY_FETCHES,
|
||||
'STATEMENT_CLASS' => \PDO::ATTR_STATEMENT_CLASS,
|
||||
'TIMEOUT' => \PDO::ATTR_TIMEOUT,
|
||||
'EMULATE_PREPARES' => \PDO::ATTR_EMULATE_PREPARES,
|
||||
'DEFAULT_FETCH_MODE' => \PDO::ATTR_DEFAULT_FETCH_MODE,
|
||||
];
|
||||
|
||||
foreach ($attributesToCheck as $name => $constant) {
|
||||
try {
|
||||
$attributes[$name] = $pdo->getAttribute($constant);
|
||||
} catch (\Throwable) {
|
||||
$attributes[$name] = 'Not supported';
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
} catch (\Throwable $e) {
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory-Methoden für verschiedene Health Check-Konfigurationen
|
||||
*/
|
||||
public static function quick(int $timeoutSeconds = 2): self
|
||||
{
|
||||
return new self($timeoutSeconds);
|
||||
}
|
||||
|
||||
public static function detailed(int $timeoutSeconds = 10, array $customQueries = []): self
|
||||
{
|
||||
return new self($timeoutSeconds, $customQueries);
|
||||
}
|
||||
|
||||
public static function withCustomQueries(array $customQueries, int $timeoutSeconds = 5): self
|
||||
{
|
||||
return new self($timeoutSeconds, $customQueries);
|
||||
}
|
||||
}
|
||||
75
src/Framework/Database/HealthCheck/HealthCheckResult.php
Normal file
75
src/Framework/Database/HealthCheck/HealthCheckResult.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\HealthCheck;
|
||||
|
||||
final readonly class HealthCheckResult
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isHealthy,
|
||||
public float $responseTimeMs,
|
||||
public ?string $message = null,
|
||||
public ?\Throwable $exception = null,
|
||||
public array $additionalData = []
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'healthy' => $this->isHealthy,
|
||||
'response_time_ms' => round($this->responseTimeMs, 2),
|
||||
'message' => $this->message,
|
||||
'error' => $this->exception?->getMessage(),
|
||||
'error_code' => $this->exception?->getCode(),
|
||||
'additional_data' => $this->additionalData,
|
||||
'timestamp' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function withAdditionalData(string $key, mixed $value): self
|
||||
{
|
||||
return new self(
|
||||
$this->isHealthy,
|
||||
$this->responseTimeMs,
|
||||
$this->message,
|
||||
$this->exception,
|
||||
array_merge($this->additionalData, [$key => $value])
|
||||
);
|
||||
}
|
||||
|
||||
public function getAdditionalData(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->additionalData[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function hasAdditionalData(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->additionalData);
|
||||
}
|
||||
|
||||
public static function healthy(float $responseTimeMs, ?string $message = null, array $additionalData = []): self
|
||||
{
|
||||
return new self(true, $responseTimeMs, $message, null, $additionalData);
|
||||
}
|
||||
|
||||
public static function unhealthy(float $responseTimeMs, string $message, ?\Throwable $exception = null, array $additionalData = []): self
|
||||
{
|
||||
return new self(false, $responseTimeMs, $message, $exception, $additionalData);
|
||||
}
|
||||
|
||||
public static function timeout(float $timeoutMs): self
|
||||
{
|
||||
return self::unhealthy($timeoutMs, "Health check timed out after {$timeoutMs}ms");
|
||||
}
|
||||
|
||||
public static function error(\Throwable $exception, float $responseTimeMs = 0): self
|
||||
{
|
||||
return self::unhealthy($responseTimeMs, $exception->getMessage(), $exception);
|
||||
}
|
||||
}
|
||||
162
src/Framework/Database/HealthCheck/HealthCheckScheduler.php
Normal file
162
src/Framework/Database/HealthCheck/HealthCheckScheduler.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\HealthCheck;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
|
||||
final class HealthCheckScheduler
|
||||
{
|
||||
private array $healthChecks = [];
|
||||
private array $lastResults = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ConnectionHealthChecker $healthChecker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registriert einen Health Check
|
||||
*/
|
||||
public function registerHealthCheck(
|
||||
string $name,
|
||||
ConnectionInterface $connection,
|
||||
int $intervalSeconds = 30,
|
||||
bool $detailed = false
|
||||
): self {
|
||||
$this->healthChecks[$name] = [
|
||||
'connection' => $connection,
|
||||
'interval' => $intervalSeconds,
|
||||
'detailed' => $detailed,
|
||||
'last_check' => 0,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt alle fälligen Health Checks aus
|
||||
*/
|
||||
public function runScheduledChecks(): array
|
||||
{
|
||||
$results = [];
|
||||
$currentTime = time();
|
||||
|
||||
foreach ($this->healthChecks as $name => $config) {
|
||||
if (($currentTime - $config['last_check']) >= $config['interval']) {
|
||||
$result = $this->runHealthCheck($name, $config);
|
||||
$results[$name] = $result;
|
||||
|
||||
$this->lastResults[$name] = $result;
|
||||
$this->healthChecks[$name]['last_check'] = $currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen spezifischen Health Check aus
|
||||
*/
|
||||
public function runHealthCheck(string $name, ?array $config = null): HealthCheckResult
|
||||
{
|
||||
$config = $config ?? $this->healthChecks[$name] ?? null;
|
||||
|
||||
if (!$config) {
|
||||
return HealthCheckResult::error(
|
||||
new \InvalidArgumentException("Health check '{$name}' not found")
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($config['detailed']) {
|
||||
return $this->healthChecker->checkDetailedHealth($config['connection']);
|
||||
} else {
|
||||
return $this->healthChecker->checkHealth($config['connection']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return HealthCheckResult::error($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle letzten Health Check-Ergebnisse zurück
|
||||
*/
|
||||
public function getAllResults(): array
|
||||
{
|
||||
return $this->lastResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das letzte Ergebnis für einen spezifischen Health Check zurück
|
||||
*/
|
||||
public function getLastResult(string $name): ?HealthCheckResult
|
||||
{
|
||||
return $this->lastResults[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob alle registrierten Connections gesund sind
|
||||
*/
|
||||
public function areAllHealthy(): bool
|
||||
{
|
||||
foreach ($this->lastResults as $result) {
|
||||
if (!$result->isHealthy) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine Zusammenfassung aller Health Checks zurück
|
||||
*/
|
||||
public function getSummary(): array
|
||||
{
|
||||
$total = count($this->healthChecks);
|
||||
$healthy = 0;
|
||||
$unhealthy = 0;
|
||||
$unknown = 0;
|
||||
|
||||
foreach ($this->healthChecks as $name => $config) {
|
||||
if (isset($this->lastResults[$name])) {
|
||||
if ($this->lastResults[$name]->isHealthy) {
|
||||
$healthy++;
|
||||
} else {
|
||||
$unhealthy++;
|
||||
}
|
||||
} else {
|
||||
$unknown++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_checks' => $total,
|
||||
'healthy' => $healthy,
|
||||
'unhealthy' => $unhealthy,
|
||||
'unknown' => $unknown,
|
||||
'overall_status' => $unhealthy === 0 ? 'healthy' : 'unhealthy',
|
||||
'last_updated' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt einen Health Check
|
||||
*/
|
||||
public function unregisterHealthCheck(string $name): self
|
||||
{
|
||||
unset($this->healthChecks[$name], $this->lastResults[$name]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alle Health Checks
|
||||
*/
|
||||
public function clear(): self
|
||||
{
|
||||
$this->healthChecks = [];
|
||||
$this->lastResults = [];
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user