6a29fbd99e
Rename class, file, and all references across the codebase to align with the moko-platform naming convention. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
228 lines
5.1 KiB
PHP
228 lines
5.1 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* 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: 01.00.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 <site-url> [--timeout <seconds>] [--checks <http,admin,api>]');
|
|
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());
|