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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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'
};
}
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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";
}
}

View 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
};
}
}