From c81ad5cd03a86be77b4a0702bb1ec28d4acedfe2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 20 Jun 2024 13:17:06 +0200 Subject: [PATCH] feat: container metrics --- app/Actions/Server/StartSentinel.php | 2 +- app/Console/Kernel.php | 2 +- app/Jobs/DockerCleanupJob.php | 4 +- app/Jobs/PullSentinelImageJob.php | 6 +- app/Jobs/ServerStatusJob.php | 6 +- app/Livewire/Project/Shared/Metrics.php | 64 +++++ app/Livewire/Server/Form.php | 29 ++- app/Models/Application.php | 29 +++ app/Models/Server.php | 38 ++- app/Models/StandaloneClickhouse.php | 29 +++ app/Models/StandaloneDragonfly.php | 29 +++ app/Models/StandaloneKeydb.php | 29 +++ app/Models/StandaloneMariadb.php | 29 +++ app/Models/StandaloneMongodb.php | 29 +++ app/Models/StandaloneMysql.php | 29 +++ app/Models/StandalonePostgresql.php | 29 +++ app/Models/StandaloneRedis.php | 29 +++ bootstrap/helpers/shared.php | 3 - ...4_06_20_102551_add_server_api_sentinel.php | 28 ++ resources/views/layouts/base.blade.php | 14 + .../livewire/charts/server-cpu.blade.php | 2 +- .../livewire/charts/server-memory.blade.php | 2 +- .../application/configuration.blade.php | 6 + .../project/database/configuration.blade.php | 6 + .../livewire/project/shared/metrics.blade.php | 243 ++++++++++++++++++ .../views/livewire/server/form.blade.php | 11 +- .../views/livewire/server/show.blade.php | 16 -- 27 files changed, 704 insertions(+), 39 deletions(-) create mode 100644 app/Livewire/Project/Shared/Metrics.php create mode 100644 database/migrations/2024_06_20_102551_add_server_api_sentinel.php create mode 100644 resources/views/livewire/project/shared/metrics.blade.php diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index a2afea3bb..b79bc8f67 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -21,6 +21,6 @@ class StartSentinel "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', - ], $server, false); + ], $server, true); } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c2f679699..f529f63b9 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -61,7 +61,7 @@ class Kernel extends ConsoleKernel { $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { - if ($server->isMetricsEnabled()) { + if ($server->isSentinelEnabled()) { $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); } $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index e637fb6d4..e3ac193dc 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -22,7 +22,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?int $usageBefore = null; - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + } public function handle(): void { diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php index 30b36d99f..e6252df0f 100644 --- a/app/Jobs/PullSentinelImageJob.php +++ b/app/Jobs/PullSentinelImageJob.php @@ -28,7 +28,9 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue return $this->server->uuid; } - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + } public function handle(): void { @@ -50,7 +52,7 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue } ray('Sentinel image is up to date'); } catch (\Throwable $e) { - send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); + // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index 938f3fe40..1bba912c2 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -25,7 +25,9 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue return isDev() ? 1 : 3; } - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + } public function middleware(): array { @@ -46,7 +48,7 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue if ($this->server->isFunctional()) { $this->cleanup(notify: false); $this->remove_unnecessary_coolify_yaml(); - if ($this->server->isMetricsEnabled()) { + if ($this->server->isSentinelEnabled()) { $this->server->checkSentinel(); } } diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php new file mode 100644 index 000000000..d9d7dd3ef --- /dev/null +++ b/app/Livewire/Project/Shared/Metrics.php @@ -0,0 +1,64 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $metrics = $this->resource->getMetrics($this->interval); + $cpuMetrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $memoryMetrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[2]]; + }); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ + 'seriesData' => $cpuMetrics, + ]); + $this->dispatch("refreshChartData-{$this->chartId}-memory", [ + 'seriesData' => $memoryMetrics, + ]); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } + + public function render() + { + return view('livewire.project.shared.metrics'); + } +} diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 87c0d09d1..5616123a5 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -44,6 +44,7 @@ class Form extends Component 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', 'server.settings.metrics_history_days' => 'required|integer|min:1', 'wildcard_domain' => 'nullable|url', + 'server.settings.is_server_api_enabled' => 'required|boolean', ]; protected $validationAttributes = [ @@ -63,7 +64,7 @@ class Form extends Component 'server.settings.metrics_token' => 'Metrics Token', 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', 'server.settings.metrics_history_days' => 'Metrics History', - + 'server.settings.is_server_api_enabled' => 'Server API', ]; public function mount() @@ -85,6 +86,18 @@ class Form extends Component $this->dispatch('proxyStatusUpdated'); } + public function checkPortForServerApi() + { + try { + if ($this->server->settings->is_server_api_enabled === true) { + $this->server->checkServerApi(); + $this->dispatch('success', 'Server API is reachable.'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function instantSave() { try { @@ -94,12 +107,22 @@ class Form extends Component $this->server->save(); $this->dispatch('success', 'Server updated.'); $this->dispatch('refreshServerShow'); - if ($this->server->isMetricsEnabled()) { + if ($this->server->isSentinelEnabled()) { PullSentinelImageJob::dispatchSync($this->server); - $this->dispatch('reloadWindow'); + ray('Sentinel is enabled'); + if ($this->server->settings->isDirty('is_metrics_enabled')) { + $this->dispatch('reloadWindow'); + } + if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) { + ray('Starting sentinel'); + + } } else { + ray('Sentinel is not enabled'); StopSentinel::dispatch($this->server); } + // $this->checkPortForServerApi(); + } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Models/Application.php b/app/Models/Application.php index f2a7ce51c..c76a42d71 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1167,4 +1167,33 @@ class Application extends BaseModel return $preview; } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = generateApplicationContainerName($this); + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 3d40042bb..7a99940fd 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Illuminate\Support\Stringable; @@ -460,15 +461,44 @@ $schema://$host { Storage::disk('ssh-mux')->delete($this->muxFilename()); } + public function isSentinelEnabled() + { + return $this->isMetricsEnabled() || $this->isServerApiEnabled(); + } + public function isMetricsEnabled() { return $this->settings->is_metrics_enabled; } + public function isServerApiEnabled() + { + return $this->settings->is_server_api_enabled; + } + + public function checkServerApi() + { + if ($this->isServerApiEnabled()) { + $server_ip = $this->ip; + if (isDev()) { + if ($this->id === 0) { + $server_ip = 'localhost'; + } + } + $command = "curl -s http://{$server_ip}:12172/api/health"; + $process = Process::timeout(5)->run($command); + if ($process->failed()) { + ray($process->exitCode(), $process->output(), $process->errorOutput()); + throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); + } + + } + } + public function checkSentinel() { ray("Checking sentinel on server: {$this->name}"); - if ($this->isMetricsEnabled()) { + if ($this->isSentinelEnabled()) { $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); @@ -497,10 +527,10 @@ $schema://$host { $cpu = str($cpu)->explode("\n")->skip(1)->all(); $parsedCollection = collect($cpu)->flatMap(function ($item) { return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $value] = explode(',', trim($line)); - $value = number_format($value, 0); + [$time, $cpu_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 0); - return [(int) $time, (float) $value]; + return [(int) $time, (float) $cpu_usage_percent]; }); }); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index c5e252c34..e968db18d 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -226,4 +226,33 @@ class StandaloneClickhouse extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 8c739d984..c6718acfe 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -226,4 +226,33 @@ class StandaloneDragonfly extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 5216681c9..142f960aa 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -226,4 +226,33 @@ class StandaloneKeydb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 33fd2cbc2..7e6d2e0d1 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -226,4 +226,33 @@ class StandaloneMariadb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 0cc52b3f7..df895bb34 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -246,4 +246,33 @@ class StandaloneMongodb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 174736f77..bd160f877 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -227,4 +227,33 @@ class StandaloneMysql extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index a5bf4dc2a..114d376e8 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -227,4 +227,33 @@ class StandalonePostgresql extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index ed379750e..022cd8d09 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -222,4 +222,33 @@ class StandaloneRedis extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(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->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/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 = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2feda420f..b9dc685b7 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -161,9 +161,6 @@ function get_route_parameters(): array function get_latest_sentinel_version(): string { - if (isDev()) { - return '0.0.8'; - } try { $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json'); $versions = $response->json(); diff --git a/database/migrations/2024_06_20_102551_add_server_api_sentinel.php b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php new file mode 100644 index 000000000..b840195af --- /dev/null +++ b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php @@ -0,0 +1,28 @@ +boolean('is_server_api_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_server_api_enabled'); + }); + } +}; diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index c1851e2f9..1b2628db1 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -60,6 +60,20 @@ document.documentElement.classList.remove('dark') } } + let theme = localStorage.theme + let baseColor = '#FCD452' + let textColor = '#ffffff' + + function checkTheme() { + theme = localStorage.theme + if (theme == 'dark') { + baseColor = '#FCD452' + textColor = '#ffffff' + } else { + baseColor = 'black' + textColor = '#000000' + } + } @auth window.Pusher = Pusher; window.Echo = new Echo({ diff --git a/resources/views/livewire/charts/server-cpu.blade.php b/resources/views/livewire/charts/server-cpu.blade.php index fe7131ade..3aab224f9 100644 --- a/resources/views/livewire/charts/server-cpu.blade.php +++ b/resources/views/livewire/charts/server-cpu.blade.php @@ -1,5 +1,5 @@
-

