Files
dewemoji-api/app/controllers/Metrics.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);