mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(metrics): aggregate fleet KPIs with weighted math
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Support\Metrics\PressureLevel;
|
||||
|
||||
class FleetMetricsAggregator
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function fleetKpis(array $rows): array
|
||||
{
|
||||
$online = array_values(array_filter($rows, fn ($r) => $r['online'] === true));
|
||||
|
||||
$cpuValues = self::present($online, 'cpu');
|
||||
$cpuAvg = $cpuValues === [] ? 0.0 : round(array_sum($cpuValues) / count($cpuValues), 1);
|
||||
|
||||
$busiest = null;
|
||||
foreach ($online as $r) {
|
||||
if ($r['cpu'] !== null && ($busiest === null || $r['cpu'] > $busiest['percent'])) {
|
||||
$busiest = ['name' => $r['name'], 'percent' => (float) $r['cpu']];
|
||||
}
|
||||
}
|
||||
|
||||
$memUsed = (int) self::sum($online, 'memUsed');
|
||||
$memTotal = (int) self::sum($online, 'memTotal');
|
||||
$diskUsed = (int) self::sum($online, 'diskUsed');
|
||||
$diskTotal = (int) self::sum($online, 'diskTotal');
|
||||
$load1 = self::present($online, 'load1');
|
||||
|
||||
$needsAttention = 0;
|
||||
foreach ($online as $r) {
|
||||
$hot = PressureLevel::for($r['cpu'], 'cpu') !== 'ok'
|
||||
|| PressureLevel::for(self::percent($r['memUsed'], $r['memTotal']), 'memory') !== 'ok'
|
||||
|| PressureLevel::for($r['diskPercent'], 'disk') !== 'ok';
|
||||
if ($hot) {
|
||||
$needsAttention++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'serversOnline' => count($online),
|
||||
'serversTotal' => count($rows),
|
||||
'needsAttention' => $needsAttention,
|
||||
'cpuAvg' => $cpuAvg,
|
||||
'cpuApproximate' => count($cpuValues) > 1,
|
||||
'cpuBusiest' => $busiest,
|
||||
'memUsed' => $memUsed,
|
||||
'memTotal' => $memTotal,
|
||||
'memPercent' => self::percent($memUsed, $memTotal),
|
||||
'diskUsed' => $diskUsed,
|
||||
'diskTotal' => $diskTotal,
|
||||
'diskPercent' => self::percent($diskUsed, $diskTotal),
|
||||
'netRx' => (float) self::sum($online, 'netRx'),
|
||||
'netTx' => (float) self::sum($online, 'netTx'),
|
||||
'loadBusiest' => $load1 === [] ? 0.0 : max($load1),
|
||||
'loadAvg' => $load1 === [] ? 0.0 : round(array_sum($load1) / count($load1), 2),
|
||||
'containers' => (int) self::sum($online, 'containers'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array<int, float>
|
||||
*/
|
||||
private static function present(array $rows, string $key): array
|
||||
{
|
||||
return array_values(array_map(
|
||||
fn ($r) => (float) $r[$key],
|
||||
array_filter($rows, fn ($r) => $r[$key] !== null)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private static function sum(array $rows, string $key): float
|
||||
{
|
||||
return array_sum(array_map(fn ($r) => $r[$key] ?? 0, $rows));
|
||||
}
|
||||
|
||||
private static function percent(int|float|null $used, int|float|null $total): float
|
||||
{
|
||||
if (! $total) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return round(($used / $total) * 100, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
use App\Services\FleetMetricsAggregator;
|
||||
|
||||
function row(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'uuid' => 'u', 'name' => 'srv', 'online' => true,
|
||||
'cpu' => 10.0, 'memUsed' => 1_000, 'memTotal' => 10_000,
|
||||
'diskUsed' => 1_000, 'diskTotal' => 10_000, 'diskPercent' => 10.0,
|
||||
'load1' => 0.5, 'netRx' => 100.0, 'netTx' => 50.0, 'containers' => 2,
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
it('weights memory and disk by totals, not mean of percents', function () {
|
||||
$rows = [
|
||||
row(['memUsed' => 100, 'memTotal' => 100, 'diskUsed' => 100, 'diskTotal' => 100, 'diskPercent' => 100.0]),
|
||||
row(['memUsed' => 10, 'memTotal' => 1_000, 'diskUsed' => 10, 'diskTotal' => 1_000, 'diskPercent' => 1.0]),
|
||||
];
|
||||
|
||||
$kpis = FleetMetricsAggregator::fleetKpis($rows);
|
||||
|
||||
// Σused/Σtotal = 110/1100 = 10%, not the (100+1)/2 = 50.5% a naive mean gives.
|
||||
expect(round($kpis['memPercent'], 1))->toBe(10.0);
|
||||
expect(round($kpis['diskPercent'], 1))->toBe(10.0);
|
||||
});
|
||||
|
||||
it('means cpu, flags approximate for multiple servers, and finds the busiest', function () {
|
||||
$kpis = FleetMetricsAggregator::fleetKpis([
|
||||
row(['name' => 'a', 'cpu' => 20.0]),
|
||||
row(['name' => 'b', 'cpu' => 80.0]),
|
||||
]);
|
||||
|
||||
expect($kpis['cpuAvg'])->toBe(50.0);
|
||||
expect($kpis['cpuApproximate'])->toBeTrue();
|
||||
expect($kpis['cpuBusiest'])->toBe(['name' => 'b', 'percent' => 80.0]);
|
||||
});
|
||||
|
||||
it('sums network rates and reports busiest and average load', function () {
|
||||
$kpis = FleetMetricsAggregator::fleetKpis([
|
||||
row(['netRx' => 100.0, 'netTx' => 10.0, 'load1' => 0.5]),
|
||||
row(['netRx' => 200.0, 'netTx' => 20.0, 'load1' => 2.5]),
|
||||
]);
|
||||
|
||||
expect($kpis['netRx'])->toBe(300.0);
|
||||
expect($kpis['netTx'])->toBe(30.0);
|
||||
expect($kpis['loadBusiest'])->toBe(2.5);
|
||||
expect($kpis['loadAvg'])->toBe(1.5);
|
||||
});
|
||||
|
||||
it('ignores offline rows and counts servers needing attention', function () {
|
||||
$kpis = FleetMetricsAggregator::fleetKpis([
|
||||
row(['cpu' => 95.0]), // over cpu error cutoff
|
||||
row(['online' => false, 'cpu' => 99.0]),
|
||||
]);
|
||||
|
||||
expect($kpis['serversOnline'])->toBe(1);
|
||||
expect($kpis['serversTotal'])->toBe(2);
|
||||
expect($kpis['needsAttention'])->toBe(1);
|
||||
expect($kpis['cpuAvg'])->toBe(95.0); // offline row excluded from the mean
|
||||
});
|
||||
|
||||
it('returns zeroed kpis for no rows', function () {
|
||||
$kpis = FleetMetricsAggregator::fleetKpis([]);
|
||||
|
||||
expect($kpis['serversOnline'])->toBe(0);
|
||||
expect($kpis['cpuAvg'])->toBe(0.0);
|
||||
expect($kpis['cpuBusiest'])->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user