142 lines
4.6 KiB
PHP
142 lines
4.6 KiB
PHP
<?php
|
|
require_once __DIR__.'/../helpers/json.php';
|
|
|
|
// Optional DB health
|
|
$db_ok = null;
|
|
try {
|
|
require_once __DIR__.'/../db.php';
|
|
$pdo = getPDO();
|
|
$pdo->query('SELECT 1');
|
|
$db_ok = true;
|
|
} catch (Throwable $e) {
|
|
$db_ok = false;
|
|
}
|
|
|
|
// Tail N lines from access log to compute quick metrics
|
|
$logFile = __DIR__.'/../../storage/logs/access.log';
|
|
$lookback = 1000; // number of lines to parse
|
|
|
|
function tail_lines($fname, $lines = 1000, $buffer = 4096) {
|
|
if (!is_file($fname)) return [];
|
|
$f = fopen($fname, 'rb');
|
|
if ($f === false) return [];
|
|
fseek($f, 0, SEEK_END);
|
|
$pos = ftell($f);
|
|
$data = '';
|
|
$linecnt = 0;
|
|
while ($pos > 0 && $linecnt <= $lines) {
|
|
$step = ($pos - $buffer) >= 0 ? $buffer : $pos;
|
|
$pos -= $step;
|
|
fseek($f, $pos);
|
|
$chunk = fread($f, $step);
|
|
$data = $chunk . $data;
|
|
$linecnt = substr_count($data, "\n");
|
|
}
|
|
fclose($f);
|
|
$arr = explode("\n", trim($data));
|
|
if (count($arr) > $lines) $arr = array_slice($arr, -$lines);
|
|
return $arr;
|
|
}
|
|
|
|
$total = 0; $s2=0; $s3=0; $s4=0; $s5=0; $durations=[]; $paths=[]; $lastTs=null;
|
|
|
|
// Support new logger format (with plan/bucket/ua/ref) and legacy fallback
|
|
$re_new = '/^\[(?<ts>[^\]]+)\]\s+(?<id>\S+)\s+(?<ip>\S+)\s+(?<path>\S+)\s+plan=(?<plan>\S+)\s+bucket=(?<bucket>\S+)\s+(?<status>\d{3})\s+(?<ms>\d+)ms(?:\s+ua="(?<ua>[^"]*)")?(?:\s+ref="(?<ref>[^"]*)")?$/';
|
|
$re_legacy = '/^\[(?<ts>[^\]]+)\]\s+(?<id>\S+)\s+(?<ip>\S+)\s+(?<path>\S+)\s+(?<status>\d{3})\s+(?<ms>\d+)ms$/';
|
|
|
|
$lines = tail_lines($logFile, $lookback);
|
|
$log_size = is_file($logFile) ? filesize($logFile) : 0;
|
|
|
|
$by_plan = [];
|
|
$by_prefix = []; // changed to map prefix -> status counts
|
|
$prefix_depth = isset($_GET['prefix_depth']) ? max(1, (int)$_GET['prefix_depth']) : 2;
|
|
|
|
function path_prefix(string $path, int $depth): string {
|
|
// strip query string
|
|
$p = explode('?', $path, 2)[0];
|
|
// normalize leading slashes and split
|
|
$parts = array_values(array_filter(explode('/', $p), fn($s) => $s !== ''));
|
|
if (empty($parts)) return '/';
|
|
$keep = array_slice($parts, 0, $depth);
|
|
return '/' . implode('/', $keep);
|
|
}
|
|
$matched = 0;
|
|
foreach ($lines as $ln) {
|
|
$m = [];
|
|
if (!preg_match($re_new, $ln, $m)) {
|
|
$m = [];
|
|
if (!preg_match($re_legacy, $ln, $m)) continue; // skip non-matching line
|
|
}
|
|
$matched++;
|
|
$total++;
|
|
$st = (int)($m['status'] ?? 0);
|
|
if ($st >=200 && $st <300) $s2++;
|
|
else if($st >=300 && $st <400) $s3++;
|
|
else if($st >=400 && $st <500) $s4++;
|
|
else if($st >=500) $s5++;
|
|
|
|
$ms = (int)($m['ms'] ?? 0);
|
|
if ($ms > 0) $durations[] = $ms;
|
|
|
|
$p = $m['path'] ?? '-';
|
|
$paths[$p] = ($paths[$p] ?? 0) + 1;
|
|
$pref = path_prefix($p, $prefix_depth);
|
|
if (!isset($by_prefix[$pref])) $by_prefix[$pref] = ['total'=>0,'2xx'=>0,'3xx'=>0,'4xx'=>0,'5xx'=>0];
|
|
$by_prefix[$pref]['total']++;
|
|
if ($st >=200 && $st<300) $by_prefix[$pref]['2xx']++;
|
|
else if($st >=300 && $st<400) $by_prefix[$pref]['3xx']++;
|
|
else if($st >=400 && $st<500) $by_prefix[$pref]['4xx']++;
|
|
else if($st >=500) $by_prefix[$pref]['5xx']++;
|
|
|
|
$lastTs = $m['ts'] ?? $lastTs;
|
|
|
|
$plan = $m['plan'] ?? 'unknown';
|
|
$by_plan[$plan] = ($by_plan[$plan] ?? 0) + 1;
|
|
}
|
|
|
|
sort($durations);
|
|
$avg = count($durations) ? array_sum($durations)/count($durations) : 0;
|
|
function pct($arr, $p){ if (!$arr) return 0; $k = (int)ceil($p/100*count($arr))-1; $k = max(0,min($k,count($arr)-1)); return $arr[$k]; }
|
|
$p50 = pct($durations,50); $p95 = pct($durations,95); $p99 = pct($durations,99);
|
|
arsort($paths);
|
|
$top_paths = [];
|
|
foreach (array_slice($paths, 0, 5, true) as $p=>$c) { $top_paths[] = ['path'=>$p,'count'=>$c]; }
|
|
|
|
$loadavg_raw = function_exists('sys_getloadavg') ? sys_getloadavg() : null;
|
|
$loadavg = null;
|
|
if (is_array($loadavg_raw)) {
|
|
$loadavg = array_map(function($v){
|
|
// Output as strings to avoid jq numeric parsing issues on some shells
|
|
return number_format((float)$v, 2, '.', ''); // e.g., "6.03"
|
|
}, $loadavg_raw);
|
|
}
|
|
|
|
$resp = [
|
|
'ok' => true,
|
|
'time' => gmdate('c'),
|
|
'php_version' => PHP_VERSION,
|
|
'memory_used_bytes' => memory_get_usage(true),
|
|
'db' => $db_ok ? 'ok' : 'down',
|
|
'log' => [
|
|
'file_exists' => is_file($logFile),
|
|
'size_bytes' => $log_size,
|
|
'lookback_lines' => $lookback,
|
|
'last_request_at' => $lastTs,
|
|
],
|
|
'requests' => [
|
|
'total' => $total,
|
|
'parsed_lines' => $matched,
|
|
'by_status' => [ '2xx'=>$s2, '3xx'=>$s3, '4xx'=>$s4, '5xx'=>$s5 ],
|
|
'by_plan' => $by_plan,
|
|
'by_path_prefix' => $by_prefix,
|
|
'latency_ms' => [ 'avg'=>round($avg,2), 'p50'=>$p50, 'p95'=>$p95, 'p99'=>$p99 ],
|
|
'top_paths' => $top_paths,
|
|
],
|
|
'system' => [
|
|
'loadavg' => $loadavg,
|
|
'uptime' => @trim(shell_exec('uptime')),
|
|
],
|
|
];
|
|
|
|
// --- safe JSON emit (prevent extra bytes after JSON) ---
|
|
json_ok($resp); |