Files
coolify/app/Traits/HasMetrics.php
Andras Bacsai 7162185400 fix(metrics): convert container memory samples from bytes to MB
Sentinel stores container used memory in bytes while application and
database charts label the series as megabytes. Convert those samples
before they reach the frontend so the graph is not inflated by ~1024x.
2026-08-13 10:58:28 +02:00

97 lines
3.0 KiB
PHP

<?php
namespace App\Traits;
use App\Models\Server;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Log;
trait HasMetrics
{
public function getCpuMetrics(int $mins = 5): ?array
{
return $this->getMetrics('cpu', $mins, 'percent');
}
public function getMemoryMetrics(int $mins = 5): ?array
{
if ($this->isServerMetrics()) {
return $this->getMetrics('memory', $mins, 'usedPercent');
}
$metrics = $this->getMetrics('memory', $mins, 'used');
if ($metrics === null) {
return null;
}
return convertContainerMemoryBytesToMegabytes($metrics);
}
private function getMetrics(string $type, int $mins, string $valueField): ?array
{
$server = $this->getMetricsServer();
if (! $server->isMetricsEnabled()) {
return null;
}
$from = now()->subMinutes($mins)->toIso8601ZuluString();
$endpoint = $this->getMetricsEndpoint($type, $from);
$previousToken = null;
try {
$previousToken = $server->settings->sentinel_token;
} catch (DecryptException) {
// fall through to ensureValidSentinelToken which will regenerate
}
$token = $server->settings->ensureValidSentinelToken();
if ($token !== $previousToken) {
Log::warning('Regenerated sentinel token during metrics read; sentinel container restart required', ['server_id' => $server->id]);
}
$response = instant_remote_process(
["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$token}\" {$endpoint}'"],
$server,
false
);
if (str($response)->contains('error')) {
$error = json_decode($response, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
if ($error === 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
$metrics = collect(json_decode($response, true))->map(function ($metric) use ($valueField) {
return [(int) $metric['time'], (float) ($metric[$valueField] ?? 0.0)];
})->toArray();
if ($mins > 60 && count($metrics) > 1000) {
$metrics = downsampleLTTB($metrics, 1000);
}
return $metrics;
}
private function isServerMetrics(): bool
{
return $this instanceof Server;
}
private function getMetricsServer(): Server
{
return $this->isServerMetrics() ? $this : $this->destination->server;
}
private function getMetricsEndpoint(string $type, string $from): string
{
$base = 'http://localhost:8888/api';
if ($this->isServerMetrics()) {
return "{$base}/{$type}/history?from={$from}";
}
return "{$base}/container/{$this->uuid}/{$type}/history?from={$from}";
}
}