diff --git a/app/Jobs/ConnectProxyToNetworksJob.php b/app/Jobs/ConnectProxyToNetworksJob.php new file mode 100644 index 000000000..83c175978 --- /dev/null +++ b/app/Jobs/ConnectProxyToNetworksJob.php @@ -0,0 +1,55 @@ +server->uuid)) + ->expireAfter(60) + ->dontRelease(), + ]; + } + + public function __construct(public Server $server) {} + + public function handle() + { + if (! $this->server->isFunctional()) { + return; + } + + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + + if (empty($connectProxyToDockerNetworks)) { + return; + } + + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } +} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index e6c64ada7..611e06b0b 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -9,6 +9,7 @@ use App\Actions\Server\StartLogDrain; use App\Actions\Shared\ComplexStatusCheck; use App\Models\Application; +use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -134,29 +135,29 @@ public function handle() if ($this->containers->isEmpty()) { return; } + $this->applications = $this->server->applications(); $this->databases = $this->server->databases(); $this->previews = $this->server->previews(); - $this->services = $this->server->services()->get(); + // Eager load service applications and databases to avoid N+1 queries + $this->services = $this->server->services() + ->with(['applications:id,service_id', 'databases:id,service_id']) + ->get(); + $this->allApplicationIds = $this->applications->filter(function ($application) { - return $application->additional_servers->count() === 0; + return $application->additional_servers_count === 0; })->pluck('id'); $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) { - return $application->additional_servers->count() > 0; + return $application->additional_servers_count > 0; }); $this->allApplicationPreviewsIds = $this->previews->map(function ($preview) { return $preview->application_id.':'.$preview->pull_request_id; }); $this->allDatabaseUuids = $this->databases->pluck('uuid'); $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid'); - $this->services->each(function ($service) { - $service->applications()->pluck('id')->each(function ($applicationId) { - $this->allServiceApplicationIds->push($applicationId); - }); - $service->databases()->pluck('id')->each(function ($databaseId) { - $this->allServiceDatabaseIds->push($databaseId); - }); - }); + // Use eager-loaded relationships instead of querying in loop + $this->allServiceApplicationIds = $this->services->flatMap(fn ($service) => $service->applications->pluck('id')); + $this->allServiceDatabaseIds = $this->services->flatMap(fn ($service) => $service->databases->pluck('id')); foreach ($this->containers as $container) { $containerStatus = data_get($container, 'state', 'exited'); @@ -402,66 +403,59 @@ private function updateApplicationPreviewStatus(string $applicationId, string $p private function updateNotFoundApplicationStatus() { $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds); - if ($notFoundApplicationIds->isNotEmpty()) { - $notFoundApplicationIds->each(function ($applicationId) { - $application = Application::find($applicationId); - if ($application) { - // Don't mark as exited if already exited - if (str($application->status)->startsWith('exited')) { - return; - } - - // Only protection: Verify we received any container data at all - // If containers collection is completely empty, Sentinel might have failed - if ($this->containers->isEmpty()) { - return; - } - - if ($application->status !== 'exited') { - $application->status = 'exited'; - $application->save(); - } - } - }); + if ($notFoundApplicationIds->isEmpty()) { + return; } + + // Only protection: Verify we received any container data at all + // If containers collection is completely empty, Sentinel might have failed + if ($this->containers->isEmpty()) { + return; + } + + // Batch update: mark all not-found applications as exited (excluding already exited ones) + Application::whereIn('id', $notFoundApplicationIds) + ->where('status', 'not like', 'exited%') + ->update(['status' => 'exited']); } private function updateNotFoundApplicationPreviewStatus() { $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds); - if ($notFoundApplicationPreviewsIds->isNotEmpty()) { - $notFoundApplicationPreviewsIds->each(function ($previewKey) { - // Parse the previewKey format "application_id:pull_request_id" - $parts = explode(':', $previewKey); - if (count($parts) !== 2) { - return; - } + if ($notFoundApplicationPreviewsIds->isEmpty()) { + return; + } - $applicationId = $parts[0]; - $pullRequestId = $parts[1]; + // Only protection: Verify we received any container data at all + // If containers collection is completely empty, Sentinel might have failed + if ($this->containers->isEmpty()) { + return; + } - $applicationPreview = $this->previews->where('application_id', $applicationId) - ->where('pull_request_id', $pullRequestId) - ->first(); + // Collect IDs of previews that need to be marked as exited + $previewIdsToUpdate = collect(); + foreach ($notFoundApplicationPreviewsIds as $previewKey) { + // Parse the previewKey format "application_id:pull_request_id" + $parts = explode(':', $previewKey); + if (count($parts) !== 2) { + continue; + } - if ($applicationPreview) { - // Don't mark as exited if already exited - if (str($applicationPreview->status)->startsWith('exited')) { - return; - } + $applicationId = $parts[0]; + $pullRequestId = $parts[1]; - // Only protection: Verify we received any container data at all - // If containers collection is completely empty, Sentinel might have failed - if ($this->containers->isEmpty()) { + $applicationPreview = $this->previews->where('application_id', $applicationId) + ->where('pull_request_id', $pullRequestId) + ->first(); - return; - } - if ($applicationPreview->status !== 'exited') { - $applicationPreview->status = 'exited'; - $applicationPreview->save(); - } - } - }); + if ($applicationPreview && ! str($applicationPreview->status)->startsWith('exited')) { + $previewIdsToUpdate->push($applicationPreview->id); + } + } + + // Batch update all collected preview IDs + if ($previewIdsToUpdate->isNotEmpty()) { + ApplicationPreview::whereIn('id', $previewIdsToUpdate)->update(['status' => 'exited']); } } @@ -478,8 +472,8 @@ private function updateProxyStatus() } catch (\Throwable $e) { } } else { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + // Connect proxy to networks asynchronously to avoid blocking the status update + ConnectProxyToNetworksJob::dispatch($this->server); } } } @@ -554,27 +548,19 @@ private function updateNotFoundServiceStatus() { $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds); $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds); + + // Batch update service applications if ($notFoundServiceApplicationIds->isNotEmpty()) { - $notFoundServiceApplicationIds->each(function ($serviceApplicationId) { - $application = ServiceApplication::find($serviceApplicationId); - if ($application) { - if ($application->status !== 'exited') { - $application->status = 'exited'; - $application->save(); - } - } - }); + ServiceApplication::whereIn('id', $notFoundServiceApplicationIds) + ->where('status', '!=', 'exited') + ->update(['status' => 'exited']); } + + // Batch update service databases if ($notFoundServiceDatabaseIds->isNotEmpty()) { - $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { - $database = ServiceDatabase::find($serviceDatabaseId); - if ($database) { - if ($database->status !== 'exited') { - $database->status = 'exited'; - $database->save(); - } - } - }); + ServiceDatabase::whereIn('id', $notFoundServiceDatabaseIds) + ->where('status', '!=', 'exited') + ->update(['status' => 'exited']); } } diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 499035237..2ac92e72d 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -76,8 +76,7 @@ public function handle() } else { $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + ConnectProxyToNetworksJob::dispatchSync($this->server); } } } diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php index 3dbb3fcf8..117b43ad6 100644 --- a/app/Livewire/Server/Destinations.php +++ b/app/Livewire/Server/Destinations.php @@ -2,6 +2,7 @@ namespace App\Livewire\Server; +use App\Jobs\ConnectProxyToNetworksJob; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; @@ -29,8 +30,7 @@ public function mount(string $server_uuid) private function createNetworkAndAttachToProxy() { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + ConnectProxyToNetworksJob::dispatchSync($this->server); } public function add($name) diff --git a/app/Models/Server.php b/app/Models/Server.php index 82ee6721d..be39e3f8d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -831,34 +831,67 @@ public function hasDefinedResources() public function databases() { - return $this->destinations()->map(function ($standaloneDocker) { - $postgresqls = data_get($standaloneDocker, 'postgresqls', collect([])); - $redis = data_get($standaloneDocker, 'redis', collect([])); - $mongodbs = data_get($standaloneDocker, 'mongodbs', collect([])); - $mysqls = data_get($standaloneDocker, 'mysqls', collect([])); - $mariadbs = data_get($standaloneDocker, 'mariadbs', collect([])); - $keydbs = data_get($standaloneDocker, 'keydbs', collect([])); - $dragonflies = data_get($standaloneDocker, 'dragonflies', collect([])); - $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([])); + // Get destination IDs for this server in two efficient queries + $standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id'); + $swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id'); - return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); - })->flatten()->filter(function ($item) { - return data_get($item, 'name') !== 'coolify-db'; - }); + $destinationCondition = function ($query) use ($standaloneDockerIds, $swarmDockerIds) { + $query->where(function ($q) use ($standaloneDockerIds) { + $q->where('destination_type', StandaloneDocker::class) + ->whereIn('destination_id', $standaloneDockerIds); + })->orWhere(function ($q) use ($swarmDockerIds) { + $q->where('destination_type', SwarmDocker::class) + ->whereIn('destination_id', $swarmDockerIds); + }); + }; + + // Query each database type with the destination condition + $postgresqls = StandalonePostgresql::where($destinationCondition)->get(); + $redis = StandaloneRedis::where($destinationCondition)->get(); + $mongodbs = StandaloneMongodb::where($destinationCondition)->get(); + $mysqls = StandaloneMysql::where($destinationCondition)->get(); + $mariadbs = StandaloneMariadb::where($destinationCondition)->get(); + $keydbs = StandaloneKeydb::where($destinationCondition)->get(); + $dragonflies = StandaloneDragonfly::where($destinationCondition)->get(); + $clickhouses = StandaloneClickhouse::where($destinationCondition)->get(); + + return $postgresqls + ->concat($redis) + ->concat($mongodbs) + ->concat($mysqls) + ->concat($mariadbs) + ->concat($keydbs) + ->concat($dragonflies) + ->concat($clickhouses) + ->filter(fn ($item) => data_get($item, 'name') !== 'coolify-db'); } public function applications() { - $applications = $this->destinations()->map(function ($standaloneDocker) { - return $standaloneDocker->applications; - })->flatten(); - $additionalApplicationIds = DB::table('additional_destinations')->where('server_id', $this->id)->get('application_id'); - $additionalApplicationIds = collect($additionalApplicationIds)->map(function ($item) { - return $item->application_id; - }); - Application::whereIn('id', $additionalApplicationIds)->get()->each(function ($application) use ($applications) { - $applications->push($application); - }); + // Get destination IDs for this server in two efficient queries + $standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id'); + $swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id'); + + // Query all applications in a single query using polymorphic conditions + $applications = Application::where(function ($query) use ($standaloneDockerIds, $swarmDockerIds) { + $query->where(function ($q) use ($standaloneDockerIds) { + $q->where('destination_type', StandaloneDocker::class) + ->whereIn('destination_id', $standaloneDockerIds); + })->orWhere(function ($q) use ($swarmDockerIds) { + $q->where('destination_type', SwarmDocker::class) + ->whereIn('destination_id', $swarmDockerIds); + }); + })->get(); + + // Get additional server applications + $additionalApplicationIds = DB::table('additional_destinations') + ->where('server_id', $this->id) + ->pluck('application_id'); + + if ($additionalApplicationIds->isNotEmpty()) { + $additionalApps = Application::whereIn('id', $additionalApplicationIds)->get(); + $applications = $applications->concat($additionalApps); + } return $applications; } diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index aeb99d34a..9f5f0b33e 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Jobs\ConnectProxyToNetworksJob; use App\Traits\HasSafeStringAttribute; class StandaloneDocker extends BaseModel @@ -18,8 +19,7 @@ protected static function boot() instant_remote_process([ "docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null", ], $server, false); - $connectProxyToDockerNetworks = connectProxyToNetworks($server); - instant_remote_process($connectProxyToDockerNetworks, $server, false); + ConnectProxyToNetworksJob::dispatchSync($server); }); } diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 43be1374f..76d5118d0 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -4272,7 +4272,7 @@ "umami": { "documentation": "https://umami.is?utm_source=coolify.io", "slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.", - "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "analytics", "insights", diff --git a/templates/service-templates.json b/templates/service-templates.json index a5e7d632e..9ea3317e5 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -4272,7 +4272,7 @@ "umami": { "documentation": "https://umami.is?utm_source=coolify.io", "slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.", - "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "analytics", "insights",