diff --git a/app/Models/Application.php b/app/Models/Application.php index 40e41c2a7..2eab8df20 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -6,6 +6,7 @@ use App\Services\ConfigurationGenerator; use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasConfiguration; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -111,7 +112,7 @@ class Application extends BaseModel { - use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; private static $parserVersion = '5'; @@ -1977,54 +1978,6 @@ public static function getDomainsByUuid(string $uuid): array return []; } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - } - public function getLimits(): array { return [ diff --git a/app/Models/Server.php b/app/Models/Server.php index fd1ce3e69..696b3529b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -16,6 +16,7 @@ use App\Notifications\Server\Unreachable; use App\Services\ConfigurationRepository; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -103,7 +104,7 @@ class Server extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -667,141 +668,6 @@ public function checkSentinel() CheckAndStartSentinelJob::dispatch($this); } - public function getCpuMetrics(int $mins = 5) - { - if ($this->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); - if (str($cpu)->contains('error')) { - $error = json_decode($cpu, 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); - } - $cpu = json_decode($cpu, true); - - $metrics = collect($cpu)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - })->toArray(); - - // Downsample for intervals > 60 minutes to prevent browser freeze - if ($mins > 60 && count($metrics) > 1000) { - $metrics = $this->downsampleLTTB($metrics, 1000); - } - - return collect($metrics); - } - } - - public function getMemoryMetrics(int $mins = 5) - { - if ($this->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); - if (str($memory)->contains('error')) { - $error = json_decode($memory, 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); - } - $memory = json_decode($memory, true); - $metrics = collect($memory)->map(function ($metric) { - $usedPercent = $metric['usedPercent'] ?? 0.0; - - return [(int) $metric['time'], (float) $usedPercent]; - })->toArray(); - - // Downsample for intervals > 60 minutes to prevent browser freeze - if ($mins > 60 && count($metrics) > 1000) { - $metrics = $this->downsampleLTTB($metrics, 1000); - } - - return collect($metrics); - } - } - - /** - * Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm. - * This preserves the visual shape of the data better than simple averaging. - * - * @param array $data Array of [timestamp, value] pairs - * @param int $threshold Target number of points - * @return array Downsampled data - */ - private function downsampleLTTB(array $data, int $threshold): array - { - $dataLength = count($data); - - // Return unchanged if threshold >= data length, or if threshold <= 2 - // (threshold <= 2 would cause division by zero in bucket calculation) - if ($threshold >= $dataLength || $threshold <= 2) { - return $data; - } - - $sampled = []; - $sampled[] = $data[0]; // Always keep first point - - $bucketSize = ($dataLength - 2) / ($threshold - 2); - - $a = 0; // Index of previous selected point - - for ($i = 0; $i < $threshold - 2; $i++) { - // Calculate bucket range - $bucketStart = (int) floor(($i + 1) * $bucketSize) + 1; - $bucketEnd = (int) floor(($i + 2) * $bucketSize) + 1; - $bucketEnd = min($bucketEnd, $dataLength - 1); - - // Calculate average point for next bucket (used as reference) - $nextBucketStart = (int) floor(($i + 2) * $bucketSize) + 1; - $nextBucketEnd = (int) floor(($i + 3) * $bucketSize) + 1; - $nextBucketEnd = min($nextBucketEnd, $dataLength - 1); - - $avgX = 0; - $avgY = 0; - $nextBucketCount = $nextBucketEnd - $nextBucketStart + 1; - - if ($nextBucketCount > 0) { - for ($j = $nextBucketStart; $j <= $nextBucketEnd; $j++) { - $avgX += $data[$j][0]; - $avgY += $data[$j][1]; - } - $avgX /= $nextBucketCount; - $avgY /= $nextBucketCount; - } - - // Find point in current bucket with largest triangle area - $maxArea = -1; - $maxAreaIndex = $bucketStart; - - $pointAX = $data[$a][0]; - $pointAY = $data[$a][1]; - - for ($j = $bucketStart; $j <= $bucketEnd; $j++) { - // Triangle area calculation - $area = abs( - ($pointAX - $avgX) * ($data[$j][1] - $pointAY) - - ($pointAX - $data[$j][0]) * ($avgY - $pointAY) - ) * 0.5; - - if ($area > $maxArea) { - $maxArea = $area; - $maxAreaIndex = $j; - } - } - - $sampled[] = $data[$maxAreaIndex]; - $a = $maxAreaIndex; - } - - $sampled[] = $data[$dataLength - 1]; // Always keep last point - - return $sampled; - } - public function getDiskUsage(): ?string { return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index a76d55abb..ee8a27162 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandaloneClickhouse extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -320,50 +321,6 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - public function isBackupSolutionAvailable() { return false; diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index f5337b1d5..051a95a80 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandaloneDragonfly extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -316,50 +317,6 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - public function isBackupSolutionAvailable() { return false; diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index ab24cae2c..6cd51edce 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandaloneKeydb extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -316,50 +317,6 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - public function isBackupSolutionAvailable() { return false; diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index e48cfc1e6..f312473be 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -11,7 +12,7 @@ class StandaloneMariadb extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -319,50 +320,6 @@ public function sslCertificates() return $this->morphMany(SslCertificate::class, 'resource'); } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - public function isBackupSolutionAvailable() { return true; diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 9e271b19a..d2d8fef12 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandaloneMongodb extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -341,50 +342,6 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - public function isBackupSolutionAvailable() { return true; diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 377765697..c27b2a679 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandaloneMysql extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -320,50 +321,6 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - public function isBackupSolutionAvailable() { return true; diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index d9993426a..73b00742a 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandalonePostgresql extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -337,51 +338,4 @@ public function isBackupSolutionAvailable() { return true; } - - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [ - (int) $metric['time'], - (float) ($metric['percent'] ?? 0.0), - ]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 684bcaeb7..46e8da434 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\ClearsGlobalSearchCache; +use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandaloneRedis extends BaseModel { - use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -332,50 +333,6 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getCpuMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['percent']]; - }); - - return $parsedCollection->toArray(); - } - - public function getMemoryMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, 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 = json_decode($metrics, true); - $parsedCollection = collect($metrics)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['used']]; - }); - - return $parsedCollection->toArray(); - } - public function isBackupSolutionAvailable() { return false; diff --git a/app/Traits/HasMetrics.php b/app/Traits/HasMetrics.php new file mode 100644 index 000000000..667d58441 --- /dev/null +++ b/app/Traits/HasMetrics.php @@ -0,0 +1,74 @@ +getMetrics('cpu', $mins, 'percent'); + } + + public function getMemoryMetrics(int $mins = 5): ?array + { + $field = $this->isServerMetrics() ? 'usedPercent' : 'used'; + + return $this->getMetrics('memory', $mins, $field); + } + + 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); + + $response = instant_remote_process( + ["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_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 \App\Models\Server; + } + + private function getMetricsServer(): \App\Models\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}"; + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 9fc1e6f1c..e26b7b6c7 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3416,3 +3416,81 @@ function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $compon return true; } + +/** + * Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm. + * This preserves the visual shape of the data better than simple averaging. + * + * @param array $data Array of [timestamp, value] pairs + * @param int $threshold Target number of points + * @return array Downsampled data + */ +function downsampleLTTB(array $data, int $threshold): array +{ + $dataLength = count($data); + + // Return unchanged if threshold >= data length, or if threshold <= 2 + // (threshold <= 2 would cause division by zero in bucket calculation) + if ($threshold >= $dataLength || $threshold <= 2) { + return $data; + } + + $sampled = []; + $sampled[] = $data[0]; // Always keep first point + + $bucketSize = ($dataLength - 2) / ($threshold - 2); + + $a = 0; // Index of previous selected point + + for ($i = 0; $i < $threshold - 2; $i++) { + // Calculate bucket range + $bucketStart = (int) floor(($i + 1) * $bucketSize) + 1; + $bucketEnd = (int) floor(($i + 2) * $bucketSize) + 1; + $bucketEnd = min($bucketEnd, $dataLength - 1); + + // Calculate average point for next bucket (used as reference) + $nextBucketStart = (int) floor(($i + 2) * $bucketSize) + 1; + $nextBucketEnd = (int) floor(($i + 3) * $bucketSize) + 1; + $nextBucketEnd = min($nextBucketEnd, $dataLength - 1); + + $avgX = 0; + $avgY = 0; + $nextBucketCount = $nextBucketEnd - $nextBucketStart + 1; + + if ($nextBucketCount > 0) { + for ($j = $nextBucketStart; $j <= $nextBucketEnd; $j++) { + $avgX += $data[$j][0]; + $avgY += $data[$j][1]; + } + $avgX /= $nextBucketCount; + $avgY /= $nextBucketCount; + } + + // Find point in current bucket with largest triangle area + $maxArea = -1; + $maxAreaIndex = $bucketStart; + + $pointAX = $data[$a][0]; + $pointAY = $data[$a][1]; + + for ($j = $bucketStart; $j <= $bucketEnd; $j++) { + // Triangle area calculation + $area = abs( + ($pointAX - $avgX) * ($data[$j][1] - $pointAY) - + ($pointAX - $data[$j][0]) * ($avgY - $pointAY) + ) * 0.5; + + if ($area > $maxArea) { + $maxArea = $area; + $maxAreaIndex = $j; + } + } + + $sampled[] = $data[$maxAreaIndex]; + $a = $maxAreaIndex; + } + + $sampled[] = $data[$dataLength - 1]; // Always keep last point + + return $sampled; +} diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index 57e09af6d..728f7fef4 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -29,230 +29,179 @@ class="pt-5">

