mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-15 09:12:32 -04:00
Allow full, anonymized, or disabled IP recording per server through the UI and API, and pass the selected mode to Sentinel. Adapt traffic breakdowns to the effective IP mode, including views that aggregate multiple servers.
176 lines
7.4 KiB
PHP
176 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Actions\Server;
|
|
|
|
use App\Enums\TrafficIpMode;
|
|
use App\Events\SentinelRestarted;
|
|
use App\Models\Server;
|
|
use Illuminate\Contracts\Cache\LockTimeoutException;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Lorisleiva\Actions\Concerns\AsAction;
|
|
|
|
class StartSentinel
|
|
{
|
|
use AsAction;
|
|
|
|
/** Longer than one start, including the image pull. */
|
|
public const LOCK_SECONDS = 300;
|
|
|
|
/** Shorter than the 120 second timeout of CheckAndStartSentinelJob. */
|
|
public const LOCK_WAIT_SECONDS = 90;
|
|
|
|
public static function lockKey(Server $server): string
|
|
{
|
|
return 'sentinel-start:'.$server->uuid;
|
|
}
|
|
|
|
/**
|
|
* Sentinel and the proxy both mount this host path, and Sentinel reads the access log at the same path.
|
|
*/
|
|
public static function trafficLogDirectory(Server $server): string
|
|
{
|
|
return devHostDockerPath($server, rtrim($server->proxyPath(), '/'));
|
|
}
|
|
|
|
/**
|
|
* Sentinel reads the proxy access log only when analytics is on and a Traefik or Caddy proxy writes it.
|
|
* After a switch to a proxy without analytics support, Sentinel must not keep the old log mount.
|
|
*/
|
|
public static function collectsTraffic(Server $server): bool
|
|
{
|
|
return $server->isTrafficAnalyticsEnabled() && $server->hasTrafficAnalyticsProxy();
|
|
}
|
|
|
|
public static function sentinelTrafficEnvironment(Server $server): array
|
|
{
|
|
if (! self::collectsTraffic($server)) {
|
|
return [];
|
|
}
|
|
|
|
$logPath = self::trafficLogDirectory($server).'/access.log';
|
|
$settings = $server->settings;
|
|
$env = [
|
|
'TRAFFIC_ENABLED' => 'true',
|
|
'TRAFFIC_PROXY_TYPE' => 'auto',
|
|
'TRAFFIC_ACCESS_LOG_PATH' => $logPath,
|
|
'TRAFFIC_TOPN' => (string) ($settings->traffic_topn ?: 50),
|
|
'TRAFFIC_SAMPLE_THRESHOLD' => (string) ($settings->traffic_sample_threshold ?? 0),
|
|
'TRAFFIC_RETENTION_1H_DAYS' => (string) ($settings->traffic_retention_1h_days ?: 30),
|
|
'TRAFFIC_RETENTION_1D_DAYS' => (string) ($settings->traffic_retention_1d_days ?: 395),
|
|
'GEOIP_ENABLED' => $settings->is_geoip_enabled ? 'true' : 'false',
|
|
'GEOIP_REFRESH_DAYS' => (string) ($settings->geoip_refresh_days ?: 30),
|
|
'TRAFFIC_IP_MODE' => TrafficIpMode::forServer($server)->value,
|
|
];
|
|
$license = data_get($settings, 'geoip_maxmind_license_key');
|
|
if ($settings->is_geoip_enabled && filled($license)) {
|
|
$env['GEOIP_MAXMIND_LICENSE_KEY'] = $license;
|
|
}
|
|
|
|
return $env;
|
|
}
|
|
|
|
/**
|
|
* Sentinel opens the access log once at startup and never retries, so the file must exist
|
|
* before the container starts. `touch` keeps an existing log intact.
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
public static function trafficLogPreparationCommands(Server $server): array
|
|
{
|
|
if (! $server->isTrafficAnalyticsEnabled() || ! $server->supportsTrafficAnalytics()) {
|
|
return [];
|
|
}
|
|
|
|
$directory = self::trafficLogDirectory($server);
|
|
|
|
return [
|
|
'mkdir -p '.escapeshellarg($directory),
|
|
'touch '.escapeshellarg($directory.'/access.log'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @throws LockTimeoutException when another start for the server does not finish in time
|
|
*/
|
|
public function handle(Server $server, bool $restart = false, ?string $latestVersion = null, ?string $customImage = null): void
|
|
{
|
|
if ($server->isSwarm() || $server->isBuildServer()) {
|
|
return;
|
|
}
|
|
|
|
// Two starts at the same time both remove the container, and then the second `docker run`
|
|
// fails because the first one already uses the coolify-sentinel name.
|
|
Cache::lock(self::lockKey($server), self::LOCK_SECONDS)->block(
|
|
self::LOCK_WAIT_SECONDS,
|
|
fn () => $this->start($server, $restart, $latestVersion, $customImage),
|
|
);
|
|
}
|
|
|
|
private function start(Server $server, bool $restart, ?string $latestVersion, ?string $customImage): void
|
|
{
|
|
// A start that waited for the lock must use the settings saved in the meantime.
|
|
$server->refresh();
|
|
if ($restart) {
|
|
StopSentinel::run($server);
|
|
}
|
|
$version = $latestVersion ?? get_latest_sentinel_version();
|
|
$metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days');
|
|
$refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
|
|
$pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds');
|
|
$token = $server->settings->ensureValidSentinelToken();
|
|
$endpoint = $server->settings->ensureSentinelUrl();
|
|
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
|
$mountDir = devHostDockerPath($server, base_configuration_dir().'/sentinel');
|
|
$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version;
|
|
$environments = [
|
|
'TOKEN' => $token,
|
|
'DEBUG' => $debug ? 'true' : 'false',
|
|
'PUSH_ENDPOINT' => $endpoint,
|
|
'PUSH_INTERVAL_SECONDS' => $pushInterval,
|
|
'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
|
|
'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate,
|
|
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory,
|
|
];
|
|
$environments = array_merge($environments, self::sentinelTrafficEnvironment($server));
|
|
$labels = [
|
|
'coolify.managed' => 'true',
|
|
];
|
|
if (isDev()) {
|
|
// data_set($environments, 'DEBUG', 'true');
|
|
if ($customImage && ! empty($customImage)) {
|
|
$image = $customImage;
|
|
}
|
|
}
|
|
$dockerEnvironments = implode(' ', array_map(fn ($key, $value) => '-e '.escapeshellarg("$key=$value"), array_keys($environments), $environments));
|
|
$dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels));
|
|
$trafficLogDirectory = self::trafficLogDirectory($server);
|
|
$trafficMount = self::collectsTraffic($server)
|
|
? '-v '.escapeshellarg("{$trafficLogDirectory}:{$trafficLogDirectory}:ro").' '
|
|
: '';
|
|
$network = $server->isLocalhost() ? ' --network coolify' : '';
|
|
$dockerCommand = "docker run -d$network $dockerEnvironments --name coolify-sentinel --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db {$trafficMount}--pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-start-period 120s --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image";
|
|
|
|
$server->sentinelHeartbeat(isReset: true);
|
|
$server->forceFill(['sentinel_waiting_since' => now()])->save();
|
|
|
|
instant_remote_process([
|
|
'docker rm -f coolify-sentinel || true',
|
|
"mkdir -p $mountDir",
|
|
...self::trafficLogPreparationCommands($server),
|
|
$dockerCommand,
|
|
"chown -R 9999:root $mountDir",
|
|
"chmod -R 700 $mountDir",
|
|
], $server);
|
|
|
|
$server->settings->is_sentinel_enabled = true;
|
|
$server->settings->save();
|
|
$server->refresh();
|
|
if ($server->sentinel_waiting_since !== null) {
|
|
$server->forceFill(['sentinel_waiting_since' => now()])->save();
|
|
}
|
|
|
|
// Dispatch event to notify UI components
|
|
SentinelRestarted::dispatch($server, $version);
|
|
}
|
|
}
|