| File: IsolatedInstancesOtelLogsTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.EndToEnd.Tests.Helpers; using Hex1b.Automation; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; /// <summary> /// End-to-end tests verifying that two isolated Aspire instances produce distinct telemetry. /// Each test class runs as a separate CI job for parallelization. /// </summary> public sealed class IsolatedInstancesOtelLogsTests(ITestOutputHelper output) { [Fact] [CaptureWorkspaceOnFailure] public async Task TwoIsolatedInstancesProduceDifferentOtelLogs() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); // Create a new Starter project (without Redis to avoid extra Docker dependencies) await auto.AspireNewAsync("IsolatedApp", counter, useRedisCache: false); // Move the created project into instance1 directory and copy to instance2 await auto.TypeAsync("mv IsolatedApp instance1"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("cp -r instance1 instance2"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Define AppHost paths for both instances const string appHost1 = "instance1/IsolatedApp.AppHost/IsolatedApp.AppHost.csproj"; const string appHost2 = "instance2/IsolatedApp.AppHost/IsolatedApp.AppHost.csproj"; // Start both instances with --isolated await auto.AspireStartAsync(counter, isolated: true, apphost: appHost1); await auto.AspireStartAsync(counter, isolated: true, apphost: appHost2); // Wait for the apiservice resource in instance1 to be running await auto.TypeAsync($"aspire wait apiservice --apphost {appHost1} --status up --timeout 300"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("is up (running).", timeout: TimeSpan.FromMinutes(6)); await auto.WaitForSuccessPromptAsync(counter); // Wait for the apiservice resource in instance2 to be running await auto.TypeAsync($"aspire wait apiservice --apphost {appHost2} --status up --timeout 300"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("is up (running).", timeout: TimeSpan.FromMinutes(6)); await auto.WaitForSuccessPromptAsync(counter); // Capture otel logs from both instances into workspace-relative files // so they are included in [CaptureWorkspaceOnFailure] artifacts. await auto.TypeAsync($"aspire otel logs --apphost {appHost1} --format json > otel_instance1.json 2>&1"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync($"aspire otel logs --apphost {appHost2} --format json > otel_instance2.json 2>&1"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Assert both files have structured log content (resourceLogs key) await auto.TypeAsync("grep -q 'resourceLogs' otel_instance1.json && echo 'INSTANCE1_HAS_LOGS' || echo 'INSTANCE1_NO_LOGS'"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("INSTANCE1_HAS_LOGS", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForAnyPromptAsync(counter); await auto.TypeAsync("grep -q 'resourceLogs' otel_instance2.json && echo 'INSTANCE2_HAS_LOGS' || echo 'INSTANCE2_NO_LOGS'"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("INSTANCE2_HAS_LOGS", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForAnyPromptAsync(counter); // Extract the ContentRoot values from each instance's structured logs and verify they // point at the correct instance directory. The OTLP JSON body contains log messages like: // "Content root path: /workspace/instance1/IsolatedApp.AppHost" // Matching against the instance directory proves each AppHost runs from its own root. // Use unique sentinel markers so WaitUntilTextAsync doesn't match pre-existing scrollback // (e.g. from the mv/cp/--apphost commands that also contain "instance1"/"instance2"). await auto.TypeAsync("grep -o 'Content root path: [^\"]*' otel_instance1.json | grep -q 'instance1' && echo 'CONTENTROOT1_OK' || echo 'CONTENTROOT1_FAIL'"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("CONTENTROOT1_OK", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForAnyPromptAsync(counter); await auto.TypeAsync("grep -o 'Content root path: [^\"]*' otel_instance2.json | grep -q 'instance2' && echo 'CONTENTROOT2_OK' || echo 'CONTENTROOT2_FAIL'"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("CONTENTROOT2_OK", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForAnyPromptAsync(counter); // Stop both instances await auto.AspireStopAsync(counter, apphost: appHost1); await auto.AspireStopAsync(counter, apphost: appHost2); } }