clusev/app/Services/MetricHistory.php

70 lines
2.7 KiB
PHP

<?php
namespace App\Services;
use App\Models\MetricSample;
use App\Models\Server;
/**
* Turns persisted per-minute metric samples into a downsampled cpu/mem/disk (% average) + load
* series for the Server-Details history chart. Bucketing is done in PHP (DB-agnostic, like the
* WireGuard traffic series) — each returned point is the AVERAGE of the samples in a bucket, and
* ONLY non-empty buckets are returned (the chart connects consecutive points and the client breaks
* the line only on a real time gap, so normal sampling renders a continuous curve, not dropouts).
*/
class MetricHistory
{
/**
* @return array{window:int, from:int, now:int, bucket:int,
* points:array<int,array{t:int,cpu:int,mem:int,disk:int,load:float}>}
*/
public function series(Server $server, int $windowSeconds): array
{
$windowSeconds = max(300, $windowSeconds);
// Bucket width is at least the 1/min sample interval, so a bucket holds ~one sample and the
// line stays continuous instead of zig-zagging between half-empty buckets.
$width = max(60, (int) floor($windowSeconds / 120));
$buckets = (int) ceil($windowSeconds / $width);
$now = time();
$from = $now - $windowSeconds;
$rows = MetricSample::query()
->where('server_id', $server->id)
->where('sampled_at', '>=', now()->subSeconds($windowSeconds))
->orderBy('sampled_at')
->get(['cpu', 'mem', 'disk', 'load', 'sampled_at']);
$sum = [];
foreach ($rows as $r) {
$idx = (int) floor(($r->sampled_at->getTimestamp() - $from) / $width);
if ($idx < 0) {
continue;
}
if ($idx >= $buckets) {
$idx = $buckets - 1;
}
$sum[$idx] ??= ['cpu' => 0, 'mem' => 0, 'disk' => 0, 'load' => 0.0, 'n' => 0];
$sum[$idx]['cpu'] += (int) $r->cpu;
$sum[$idx]['mem'] += (int) $r->mem;
$sum[$idx]['disk'] += (int) $r->disk;
$sum[$idx]['load'] += (float) $r->load;
$sum[$idx]['n']++;
}
ksort($sum);
$points = [];
foreach ($sum as $i => $b) {
$n = $b['n'];
$points[] = [
't' => (int) round($from + ($i + 0.5) * $width),
'cpu' => (int) round($b['cpu'] / $n),
'mem' => (int) round($b['mem'] / $n),
'disk' => (int) round($b['disk'] / $n),
'load' => round($b['load'] / $n, 2),
];
}
return ['window' => $windowSeconds, 'from' => $from, 'now' => $now, 'bucket' => $width, 'points' => $points];
}
}