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 = '/^\[(?[^\]]+)\]\s+(?\S+)\s+(?\S+)\s+(?\S+)\s+plan=(?\S+)\s+bucket=(?\S+)\s+(?\d{3})\s+(?\d+)ms(?:\s+ua="(?[^"]*)")?(?:\s+ref="(?[^"]*)")?$/'; $re_legacy = '/^\[(?[^\]]+)\]\s+(?\S+)\s+(?\S+)\s+(?\S+)\s+(?\d{3})\s+(?\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);