clusev/app/Services/MetricHistory.php

65 lines
2.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\MetricSample;
use App\Models\Server;
/**
* Turns persisted per-minute metric samples into a bucketed cpu/mem/disk (% average) + load series
* for the Server-Details history chart. Each bucket is the AVERAGE of the samples that fall in it
* (gauges, not counters); a bucket with no samples is null so the chart shows a gap instead of
* fabricating a value. Mirrors the WireGuard traffic-series shape.
*/
class MetricHistory
{
/**
* @return array{window:int, points:array<int,array{t:int,cpu:?int,mem:?int,disk:?int,load:?float}>}
*/
public function series(Server $server, int $windowSeconds, int $buckets = 80): array
{
$windowSeconds = max(60, $windowSeconds);
$buckets = max(1, $buckets);
$now = time();
$from = $now - $windowSeconds;
$width = $windowSeconds / $buckets;
$sum = array_fill(0, $buckets, ['cpu' => 0, 'mem' => 0, 'disk' => 0, 'load' => 0.0, 'n' => 0]);
$rows = MetricSample::query()
->where('server_id', $server->id)
->where('sampled_at', '>=', now()->subSeconds($windowSeconds))
->orderBy('sampled_at')
->get(['cpu', 'mem', 'disk', 'load', 'sampled_at']);
foreach ($rows as $r) {
$idx = (int) floor(($r->sampled_at->getTimestamp() - $from) / $width);
if ($idx < 0) {
continue;
}
if ($idx >= $buckets) {
$idx = $buckets - 1; // a sample exactly on the right edge belongs to the last bucket
}
$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']++;
}
$points = [];
for ($i = 0; $i < $buckets; $i++) {
$n = $sum[$i]['n'];
$points[] = [
't' => (int) round($from + ($i + 0.5) * $width),
'cpu' => $n ? (int) round($sum[$i]['cpu'] / $n) : null,
'mem' => $n ? (int) round($sum[$i]['mem'] / $n) : null,
'disk' => $n ? (int) round($sum[$i]['disk'] / $n) : null,
'load' => $n ? round($sum[$i]['load'] / $n, 2) : null,
];
}
return ['window' => $windowSeconds, 'points' => $points];
}
}