From 6254f44f168c38d9eafc4228322997c4cb959306 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:53:15 +0100 Subject: [PATCH] fix(metrics): show disk usage when Sentinel names the data disk by another mount Use the only reported disk when no known root mount is reported, and stop filtering disk history by mount names so Sentinel 1.1.4+ keeps that disk. Closes #12448 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + app/Services/SentinelMetricsClient.php | 18 ++++++++------- tests/Feature/FleetMetricsPrefetchTest.php | 2 +- tests/Unit/Metrics/HasMetricsMappingTest.php | 17 ++++++++++++-- .../Services/SentinelMetricsClientTest.php | 22 +++++++++++++++++-- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6290e382e5..727e31afc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Save the healthcheck **Check type** when it is the only change. Before, the "Save changes" bar did not open when you changed only the check type, for example from **Container command** back to **HTTP request** - Coolify Cloud: end a paid subscription that lost its link to Stripe when the customer cancels it. Before, it stayed active, and `cloud:sync-stripe-subscriptions` did not report it - Keep the dashboard proxy configuration of a server with the IP `host.docker.internal` that is not the default localhost server. Before, Coolify did not see this server as localhost after it saved the server, for example after a proxy restart, and deleted `coolify.yaml`, so the dashboard was not reachable at its domain ([#12444](https://github.com/coollabsio/coolify/issues/12444)) +- Show the disk usage of a server on the **Metrics** tab, on the **Fleet** page, and in the MCP `get_server_metrics` tool when Sentinel reports the Coolify data disk under a different mount point than `/`, `/data`, or `/data/coolify`. For example, this occurs on a NAS where `/data` is a symlink into a data volume. Before, the disk usage was always empty on these servers ([#12448](https://github.com/coollabsio/coolify/issues/12448)) ## [v4.6.0] - 2026-11-02 diff --git a/app/Services/SentinelMetricsClient.php b/app/Services/SentinelMetricsClient.php index 45bb91aeaf..c03dd7a85f 100644 --- a/app/Services/SentinelMetricsClient.php +++ b/app/Services/SentinelMetricsClient.php @@ -313,14 +313,19 @@ class SentinelMetricsClient * Sentinel runs in a container and reports only the disks of its own mounts: * 1.1.2 and older as `/app/db`, 1.1.3 and newer by host mount point, which is * `/data` or `/data/coolify` when that directory is on its own disk. + * Sentinel 1.1.3 names a disk by its shortest host mount point, which can be any + * path (for example `/home` when `/data` is a symlink into a NAS volume). When none + * of ROOT_MOUNTS is reported, the only reported disk is the one under Sentinel's + * `/app/db`; with several, the right one is unknown. * * @param array> $rows */ public static function rootMountName(array $rows): ?string { - $mounts = array_column($rows, 'mount'); + $mounts = array_values(array_unique(array_column($rows, 'mount'))); - return collect(self::ROOT_MOUNTS)->first(fn ($mount) => in_array($mount, $mounts, true)); + return collect(self::ROOT_MOUNTS)->first(fn ($mount) => in_array($mount, $mounts, true)) + ?? (count($mounts) === 1 ? $mounts[0] : null); } /** @@ -352,13 +357,10 @@ class SentinelMetricsClient 'memory', 'disk', 'load', 'network' => "/{$metric}/history", default => '/cpu/history', }; - $query = self::historyQuery($range); - if ($metric === 'disk') { - // Sentinel 1.1.4+ returns only these mounts; older versions ignore the param. - $query['mount'] = implode(',', self::ROOT_MOUNTS); - } - return $this->url($path, $query); + // No `mount` filter for disk history: rootMountName() can pick a disk that is not in + // ROOT_MOUNTS, and Sentinel reports only the few disks of its own mounts. + return $this->url($path, self::historyQuery($range)); } /** diff --git a/tests/Feature/FleetMetricsPrefetchTest.php b/tests/Feature/FleetMetricsPrefetchTest.php index fa3f6094fd..a4403f6eda 100644 --- a/tests/Feature/FleetMetricsPrefetchTest.php +++ b/tests/Feature/FleetMetricsPrefetchTest.php @@ -64,7 +64,7 @@ it('fetches the page metrics of every server in one SSH command each', function expect($commands)->toHaveCount(2) ->and($commands[0])->toContain('/api/summary', '/api/containers/current', '/api/load/history', '/api/network/history', '-H @-') ->and($commands[0])->toContain('/api/disk/history?from=') - ->and($commands[0])->toContain('mount=%2F%2C%2Fapp%2Fdb%2C%2Fdata%2Fcoolify%2C%2Fdata'); + ->and($commands[0])->not->toContain('mount='); }); it('does not fetch each metric again when the batch fails', function () { diff --git a/tests/Unit/Metrics/HasMetricsMappingTest.php b/tests/Unit/Metrics/HasMetricsMappingTest.php index e6c70be05b..65dc52265f 100644 --- a/tests/Unit/Metrics/HasMetricsMappingTest.php +++ b/tests/Unit/Metrics/HasMetricsMappingTest.php @@ -67,13 +67,26 @@ it('returns null for a metric when metrics are unavailable', function () { expect($d->getLoadMetrics(5))->toBeNull(); }); -it('does not use non-root mounts when root disk samples are missing', function () { +it('does not guess between several non-root mounts when root disk samples are missing', function () { $double = new MetricsMappingDouble; - $double->rowsByType['disk'] = [['time' => 1000, 'mount' => '/boot', 'usedPercent' => 90]]; + $double->rowsByType['disk'] = [ + ['time' => 1000, 'mount' => '/boot', 'usedPercent' => 90], + ['time' => 1000, 'mount' => '/home', 'usedPercent' => 20], + ]; expect($double->getDiskMetrics(5))->toBe([]); }); +it('uses the only reported disk for disk metrics when it has another mount name', function () { + $double = new MetricsMappingDouble; + $double->rowsByType['disk'] = [ + ['time' => 1000, 'mount' => '/home', 'usedPercent' => 19], + ['time' => 2000, 'mount' => '/home', 'usedPercent' => 20], + ]; + + expect($double->getDiskMetrics(5))->toBe([[1000, 19.0], [2000, 20.0]]); +}); + it('uses the Sentinel data mount for disk metrics when no root mount is reported', function () { $double = new MetricsMappingDouble; $double->rowsByType['disk'] = [ diff --git a/tests/Unit/Services/SentinelMetricsClientTest.php b/tests/Unit/Services/SentinelMetricsClientTest.php index c8c42286b0..ef2ffbaf56 100644 --- a/tests/Unit/Services/SentinelMetricsClientTest.php +++ b/tests/Unit/Services/SentinelMetricsClientTest.php @@ -172,15 +172,32 @@ it('rounds the range start to the minute so history responses are cacheable', fu Carbon::setTestNow(); }); -it('does not substitute another mount when root disk history is absent', function () { +it('does not guess between several other mounts when root disk history is absent', function () { $client = fakeMetricsClient(); $client->bodies = ['/disk/history' => json_encode([ ['time' => 1700000000000, 'mount' => '/boot', 'usedPercent' => 90], + ['time' => 1700000000000, 'mount' => '/home', 'usedPercent' => 20], ])]; expect($client->history('disk', '24h'))->toBe([]); }); +it('uses the only reported disk when Sentinel names it by another host mount point', function () { + $client = fakeMetricsClient(); + $client->bodies = [ + '/summary' => json_encode(['disk' => [ + ['mount' => '/home', 'used' => 19, 'total' => 100, 'usedPercent' => 19.02], + ]]), + '/disk/history' => json_encode([ + ['time' => 1700000000000, 'mount' => '/home', 'usedPercent' => 19], + ['time' => 1700000300000, 'mount' => '/home', 'usedPercent' => 21], + ]), + ]; + + expect($client->summary()['diskPercent'])->toBe(19.02) + ->and($client->history('disk', '24h'))->toBe([[1699999800000, 19.0], [1700000100000, 21.0]]); +}); + it('supports disk history without mount fields', function () { $client = fakeMetricsClient(); $client->bodies = ['/disk/history' => json_encode([ @@ -190,10 +207,11 @@ it('supports disk history without mount fields', function () { expect($client->history('disk', '24h'))->toBe([[1699999800000, 40.0]]); }); -it('does not substitute another disk mount in the summary', function () { +it('does not guess between several other disk mounts in the summary', function () { $client = fakeMetricsClient(); $client->bodies = ['/summary' => json_encode(['disk' => [ ['mount' => '/boot', 'used' => 90, 'total' => 100, 'usedPercent' => 90], + ['mount' => '/home', 'used' => 20, 'total' => 100, 'usedPercent' => 20], ]])]; expect($client->summary()['diskPercent'])->toBeNull();