From 9a8b4eb53c66a6063bed8225852f685a640268eb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:05:48 +0200 Subject: [PATCH] fix(metrics): keep reachable servers online when Sentinel has no summary Closes #12289 Co-Authored-By: Claude Opus 5.5 --- app/Livewire/Fleet/Metrics.php | 13 ++++++---- app/Services/FleetMetricsAggregator.php | 25 +++++++++++-------- .../views/livewire/fleet/metrics.blade.php | 18 ++++++++----- .../Livewire/Fleet/MetricsDataTest.php | 17 +++++++++++++ .../Services/FleetMetricsAggregatorTest.php | 16 ++++++++++-- 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/app/Livewire/Fleet/Metrics.php b/app/Livewire/Fleet/Metrics.php index bcf6917499..5a681460e6 100644 --- a/app/Livewire/Fleet/Metrics.php +++ b/app/Livewire/Fleet/Metrics.php @@ -251,15 +251,15 @@ class Metrics extends Component } if ($summary === null) { - $rows[] = $this->offlineRow($server); + $rows[] = $this->rowWithoutMetrics($server); continue; } $current = $results[$server->uuid.':containers'] ?? []; $rows[] = array_merge( - ['uuid' => $server->uuid, 'name' => $server->name, 'online' => true, 'containers' => count($current)], - $summary + $summary, + ['uuid' => $server->uuid, 'name' => $server->name, 'online' => true, 'reporting' => true, 'containers' => count($current)] ); foreach ($current as $c) { @@ -308,12 +308,15 @@ class Metrics extends Component } /** + * A server whose Sentinel returned no current metrics. It is online when Coolify can reach it, + * because a failed metrics request does not mean that the server is down. + * * @return array */ - protected function offlineRow(Server $server): array + protected function rowWithoutMetrics(Server $server): array { return [ - 'uuid' => $server->uuid, 'name' => $server->name, 'online' => false, + 'uuid' => $server->uuid, 'name' => $server->name, 'online' => (bool) $server->isFunctional(), 'reporting' => false, 'cpu' => null, 'memUsed' => null, 'memTotal' => null, 'diskUsed' => null, 'diskTotal' => null, 'diskPercent' => null, 'load1' => null, 'netRx' => null, 'netTx' => null, 'containers' => null, diff --git a/app/Services/FleetMetricsAggregator.php b/app/Services/FleetMetricsAggregator.php index 29e0459d3b..f739abf2e8 100644 --- a/app/Services/FleetMetricsAggregator.php +++ b/app/Services/FleetMetricsAggregator.php @@ -13,25 +13,27 @@ class FleetMetricsAggregator public static function fleetKpis(array $rows): array { $online = array_values(array_filter($rows, fn ($r) => $r['online'] === true)); + // Only servers whose Sentinel returned current metrics give values. + $reporting = array_values(array_filter($online, fn ($r) => $r['reporting'] === true)); - $cpuValues = self::present($online, 'cpu'); + $cpuValues = self::present($reporting, 'cpu'); $cpuAvg = $cpuValues === [] ? 0.0 : round(array_sum($cpuValues) / count($cpuValues), 1); $busiest = null; - foreach ($online as $r) { + foreach ($reporting 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'); + $memUsed = (int) self::sum($reporting, 'memUsed'); + $memTotal = (int) self::sum($reporting, 'memTotal'); + $diskUsed = (int) self::sum($reporting, 'diskUsed'); + $diskTotal = (int) self::sum($reporting, 'diskTotal'); + $load1 = self::present($reporting, 'load1'); $needsAttention = 0; - foreach ($online as $r) { + foreach ($reporting 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'; @@ -42,6 +44,7 @@ class FleetMetricsAggregator return [ 'serversOnline' => count($online), + 'serversReporting' => count($reporting), 'serversTotal' => count($rows), 'needsAttention' => $needsAttention, 'cpuAvg' => $cpuAvg, @@ -53,11 +56,11 @@ class FleetMetricsAggregator 'diskUsed' => $diskUsed, 'diskTotal' => $diskTotal, 'diskPercent' => self::percent($diskUsed, $diskTotal), - 'netRx' => (float) self::sum($online, 'netRx'), - 'netTx' => (float) self::sum($online, 'netTx'), + 'netRx' => (float) self::sum($reporting, 'netRx'), + 'netTx' => (float) self::sum($reporting, 'netTx'), 'loadBusiest' => $load1 === [] ? 0.0 : max($load1), 'loadAvg' => $load1 === [] ? 0.0 : round(array_sum($load1) / count($load1), 2), - 'containers' => (int) self::sum($online, 'containers'), + 'containers' => (int) self::sum($reporting, 'containers'), ]; } diff --git a/resources/views/livewire/fleet/metrics.blade.php b/resources/views/livewire/fleet/metrics.blade.php index 79103f6de2..b215ed1f17 100644 --- a/resources/views/livewire/fleet/metrics.blade.php +++ b/resources/views/livewire/fleet/metrics.blade.php @@ -109,8 +109,10 @@ $serverListboxOptions = array_merge( @php $sTotal = max(1, (int) ($kpis['serversTotal'] ?? 0)); $sOnline = (int) ($kpis['serversOnline'] ?? 0); - $sAttention = min($sOnline, (int) ($kpis['needsAttention'] ?? 0)); - $sHealthy = max(0, $sOnline - $sAttention); + $sReporting = min($sOnline, (int) ($kpis['serversReporting'] ?? 0)); + $sAttention = min($sReporting, (int) ($kpis['needsAttention'] ?? 0)); + $sHealthy = max(0, $sReporting - $sAttention); + $sNoMetrics = $sOnline - $sReporting; $sOffline = max(0, $sTotal - $sOnline); @endphp
@@ -118,11 +120,13 @@ $serverListboxOptions = array_merge(
@if ($sHealthy > 0)
@endif @if ($sAttention > 0)
@endif + @if ($sNoMetrics > 0)
@endif @if ($sOffline > 0)
@endif
{{ $sHealthy }} healthy @if ($sAttention > 0){{ $sAttention }} attention@endif + @if ($sNoMetrics > 0){{ $sNoMetrics }} no metrics@endif @if ($sOffline > 0){{ $sOffline }} offline@endif
@@ -259,16 +263,18 @@ $serverListboxOptions = array_merge( 'containers' => $row['containers'], ]; @endphp - {{-- Offline servers have no values, so they stay last in each sort. --}} -
{{ $row['name'] }} - @unless ($row['online']) + @if (! $row['online']) Offline - @endunless + @elseif (! $row['reporting']) + No metrics + @endif
@include('livewire.fleet._usage-cell', ['percent' => $row['cpu'], 'kind' => 'cpu'])
@include('livewire.fleet._usage-cell', ['percent' => $memPercent, 'kind' => 'memory'])
diff --git a/tests/Feature/Livewire/Fleet/MetricsDataTest.php b/tests/Feature/Livewire/Fleet/MetricsDataTest.php index 90693900af..c304fa2610 100644 --- a/tests/Feature/Livewire/Fleet/MetricsDataTest.php +++ b/tests/Feature/Livewire/Fleet/MetricsDataTest.php @@ -131,6 +131,23 @@ it('keeps an unreachable server in the table but excludes it from KPIs', functio expect($component->get('kpis')['cpuAvg'])->toBe(30.0); }); +it('counts a reachable server as online when Sentinel returns no summary', function () { + $server = metricsServer($this->team, $this->privateKey); + $server->update(['name' => 'web-1']); + $server->settings->forceFill(['is_reachable' => true, 'is_usable' => true])->saveQuietly(); + StubMetricsClient::$summaries = [$server->uuid => null]; + + $component = Livewire::test(TestableMetrics::class) + ->assertSee('No metrics') + ->assertDontSee('Offline'); + + $row = collect($component->get('serverRows'))->firstWhere('uuid', $server->uuid); + expect($row['online'])->toBeTrue(); + expect($row['reporting'])->toBeFalse(); + expect($component->get('kpis')['serversOnline'])->toBe(1); + expect($component->get('kpis')['serversReporting'])->toBe(0); +}); + it('ranks containers across servers by the selected metric', function () { $s = metricsServer($this->team, $this->privateKey); diff --git a/tests/Unit/Services/FleetMetricsAggregatorTest.php b/tests/Unit/Services/FleetMetricsAggregatorTest.php index 2de55616f7..277d770bcb 100644 --- a/tests/Unit/Services/FleetMetricsAggregatorTest.php +++ b/tests/Unit/Services/FleetMetricsAggregatorTest.php @@ -5,7 +5,7 @@ use App\Services\FleetMetricsAggregator; function row(array $overrides = []): array { return array_merge([ - 'uuid' => 'u', 'name' => 'srv', 'online' => true, + 'uuid' => 'u', 'name' => 'srv', 'online' => true, 'reporting' => 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, @@ -51,7 +51,7 @@ it('sums network rates and reports busiest and average load', function () { 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]), + row(['online' => false, 'reporting' => false, 'cpu' => 99.0]), ]); expect($kpis['serversOnline'])->toBe(1); @@ -60,6 +60,18 @@ it('ignores offline rows and counts servers needing attention', function () { expect($kpis['cpuAvg'])->toBe(95.0); // offline row excluded from the mean }); +it('counts an online server without metrics as online but leaves it out of the values', function () { + $kpis = FleetMetricsAggregator::fleetKpis([ + row(['cpu' => 20.0]), + row(['reporting' => false, 'cpu' => 99.0]), + ]); + + expect($kpis['serversOnline'])->toBe(2); + expect($kpis['serversReporting'])->toBe(1); + expect($kpis['needsAttention'])->toBe(0); + expect($kpis['cpuAvg'])->toBe(20.0); +}); + it('returns zeroed kpis for no rows', function () { $kpis = FleetMetricsAggregator::fleetKpis([]);