mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed03a31b3a
commit
6254f44f16
5 files changed
+47
-13
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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<int, array<string, mixed>> $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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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'] = [
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user