CPU

+

CPU (%)

diff --git a/resources/views/livewire/charts/server-memory.blade.php b/resources/views/livewire/charts/server-memory.blade.php index 737526b0d..851b9b975 100644 --- a/resources/views/livewire/charts/server-memory.blade.php +++ b/resources/views/livewire/charts/server-memory.blade.php @@ -1,5 +1,5 @@
-

Memory

+

Memory (MB)

diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index 4be672063..639776730 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -74,6 +74,9 @@ @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" href="#">Resource Operations + Metrics + Tags @@ -126,6 +129,9 @@
+
+ +
diff --git a/resources/views/livewire/project/database/configuration.blade.php b/resources/views/livewire/project/database/configuration.blade.php index 809a48c0f..aa5b8ec5e 100644 --- a/resources/views/livewire/project/database/configuration.blade.php +++ b/resources/views/livewire/project/database/configuration.blade.php @@ -42,6 +42,9 @@ @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" href="#">Resource Operations + Metrics + Tags @@ -92,6 +95,9 @@
+
+ +
diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php new file mode 100644 index 000000000..f27c71bf5 --- /dev/null +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -0,0 +1,243 @@ +
+
+

Metrics

+
+
Basic metrics for your container.
+ @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') +
Metrics are not available for Docker Compose applications yet!
+ @else + @if (!str($resource->status)->contains('running')) +
Metrics are only available when the application is running!
+ @else + + + + + + + + + +
+

CPU (%)

+
+ + + +

Memory (MB)

+
+ + +
+ @endif + @endif +
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 9f061ba54..938e47cbd 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -145,13 +145,16 @@ helper="You can define the maximum duration for a deployment to run before timing it out." />
-

Metrics

- @if ($server->isMetricsEnabled()) - Restart Collector +

Sentinel

+ @if ($server->isSentinelEnabled()) + Restart @endif
- + + {{-- + Check Port for Server API --}}
diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index d3a3bb8c6..0e8a37717 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -7,22 +7,6 @@ @if ($server->isFunctional() && $server->isMetricsEnabled())
-