Files
michaelschiemer/src/Application/Health/HealthCheckController.php
Michael Schiemer 9b74ade5b0 feat: Fix discovery system critical issues
Resolved multiple critical discovery system issues:

## Discovery System Fixes
- Fixed console commands not being discovered on first run
- Implemented fallback discovery for empty caches
- Added context-aware caching with separate cache keys
- Fixed object serialization preventing __PHP_Incomplete_Class

## Cache System Improvements
- Smart caching that only caches meaningful results
- Separate caches for different execution contexts (console, web, test)
- Proper array serialization/deserialization for cache compatibility
- Cache hit logging for debugging and monitoring

## Object Serialization Fixes
- Fixed DiscoveredAttribute serialization with proper string conversion
- Sanitized additional data to prevent object reference issues
- Added fallback for corrupted cache entries

## Performance & Reliability
- All 69 console commands properly discovered and cached
- 534 total discovery items successfully cached and restored
- No more __PHP_Incomplete_Class cache corruption
- Improved error handling and graceful fallbacks

## Testing & Quality
- Fixed code style issues across discovery components
- Enhanced logging for better debugging capabilities
- Improved cache validation and error recovery

Ready for production deployment with stable discovery system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-13 12:04:17 +02:00

263 lines
8.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Application\Health;
use App\Framework\Attributes\Route;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\VersionInfo;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\HealthCheck\ConnectionHealthChecker;
use App\Framework\DateTime\Clock;
use App\Framework\Http\Method;
use App\Framework\Http\Status;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Redis\RedisConnectionPool;
use App\Framework\Router\Result\JsonResult;
final readonly class HealthCheckController
{
public function __construct(
private ConnectionInterface $database,
private RedisConnectionPool $redisPool,
private Clock $clock,
private ConnectionHealthChecker $dbHealthChecker,
private MemoryMonitor $memoryMonitor,
) {
}
#[Route(path: '/health', method: Method::GET, name: 'health_check')]
public function check(): JsonResult
{
$startTime = $this->clock->time();
$checks = [];
// Overall status
$healthy = true;
// Framework version
$checks['version'] = new VersionInfo()->getVersion();
$checks['timestamp'] = $this->clock->now()->format('c');
// PHP checks
$phpCheck = $this->checkPhp();
$checks['php'] = $phpCheck;
$healthy = $healthy && $phpCheck['healthy'];
// Database check
$dbCheck = $this->checkDatabase();
$checks['database'] = $dbCheck;
$healthy = $healthy && $dbCheck['healthy'];
// Redis check
$redisCheck = $this->checkRedis();
$checks['redis'] = $redisCheck;
$healthy = $healthy && $redisCheck['healthy'];
// Filesystem check
$fsCheck = $this->checkFilesystem();
$checks['filesystem'] = $fsCheck;
$healthy = $healthy && $fsCheck['healthy'];
// Memory check
$memoryCheck = $this->checkMemory();
$checks['memory'] = $memoryCheck;
$healthy = $healthy && $memoryCheck['healthy'];
// Response time
$checks['response_time_ms'] = round($this->clock->time()->diff($startTime)->toMilliseconds(), 2);
// Overall status
$checks['status'] = $healthy ? 'healthy' : 'unhealthy';
return new JsonResult(
data: $checks,
status: Status::from($healthy ? 200 : 503)
);
}
#[Route(path: '/health/live', method: Method::GET, name: 'health_liveness')]
public function liveness(): JsonResult
{
// Simple liveness check - just return OK if the app is running
return new JsonResult([
'status' => 'ok',
'timestamp' => $this->clock->now()->format('c'),
]);
}
#[Route(path: '/health/ready', method: Method::GET, name: 'health_readiness')]
public function readiness(): JsonResult
{
// Readiness check - check if all services are ready
$ready = true;
$checks = [];
// Check database
$dbResult = $this->dbHealthChecker->checkHealth($this->database);
if ($dbResult->isHealthy) {
$checks['database'] = 'ready';
} else {
$checks['database'] = 'not_ready';
$ready = false;
}
// Check Redis
try {
$defaultRedis = $this->redisPool->getConnection('default');
$defaultRedis->getClient()->ping();
$checks['redis'] = 'ready';
} catch (\Exception $e) {
$checks['redis'] = 'not_ready';
$ready = false;
}
return new JsonResult(
data: [
'ready' => $ready,
'checks' => $checks,
'timestamp' => $this->clock->now()->format('c'),
],
status: $ready ? Status::OK : Status::SERVICE_UNAVAILABLE
);
}
private function checkPhp(): array
{
return [
'healthy' => true,
'version' => PHP_VERSION,
'extensions' => [
'opcache' => extension_loaded('opcache'),
'apcu' => extension_loaded('apcu'),
'redis' => extension_loaded('redis'),
'pdo' => extension_loaded('pdo'),
'openssl' => extension_loaded('openssl'),
'mbstring' => extension_loaded('mbstring'),
'json' => extension_loaded('json'),
],
'sapi' => PHP_SAPI,
];
}
private function checkDatabase(): array
{
$result = $this->dbHealthChecker->checkHealth($this->database);
$data = [
'healthy' => $result->isHealthy,
'latency_ms' => $result->responseTimeMs,
];
if ($result->message) {
$data['message'] = $result->message;
}
if ($result->exception) {
$data['error'] = $result->exception->getMessage();
}
if (! empty($result->additionalData)) {
$data['additional_data'] = $result->additionalData;
}
return $data;
}
private function checkRedis(): array
{
try {
$defaultRedis = $this->redisPool->getConnection('default');
$redisClient = $defaultRedis->getClient();
$start = $this->clock->time();
$pong = $redisClient->ping();
$latency = round($this->clock->time()->diff($start)->toMilliseconds(), 2);
$info = $redisClient->info('server');
$memoryInfo = $redisClient->info('memory');
$usedMemory = isset($memoryInfo['used_memory'])
? Byte::fromBytes((int) $memoryInfo['used_memory'])
: null;
return [
'healthy' => $pong === 'PONG',
'latency_ms' => $latency,
'version' => $info['redis_version'] ?? 'unknown',
'connected_clients' => $info['connected_clients'] ?? 0,
'used_memory' => $usedMemory?->toHumanReadable() ?? 'unknown',
];
} catch (\Exception $e) {
return [
'healthy' => false,
'error' => 'Connection failed: ' . $e->getMessage(),
];
}
}
private function checkFilesystem(): array
{
$tempDir = sys_get_temp_dir();
$testFile = $tempDir . '/health_check_' . uniqid() . '.tmp';
try {
// Test write
file_put_contents($testFile, 'health check');
// Test read
$content = file_get_contents($testFile);
// Cleanup
unlink($testFile);
// Check disk space
$freeSpace = disk_free_space($tempDir);
$totalSpace = disk_total_space($tempDir);
if ($freeSpace === false || $totalSpace === false) {
throw new \RuntimeException('Unable to determine disk space');
}
$freeBytes = Byte::fromBytes((int)$freeSpace);
$totalBytes = Byte::fromBytes((int)$totalSpace);
$usedBytes = $totalBytes->subtract($freeBytes);
$usagePercent = $usedBytes->percentOf($totalBytes);
return [
'healthy' => true,
'writable' => true,
'temp_dir' => $tempDir,
'disk_usage_percent' => round($usagePercent->getValue(), 2),
'disk_free' => $freeBytes->toHumanReadable(),
'disk_total' => $totalBytes->toHumanReadable(),
];
} catch (\Exception $e) {
return [
'healthy' => false,
'writable' => false,
'error' => 'Filesystem check failed',
];
}
}
private function checkMemory(): array
{
$memoryLimit = $this->memoryMonitor->getMemoryLimit();
$memoryUsage = $this->memoryMonitor->getCurrentMemory();
$memoryPeakUsage = $this->memoryMonitor->getPeakMemory();
$usagePercent = $this->memoryMonitor->getMemoryUsagePercentage();
return [
'healthy' => $usagePercent->greaterThan(Percentage::from(80.0)), // Unhealthy if over 80%
'limit' => $memoryLimit->toHumanReadable(),
'usage' => $memoryUsage->toHumanReadable(),
'peak_usage' => $memoryPeakUsage->toHumanReadable(),
'usage_percent' => round($usagePercent->getValue(), 2),
];
}
}