diff --git a/.env.example b/.env.example index 0bb9e72..429e189 100644 --- a/.env.example +++ b/.env.example @@ -2,16 +2,6 @@ # PHP API Stack — Environment Configuration # Image: kariricode/php-api-stack # ============================================ -# Demo Mode (optional) -# Set to 'true' to install demo landing page when no application exists -DEMO_MODE=true -# Mode debug (optional) 0=inactive 1=acrive -ENABLE_XDEBUG=1 -# Health Check Installation (optional) -# Set to 'true' to install health check endpoint -# Automatically enabled when DEMO_MODE=true -HEALTH_CHECK_INSTALL=true -# ============================================ # Application # ============================================ APP_NAME=php-api-stack @@ -110,7 +100,7 @@ PHP_FPM_REQUEST_SLOWLOG_TIMEOUT=30s PHP_OPCACHE_ENABLE=1 PHP_OPCACHE_MEMORY=256 PHP_OPCACHE_MAX_FILES=20000 -PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 +PHP_OPCACHE_VALIDATE_TIMESTAMPS=1 PHP_OPCACHE_REVALIDATE_FREQ=0 PHP_OPCACHE_JIT=tracing PHP_OPCACHE_JIT_BUFFER_SIZE=128M diff --git a/.gitignore b/.gitignore index a5d2acc..98f6752 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ ./app/public/health.php ./app/public/index.php +# Public directory (application-specific files) +/public/ + # Logs ./logs/ *.log diff --git a/Dockerfile b/Dockerfile index 89f76e7..1553dbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -425,20 +425,6 @@ COPY scripts/quick-start.sh /usr/local/bin/quick-start RUN chmod +x /usr/local/bin/docker-entrypoint /usr/local/bin/process-configs /usr/local/bin/quick-start -# ---------------------------------------------------------------------------- -# Healthcheck templates (staged, published at runtime) -# ---------------------------------------------------------------------------- -RUN install -d -m 0755 /opt/php-api-stack-templates - -COPY --chown=nginx:nginx php/index.php /opt/php-api-stack-templates/index.php -COPY --chown=nginx:nginx php/health.php /opt/php-api-stack-templates/health.php - -# Validate syntax at build time -RUN set -eux; \ - php -l /opt/php-api-stack-templates/index.php; \ - php -l /opt/php-api-stack-templates/health.php; \ - echo "✓ Healthcheck templates validated successfully" - WORKDIR /var/www/html # ============================================================================ diff --git a/Makefile b/Makefile index f0df04e..c2ce12b 100644 --- a/Makefile +++ b/Makefile @@ -54,13 +54,8 @@ ALPINE_VERSION?=3.21 COMPOSER_VERSION?=2.8.12 SYMFONY_CLI_VERSION?=5.15.1 -DEMO_MODE ?= false -HEALTH_CHECK_INSTALL ?= false - # Common build args block BUILD_ARGS := \ - --build-arg DEMO_MODE=$(DEMO_MODE) \ - --build-arg HEALTH_CHECK_INSTALL=$(HEALTH_CHECK_INSTALL) \ --build-arg VERSION=$(VERSION) \ --build-arg PHP_VERSION=$(PHP_VERSION) \ --build-arg PHP_CORE_EXTENSIONS=$(PHP_CORE_EXTENSIONS) \ @@ -85,9 +80,7 @@ PROD_BUILD_ARGS := \ --build-arg PHP_OPCACHE_ENABLE=1 \ --build-arg PHP_OPCACHE_MEMORY_CONSUMPTION=256 \ --build-arg XDEBUG_ENABLE=0 \ - --build-arg APP_DEBUG=false \ - --build-arg DEMO_MODE=false \ - --build-arg HEALTH_CHECK_INSTALL=false + --build-arg APP_DEBUG=false # Development build args @@ -98,10 +91,8 @@ XDEBUG_VERSION ?= 3.4.6 DEV_BUILD_ARGS := \ --build-arg APP_ENV=development \ --build-arg APP_DEBUG=true \ - --build-arg DEMO_MODE=true \ --build-arg SYMFONY_CLI_VERSION=$(SYMFONY_CLI_VERSION) \ --build-arg XDEBUG_VERSION=$(XDEBUG_VERSION) \ - --build-arg HEALTH_CHECK_INSTALL=true \ --build-arg XDEBUG_ENABLE=$(XDEBUG_ENABLE) .PHONY: help @@ -186,11 +177,10 @@ build-base: ## Build base image (target=base) - for debugging only .PHONY: build-test build-test: ## Build test image (production with health check) - @echo "$(GREEN)Building test image with comprehensive health check...$(NC)" + @echo "$(GREEN)Building test image...$(NC)" @docker build \ $(BUILD_ARGS) \ $(PROD_BUILD_ARGS) \ - --build-arg HEALTH_CHECK_INSTALL=true \ --target production \ -t $(FULL_IMAGE):test \ . @@ -229,11 +219,13 @@ run-dev: ## Run dev container with Xdebug @echo "$(GREEN)Starting dev container...$(NC)" @docker stop $(DEV_CONTAINER) >/dev/null 2>&1 || true @docker rm $(DEV_CONTAINER) >/dev/null 2>&1 || true + @if docker ps --format '{{.Ports}}' | grep -q '$(DEV_PORT)->'; then \ echo "$(RED)X Port $(DEV_PORT) is already in use!$(NC)"; \ echo "$(YELLOW)Try another port:$(NC) make run-dev DEV_PORT=9000"; \ exit 1; \ fi + @docker run -d \ --name $(DEV_CONTAINER) \ -p $(DEV_PORT):80 \ @@ -241,11 +233,14 @@ run-dev: ## Run dev container with Xdebug --env-file $(ENV_FILE) \ -e APP_ENV=development \ -e XDEBUG_ENABLE=1 \ + -v $(PWD):/var/www/html \ -v $(PWD)/logs:/var/log \ $(FULL_IMAGE):$(IMAGE_TAG) + @echo "$(GREEN)OK: Dev container running at http://localhost:$(DEV_PORT)$(NC)" @echo "$(CYAN)Xdebug enabled on port 9003$(NC)" + .PHONY: run-test run-test: build-test ## Run test container @echo "$(GREEN)Starting test container...$(NC)" diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 4791a35..26a9093 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -3,9 +3,6 @@ services: image: kariricode/php-api-stack:latest container_name: php-api-stack restart: unless-stopped - environment: - - DEMO_MODE=true - - HEALTH_CHECK_INSTALL=true env_file: - .env ports: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index d37cd95..cbd4c72 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -180,30 +180,6 @@ if [ -f "/var/www/html/bin/console" ]; then fi fi -# ---------------------------------------------------------------------------- -# DEMO_MODE -# ---------------------------------------------------------------------------- -if [ "${DEMO_MODE}" = "true" ] && [ -f "/opt/php-api-stack-templates/index.php" ]; then - log_info "Publishing demo index.php to /var/www/html/public" - mkdir -p /var/www/html/public - cp /opt/php-api-stack-templates/index.php /var/www/html/public/index.php - chown nginx:nginx /var/www/html/public/index.php - chmod 644 /var/www/html/public/index.php -else - rm -f /var/www/html/public/index.php 2>/dev/null || true -fi - -# HEALTH_CHECK_INSTALL -if [ "${HEALTH_CHECK_INSTALL}" = "true" ] && [ -f "/opt/php-api-stack-templates/health.php" ]; then - log_info "Publishing health.php to /var/www/html/public" - mkdir -p /var/www/html/public - cp /opt/php-api-stack-templates/health.php /var/www/html/public/health.php - chown nginx:nginx /var/www/html/public/health.php - chmod 644 /var/www/html/public/health.php -else - rm -f /var/www/html/public/health.php 2>/dev/null || true -fi - # ----------------------------------------------------------------------------- # XDEBUG # ----------------------------------------------------------------------------- diff --git a/php/health.php b/php/health.php deleted file mode 100644 index 41ce9ae..0000000 --- a/php/health.php +++ /dev/null @@ -1,709 +0,0 @@ - $this->healthy, - 'status' => $this->status, - ]; - - if (!empty($this->details)) { - $result['details'] = $this->details; - } - - if ($this->error !== null) { - $result['error'] = $this->error; - } - - if ($this->duration !== null) { - $result['duration_ms'] = round($this->duration * 1000, 2); - } - - return $result; - } -} - -// ============================================================================ -// UTILITIES (DRY Principle) -// ============================================================================ - -final class ByteFormatter -{ - private const UNITS = ['B', 'KB', 'MB', 'GB']; - - public static function format(int $bytes): string - { - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count(self::UNITS) - 1); - $bytes /= (1 << (10 * $pow)); - - return round($bytes, 2) . ' ' . self::UNITS[$pow]; - } -} - -// ============================================================================ -// ABSTRACT BASE CHECKER (Template Method Pattern) -// ============================================================================ - -abstract class AbstractHealthCheck implements HealthCheckInterface -{ - protected bool $critical = true; - - final public function check(): CheckResult - { - $start = microtime(true); - - try { - $result = $this->performCheck(); - $duration = microtime(true) - $start; - - return new CheckResult( - healthy: $result['healthy'], - status: $result['status'], - details: $result['details'] ?? [], - error: $result['error'] ?? null, - duration: $duration - ); - } catch (\Throwable $e) { - $duration = microtime(true) - $start; - - return new CheckResult( - healthy: false, - status: 'error', - details: [], - error: $e->getMessage(), - duration: $duration - ); - } - } - - abstract protected function performCheck(): array; - - final public function isCritical(): bool - { - return $this->critical; - } -} - -// ============================================================================ -// CONCRETE HEALTH CHECKERS (Single Responsibility Principle) -// ============================================================================ - -final class PhpRuntimeCheck extends AbstractHealthCheck -{ - public function getName(): string - { - return 'php'; - } - - protected function performCheck(): array - { - $memoryLimit = $this->parseMemory(ini_get('memory_limit')); - $memoryUsage = memory_get_usage(true); - $memoryPeakUsage = memory_get_peak_usage(true); - $memoryUsagePercent = $memoryLimit > 0 - ? round(($memoryUsage / $memoryLimit) * 100, 2) - : 0; - - $healthy = $memoryUsagePercent < 90; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'version' => PHP_VERSION, - 'sapi' => PHP_SAPI, - 'memory' => [ - 'limit' => ByteFormatter::format($memoryLimit), - 'usage' => ByteFormatter::format($memoryUsage), - 'peak' => ByteFormatter::format($memoryPeakUsage), - 'usage_percent' => $memoryUsagePercent, - ], - 'zend_version' => zend_version(), - ], - ]; - } - - private function parseMemory(string $value): int - { - $value = trim($value); - if ($value === '-1') { - return PHP_INT_MAX; - } - - $unit = strtolower($value[strlen($value) - 1]); - $value = (int) $value; - - return match ($unit) { - 'g' => $value * 1024 * 1024 * 1024, - 'm' => $value * 1024 * 1024, - 'k' => $value * 1024, - default => $value, - }; - } -} - -final class PhpExtensionsCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const REQUIRED_EXTENSIONS = ['pdo', 'mbstring', 'json', 'curl']; - private const OPTIONAL_EXTENSIONS = ['redis', 'apcu', 'intl', 'zip', 'gd', 'xml']; - - public function getName(): string - { - return 'php_extensions'; - } - - protected function performCheck(): array - { - $loadedExtensions = get_loaded_extensions(); - $requiredLoaded = []; - $requiredMissing = []; - $optionalLoaded = []; - - foreach (self::REQUIRED_EXTENSIONS as $ext) { - if (extension_loaded($ext)) { - $requiredLoaded[] = $ext; - } else { - $requiredMissing[] = $ext; - } - } - - foreach (self::OPTIONAL_EXTENSIONS as $ext) { - if (extension_loaded($ext)) { - $optionalLoaded[] = $ext; - } - } - - $healthy = empty($requiredMissing); - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'critical', - 'details' => [ - 'total_loaded' => count($loadedExtensions), - 'required' => [ - 'loaded' => $requiredLoaded, - 'missing' => $requiredMissing, - ], - 'optional_loaded' => $optionalLoaded, - ], - 'error' => !$healthy ? 'Missing required extensions: ' . implode(', ', $requiredMissing) : null, - ]; - } -} - -final class OpcacheCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const MIN_HIT_RATE = 90.0; - private const MAX_MEMORY_USAGE = 90.0; - - public function getName(): string - { - return 'opcache'; - } - - protected function performCheck(): array - { - if (!function_exists('opcache_get_status')) { - return [ - 'healthy' => false, - 'status' => 'unavailable', - 'details' => [], - 'error' => 'OPcache extension not available', - ]; - } - - $status = @opcache_get_status(false); - - if ($status === false) { - return [ - 'healthy' => false, - 'status' => 'disabled', - 'details' => [], - 'error' => 'OPcache is disabled', - ]; - } - - $memoryUsed = $status['memory_usage']['used_memory'] ?? 0; - $memoryFree = $status['memory_usage']['free_memory'] ?? 0; - $memoryTotal = $memoryUsed + $memoryFree; - $memoryUsagePercent = $memoryTotal > 0 - ? round(($memoryUsed / $memoryTotal) * 100, 2) - : 0; - - $hits = $status['opcache_statistics']['hits'] ?? 0; - $misses = $status['opcache_statistics']['misses'] ?? 0; - $total = $hits + $misses; - $hitRate = $total > 0 ? round(($hits / $total) * 100, 2) : 0; - - $healthy = $hitRate >= self::MIN_HIT_RATE && $memoryUsagePercent < self::MAX_MEMORY_USAGE; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'enabled' => true, - 'memory' => [ - 'used' => ByteFormatter::format($memoryUsed), - 'free' => ByteFormatter::format($memoryFree), - 'usage_percent' => $memoryUsagePercent, - 'wasted_percent' => round($status['memory_usage']['current_wasted_percentage'] ?? 0, 2), - ], - 'statistics' => [ - 'hits' => $hits, - 'misses' => $misses, - 'hit_rate' => $hitRate, - 'cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? 0, - 'max_cached_keys' => $status['opcache_statistics']['max_cached_keys'] ?? 0, - ], - 'jit' => [ - 'enabled' => $status['jit']['enabled'] ?? false, - 'on' => $status['jit']['on'] ?? false, - 'buffer_size' => ByteFormatter::format($status['jit']['buffer_size'] ?? 0), - ], - 'restarts' => [ - 'oom' => $status['opcache_statistics']['oom_restarts'] ?? 0, - 'hash' => $status['opcache_statistics']['hash_restarts'] ?? 0, - 'manual' => $status['opcache_statistics']['manual_restarts'] ?? 0, - ], - ], - ]; - } -} - -final class RedisCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const TIMEOUT = 1.0; - private const MAX_LATENCY_MS = 100.0; - - public function getName(): string - { - return 'redis'; - } - - /** - * Smart Redis Host Resolution - * - * Tenta resolver o hostname. Se falhar, usa 127.0.0.1 (standalone mode). - */ - protected function getRedisHost(): string - { - $host = getenv('REDIS_HOST') ?: '127.0.0.1'; - - // Se for "redis" (docker-compose), verifica se resolve - if ($host === 'redis') { - // Suprime warning de DNS - $resolved = @gethostbyname($host); - - // Se não resolveu (retorna o próprio hostname), usa localhost - if ($resolved === $host) { - return '127.0.0.1'; - } - } - - return $host; - } - - protected function performCheck(): array - { - if (!extension_loaded('redis')) { - return [ - 'healthy' => false, - 'status' => 'unavailable', - 'details' => [], - 'error' => 'Redis extension not installed', - ]; - } - - $redis = new \Redis(); - - $host = $this->getRedisHost(); - $password = getenv('REDIS_PASSWORD') ?: null; - $port = 6379; - - try { - $connectStart = microtime(true); - $connected = @$redis->connect($host, $port, self::TIMEOUT); - $connectDuration = (microtime(true) - $connectStart) * 1000; - - if (!$connected) { - return [ - 'healthy' => false, - 'status' => 'unavailable', - 'details' => [ - 'connect_latency_ms' => round($connectDuration, 2), - ], - 'error' => 'Cannot connect to Redis', - ]; - } - - if ($password !== null && $password !== '') { - if (!@$redis->auth($password)) { - return [ - 'healthy' => false, - 'status' => 'unauthenticated', - 'details' => ['host' => $host], - 'error' => 'Redis authentication failed (NOAUTH). Check REDIS_PASSWORD.', - ]; - } - } - - $pingStart = microtime(true); - $pong = $redis->ping(); - $pingDuration = (microtime(true) - $pingStart) * 1000; - - if ($pong !== true && $pong !== '+PONG') { - throw new \RuntimeException('Redis ping failed'); - } - - $info = $redis->info(); - $healthy = $pingDuration < self::MAX_LATENCY_MS; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'connected' => true, - 'version' => $info['redis_version'] ?? 'unknown', - 'uptime_seconds' => (int) ($info['uptime_in_seconds'] ?? 0), - 'memory' => [ - 'used' => $info['used_memory_human'] ?? 'unknown', - 'peak' => $info['used_memory_peak_human'] ?? 'unknown', - 'fragmentation_ratio' => (float) ($info['mem_fragmentation_ratio'] ?? 0), - ], - 'stats' => [ - 'connected_clients' => (int) ($info['connected_clients'] ?? 0), - 'total_commands_processed' => (int) ($info['total_commands_processed'] ?? 0), - 'keyspace_hits' => (int) ($info['keyspace_hits'] ?? 0), - 'keyspace_misses' => (int) ($info['keyspace_misses'] ?? 0), - ], - 'latency' => [ - 'connect_ms' => round($connectDuration, 2), - 'ping_ms' => round($pingDuration, 2), - ], - ], - ]; - } catch (\Throwable $e) { - return [ - 'healthy' => false, - 'status' => 'error', - 'details' => [], - 'error' => $e->getMessage(), - ]; - } finally { - @$redis->close(); - } - } -} - -/** - * System Resources Check - Defensive Programming - * - * Gracefully handles open_basedir restrictions by using fallback strategies. - * - * @link https://www.php.net/manual/en/ini.core.php#ini.open-basedir - */ -final class SystemResourcesCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const MAX_DISK_USAGE = 90.0; - private const MAX_LOAD_AVERAGE = 10.0; - - public function getName(): string - { - return 'system'; - } - - protected function performCheck(): array - { - $diskFree = @disk_free_space('/'); - $diskTotal = @disk_total_space('/'); - $diskUsagePercent = $diskTotal > 0 - ? round((($diskTotal - $diskFree) / $diskTotal) * 100, 2) - : 0; - - $loadAvg = @sys_getloadavg(); - $load1 = $loadAvg[0] ?? 0; - - $memoryInfo = $this->getMemoryInfoSafely(); - - $healthy = $diskUsagePercent < self::MAX_DISK_USAGE && $load1 < self::MAX_LOAD_AVERAGE; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'disk' => [ - 'total' => ByteFormatter::format($diskTotal ?: 0), - 'free' => ByteFormatter::format($diskFree ?: 0), - 'usage_percent' => $diskUsagePercent, - ], - 'load_average' => [ - '1min' => round($load1, 2), - '5min' => round($loadAvg[1] ?? 0, 2), - '15min' => round($loadAvg[2] ?? 0, 2), - ], - 'memory' => $memoryInfo, - ], - ]; - } - - /** - * Safely reads memory info with fallback strategy. - * - * Strategy 1: Try /proc/meminfo (full system memory) - * Strategy 2: Fallback to PHP memory usage (if restricted) - */ - private function getMemoryInfoSafely(): array - { - set_error_handler(static fn() => true); - $content = @file_get_contents('/proc/meminfo'); - restore_error_handler(); - - if ($content === false) { - return [ - 'source' => 'php_fallback', - 'note' => 'System memory unavailable (open_basedir restriction)', - 'php_memory_usage' => ByteFormatter::format(memory_get_usage(true)), - 'php_memory_peak' => ByteFormatter::format(memory_get_peak_usage(true)), - ]; - } - - $matches = []; - preg_match_all('/^(\w+):\s+(\d+)\s+kB/m', $content, $matches, PREG_SET_ORDER); - - $meminfo = []; - foreach ($matches as $match) { - $meminfo[$match[1]] = (int) $match[2] * 1024; - } - - $memTotal = $meminfo['MemTotal'] ?? 0; - $memAvailable = $meminfo['MemAvailable'] ?? $meminfo['MemFree'] ?? 0; - $memUsed = $memTotal - $memAvailable; - $memUsagePercent = $memTotal > 0 ? round(($memUsed / $memTotal) * 100, 2) : 0; - - return [ - 'source' => 'proc_meminfo', - 'total' => ByteFormatter::format($memTotal), - 'available' => ByteFormatter::format($memAvailable), - 'used' => ByteFormatter::format($memUsed), - 'usage_percent' => $memUsagePercent, - ]; - } -} - -/** - * Application Check - Respects Open Basedir - * - * Only checks directories within open_basedir scope. - * Provides clear feedback about security restrictions. - */ -final class ApplicationCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - /** - * CRITICAL: Only check paths within typical open_basedir scope. - * - * Common open_basedir: /var/www/html:/tmp:/usr/local/lib/php:/usr/share/php - * - * Excluded: /var/log/* (security policy prevents access) - */ - private const ACCESSIBLE_DIRS = [ - '/var/www/html', - '/var/www/html/public', - '/tmp', - ]; - - public function getName(): string - { - return 'application'; - } - - protected function performCheck(): array - { - $directoryStatus = []; - $allHealthy = true; - $openBasedir = ini_get('open_basedir'); - - foreach (self::ACCESSIBLE_DIRS as $dir) { - $status = $this->checkDirectorySafely($dir); - $directoryStatus[basename($dir)] = $status; - - if (!$status['exists'] || !$status['readable']) { - $allHealthy = false; - } - } - - $details = [ - 'directories' => $directoryStatus, - 'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? 'unknown', - ]; - - // Add informational note about security policy - if (!empty($openBasedir)) { - $details['security_note'] = 'Log directories (/var/log) excluded per open_basedir policy'; - $details['open_basedir'] = $openBasedir; - } - - return [ - 'healthy' => $allHealthy, - 'status' => $allHealthy ? 'healthy' : 'warning', - 'details' => $details, - ]; - } - - /** - * Defensive directory check with error suppression. - * - * Prevents warnings when open_basedir blocks access. - */ - private function checkDirectorySafely(string $path): array - { - set_error_handler(static fn() => true); - - $exists = @is_dir($path); - $readable = $exists && @is_readable($path); - $writable = $exists && @is_writable($path); - - restore_error_handler(); - - return [ - 'path' => $path, - 'exists' => $exists, - 'readable' => $readable, - 'writable' => $writable, - ]; - } -} - -// ============================================================================ -// HEALTH CHECK MANAGER (Facade Pattern) -// ============================================================================ - -final class HealthCheckManager -{ - /** @var array */ - private array $checkers = []; - - public function addChecker(HealthCheckInterface $checker): self - { - $this->checkers[$checker->getName()] = $checker; - return $this; - } - - public function runAll(): array - { - $startTime = microtime(true); - $results = []; - $overallHealthy = true; - - foreach ($this->checkers as $name => $checker) { - $result = $checker->check(); - $results[$name] = $result->toArray(); - - if (!$result->healthy && $checker->isCritical()) { - $overallHealthy = false; - } - } - - $duration = microtime(true) - $startTime; - - return [ - 'status' => $overallHealthy ? 'healthy' : 'unhealthy', - 'timestamp' => date('c'), - 'duration_ms' => round($duration * 1000, 2), - 'checks' => $results, - ]; - } -} - -// ============================================================================ -// MAIN EXECUTION -// ============================================================================ - -header('Content-Type: application/json; charset=utf-8'); -header('Cache-Control: no-cache, no-store, must-revalidate'); -header('Pragma: no-cache'); -header('Expires: 0'); - -try { - $manager = new HealthCheckManager(); - - $manager - ->addChecker(new PhpRuntimeCheck()) - ->addChecker(new PhpExtensionsCheck()) - ->addChecker(new OpcacheCheck()) - ->addChecker(new RedisCheck()) - ->addChecker(new SystemResourcesCheck()) - ->addChecker(new ApplicationCheck()); - - $health = $manager->runAll(); - - $statusCode = $health['status'] === 'healthy' ? 200 : 503; - http_response_code($statusCode); - - echo json_encode($health, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); -} catch (\Throwable $e) { - http_response_code(500); - echo json_encode([ - 'status' => 'error', - 'timestamp' => date('c'), - 'error' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ], JSON_PRETTY_PRINT); -} diff --git a/php/index.php b/php/index.php deleted file mode 100644 index 4764b8d..0000000 --- a/php/index.php +++ /dev/null @@ -1,642 +0,0 @@ -performCheck(); - } catch (\Throwable $e) { - return new StatusResult( - healthy: false, - message: 'Error: ' . $e->getMessage(), - details: [] - ); - } - } -} - -/** - * PHP Runtime Check - */ -final class PhpCheck extends AbstractComponentCheck -{ - public function getName(): string - { - return 'PHP'; - } - - protected function performCheck(): StatusResult - { - $version = PHP_VERSION; - $extensions = get_loaded_extensions(); - - return new StatusResult( - healthy: true, - message: "PHP {$version} with " . count($extensions) . " extensions", - details: [ - 'version' => $version, - 'sapi' => PHP_SAPI, - 'extensions_count' => count($extensions), - 'memory_limit' => ini_get('memory_limit'), - 'max_execution_time' => ini_get('max_execution_time') . 's', - ] - ); - } -} - -/** - * OPcache Check - */ -final class OpcacheCheck extends AbstractComponentCheck -{ - public function getName(): string - { - return 'OPcache'; - } - - protected function performCheck(): StatusResult - { - if (!function_exists('opcache_get_status')) { - return new StatusResult( - healthy: false, - message: 'OPcache extension not available', - details: [] - ); - } - - $status = @opcache_get_status(false); - - if ($status === false) { - return new StatusResult( - healthy: false, - message: 'OPcache is disabled', - details: [] - ); - } - - $memoryUsed = $status['memory_usage']['used_memory'] ?? 0; - $memoryFree = $status['memory_usage']['free_memory'] ?? 0; - $memoryTotal = $memoryUsed + $memoryFree; - $usagePercent = $memoryTotal > 0 ? round(($memoryUsed / $memoryTotal) * 100, 1) : 0; - - return new StatusResult( - healthy: true, - message: "OPcache enabled ({$usagePercent}% memory used)", - details: [ - 'memory_used' => $this->formatBytes($memoryUsed), - 'cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? 0, - 'jit_enabled' => ($status['jit']['enabled'] ?? false) ? 'Yes' : 'No', - ] - ); - } - - private function formatBytes(int $bytes): string - { - $units = ['B', 'KB', 'MB', 'GB']; - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); - $bytes /= (1 << (10 * $pow)); - return round($bytes, 2) . ' ' . $units[$pow]; - } -} - -/** - * Redis Connectivity Check - */ -final class RedisCheck extends AbstractComponentCheck -{ - public function getName(): string - { - return 'Redis'; - } - - - /** - * Smart Redis Host Resolution - * - * Tenta resolver o hostname. Se falhar, usa 127.0.0.1 (standalone mode). - */ - protected function getRedisHost(): string - { - $host = getenv('REDIS_HOST') ?: '127.0.0.1'; - - // Se for "redis" (docker-compose), verifica se resolve - if ($host === 'redis') { - // Suprime warning de DNS - $resolved = @gethostbyname($host); - - // Se não resolveu (retorna o próprio hostname), usa localhost - if ($resolved === $host) { - return '127.0.0.1'; - } - } - - return $host; - } - - - protected function performCheck(): StatusResult - { - if (!extension_loaded('redis')) { - return new StatusResult( - healthy: false, - message: 'Redis extension not installed', - details: [] - ); - } - - $redis = new \Redis(); - - $host = $this->getRedisHost(); - $password = getenv('REDIS_PASSWORD') ?: null; - $port = 6379; - - try { - $connected = @$redis->connect($host, $port, 1.0); - - if (!$connected) { - return new StatusResult( - healthy: false, - message: 'Cannot connect to Redis server', - details: [] - ); - } - - - if ($password !== null && $password !== '') { - if (!@$redis->auth($password)) { - return new StatusResult( - healthy: false, - message: 'Redis authentication failed (NOAUTH). Check REDIS_PASSWORD.', - details: ['host' => $host] - ); - } - } - - - $pong = $redis->ping(); - - if ($pong !== true && $pong !== '+PONG') { - return new StatusResult( - healthy: false, - message: 'Redis ping failed', - details: [] - ); - } - - $info = $redis->info(); - - return new StatusResult( - healthy: true, - message: 'Redis ' . ($info['redis_version'] ?? 'unknown') . ' connected', - details: [ - 'version' => $info['redis_version'] ?? 'unknown', - 'uptime' => $this->formatUptime((int)($info['uptime_in_seconds'] ?? 0)), - 'memory' => $info['used_memory_human'] ?? 'unknown', - ] - ); - } catch (\Throwable $e) { - return new StatusResult( - healthy: false, - message: 'Redis error: ' . $e->getMessage(), - details: [] - ); - } finally { - @$redis->close(); - } - } - - private function formatUptime(int $seconds): string - { - if ($seconds < 60) return "{$seconds}s"; - if ($seconds < 3600) return floor($seconds / 60) . "m"; - if ($seconds < 86400) return floor($seconds / 3600) . "h"; - return floor($seconds / 86400) . "d"; - } -} - -/** - * Status Dashboard (Facade Pattern) - */ -final class StatusDashboard -{ - /** @var ComponentCheckInterface[] */ - private array $checks = []; - - public function addCheck(ComponentCheckInterface $check): self - { - $this->checks[] = $check; - return $this; - } - - public function runAll(): array - { - $results = []; - $allHealthy = true; - - foreach ($this->checks as $check) { - $result = $check->check(); - $results[$check->getName()] = $result; - - if (!$result->healthy) { - $allHealthy = false; - } - } - - return [ - 'overall_healthy' => $allHealthy, - 'checks' => $results, - ]; - } -} - -// Run status checks -$dashboard = new StatusDashboard(); -$dashboard - ->addCheck(new PhpCheck()) - ->addCheck(new OpcacheCheck()) - ->addCheck(new RedisCheck()); - -$status = $dashboard->runAll(); - -// Collect system information -$stackInfo = [ - 'image' => 'kariricode/php-api-stack', - 'version' => getenv('STACK_VERSION') ?: '1.2.1', - 'php_version' => PHP_VERSION, - 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'nginx', - 'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? '/var/www/html/public', -]; - -?> - - - - - - - PHP API Stack - Ready for Development - - - - -
-
-

