124 lines
4.5 KiB
PHP
124 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Proxmox;
|
|
|
|
use App\Models\Host;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Throwable;
|
|
|
|
/**
|
|
* The last hour of a host's CPU and memory load, as two percentage series.
|
|
*
|
|
* Read from Proxmox's OWN recorded history (`/nodes/{node}/rrddata`) rather
|
|
* than sampled into a table of ours. PVE keeps hour/day/week/month/year for
|
|
* every node whether we ask or not, so a sampler would mean a new table, a
|
|
* minutely job, a pruning job and ~1440 rows per host per day — to reproduce,
|
|
* less accurately, something already on disk. It also means the curve is full
|
|
* from the first second, including the hour before anyone opened the page, and
|
|
* that it cannot disagree with what Proxmox's own interface shows.
|
|
*
|
|
* The price is stated rather than hidden: a host that is not reachable has no
|
|
* curve. See `available` below.
|
|
*/
|
|
final class HostLoadSeries
|
|
{
|
|
/** Just under the minute at which PVE writes a fresh sample. */
|
|
private const TTL_SECONDS = 55;
|
|
|
|
public function __construct(private ProxmoxClient $pve) {}
|
|
|
|
/** Bytes per second → MiB/s. The tiles carry the unit, so nobody has to guess. */
|
|
private const MIB = 1048576;
|
|
|
|
/**
|
|
* @return array{labels: array<int, int>, cpu: array<int, ?float>, ram: array<int, ?float>, netin: array<int, ?float>, netout: array<int, ?float>, available: bool}
|
|
*/
|
|
public function forHost(Host $host): array
|
|
{
|
|
// The `v2` is not decoration. A cache entry outlives a deploy, and an
|
|
// entry written by the version before the network series has no
|
|
// `netin`/`netout` — read as one of today's it kills the host page with
|
|
// an undefined key for the length of the TTL, which is exactly the
|
|
// minute somebody is looking to see whether the update went through.
|
|
// The key moves with the shape; the old entry is simply never read
|
|
// again and expires on its own.
|
|
return Cache::remember(
|
|
'host-load:v2:'.$host->id,
|
|
self::TTL_SECONDS,
|
|
fn () => $this->read($host),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array{labels: array<int, int>, cpu: array<int, ?float>, ram: array<int, ?float>, netin: array<int, ?float>, netout: array<int, ?float>, available: bool}
|
|
*/
|
|
private function read(Host $host): array
|
|
{
|
|
$empty = ['labels' => [], 'cpu' => [], 'ram' => [], 'netin' => [], 'netout' => [], 'available' => false];
|
|
|
|
try {
|
|
$rows = $this->pve->forHost($host)->nodeRrdData($host->node ?? 'pve', 'hour');
|
|
} catch (Throwable) {
|
|
// Deliberately not a series of zeros. A flat line at zero is a
|
|
// claim about the host — that it was idle — and this is the one
|
|
// case where we know nothing at all. The panel says so instead.
|
|
return $empty;
|
|
}
|
|
|
|
if ($rows === []) {
|
|
return $empty;
|
|
}
|
|
|
|
$labels = [];
|
|
$cpu = [];
|
|
$ram = [];
|
|
$netin = [];
|
|
$netout = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$labels[] = (int) ($row['time'] ?? 0);
|
|
$cpu[] = $this->percent($row['cpu'] ?? null, 1);
|
|
$ram[] = $this->percent($row['memused'] ?? null, $row['memtotal'] ?? null);
|
|
$netin[] = $this->rate($row['netin'] ?? null);
|
|
$netout[] = $this->rate($row['netout'] ?? null);
|
|
}
|
|
|
|
return [
|
|
'labels' => $labels,
|
|
'cpu' => $cpu,
|
|
'ram' => $ram,
|
|
'netin' => $netin,
|
|
'netout' => $netout,
|
|
'available' => true,
|
|
];
|
|
}
|
|
|
|
/** Bytes per second as MiB/s — or null, for the same reason percent() returns null. */
|
|
private function rate(mixed $bytesPerSecond): ?float
|
|
{
|
|
if (! is_numeric($bytesPerSecond)) {
|
|
return null;
|
|
}
|
|
|
|
return round((float) $bytesPerSecond / self::MIB, 2);
|
|
}
|
|
|
|
/**
|
|
* A share of a whole, in percent — or null when either half is missing.
|
|
*
|
|
* Null rather than 0.0, and that is the whole care taken here: Proxmox
|
|
* simply omits the fields for a sample it has no data for, and charting
|
|
* that omission as zero draws a quiet hour the host never had. The same
|
|
* distinction `instance_metrics` makes for its nullable columns, for the
|
|
* same reason.
|
|
*/
|
|
private function percent(mixed $part, mixed $whole): ?float
|
|
{
|
|
if (! is_numeric($part) || ! is_numeric($whole) || (float) $whole <= 0.0) {
|
|
return null;
|
|
}
|
|
|
|
return round((float) $part / (float) $whole * 100, 2);
|
|
}
|
|
}
|