Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
86
src/Framework/Health/Checks/CacheHealthCheck.php
Normal file
86
src/Framework/Health/Checks/CacheHealthCheck.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health\Checks;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Health\HealthCheckCategory;
|
||||
use App\Framework\Health\HealthCheckInterface;
|
||||
use App\Framework\Health\HealthCheckResult;
|
||||
|
||||
final readonly class CacheHealthCheck implements HealthCheckInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $cache
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(): HealthCheckResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$testKey = CacheKey::fromString('health_check_' . time());
|
||||
$testValue = 'health_check_value';
|
||||
|
||||
try {
|
||||
// Test write
|
||||
$testItem = CacheItem::forSet($testKey, $testValue, Duration::fromSeconds(60));
|
||||
$this->cache->set($testItem);
|
||||
|
||||
// Test read
|
||||
$cachedValue = $this->cache->get($testKey);
|
||||
|
||||
// Test delete
|
||||
$this->cache->forget($testKey);
|
||||
|
||||
$responseTime = microtime(true) - $startTime;
|
||||
|
||||
if ($cachedValue->isHit && $cachedValue->value === $testValue) {
|
||||
return HealthCheckResult::healthy(
|
||||
'Cache system is working properly',
|
||||
[
|
||||
'operations' => ['set', 'get', 'delete'],
|
||||
'response_time_ms' => round($responseTime * 1000, 2),
|
||||
],
|
||||
$responseTime
|
||||
);
|
||||
}
|
||||
|
||||
return HealthCheckResult::warning(
|
||||
'Cache operations partially failed',
|
||||
[
|
||||
'hit' => $cachedValue->isHit,
|
||||
'value_match' => $cachedValue->value === $testValue,
|
||||
],
|
||||
$responseTime
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$responseTime = microtime(true) - $startTime;
|
||||
|
||||
return HealthCheckResult::unhealthy(
|
||||
'Cache system failed: ' . $e->getMessage(),
|
||||
responseTime: $responseTime,
|
||||
exception: $e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Cache System';
|
||||
}
|
||||
|
||||
public function getCategory(): HealthCheckCategory
|
||||
{
|
||||
return HealthCheckCategory::CACHE;
|
||||
}
|
||||
|
||||
public function getTimeout(): int
|
||||
{
|
||||
return 3000; // 3 seconds
|
||||
}
|
||||
}
|
||||
83
src/Framework/Health/Checks/DatabaseHealthCheck.php
Normal file
83
src/Framework/Health/Checks/DatabaseHealthCheck.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health\Checks;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
use App\Framework\Health\HealthCheckCategory;
|
||||
use App\Framework\Health\HealthCheckInterface;
|
||||
use App\Framework\Health\HealthCheckResult;
|
||||
|
||||
final readonly class DatabaseHealthCheck implements HealthCheckInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DatabaseManager $databaseManager
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(): HealthCheckResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$connection = $this->databaseManager->getConnection();
|
||||
|
||||
// Simple query to test connection
|
||||
$result = $connection->query('SELECT 1 as health_check');
|
||||
$responseTime = Duration::fromSeconds(microtime(true) - $startTime);
|
||||
|
||||
// Get first row and extract the value
|
||||
$row = $result->fetch();
|
||||
$value = $row['health_check'] ?? null;
|
||||
|
||||
if ($value == 1 || $value === '1') {
|
||||
return HealthCheckResult::healthy(
|
||||
'Database connection is working',
|
||||
[
|
||||
'query_time' => $responseTime->toHumanReadable(),
|
||||
'query_time_ms' => $responseTime->toMilliseconds(),
|
||||
],
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
}
|
||||
|
||||
return HealthCheckResult::unhealthy(
|
||||
'Database query returned unexpected result',
|
||||
[
|
||||
'expected' => 1,
|
||||
'actual' => $value,
|
||||
'type' => gettype($value),
|
||||
'query_time' => $responseTime->toHumanReadable(),
|
||||
'query_time_ms' => $responseTime->toMilliseconds(),
|
||||
],
|
||||
responseTime: $responseTime->toSeconds()
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$responseTime = Duration::fromSeconds(microtime(true) - $startTime);
|
||||
|
||||
return HealthCheckResult::unhealthy(
|
||||
'Database connection failed: ' . $e->getMessage(),
|
||||
responseTime: $responseTime->toSeconds(),
|
||||
exception: $e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Database Connection';
|
||||
}
|
||||
|
||||
public function getCategory(): HealthCheckCategory
|
||||
{
|
||||
return HealthCheckCategory::DATABASE;
|
||||
}
|
||||
|
||||
public function getTimeout(): int
|
||||
{
|
||||
return 5000; // 5 seconds
|
||||
}
|
||||
}
|
||||
101
src/Framework/Health/Checks/DiskSpaceHealthCheck.php
Normal file
101
src/Framework/Health/Checks/DiskSpaceHealthCheck.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health\Checks;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Health\HealthCheckCategory;
|
||||
use App\Framework\Health\HealthCheckInterface;
|
||||
use App\Framework\Health\HealthCheckResult;
|
||||
|
||||
final readonly class DiskSpaceHealthCheck implements HealthCheckInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $path = '/',
|
||||
private int $warningThreshold = 85, // Warn at 85% usage
|
||||
private int $criticalThreshold = 95 // Critical at 95% usage
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(): HealthCheckResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$freeBytes = disk_free_space($this->path);
|
||||
$totalBytes = disk_total_space($this->path);
|
||||
|
||||
if ($freeBytes === false || $totalBytes === false) {
|
||||
return HealthCheckResult::unhealthy(
|
||||
'Could not retrieve disk space information',
|
||||
['path' => $this->path]
|
||||
);
|
||||
}
|
||||
|
||||
$freeSpace = Byte::fromBytes((int) $freeBytes);
|
||||
$totalSpace = Byte::fromBytes((int) $totalBytes);
|
||||
$usedSpace = $totalSpace->subtract($freeSpace);
|
||||
$usagePercentage = $usedSpace->percentOf($totalSpace);
|
||||
|
||||
$responseTime = Duration::fromSeconds(microtime(true) - $startTime);
|
||||
|
||||
$details = [
|
||||
'path' => $this->path,
|
||||
'usage_percentage' => $usagePercentage->format(),
|
||||
'free_space' => $freeSpace->toHumanReadable(),
|
||||
'total_space' => $totalSpace->toHumanReadable(),
|
||||
'used_space' => $usedSpace->toHumanReadable(),
|
||||
'check_time' => $responseTime->toHumanReadable(),
|
||||
];
|
||||
|
||||
if ($usagePercentage->getValue() >= $this->criticalThreshold) {
|
||||
return HealthCheckResult::unhealthy(
|
||||
"Disk space critically low: {$usagePercentage->format()} used",
|
||||
$details,
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
}
|
||||
|
||||
if ($usagePercentage->getValue() >= $this->warningThreshold) {
|
||||
return HealthCheckResult::warning(
|
||||
"Disk space warning: {$usagePercentage->format()} used",
|
||||
$details,
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
}
|
||||
|
||||
return HealthCheckResult::healthy(
|
||||
"Disk space healthy: {$usagePercentage->format()} used",
|
||||
$details,
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$responseTime = Duration::fromSeconds(microtime(true) - $startTime);
|
||||
|
||||
return HealthCheckResult::unhealthy(
|
||||
'Disk space check failed: ' . $e->getMessage(),
|
||||
['path' => $this->path],
|
||||
$responseTime->toSeconds(),
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Disk Space';
|
||||
}
|
||||
|
||||
public function getCategory(): HealthCheckCategory
|
||||
{
|
||||
return HealthCheckCategory::STORAGE;
|
||||
}
|
||||
|
||||
public function getTimeout(): int
|
||||
{
|
||||
return 2000; // 2 seconds
|
||||
}
|
||||
}
|
||||
138
src/Framework/Health/Checks/SystemHealthCheck.php
Normal file
138
src/Framework/Health/Checks/SystemHealthCheck.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health\Checks;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Health\HealthCheckCategory;
|
||||
use App\Framework\Health\HealthCheckInterface;
|
||||
use App\Framework\Health\HealthCheckResult;
|
||||
|
||||
final readonly class SystemHealthCheck implements HealthCheckInterface
|
||||
{
|
||||
public function __construct(
|
||||
private int $memoryWarningThreshold = 85, // Warn at 85% memory usage
|
||||
private int $memoryCriticalThreshold = 95 // Critical at 95% memory usage
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(): HealthCheckResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$memoryUsage = Byte::fromBytes(memory_get_usage(true));
|
||||
$memoryPeak = Byte::fromBytes(memory_get_peak_usage(true));
|
||||
$memoryLimit = $this->getMemoryLimit();
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$phpExtensions = get_loaded_extensions();
|
||||
$keyExtensions = array_intersect($phpExtensions, ['json', 'mbstring', 'curl', 'pdo', 'redis', 'apcu']);
|
||||
|
||||
$responseTime = Duration::fromSeconds(microtime(true) - $startTime);
|
||||
$memoryUsagePercentage = ! $memoryLimit->isEmpty()
|
||||
? $memoryUsage->percentOf($memoryLimit)
|
||||
: Percentage::zero();
|
||||
|
||||
$details = [
|
||||
'php_version' => $phpVersion,
|
||||
'memory_usage' => $memoryUsage->toHumanReadable(),
|
||||
'memory_peak' => $memoryPeak->toHumanReadable(),
|
||||
'memory_limit' => $memoryLimit->toHumanReadable(),
|
||||
'memory_usage_percentage' => $memoryUsagePercentage->format(),
|
||||
'loaded_extensions_count' => count($phpExtensions),
|
||||
'key_extensions' => implode(', ', $keyExtensions), // Convert array to string for JavaScript
|
||||
'response_time' => $responseTime->toHumanReadable(),
|
||||
];
|
||||
|
||||
// Check memory usage
|
||||
if (! $memoryLimit->isEmpty()) {
|
||||
if ($memoryUsagePercentage->getValue() >= $this->memoryCriticalThreshold) {
|
||||
return HealthCheckResult::unhealthy(
|
||||
"Critical memory usage: {$memoryUsagePercentage->format()}",
|
||||
$details,
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
}
|
||||
|
||||
if ($memoryUsagePercentage->getValue() >= $this->memoryWarningThreshold) {
|
||||
return HealthCheckResult::warning(
|
||||
"High memory usage: {$memoryUsagePercentage->format()}",
|
||||
$details,
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check critical extensions
|
||||
$missingExtensions = array_diff(['json', 'mbstring'], $phpExtensions);
|
||||
if (! empty($missingExtensions)) {
|
||||
return HealthCheckResult::unhealthy(
|
||||
'Critical PHP extensions missing: ' . implode(', ', $missingExtensions),
|
||||
$details,
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
}
|
||||
|
||||
return HealthCheckResult::healthy(
|
||||
'System is running normally',
|
||||
$details,
|
||||
$responseTime->toSeconds()
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$responseTime = Duration::fromSeconds(microtime(true) - $startTime);
|
||||
|
||||
return HealthCheckResult::unhealthy(
|
||||
'System health check failed: ' . $e->getMessage(),
|
||||
responseTime: $responseTime->toSeconds(),
|
||||
exception: $e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getMemoryLimit(): Byte
|
||||
{
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
|
||||
if ($memoryLimit === '-1') {
|
||||
return Byte::zero(); // No limit
|
||||
}
|
||||
|
||||
// Parse memory limit string (e.g. "128M", "1G", "512K")
|
||||
try {
|
||||
return Byte::parse($memoryLimit);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Fallback for old-style parsing
|
||||
$value = (int) $memoryLimit;
|
||||
$unit = strtolower(substr($memoryLimit, -1));
|
||||
|
||||
$bytes = match ($unit) {
|
||||
'g' => $value * 1024 * 1024 * 1024,
|
||||
'm' => $value * 1024 * 1024,
|
||||
'k' => $value * 1024,
|
||||
default => $value
|
||||
};
|
||||
|
||||
return Byte::fromBytes($bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'System Resources';
|
||||
}
|
||||
|
||||
public function getCategory(): HealthCheckCategory
|
||||
{
|
||||
return HealthCheckCategory::SYSTEM;
|
||||
}
|
||||
|
||||
public function getTimeout(): int
|
||||
{
|
||||
return 1000; // 1 second
|
||||
}
|
||||
}
|
||||
45
src/Framework/Health/HealthCheckCategory.php
Normal file
45
src/Framework/Health/HealthCheckCategory.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health;
|
||||
|
||||
enum HealthCheckCategory: string
|
||||
{
|
||||
case DATABASE = 'database';
|
||||
case CACHE = 'cache';
|
||||
case STORAGE = 'storage';
|
||||
case NETWORK = 'network';
|
||||
case SYSTEM = 'system';
|
||||
case EXTERNAL = 'external';
|
||||
case SECURITY = 'security';
|
||||
case DISCOVERY = 'discovery';
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::DATABASE => '🗄️',
|
||||
self::CACHE => '⚡',
|
||||
self::STORAGE => '💾',
|
||||
self::NETWORK => '🌐',
|
||||
self::SYSTEM => '🖥️',
|
||||
self::EXTERNAL => '🔗',
|
||||
self::SECURITY => '🔒',
|
||||
self::DISCOVERY => '🔍'
|
||||
};
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::DATABASE => 'Database',
|
||||
self::CACHE => 'Cache',
|
||||
self::STORAGE => 'Storage',
|
||||
self::NETWORK => 'Network',
|
||||
self::SYSTEM => 'System',
|
||||
self::EXTERNAL => 'External Services',
|
||||
self::SECURITY => 'Security',
|
||||
self::DISCOVERY => 'Discovery System'
|
||||
};
|
||||
}
|
||||
}
|
||||
28
src/Framework/Health/HealthCheckInterface.php
Normal file
28
src/Framework/Health/HealthCheckInterface.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health;
|
||||
|
||||
interface HealthCheckInterface
|
||||
{
|
||||
/**
|
||||
* Perform the health check
|
||||
*/
|
||||
public function check(): HealthCheckResult;
|
||||
|
||||
/**
|
||||
* Get the name of this health check
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Get the category of this health check
|
||||
*/
|
||||
public function getCategory(): HealthCheckCategory;
|
||||
|
||||
/**
|
||||
* Get the timeout for this health check in milliseconds
|
||||
*/
|
||||
public function getTimeout(): int;
|
||||
}
|
||||
118
src/Framework/Health/HealthCheckManager.php
Normal file
118
src/Framework/Health/HealthCheckManager.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health;
|
||||
|
||||
final class HealthCheckManager
|
||||
{
|
||||
/** @var HealthCheckInterface[] */
|
||||
private array $healthChecks = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Default health checks will be registered via dependency injection
|
||||
}
|
||||
|
||||
public function registerHealthCheck(HealthCheckInterface $healthCheck): void
|
||||
{
|
||||
$this->healthChecks[$healthCheck->getName()] = $healthCheck;
|
||||
}
|
||||
|
||||
public function runAllChecks(): HealthReport
|
||||
{
|
||||
$results = [];
|
||||
$overallStatus = HealthStatus::HEALTHY;
|
||||
|
||||
foreach ($this->healthChecks as $name => $healthCheck) {
|
||||
try {
|
||||
$result = $this->runSingleCheck($healthCheck);
|
||||
$results[$name] = $result;
|
||||
|
||||
// Determine overall status (worst status wins)
|
||||
if ($result->status->getPriority() < $overallStatus->getPriority()) {
|
||||
$overallStatus = $result->status;
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$results[$name] = HealthCheckResult::unhealthy(
|
||||
'Health check execution failed: ' . $e->getMessage(),
|
||||
exception: $e
|
||||
);
|
||||
$overallStatus = HealthStatus::UNHEALTHY;
|
||||
}
|
||||
}
|
||||
|
||||
return new HealthReport($overallStatus, $results);
|
||||
}
|
||||
|
||||
public function runCheck(string $name): ?HealthCheckResult
|
||||
{
|
||||
if (! isset($this->healthChecks[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->runSingleCheck($this->healthChecks[$name]);
|
||||
}
|
||||
|
||||
public function runChecksByCategory(HealthCheckCategory $category): HealthReport
|
||||
{
|
||||
$results = [];
|
||||
$overallStatus = HealthStatus::HEALTHY;
|
||||
|
||||
foreach ($this->healthChecks as $name => $healthCheck) {
|
||||
if ($healthCheck->getCategory() !== $category) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->runSingleCheck($healthCheck);
|
||||
$results[$name] = $result;
|
||||
|
||||
if ($result->status->getPriority() < $overallStatus->getPriority()) {
|
||||
$overallStatus = $result->status;
|
||||
}
|
||||
}
|
||||
|
||||
return new HealthReport($overallStatus, $results);
|
||||
}
|
||||
|
||||
private function runSingleCheck(HealthCheckInterface $healthCheck): HealthCheckResult
|
||||
{
|
||||
$timeout = $healthCheck->getTimeout() / 1000; // Convert to seconds
|
||||
|
||||
// Simple timeout implementation
|
||||
$startTime = microtime(true);
|
||||
$result = $healthCheck->check();
|
||||
$elapsed = microtime(true) - $startTime;
|
||||
|
||||
if ($elapsed > $timeout) {
|
||||
return HealthCheckResult::warning(
|
||||
"Health check '{$healthCheck->getName()}' exceeded timeout ({$timeout}s)",
|
||||
['elapsed_time' => $elapsed, 'timeout' => $timeout]
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getRegisteredChecks(): array
|
||||
{
|
||||
return array_keys($this->healthChecks);
|
||||
}
|
||||
|
||||
public function getChecksByCategory(): array
|
||||
{
|
||||
$categorized = [];
|
||||
|
||||
foreach ($this->healthChecks as $name => $healthCheck) {
|
||||
$category = $healthCheck->getCategory()->value;
|
||||
$categorized[$category][] = [
|
||||
'name' => $name,
|
||||
'display_name' => $healthCheck->getName(),
|
||||
'timeout' => $healthCheck->getTimeout(),
|
||||
];
|
||||
}
|
||||
|
||||
return $categorized;
|
||||
}
|
||||
}
|
||||
47
src/Framework/Health/HealthCheckManagerInitializer.php
Normal file
47
src/Framework/Health/HealthCheckManagerInitializer.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Health\Checks\CacheHealthCheck;
|
||||
use App\Framework\Health\Checks\DatabaseHealthCheck;
|
||||
use App\Framework\Health\Checks\DiskSpaceHealthCheck;
|
||||
use App\Framework\Health\Checks\SystemHealthCheck;
|
||||
|
||||
final readonly class HealthCheckManagerInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private DatabaseManager $databaseManager,
|
||||
private Cache $cache
|
||||
) {
|
||||
}
|
||||
|
||||
#[Initializer]
|
||||
public function __invoke(): HealthCheckManager
|
||||
{
|
||||
$manager = new HealthCheckManager();
|
||||
|
||||
// Register all health checks
|
||||
$manager->registerHealthCheck(
|
||||
new DatabaseHealthCheck($this->databaseManager)
|
||||
);
|
||||
|
||||
$manager->registerHealthCheck(
|
||||
new CacheHealthCheck($this->cache)
|
||||
);
|
||||
|
||||
$manager->registerHealthCheck(
|
||||
new DiskSpaceHealthCheck()
|
||||
);
|
||||
|
||||
$manager->registerHealthCheck(
|
||||
new SystemHealthCheck()
|
||||
);
|
||||
|
||||
return $manager;
|
||||
}
|
||||
}
|
||||
62
src/Framework/Health/HealthCheckResult.php
Normal file
62
src/Framework/Health/HealthCheckResult.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health;
|
||||
|
||||
final readonly class HealthCheckResult
|
||||
{
|
||||
public function __construct(
|
||||
public HealthStatus $status,
|
||||
public string $message,
|
||||
public array $details = [],
|
||||
public ?float $responseTime = null,
|
||||
public ?\Throwable $exception = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return $this->status === HealthStatus::HEALTHY;
|
||||
}
|
||||
|
||||
public function isWarning(): bool
|
||||
{
|
||||
return $this->status === HealthStatus::WARNING;
|
||||
}
|
||||
|
||||
public function isUnhealthy(): bool
|
||||
{
|
||||
return $this->status === HealthStatus::UNHEALTHY;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'status' => $this->status->value,
|
||||
'message' => $this->message,
|
||||
'details' => $this->details,
|
||||
'response_time_ms' => $this->responseTime ? round($this->responseTime * 1000, 2) : null,
|
||||
'error' => $this->exception ? [
|
||||
'message' => $this->exception->getMessage(),
|
||||
'code' => $this->exception->getCode(),
|
||||
'type' => get_class($this->exception),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
public static function healthy(string $message, array $details = [], ?float $responseTime = null): self
|
||||
{
|
||||
return new self(HealthStatus::HEALTHY, $message, $details, $responseTime);
|
||||
}
|
||||
|
||||
public static function warning(string $message, array $details = [], ?float $responseTime = null): self
|
||||
{
|
||||
return new self(HealthStatus::WARNING, $message, $details, $responseTime);
|
||||
}
|
||||
|
||||
public static function unhealthy(string $message, array $details = [], ?float $responseTime = null, ?\Throwable $exception = null): self
|
||||
{
|
||||
return new self(HealthStatus::UNHEALTHY, $message, $details, $responseTime, $exception);
|
||||
}
|
||||
}
|
||||
98
src/Framework/Health/HealthReport.php
Normal file
98
src/Framework/Health/HealthReport.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health;
|
||||
|
||||
final readonly class HealthReport
|
||||
{
|
||||
public function __construct(
|
||||
public HealthStatus $overallStatus,
|
||||
// Todo: HealthCheckResult ...$results
|
||||
public array $results // HealthCheckResult[]
|
||||
) {
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return $this->overallStatus === HealthStatus::HEALTHY;
|
||||
}
|
||||
|
||||
public function hasWarnings(): bool
|
||||
{
|
||||
return $this->overallStatus === HealthStatus::WARNING;
|
||||
}
|
||||
|
||||
public function isUnhealthy(): bool
|
||||
{
|
||||
return $this->overallStatus === HealthStatus::UNHEALTHY;
|
||||
}
|
||||
|
||||
public function getFailedChecks(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->results,
|
||||
fn (HealthCheckResult $result) => $result->isUnhealthy()
|
||||
);
|
||||
}
|
||||
|
||||
public function getWarningChecks(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->results,
|
||||
fn (HealthCheckResult $result) => $result->isWarning()
|
||||
);
|
||||
}
|
||||
|
||||
public function getHealthyChecks(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->results,
|
||||
fn (HealthCheckResult $result) => $result->isHealthy()
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$categorized = [];
|
||||
|
||||
foreach ($this->results as $name => $result) {
|
||||
$categorized[] = [
|
||||
'name' => $name,
|
||||
'result' => $result->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'overall_status' => $this->overallStatus->value,
|
||||
'overall_icon' => $this->overallStatus->getIcon(),
|
||||
'overall_color' => $this->overallStatus->getColor(),
|
||||
'summary' => [
|
||||
'total_checks' => count($this->results),
|
||||
'healthy_count' => count($this->getHealthyChecks()),
|
||||
'warning_count' => count($this->getWarningChecks()),
|
||||
'unhealthy_count' => count($this->getFailedChecks()),
|
||||
],
|
||||
'checks' => $categorized,
|
||||
'timestamp' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getSummary(): string
|
||||
{
|
||||
$total = count($this->results);
|
||||
$healthy = count($this->getHealthyChecks());
|
||||
$warnings = count($this->getWarningChecks());
|
||||
$failed = count($this->getFailedChecks());
|
||||
|
||||
if ($failed > 0) {
|
||||
return "{$failed} failed, {$warnings} warnings, {$healthy} healthy out of {$total} checks";
|
||||
}
|
||||
|
||||
if ($warnings > 0) {
|
||||
return "{$warnings} warnings, {$healthy} healthy out of {$total} checks";
|
||||
}
|
||||
|
||||
return "All {$total} checks are healthy";
|
||||
}
|
||||
}
|
||||
39
src/Framework/Health/HealthStatus.php
Normal file
39
src/Framework/Health/HealthStatus.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Health;
|
||||
|
||||
enum HealthStatus: string
|
||||
{
|
||||
case HEALTHY = 'healthy';
|
||||
case WARNING = 'warning';
|
||||
case UNHEALTHY = 'unhealthy';
|
||||
|
||||
public function getColor(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::HEALTHY => '#30d158',
|
||||
self::WARNING => '#ff9500',
|
||||
self::UNHEALTHY => '#ff453a'
|
||||
};
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::HEALTHY => '✅',
|
||||
self::WARNING => '⚠️',
|
||||
self::UNHEALTHY => '❌'
|
||||
};
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return match($this) {
|
||||
self::UNHEALTHY => 1,
|
||||
self::WARNING => 2,
|
||||
self::HEALTHY => 3
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user