#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoStandards.Scripts.Deploy * INGROUP: MokoStandards * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/health-check.php * VERSION: 09.21.00 * BRIEF: Post-deploy health check — verify a Joomla site is responding correctly */ declare(strict_types=1); class HealthCheck { private string $url = ''; private int $timeout = 30; private array $checks = ['http']; private int $passed = 0; private int $failed = 0; public function run(): int { $this->parseArgs(); if ($this->url === '') { $this->log('Usage: health-check.php --url [--timeout ] [--checks ]'); return 1; } $this->url = rtrim($this->url, '/'); $this->log("Health check for: {$this->url}"); $this->log("Timeout: {$this->timeout}s"); $this->log("Checks: " . implode(', ', $this->checks)); $this->log(''); foreach ($this->checks as $check) { switch ($check) { case 'http': $this->checkHttp(); break; case 'admin': $this->checkAdmin(); break; case 'api': $this->checkApi(); break; default: $this->log("UNKNOWN CHECK: {$check} — skipping"); break; } } $this->log(''); $this->log("Results: {$this->passed} passed, {$this->failed} failed"); return $this->failed > 0 ? 1 : 0; } private function parseArgs(): void { $args = $_SERVER['argv'] ?? []; $count = count($args); for ($i = 1; $i < $count; $i++) { switch ($args[$i]) { case '--url': $this->url = $args[++$i] ?? ''; break; case '--timeout': $this->timeout = (int) ($args[++$i] ?? 30); break; case '--checks': $raw = $args[++$i] ?? 'http'; $this->checks = array_map('trim', explode(',', $raw)); break; } } } private function checkHttp(): void { $this->log('[http] GET ' . $this->url); $result = $this->curlGet($this->url); if ($result === null) { $this->fail('http', 'Request failed — could not connect'); return; } if ($result['http_code'] !== 200) { $this->fail('http', "Expected HTTP 200, got {$result['http_code']}"); return; } if ($this->containsFatalError($result['body'])) { $this->fail('http', 'Response body contains PHP fatal error'); return; } $this->pass('http', "HTTP 200 OK ({$result['time_ms']}ms)"); } private function checkAdmin(): void { $adminUrl = $this->url . '/administrator/'; $this->log('[admin] GET ' . $adminUrl); $result = $this->curlGet($adminUrl); if ($result === null) { $this->fail('admin', 'Request failed — could not connect'); return; } if ($result['http_code'] !== 200) { $this->fail('admin', "Expected HTTP 200, got {$result['http_code']}"); return; } $this->pass('admin', "HTTP 200 OK ({$result['time_ms']}ms)"); } private function checkApi(): void { $apiUrl = $this->url . '/api/index.php/v1'; $this->log('[api] GET ' . $apiUrl); $result = $this->curlGet($apiUrl); if ($result === null) { $this->fail('api', 'Request failed — could not connect'); return; } if ($result['http_code'] !== 200 && $result['http_code'] !== 401) { $this->fail('api', "Expected HTTP 200 or 401, got {$result['http_code']}"); return; } $this->pass('api', "HTTP {$result['http_code']} — API is alive ({$result['time_ms']}ms)"); } private function curlGet(string $url): ?array { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_CONNECTTIMEOUT => $this->timeout, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_USERAGENT => 'MokoHealthCheck/1.0', ]); $body = curl_exec($ch); if (curl_errno($ch)) { $error = curl_error($ch); $this->log(" cURL error: {$error}"); curl_close($ch); return null; } $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME); curl_close($ch); return [ 'http_code' => $httpCode, 'body' => is_string($body) ? $body : '', 'time_ms' => (int) round($totalTime * 1000), ]; } private function containsFatalError(string $body): bool { $patterns = [ 'Fatal error:', 'Fatal Error', 'Parse error:', 'Uncaught Error:', 'Uncaught Exception:', ]; foreach ($patterns as $pattern) { if (stripos($body, $pattern) !== false) { return true; } } return false; } private function pass(string $check, string $message): void { $this->passed++; $this->log("[{$check}] PASS: {$message}"); } private function fail(string $check, string $message): void { $this->failed++; $this->log("[{$check}] FAIL: {$message}"); } private function log(string $message): void { $timestamp = date('Y-m-d H:i:s'); fwrite(STDERR, "[{$timestamp}] {$message}" . PHP_EOL); } } $app = new HealthCheck(); exit($app->run());