Writing down what each endpoint actually guarantees made the places it didn't obvious — shell arguments reaching a crontab or a bash -c unescaped, master.conf written without tmp+rename, and conf edits that could be saved without ever being parsed.
120 lines
5.9 KiB
PHP
120 lines
5.9 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// High-frequency monitor poll. CPU, memory and network only, cheap enough to fetch every
|
|
// second — the live-updating subset of what monitor.php returns in full.
|
|
//
|
|
// OPERATIONAL MODEL
|
|
// Split from monitor.php on refresh rate. The fields here move second to second and are
|
|
// readable from /proc alone. Everything that needs `docker stats`, a GraphQL call, or an
|
|
// SSH round trip stays in the full payload, because those cannot be sampled at this rate.
|
|
//
|
|
// The expensive memory breakdown is borrowed rather than recomputed. Docker and VM memory,
|
|
// top processes and swap come from the last full cache — a 600s window, wide enough that
|
|
// the card stays populated even when the background writer falls behind. Stale is the right
|
|
// trade here: per-container memory does not move meaningfully between seconds, and paying
|
|
// for it would defeat the point of this endpoint.
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// Reads /proc directly, no shell.
|
|
// /proc/meminfo and the ZFS arcstats file are parsed inline. At this poll rate a single
|
|
// fork per request would dominate the cost of the endpoint.
|
|
//
|
|
// Shares the CPU baseline with everything else.
|
|
// vv_cpu_per_core() keeps its counters in a shared state file, so this endpoint, the
|
|
// header snapshot, and the full monitor payload all report the same number rather than
|
|
// three independent samples that visibly disagree.
|
|
//
|
|
// Free means available, not unused.
|
|
// MemAvailable, not MemFree — reclaimable page cache is not memory pressure, and
|
|
// reporting MemFree would show a healthy machine as nearly full.
|
|
//
|
|
// ZFS ARC is broken out of system memory.
|
|
// On a host with ZFS cache pools the ARC is most of the "used" figure and is fully
|
|
// reclaimable. Folding it into system memory would make every reading alarming and
|
|
// none of them actionable.
|
|
//
|
|
// OPERATIONAL SAFEGUARDS
|
|
// Read-only.
|
|
//
|
|
// Every source read has a fallback.
|
|
// file() with a ?: [] fallback, @file() for the arcstats path that does not exist on a
|
|
// host without ZFS, and ?? 0 on every extracted key. A missing subsystem contributes
|
|
// zero rather than a warning or a fatal — and at one request per second, a fatal here
|
|
// would be a page that never stops erroring.
|
|
//
|
|
// The borrowed fields degrade to zero independently.
|
|
// Each ?? default is applied per field, so an absent or expired full cache costs those
|
|
// four values and leaves CPU, memory and network — the reason to call this endpoint —
|
|
// intact.
|
|
//
|
|
// Swap has its own fallback path.
|
|
// When the cached figures are absent, swap is recomputed from /proc/meminfo rather than
|
|
// reported as zero, because zero swap used and unknown swap used look identical in the
|
|
// UI and mean very different things.
|
|
//
|
|
// Derived values are clamped.
|
|
// max(0, …) on used and system memory, so the subtraction cannot go negative when the
|
|
// cached docker and VM figures were sampled against a different total.
|
|
//
|
|
// REQUEST
|
|
// GET, no parameters
|
|
//
|
|
// RESPONSE
|
|
// {"cpu":{…},"mem":{total_kb,free_kb,used_kb,arc_kb,docker_kb,vm_kb,system_kb,
|
|
// swap_total_kb,swap_used_kb,top_procs},"net":{…},"ts":epoch}
|
|
//
|
|
// DEPENDS ON
|
|
// include/config.php vv_cache_read()
|
|
// include/common.php vv_cpu_per_core(), vv_network_stats()
|
|
// /proc/meminfo, /proc/spl/kstat/zfs/arcstats
|
|
// monitor cache written by Tools/api_cache_writer.sh — source of the borrowed fields
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
header('Content-Type: application/json');
|
|
require_once dirname(__DIR__) . '/include/config.php';
|
|
require_once dirname(__DIR__) . '/include/common.php';
|
|
|
|
// Pull slow fields (docker/vm/top_procs/swap) from last full cache — stale is fine,
|
|
// these don't change on a per-second basis. 600s window so the card stays populated
|
|
// even if the full cache writer is temporarily behind.
|
|
$_full = vv_cache_read('monitor', 600);
|
|
$_dockerKb = $_full['mem']['docker_kb'] ?? 0;
|
|
$_vmKb = $_full['mem']['vm_kb'] ?? 0;
|
|
$_topProcs = $_full['mem']['top_procs'] ?? [];
|
|
$_swapTotal = $_full['mem']['swap_total_kb'] ?? 0;
|
|
$_swapUsed = $_full['mem']['swap_used_kb'] ?? 0;
|
|
|
|
// Fast memory: /proc/meminfo + ZFS ARC (no docker stats, no GQL)
|
|
$_memRaw = [];
|
|
foreach (file('/proc/meminfo') ?: [] as $_l) {
|
|
if (preg_match('/^(\w+):\s+(\d+)/', $_l, $_m)) $_memRaw[$_m[1]] = (int)$_m[2];
|
|
}
|
|
$_totalKb = $_memRaw['MemTotal'] ?? 0;
|
|
$_freeKb = $_memRaw['MemAvailable'] ?? 0;
|
|
$_arcKb = 0;
|
|
foreach (@file('/proc/spl/kstat/zfs/arcstats') ?: [] as $_l) {
|
|
if (preg_match('/^size\s+\d+\s+(\d+)/', $_l, $_m)) { $_arcKb = (int)($_m[1] / 1024); break; }
|
|
}
|
|
if (!$_swapTotal) {
|
|
$_swapTotal = $_memRaw['SwapTotal'] ?? 0;
|
|
$_swapUsed = max(0, ($_memRaw['SwapTotal'] ?? 0) - ($_memRaw['SwapFree'] ?? 0));
|
|
}
|
|
|
|
echo json_encode([
|
|
'cpu' => vv_cpu_per_core(),
|
|
'mem' => [
|
|
'total_kb' => $_totalKb,
|
|
'free_kb' => $_freeKb,
|
|
'used_kb' => max(0, $_totalKb - $_freeKb),
|
|
'arc_kb' => $_arcKb,
|
|
'docker_kb' => $_dockerKb,
|
|
'vm_kb' => $_vmKb,
|
|
'system_kb' => max(0, $_totalKb - $_freeKb - $_arcKb - $_dockerKb - $_vmKb),
|
|
'swap_total_kb' => $_swapTotal,
|
|
'swap_used_kb' => $_swapUsed,
|
|
'top_procs' => $_topProcs,
|
|
],
|
|
'net' => vv_network_stats(),
|
|
'ts' => time(),
|
|
]);
|