feat: container metrics

This commit is contained in:
Andras Bacsai 2024-06-20 13:17:06 +02:00
parent 439bee1203
commit c81ad5cd03
27 changed files with 704 additions and 39 deletions

View File

@ -21,6 +21,6 @@ public function handle(Server $server, $version = 'latest', bool $restart = fals
"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", "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', 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs',
'chmod -R 700 /data/coolify/metrics /data/coolify/logs', 'chmod -R 700 /data/coolify/metrics /data/coolify/logs',
], $server, false); ], $server, true);
} }
} }

View File

@ -61,7 +61,7 @@ private function pull_images($schedule)
{ {
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) { foreach ($servers as $server) {
if ($server->isMetricsEnabled()) { if ($server->isSentinelEnabled()) {
$schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer();
} }
$schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer();

View File

@ -22,7 +22,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?int $usageBefore = null; public ?int $usageBefore = null;
public function __construct(public Server $server) {} public function __construct(public Server $server)
{
}
public function handle(): void public function handle(): void
{ {

View File

@ -28,7 +28,9 @@ public function uniqueId(): string
return $this->server->uuid; return $this->server->uuid;
} }
public function __construct(public Server $server) {} public function __construct(public Server $server)
{
}
public function handle(): void public function handle(): void
{ {
@ -50,7 +52,7 @@ public function handle(): void
} }
ray('Sentinel image is up to date'); ray('Sentinel image is up to date');
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
throw $e; throw $e;
} }

View File

@ -25,7 +25,9 @@ public function backoff(): int
return isDev() ? 1 : 3; return isDev() ? 1 : 3;
} }
public function __construct(public Server $server) {} public function __construct(public Server $server)
{
}
public function middleware(): array public function middleware(): array
{ {
@ -46,7 +48,7 @@ public function handle()
if ($this->server->isFunctional()) { if ($this->server->isFunctional()) {
$this->cleanup(notify: false); $this->cleanup(notify: false);
$this->remove_unnecessary_coolify_yaml(); $this->remove_unnecessary_coolify_yaml();
if ($this->server->isMetricsEnabled()) { if ($this->server->isSentinelEnabled()) {
$this->server->checkSentinel(); $this->server->checkSentinel();
} }
} }

View File

@ -0,0 +1,64 @@
<?php
namespace App\Livewire\Project\Shared;
use Livewire\Component;
class Metrics extends Component
{
public $resource;
public $chartId = 'container-cpu';
public $data;
public $categories;
public int $interval = 5;
public bool $poll = true;
public function pollData()
{
if ($this->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');
}
}

View File

@ -44,6 +44,7 @@ class Form extends Component
'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1',
'server.settings.metrics_history_days' => 'required|integer|min:1', 'server.settings.metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url', 'wildcard_domain' => 'nullable|url',
'server.settings.is_server_api_enabled' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@ -63,7 +64,7 @@ class Form extends Component
'server.settings.metrics_token' => 'Metrics Token', 'server.settings.metrics_token' => 'Metrics Token',
'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.metrics_history_days' => 'Metrics History', 'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API',
]; ];
public function mount() public function mount()
@ -85,6 +86,18 @@ public function updatedServerSettingsIsBuildServer()
$this->dispatch('proxyStatusUpdated'); $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() public function instantSave()
{ {
try { try {
@ -94,12 +107,22 @@ public function instantSave()
$this->server->save(); $this->server->save();
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow'); $this->dispatch('refreshServerShow');
if ($this->server->isMetricsEnabled()) { if ($this->server->isSentinelEnabled()) {
PullSentinelImageJob::dispatchSync($this->server); 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 { } else {
ray('Sentinel is not enabled');
StopSentinel::dispatch($this->server); StopSentinel::dispatch($this->server);
} }
// $this->checkPortForServerApi();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@ -1167,4 +1167,33 @@ public function generate_preview_fqdn(int $pull_request_id)
return $preview; 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();
}
}
} }

View File

@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Stringable; use Illuminate\Support\Stringable;
@ -460,15 +461,44 @@ public function forceDisableServer()
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());
} }
public function isSentinelEnabled()
{
return $this->isMetricsEnabled() || $this->isServerApiEnabled();
}
public function isMetricsEnabled() public function isMetricsEnabled()
{ {
return $this->settings->is_metrics_enabled; 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() public function checkSentinel()
{ {
ray("Checking sentinel on server: {$this->name}"); 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 = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true); $sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited'); $status = data_get($sentinel_found, '0.State.Status', 'exited');
@ -497,10 +527,10 @@ public function getCpuMetrics(int $mins = 5)
$cpu = str($cpu)->explode("\n")->skip(1)->all(); $cpu = str($cpu)->explode("\n")->skip(1)->all();
$parsedCollection = collect($cpu)->flatMap(function ($item) { $parsedCollection = collect($cpu)->flatMap(function ($item) {
return collect(explode("\n", trim($item)))->map(function ($line) { return collect(explode("\n", trim($item)))->map(function ($line) {
[$time, $value] = explode(',', trim($line)); [$time, $cpu_usage_percent] = explode(',', trim($line));
$value = number_format($value, 0); $cpu_usage_percent = number_format($cpu_usage_percent, 0);
return [(int) $time, (float) $value]; return [(int) $time, (float) $cpu_usage_percent];
}); });
}); });

View File

@ -226,4 +226,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -226,4 +226,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -226,4 +226,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -226,4 +226,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -246,4 +246,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -227,4 +227,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -227,4 +227,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -222,4 +222,33 @@ public function scheduledBackups()
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); 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();
}
}
} }

