mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(metrics): populate fleet KPIs, server rows, and container ranking
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
|
||||
namespace App\Livewire\Fleet;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Services\FleetMetricsAggregator;
|
||||
use App\Services\SentinelMetricsClient;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Lazy;
|
||||
@@ -26,8 +29,23 @@ class Metrics extends Component
|
||||
#[Url(as: 'server')]
|
||||
public string $serverUuid = '';
|
||||
|
||||
#[Url(as: 'cmetric')]
|
||||
public string $containerMetric = 'cpu';
|
||||
|
||||
public bool $live = false;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
public array $kpis = [];
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $serverRows = [];
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $topContainers = [];
|
||||
|
||||
/** @var array<string, array{name: string, link: ?string}> */
|
||||
protected array $containerMetaCache = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$allServers = Server::ownedByCurrentTeamCached();
|
||||
@@ -43,6 +61,137 @@ class Metrics extends Component
|
||||
if ($this->serverUuid !== '' && ! array_key_exists($this->serverUuid, $this->serverOptions)) {
|
||||
$this->serverUuid = '';
|
||||
}
|
||||
|
||||
if ($this->servers->isNotEmpty()) {
|
||||
$this->loadData();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedServerUuid(): void
|
||||
{
|
||||
$this->loadData();
|
||||
}
|
||||
|
||||
public function updatedContainerMetric(): void
|
||||
{
|
||||
$this->loadData();
|
||||
}
|
||||
|
||||
public function setRange(string $range): void
|
||||
{
|
||||
$this->range = in_array($range, ['24h', '7d', '30d'], true) ? $range : '24h';
|
||||
$this->loadData();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Server>
|
||||
*/
|
||||
protected function targetServers(): Collection
|
||||
{
|
||||
if ($this->serverUuid !== '') {
|
||||
return $this->servers->filter(fn (Server $s) => $s->uuid === $this->serverUuid)->values();
|
||||
}
|
||||
|
||||
return $this->servers;
|
||||
}
|
||||
|
||||
public function loadData(): void
|
||||
{
|
||||
if ($this->servers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$containers = [];
|
||||
|
||||
foreach ($this->targetServers() as $server) {
|
||||
$client = $this->metricsClient($server);
|
||||
|
||||
try {
|
||||
$summary = $client->summary();
|
||||
} catch (\Throwable) {
|
||||
$summary = null;
|
||||
}
|
||||
|
||||
if ($summary === null) {
|
||||
$rows[] = $this->offlineRow($server);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$current = $client->containersCurrent();
|
||||
$rows[] = array_merge(
|
||||
['uuid' => $server->uuid, 'name' => $server->name, 'online' => true, 'containers' => count($current)],
|
||||
$summary
|
||||
);
|
||||
|
||||
foreach ($current as $c) {
|
||||
$meta = $this->resolveContainer($server, $c['id']);
|
||||
$containers[] = array_merge($c, ['server' => $server->name, 'name' => $meta['name'], 'link' => $meta['link']]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->serverRows = $rows;
|
||||
$this->kpis = FleetMetricsAggregator::fleetKpis($rows);
|
||||
$this->topContainers = FleetMetricsAggregator::rankContainers($containers, $this->containerMetric);
|
||||
|
||||
$this->dispatch("refreshChartData-{$this->chartId}", $this->chartPayload());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function offlineRow(Server $server): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $server->uuid, 'name' => $server->name, 'online' => false,
|
||||
'cpu' => null, 'memUsed' => null, 'memTotal' => null,
|
||||
'diskUsed' => null, 'diskTotal' => null, 'diskPercent' => null,
|
||||
'load1' => null, 'netRx' => null, 'netTx' => null, 'containers' => null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function metricsClient(Server $server): SentinelMetricsClient
|
||||
{
|
||||
return app(SentinelMetricsClient::class, ['server' => $server]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Sentinel container id (the resource uuid Sentinel keys by) to a
|
||||
* team-owned resource name + metrics link, memoized. Shows the raw id when no
|
||||
* team-owned resource matches, so a Sentinel id never discloses another team's name.
|
||||
*
|
||||
* @return array{name: string, link: ?string}
|
||||
*/
|
||||
protected function resolveContainer(Server $server, string $id): array
|
||||
{
|
||||
if (isset($this->containerMetaCache[$id])) {
|
||||
return $this->containerMetaCache[$id];
|
||||
}
|
||||
|
||||
$app = Application::ownedByCurrentTeam()->with('environment.project')->whereUuid($id)->first();
|
||||
|
||||
$link = null;
|
||||
if ($app && data_get($app, 'environment.project.uuid')) {
|
||||
$link = route('project.application.metrics', [
|
||||
'project_uuid' => $app->environment->project->uuid,
|
||||
'environment_uuid' => $app->environment->uuid,
|
||||
'application_uuid' => $app->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->containerMetaCache[$id] = [
|
||||
'name' => $app?->name ?? $id,
|
||||
'link' => $link,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function chartPayload(): array
|
||||
{
|
||||
return ['range' => $this->range];
|
||||
}
|
||||
|
||||
public function placeholder(): View
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Fleet\Metrics;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Services\SentinelMetricsClient;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
class StubMetricsClient extends SentinelMetricsClient
|
||||
{
|
||||
/** @var array<string, ?array> serverUuid => summary */
|
||||
public static array $summaries = [];
|
||||
|
||||
/** @var array<string, array> serverUuid => containers */
|
||||
public static array $containers = [];
|
||||
|
||||
public function summary(): ?array
|
||||
{
|
||||
return self::$summaries[$this->server->uuid] ?? null;
|
||||
}
|
||||
|
||||
public function containersCurrent(): array
|
||||
{
|
||||
return self::$containers[$this->server->uuid] ?? [];
|
||||
}
|
||||
|
||||
public function history(string $metric, string $from): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function networkHistory(string $from): array
|
||||
{
|
||||
return ['rx' => [], 'tx' => []];
|
||||
}
|
||||
}
|
||||
|
||||
class TestableMetrics extends Metrics
|
||||
{
|
||||
protected function metricsClient(Server $server): SentinelMetricsClient
|
||||
{
|
||||
return new StubMetricsClient($server);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
StubMetricsClient::$summaries = [];
|
||||
StubMetricsClient::$containers = [];
|
||||
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
$user = User::factory()->create();
|
||||
$team = Team::factory()->create();
|
||||
$user->teams()->attach($team, ['role' => 'owner']);
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
$this->privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
|
||||
$this->team = $team;
|
||||
});
|
||||
|
||||
function metricsServer($team, $privateKey): Server
|
||||
{
|
||||
$server = Server::factory()->create(['team_id' => $team->id, 'private_key_id' => $privateKey->id]);
|
||||
$server->settings->update(['is_metrics_enabled' => true]);
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
function onlineSummary(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'online' => true, 'cpu' => 30.0, 'memUsed' => 100, 'memTotal' => 200,
|
||||
'diskUsed' => 10, 'diskTotal' => 100, 'diskPercent' => 10.0,
|
||||
'load1' => 0.4, 'netRx' => 5.0, 'netTx' => 1.0,
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
it('keeps an unreachable server in the table but excludes it from KPIs', function () {
|
||||
$ok = metricsServer($this->team, $this->privateKey);
|
||||
$dead = metricsServer($this->team, $this->privateKey);
|
||||
|
||||
StubMetricsClient::$summaries = [
|
||||
$ok->uuid => onlineSummary(['cpu' => 30.0]),
|
||||
$dead->uuid => null,
|
||||
];
|
||||
|
||||
$component = Livewire::test(TestableMetrics::class);
|
||||
|
||||
expect($component->get('serverRows'))->toHaveCount(2);
|
||||
|
||||
$deadRow = collect($component->get('serverRows'))->firstWhere('uuid', $dead->uuid);
|
||||
expect($deadRow['online'])->toBeFalse();
|
||||
expect($deadRow['cpu'])->toBeNull();
|
||||
|
||||
expect($component->get('kpis')['serversOnline'])->toBe(1);
|
||||
expect($component->get('kpis')['cpuAvg'])->toBe(30.0);
|
||||
});
|
||||
|
||||
it('ranks containers across servers by the selected metric', function () {
|
||||
$s = metricsServer($this->team, $this->privateKey);
|
||||
|
||||
StubMetricsClient::$summaries = [$s->uuid => onlineSummary(['diskPercent' => 50.0])];
|
||||
StubMetricsClient::$containers = [$s->uuid => [
|
||||
['id' => 'low', 'cpu' => 5.0, 'memUsed' => 10, 'memPercent' => 1.0, 'diskBytes' => 1, 'net' => 0.0],
|
||||
['id' => 'high', 'cpu' => 88.0, 'memUsed' => 20, 'memPercent' => 2.0, 'diskBytes' => 2, 'net' => 0.0],
|
||||
]];
|
||||
|
||||
$component = Livewire::test(TestableMetrics::class);
|
||||
|
||||
expect(array_column($component->get('topContainers'), 'id')[0])->toBe('high');
|
||||
});
|
||||
|
||||
it('cannot see another team’s servers', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
// Reuse the existing key id: the factory's private key is hardcoded, so a second
|
||||
// PrivateKey::create collides on fingerprint. Only the server's team_id matters here.
|
||||
$server = Server::factory()->create(['team_id' => $otherTeam->id, 'private_key_id' => $this->privateKey->id]);
|
||||
$server->settings->update(['is_metrics_enabled' => true]);
|
||||
|
||||
$component = Livewire::test(TestableMetrics::class);
|
||||
|
||||
expect($component->get('serverRows'))->toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user