Memory Usage

diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index 876a4d23d..51953ab9a 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -23,145 +23,16 @@
- -
-

Memory Usage

-
- - + +
+

Memory Usage

+
+ +
diff --git a/tests/Unit/ClickhouseOfficialImageMigrationTest.php b/tests/Unit/ClickhouseOfficialImageMigrationTest.php index 9edf5d09c..054dc4423 100644 --- a/tests/Unit/ClickhouseOfficialImageMigrationTest.php +++ b/tests/Unit/ClickhouseOfficialImageMigrationTest.php @@ -3,7 +3,7 @@ use App\Models\StandaloneClickhouse; test('clickhouse uses clickhouse_db field in internal connection string', function () { - $clickhouse = new StandaloneClickhouse(); + $clickhouse = new StandaloneClickhouse; $clickhouse->clickhouse_admin_user = 'testuser'; $clickhouse->clickhouse_admin_password = 'testpass'; $clickhouse->clickhouse_db = 'mydb'; @@ -18,7 +18,7 @@ }); test('clickhouse defaults to default database when clickhouse_db is null', function () { - $clickhouse = new StandaloneClickhouse(); + $clickhouse = new StandaloneClickhouse; $clickhouse->clickhouse_admin_user = 'testuser'; $clickhouse->clickhouse_admin_password = 'testpass'; $clickhouse->clickhouse_db = null; @@ -30,7 +30,7 @@ }); test('clickhouse external url uses correct database', function () { - $clickhouse = new StandaloneClickhouse(); + $clickhouse = new StandaloneClickhouse; $clickhouse->clickhouse_admin_user = 'admin'; $clickhouse->clickhouse_admin_password = 'secret'; $clickhouse->clickhouse_db = 'production'; @@ -38,12 +38,19 @@ $clickhouse->is_public = true; $clickhouse->public_port = 8123; - $clickhouse->destination = new class { + $clickhouse->destination = new class + { public $server; - public function __construct() { - $this->server = new class { - public function __get($name) { - if ($name === 'getIp') return '1.2.3.4'; + + public function __construct() + { + $this->server = new class + { + public function __get($name) + { + if ($name === 'getIp') { + return '1.2.3.4'; + } } }; } diff --git a/tests/Unit/ServerMetricsDownsamplingTest.php b/tests/Unit/MetricsDownsamplingTest.php similarity index 78% rename from tests/Unit/ServerMetricsDownsamplingTest.php rename to tests/Unit/MetricsDownsamplingTest.php index 819eecc4c..67685896f 100644 --- a/tests/Unit/ServerMetricsDownsamplingTest.php +++ b/tests/Unit/MetricsDownsamplingTest.php @@ -1,19 +1,9 @@ getMethod('downsampleLTTB'); - - return $method->invoke($server, $data, $threshold); -} - it('returns data unchanged when below threshold', function () { $data = [ [1000, 10.5], @@ -21,7 +11,7 @@ function callDownsampleLTTB(array $data, int $threshold): array [3000, 15.7], ]; - $result = callDownsampleLTTB($data, 1000); + $result = downsampleLTTB($data, 1000); expect($result)->toBe($data); }); @@ -35,10 +25,10 @@ function callDownsampleLTTB(array $data, int $threshold): array [5000, 12.0], ]; - $result = callDownsampleLTTB($data, 2); + $result = downsampleLTTB($data, 2); expect($result)->toBe($data); - $result = callDownsampleLTTB($data, 1); + $result = downsampleLTTB($data, 1); expect($result)->toBe($data); }); @@ -52,7 +42,7 @@ function callDownsampleLTTB(array $data, int $threshold): array $data[] = [$i * 1000, mt_rand(0, 100) / 10]; } - $result = callDownsampleLTTB($data, 10); + $result = downsampleLTTB($data, 10); expect(count($result))->toBe(10); }); @@ -63,7 +53,7 @@ function callDownsampleLTTB(array $data, int $threshold): array $data[] = [$i * 1000, $i * 1.5]; } - $result = callDownsampleLTTB($data, 20); + $result = downsampleLTTB($data, 20); // First point should be preserved expect($result[0])->toBe($data[0]); @@ -78,7 +68,7 @@ function callDownsampleLTTB(array $data, int $threshold): array $data[] = [$i * 60000, sin($i / 10) * 50 + 50]; // Sine wave pattern } - $result = callDownsampleLTTB($data, 50); + $result = downsampleLTTB($data, 50); // Verify all timestamps are in non-decreasing order $previousTimestamp = -1; @@ -100,7 +90,7 @@ function callDownsampleLTTB(array $data, int $threshold): array } $startTime = microtime(true); - $result = callDownsampleLTTB($data, 1000); + $result = downsampleLTTB($data, 1000); $executionTime = microtime(true) - $startTime; expect(count($result))->toBe(1000); @@ -121,7 +111,7 @@ function callDownsampleLTTB(array $data, int $threshold): array $data[] = [$i * 1000, $value]; } - $result = callDownsampleLTTB($data, 20); + $result = downsampleLTTB($data, 20); // The peak (100) and valley (0) should be preserved due to LTTB algorithm $values = array_column($result, 1);