View File

@ -161,9 +161,6 @@ function get_route_parameters(): array
function get_latest_sentinel_version(): string function get_latest_sentinel_version(): string
{ {
if (isDev()) {
return '0.0.8';
}
try { try {
$response = Http::get('https://cdn.coollabs.io/sentinel/versions.json'); $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json');
$versions = $response->json(); $versions = $response->json();

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -60,6 +60,20 @@
document.documentElement.classList.remove('dark') 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 @auth
window.Pusher = Pusher; window.Pusher = Pusher;
window.Echo = new Echo({ window.Echo = new Echo({

View File

@ -1,5 +1,5 @@
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"> <div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<h3>CPU</h3> <h3>CPU (%)</h3>
<x-forms.select label="Interval" wire:change="setInterval" id="interval"> <x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option> <option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option> <option value="10">10 minutes (live)</option>

View File

@ -1,5 +1,5 @@
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"> <div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<h3>Memory</h3> <h3>Memory (MB)</h3>
<x-forms.select label="Interval" wire:change="setInterval" id="interval"> <x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option> <option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option> <option value="10">10 minutes (live)</option>

View File

@ -74,6 +74,9 @@
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations href="#">Resource Operations
</a> </a>
<a class="menu-item" :class="activeTab === 'metrics' && 'menu-item-active'"
@click.prevent="activeTab = 'metrics'; window.location.hash = 'metrics'" href="#">Metrics
</a>
<a class="menu-item" :class="activeTab === 'tags' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'tags' && 'menu-item-active'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags @click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a> </a>
@ -126,6 +129,9 @@
<div x-cloak x-show="activeTab === 'resource-operations'"> <div x-cloak x-show="activeTab === 'resource-operations'">
<livewire:project.shared.resource-operations :resource="$application" /> <livewire:project.shared.resource-operations :resource="$application" />
</div> </div>
<div x-cloak x-show="activeTab === 'metrics'">
<livewire:project.shared.metrics :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'tags'"> <div x-cloak x-show="activeTab === 'tags'">
<livewire:project.shared.tags :resource="$application" /> <livewire:project.shared.tags :resource="$application" />
</div> </div>

View File

@ -42,6 +42,9 @@
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations href="#">Resource Operations
</a> </a>
<a class="menu-item" :class="activeTab === 'metrics' && 'menu-item-active'"
@click.prevent="activeTab = 'metrics'; window.location.hash = 'metrics'" href="#">Metrics
</a>
<a class="menu-item" :class="activeTab === 'tags' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'tags' && 'menu-item-active'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags @click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a> </a>
@ -92,6 +95,9 @@
<div x-cloak x-show="activeTab === 'resource-operations'"> <div x-cloak x-show="activeTab === 'resource-operations'">
<livewire:project.shared.resource-operations :resource="$database" /> <livewire:project.shared.resource-operations :resource="$database" />
</div> </div>
<div x-cloak x-show="activeTab === 'metrics'">
<livewire:project.shared.metrics :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'tags'"> <div x-cloak x-show="activeTab === 'tags'">
<livewire:project.shared.tags :resource="$database" /> <livewire:project.shared.tags :resource="$database" />
</div> </div>

View File

@ -0,0 +1,243 @@
<div>
<div class="flex items-center gap-2 ">
<h2>Metrics</h2>
</div>
<div class="pb-4">Basic metrics for your container.</div>
@if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose')
<div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div>
@else
@if (!str($resource->status)->contains('running'))
<div class="alert alert-warning">Metrics are only available when the application is running!</div>
@else
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="720">12 hours</option>
<option value="10080">1 week</option>
<option value="43200">30 days</option>
</x-forms.select>
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"
class="pt-5">
<h4>CPU (%)</h4>
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
<script>
checkTheme();
const optionsServerCpu = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}-cpu',
type: 'area',
toolbar: {
show: false,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
},
series: [{
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: false,
},
legend: {
show: false
}
}
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
serverCpuChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
checkTheme();
serverCpuChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
<h3>Memory (MB)</h3>
<div wire:ignore id="{!! $chartId !!}-memory"></div>
<script>
checkTheme();
const optionsServerMemory = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}-memory',
type: 'area',
toolbar: {
show: false,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
series: [{
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: false,
},
legend: {
show: false
}
}
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
optionsServerMemory);
serverMemoryChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
checkTheme();
serverMemoryChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
min: 0,
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
</div>
@endif
@endif
</div>

View File

@ -145,13 +145,16 @@ class="w-full mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-1
helper="You can define the maximum duration for a deployment to run before timing it out." /> helper="You can define the maximum duration for a deployment to run before timing it out." />
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="py-4">Metrics</h3> <h3 class="py-4">Sentinel</h3>
@if ($server->isMetricsEnabled()) @if ($server->isSentinelEnabled())
<x-forms.button wire:click='restartSentinel'>Restart Collector</x-forms.button> <x-forms.button wire:click='restartSentinel'>Restart</x-forms.button>
@endif @endif
</div> </div>
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_metrics_enabled" label="Enable metrics" /> <x-forms.checkbox instantSave id="server.settings.is_metrics_enabled" label="Enable Metrics" />
{{-- <x-forms.checkbox instantSave id="server.settings.is_server_api_enabled" label="Enable Server API"
helper="You need to open port 12172 on your firewall. This API will be used to gather data from your server, which makes Coolify a lot faster than relying on SSH connections." />
<x-forms.button wire:click='checkPortForServerApi'>Check Port for Server API</x-forms.button> --}}
</div> </div>
<div class="pt-4"> <div class="pt-4">
<div class="flex flex-wrap gap-2 sm:flex-nowrap"> <div class="flex flex-wrap gap-2 sm:flex-nowrap">

View File

@ -7,22 +7,6 @@
<livewire:server.delete :server="$server" /> <livewire:server.delete :server="$server" />
@if ($server->isFunctional() && $server->isMetricsEnabled()) @if ($server->isFunctional() && $server->isMetricsEnabled())
<div class="pt-10"> <div class="pt-10">
<script>
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'
}
}
</script>
<livewire:charts.server-cpu :server="$server" /> <livewire:charts.server-cpu :server="$server" />
<livewire:charts.server-memory :server="$server" /> <livewire:charts.server-memory :server="$server" />
</div> </div>