🚀 PHP API Stack

-

Production-ready development environment

- Version -
- -
-
-

⚠️ Demo Mode Active

-

- This is a placeholder page shown when no application is mounted. - Mount your Symfony/Laravel project to - to replace this demo page with your application. -

-
- -

Component Status

- -
- $result): ?> -
-

- - -

-
- message) ?> -
- details)): ?> -
- details as $key => $value): ?> -
:
- -
- -
- -
- -
-

Stack Information

-
-
- Docker Image: - -
-
- Stack Version: - -
-
- PHP Version: - -
-
- Web Server: - -
-
- Document Root: - -
-
- Overall Status: - - - -
-
-
- -
-

Quick Links

- -
-
- - -
- - - \ No newline at end of file diff --git a/scripts/process-configs.sh b/scripts/process-configs.sh index 1efd9a7..fac2f1b 100755 --- a/scripts/process-configs.sh +++ b/scripts/process-configs.sh @@ -249,11 +249,26 @@ else fi # Validate Nginx -if nginx -t 2>/dev/null; then +log_info "Testing Nginx configuration..." +NGINX_TEST_OUTPUT=$(nginx -t 2>&1) +NGINX_EXIT_CODE=$? + +if [ $NGINX_EXIT_CODE -eq 0 ]; then log_info " ✓ Nginx configuration is valid" +elif echo "$NGINX_TEST_OUTPUT" | grep -q "syntax is ok" && echo "$NGINX_TEST_OUTPUT" | grep -q "Permission denied"; then + log_info " ✓ Nginx configuration syntax is valid (ignoring PID permission during build)" else + echo "-----------------------------------------------" log_error " ✗ Nginx configuration test failed!" - nginx -t + echo "Exit code: $NGINX_EXIT_CODE" + echo "-----------------------------------------------" + echo "Full Nginx test output:" + echo "$NGINX_TEST_OUTPUT" + echo "-----------------------------------------------" + echo "-----------------------------------------------" + echo "Nginx error log:" + cat /var/log/nginx/error.log 2>/dev/null || echo "No error log available" + echo "-----------------------------------------------" exit 1 fi