From 32804dcbce37454b8b31670a9b724b139b1131c3 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Sun, 6 Sep 2026 18:43:11 -0500 Subject: [PATCH 1/2] Upgrade Elastic Stack 8 images to 8.19.21 --- .github/workflows/build.yaml | 72 +++++++++++++++++++ .github/workflows/elasticsearch-docker-8.yml | 12 +++- .github/workflows/preview-command.yml | 6 +- Dockerfile | 2 +- build/docker/elasticsearch/8.x/Dockerfile | 3 +- docker/docker-compose.apm.yml | 8 +-- docker/docker-compose.dev.yml | 4 +- docker/docker-compose.yml | 4 +- k8s/elastic-monitor.yaml | 8 +-- k8s/ex-dev-elasticsearch.yaml | 6 +- k8s/ex-prod-elasticsearch.yaml | 6 +- k8s/exceptionless/values.yaml | 2 +- samples/docker-compose.all-in-one.yml | 6 +- samples/docker-compose.yml | 4 +- .../Extensions/ElasticsearchExtensions.cs | 28 +++++--- src/Exceptionless.AppHost/Program.cs | 32 +++++++-- .../AppHostConfigurationTests.cs | 30 ++++++++ .../Exceptionless.Tests/AppWebHostFactory.cs | 41 +++++++++-- .../AppWebHostFactoryTests.cs | 18 +++++ 19 files changed, 245 insertions(+), 47 deletions(-) create mode 100644 tests/Exceptionless.Tests/AppHostConfigurationTests.cs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1d0725d975..087c72f58f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -83,6 +83,8 @@ jobs: timeout-minutes: 30 outputs: version: ${{ steps.version.outputs.version }} + elasticsearch_image_tag: ${{ steps.elasticsearch_image.outputs.tag }} + build_elasticsearch_image: ${{ steps.elasticsearch_image.outputs.build_locally }} should_publish: ${{ (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'dev-preview')) && secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }} is_prod_deploy: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} is_dev_deploy: ${{ (github.event_name == 'repository_dispatch' && github.event.action == 'preview') || (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'dev-preview')) }} @@ -129,9 +131,66 @@ jobs: echo "version=$version" >> $GITHUB_OUTPUT echo "### $version" >> $GITHUB_STEP_SUMMARY + - name: Resolve Elasticsearch image + id: elasticsearch_image + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + PREVIEW_BASE_SHA: ${{ github.event.client_payload.base_sha }} + PUSH_BEFORE_SHA: ${{ github.event.before }} + run: | + version=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/8.x/Dockerfile) + tag=$version + image_changed=false + build_locally=false + + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] && + ! git diff --quiet "$PR_BASE_SHA"...HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then + image_changed=true + elif [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]] && + ! git diff --quiet "$PUSH_BEFORE_SHA"..HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then + image_changed=true + elif [[ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]] && + ! git diff --quiet "${PREVIEW_BASE_SHA:-origin/main}"...HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then + image_changed=true + elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]] && + ! git diff --quiet origin/main...HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then + image_changed=true + fi + + if [[ "$image_changed" == "true" ]]; then + image_sha=$(git ls-files -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml | sort | xargs sha256sum | sha256sum | cut -d " " -f 1) + tag="$version-sha256-$image_sha" + image="exceptionless/elasticsearch:$tag" + + if [[ "$GITHUB_EVENT_NAME" == "pull_request" && "$PR_HEAD_REPOSITORY" != "$GITHUB_REPOSITORY" ]]; then + build_locally=true + else + for attempt in {1..150}; do + if docker manifest inspect "$image" > /dev/null 2>&1; then + break + fi + + if [[ "$attempt" -eq 150 ]]; then + echo "::error::Timed out waiting for $image to be published." + exit 1 + fi + + sleep 10 + done + fi + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "build_locally=$build_locally" >> "$GITHUB_OUTPUT" + echo "### Elasticsearch image: exceptionless/elasticsearch:$tag" >> "$GITHUB_STEP_SUMMARY" + test-api: + needs: version runs-on: ubuntu-latest timeout-minutes: 30 + env: + Elasticsearch__ImageTag: ${{ needs.version.outputs.elasticsearch_image_tag }} steps: - name: Checkout @@ -139,6 +198,10 @@ jobs: with: ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} + - name: Build fork Elasticsearch candidate locally + if: ${{ needs.version.outputs.build_elasticsearch_image == 'true' }} + run: docker build --tag "exceptionless/elasticsearch:${{ needs.version.outputs.elasticsearch_image_tag }}" --file build/docker/elasticsearch/8.x/Dockerfile build/docker/elasticsearch/8.x + - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: @@ -223,12 +286,21 @@ jobs: run: echo "npm run test:integration" test-e2e: + needs: version runs-on: ubuntu-latest timeout-minutes: 45 + env: + Elasticsearch__ImageTag: ${{ needs.version.outputs.elasticsearch_image_tag }} steps: - name: Checkout uses: actions/checkout@v6 + with: + ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} + + - name: Build fork Elasticsearch candidate locally + if: ${{ needs.version.outputs.build_elasticsearch_image == 'true' }} + run: docker build --tag "exceptionless/elasticsearch:${{ needs.version.outputs.elasticsearch_image_tag }}" --file build/docker/elasticsearch/8.x/Dockerfile build/docker/elasticsearch/8.x - name: Setup .NET Core uses: actions/setup-dotnet@v6 diff --git a/.github/workflows/elasticsearch-docker-8.yml b/.github/workflows/elasticsearch-docker-8.yml index ef74055e42..fdfb667602 100644 --- a/.github/workflows/elasticsearch-docker-8.yml +++ b/.github/workflows/elasticsearch-docker-8.yml @@ -40,7 +40,13 @@ jobs: with: platforms: linux/amd64,linux/arm64 - name: Build custom Elasticsearch 8.x docker image - working-directory: build/docker/elasticsearch/8.x + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | - VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) - docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . --tag exceptionless/elasticsearch:$VERSION --tag exceptionless/elasticsearch:latest + VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/8.x/Dockerfile) + IMAGE_SHA=$(git ls-files -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml | sort | xargs sha256sum | sha256sum | cut -d " " -f 1) + TAGS=(--tag "exceptionless/elasticsearch:$VERSION-sha256-$IMAGE_SHA") + if [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]; then + TAGS+=(--tag "exceptionless/elasticsearch:$VERSION" --tag "exceptionless/elasticsearch:latest") + fi + docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file build/docker/elasticsearch/8.x/Dockerfile build/docker/elasticsearch/8.x "${TAGS[@]}" diff --git a/.github/workflows/preview-command.yml b/.github/workflows/preview-command.yml index fe94875393..e4dd285c7f 100644 --- a/.github/workflows/preview-command.yml +++ b/.github/workflows/preview-command.yml @@ -77,6 +77,7 @@ jobs: const headRepository = pullRequest.head.repo.full_name; const headRef = pullRequest.head.ref; const headSha = pullRequest.head.sha; + const baseSha = pullRequest.base.sha; const headLabel = pullRequest.head.label; const headShortSha = headSha.slice(0, 12); @@ -95,6 +96,7 @@ jobs: core.setOutput("head-ref", headRef); core.setOutput("head-label", headLabel); core.setOutput("head-sha", headSha); + core.setOutput("base-sha", baseSha); core.setOutput("head-short-sha", headShortSha); const previewLabel = "dev-preview"; @@ -142,6 +144,7 @@ jobs: HEAD_REF: ${{ steps.preview.outputs.head-ref }} HEAD_LABEL: ${{ steps.preview.outputs.head-label }} HEAD_SHA: ${{ steps.preview.outputs.head-sha }} + BASE_SHA: ${{ steps.preview.outputs.base-sha }} with: script: | await github.rest.repos.createDispatchEvent({ @@ -152,7 +155,8 @@ jobs: pr_number: Number(process.env.PR_NUMBER), head_ref: process.env.HEAD_REF, head_label: process.env.HEAD_LABEL, - head_sha: process.env.HEAD_SHA + head_sha: process.env.HEAD_SHA, + base_sha: process.env.BASE_SHA } }); diff --git a/Dockerfile b/Dockerfile index 4904cdf6f9..3782994e13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,7 +100,7 @@ ENTRYPOINT ["/app/app-docker-entrypoint.sh"] # completely self-contained -FROM exceptionless/elasticsearch:8.19.15 AS exceptionless +FROM exceptionless/elasticsearch:8.19.21 AS exceptionless WORKDIR /app COPY --from=job-publish /app/src/Exceptionless.Job/out ./ diff --git a/build/docker/elasticsearch/8.x/Dockerfile b/build/docker/elasticsearch/8.x/Dockerfile index bbab4cc3bc..3be7362ff5 100644 --- a/build/docker/elasticsearch/8.x/Dockerfile +++ b/build/docker/elasticsearch/8.x/Dockerfile @@ -1,5 +1,4 @@ # https://www.docker.elastic.co/ -FROM docker.elastic.co/elasticsearch/elasticsearch:8.19.15 +FROM docker.elastic.co/elasticsearch/elasticsearch:8.19.21 RUN elasticsearch-plugin install -b mapper-size - diff --git a/docker/docker-compose.apm.yml b/docker/docker-compose.apm.yml index bd55972eb9..0592e55d38 100644 --- a/docker/docker-compose.apm.yml +++ b/docker/docker-compose.apm.yml @@ -2,7 +2,7 @@ version: "2.2" services: setup: - image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 + image: docker.elastic.co/elasticsearch/elasticsearch:8.19.21 volumes: - certs:/usr/share/elasticsearch/config/certs user: "0" @@ -53,7 +53,7 @@ services: depends_on: setup: condition: service_healthy - image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 + image: docker.elastic.co/elasticsearch/elasticsearch:8.19.21 volumes: - certs:/usr/share/elasticsearch/config/certs - esdata:/usr/share/elasticsearch/data @@ -98,7 +98,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:8.19.21 volumes: - certs:/usr/share/kibana/config/certs ports: @@ -124,7 +124,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/apm/apm-server:8.19.15 + image: docker.elastic.co/apm/apm-server:8.19.21 volumes: - certs:/usr/share/apm-server/certs ports: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index a18d81b4cf..66a8216fc2 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -50,7 +50,7 @@ services: - appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:8.19.15 + image: exceptionless/elasticsearch:8.19.21 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -74,7 +74,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:8.19.21 ports: - 5601:5601 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c81594d41f..4c1912bda9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: elasticsearch: - image: exceptionless/elasticsearch:8.19.15 + image: exceptionless/elasticsearch:8.19.21 environment: node.name: elasticsearch cluster.name: exceptionless @@ -26,7 +26,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:8.19.21 environment: xpack.security.enabled: "false" ports: diff --git a/k8s/elastic-monitor.yaml b/k8s/elastic-monitor.yaml index 943cde867d..ddde6dea81 100644 --- a/k8s/elastic-monitor.yaml +++ b/k8s/elastic-monitor.yaml @@ -4,7 +4,7 @@ metadata: name: elastic-monitor namespace: elastic-system spec: - version: 8.19.15 + version: 8.19.21 podDisruptionBudget: {} nodeSets: - name: main @@ -228,7 +228,7 @@ metadata: name: kibana-monitor namespace: elastic-system spec: - version: 8.19.15 + version: 8.19.21 count: 1 http: tls: @@ -364,7 +364,7 @@ metadata: name: fleet-server namespace: elastic-system spec: - version: 8.19.15 + version: 8.19.21 kibanaRef: name: kibana-monitor elasticsearchRefs: @@ -388,7 +388,7 @@ metadata: name: elastic-agent namespace: elastic-system spec: - version: 8.19.15 + version: 8.19.21 kibanaRef: name: kibana-monitor fleetServerRef: diff --git a/k8s/ex-dev-elasticsearch.yaml b/k8s/ex-dev-elasticsearch.yaml index 46f14cc5eb..5bd8457402 100644 --- a/k8s/ex-dev-elasticsearch.yaml +++ b/k8s/ex-dev-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 8.19.15 - image: exceptionless/elasticsearch:8.19.15 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 8.19.21 + image: exceptionless/elasticsearch:8.19.21 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch secureSettings: - secretName: ex-dev-snapshots http: @@ -68,7 +68,7 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 8.19.15 + version: 8.19.21 count: 1 elasticsearchRef: name: ex-dev diff --git a/k8s/ex-prod-elasticsearch.yaml b/k8s/ex-prod-elasticsearch.yaml index f159ae3046..12e9499c12 100644 --- a/k8s/ex-prod-elasticsearch.yaml +++ b/k8s/ex-prod-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 8.19.15 - image: exceptionless/elasticsearch:8.19.15 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 8.19.21 + image: exceptionless/elasticsearch:8.19.21 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch monitoring: metrics: elasticsearchRefs: @@ -79,7 +79,7 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 8.19.15 + version: 8.19.21 count: 1 elasticsearchRef: name: ex-prod diff --git a/k8s/exceptionless/values.yaml b/k8s/exceptionless/values.yaml index db1c202926..d210af82a0 100644 --- a/k8s/exceptionless/values.yaml +++ b/k8s/exceptionless/values.yaml @@ -36,7 +36,7 @@ elasticsearch: connectionString: image: repository: exceptionless/elasticsearch - tag: 8.19.15 + tag: 8.19.21 pullPolicy: IfNotPresent redis: diff --git a/samples/docker-compose.all-in-one.yml b/samples/docker-compose.all-in-one.yml index 5b1caf2d42..1ab2a5c820 100644 --- a/samples/docker-compose.all-in-one.yml +++ b/samples/docker-compose.all-in-one.yml @@ -19,10 +19,12 @@ services: # Runs Kibana for working with Elasticsearch data directly. This is normally not needed and takes up resources when running. kibana: depends_on: - - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + - exceptionless + image: docker.elastic.co/kibana/kibana:8.19.21 ports: - 5601:5601 + environment: + ELASTICSEARCH_HOSTS: http://exceptionless:9200 volumes: ex_esdata: diff --git a/samples/docker-compose.yml b/samples/docker-compose.yml index d73e0518c9..754d063bdb 100644 --- a/samples/docker-compose.yml +++ b/samples/docker-compose.yml @@ -44,7 +44,7 @@ services: - ex_appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:8.19.15 + image: exceptionless/elasticsearch:8.19.21 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -58,7 +58,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:8.19.21 ports: - 5601:5601 diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index ec5c5e4031..2185c9140b 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -13,7 +13,7 @@ public static class ElasticsearchBuilderExtensions private const int KibanaPort = 5601; /// - /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 8.19.15 tag of the Elasticsearch container image + /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 8.19.21 tag of the Elasticsearch container image /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. @@ -60,7 +60,11 @@ public static IResourceBuilder AddElasticsearch(this IDis .PublishAsConnectionString(); } - public static IResourceBuilder WithKibana(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) + public static IResourceBuilder WithKibana( + this IResourceBuilder builder, + Action>? configureContainer = null, + string? containerName = null, + int? port = null) { ArgumentNullException.ThrowIfNull(builder); @@ -79,7 +83,7 @@ public static IResourceBuilder WithKibana(this IResourceB var resourceBuilder = builder.ApplicationBuilder.AddResource(resource) .WithImage(ElasticsearchContainerImageTags.KibanaImage, ElasticsearchContainerImageTags.Tag) .WithImageRegistry(ElasticsearchContainerImageTags.KibanaRegistry) - .WithHttpEndpoint(targetPort: KibanaPort, name: containerName) + .WithHttpEndpoint(targetPort: KibanaPort, port: port, name: containerName) .WithUrlForEndpoint(containerName, u => u.DisplayText = "Kibana") .WithEnvironment("xpack.security.enabled", "false") .WithEnvironment(ctx => @@ -121,7 +125,7 @@ internal static class ElasticsearchContainerImageTags public const string Image = "exceptionless/elasticsearch"; public const string KibanaRegistry = "docker.elastic.co"; public const string KibanaImage = "kibana/kibana"; - public const string Tag = "8.19.15"; + public const string Tag = "8.19.21"; } internal sealed class ElasticsearchConnectionHealthCheck(Func connectionStringFactory) : IHealthCheck @@ -134,9 +138,17 @@ public async Task CheckHealthAsync(HealthCheckContext context using var settings = new ElasticsearchClientSettings(new Uri(connectionString)); var client = new ElasticsearchClient(settings); - var response = await client.PingAsync(cancellationToken); - return response.IsValidResponse - ? HealthCheckResult.Healthy() - : new HealthCheckResult(context.Registration.FailureStatus, $"Elasticsearch ping failed: {response.DebugInformation}"); + var response = await client.Cluster.HealthAsync( + request => request.WaitForStatus(Elastic.Clients.Elasticsearch.HealthStatus.Yellow), + cancellationToken); + bool isReady = response.IsValidResponse + && !response.TimedOut + && response.Status is Elastic.Clients.Elasticsearch.HealthStatus.Yellow or Elastic.Clients.Elasticsearch.HealthStatus.Green; + if (isReady) + return HealthCheckResult.Healthy(); + + return new HealthCheckResult( + context.Registration.FailureStatus, + $"Elasticsearch cluster health check failed. Timed out: {response.TimedOut}; status: {response.Status}. {response.DebugInformation}"); } } diff --git a/src/Exceptionless.AppHost/Program.cs b/src/Exceptionless.AppHost/Program.cs index b12b48aefe..ef33ee8333 100644 --- a/src/Exceptionless.AppHost/Program.cs +++ b/src/Exceptionless.AppHost/Program.cs @@ -13,6 +13,13 @@ bool servicesOnly = HasArgument("--services-only"); bool ciE2E = HasArgument("--ci-e2e"); bool includeDevTools = !ciE2E; +int elasticsearchPort = GetPort("Elasticsearch:Port", 9200); +string elasticsearchImageTag = builder.Configuration["Elasticsearch:ImageTag"] ?? ElasticsearchContainerImageTags.Tag; +string kibanaImageTag = builder.Configuration["Elasticsearch:KibanaImageTag"] ?? ElasticsearchContainerImageTags.Tag; +string elasticsearchContainerName = builder.Configuration["Elasticsearch:ContainerName"] ?? "Exceptionless-Elasticsearch"; +string elasticsearchDataVolume = builder.Configuration["Elasticsearch:DataVolume"] ?? "exceptionless.data.v1"; +string kibanaContainerName = builder.Configuration["Elasticsearch:KibanaContainerName"] ?? "Exceptionless-Kibana"; +int kibanaPort = GetPort("Elasticsearch:KibanaPort", 5601); int oldAppHttpPort = worktreePorts?.OldAppHttp ?? 7120; int oldAppPort = worktreePorts?.OldAppHttps ?? 7121; int oldAppLiveReloadPort = worktreePorts?.OldAppLiveReload ?? 35729; @@ -24,8 +31,9 @@ string exceptionlessServerUrl = worktreePorts?.ApiHttpsUrl ?? $"https://api-ex.dev.localhost:{DefaultApiHttpsPort}"; const string SharedEmailConnectionString = "smtp://localhost:1026"; -var elastic = builder.AddElasticsearch("Elasticsearch", port: 9200) - .WithDataVolume("exceptionless.data.v1") +var elastic = builder.AddElasticsearch("Elasticsearch", port: elasticsearchPort) + .WithImageTag(elasticsearchImageTag) + .WithDataVolume(elasticsearchDataVolume) .WithEndpointProxySupport(false); var storage = builder.AddAzureStorage("Storage") @@ -70,15 +78,17 @@ var ownedElastic = elastic; elastic = ownedElastic .WithLifetime(ContainerLifetime.Persistent) - .WithContainerName("Exceptionless-Elasticsearch"); + .WithContainerName(elasticsearchContainerName); if (!servicesOnly && includeDevTools) { elastic = elastic.WithKibana(b => b + .WithImageTag(kibanaImageTag) .WithLifetime(ContainerLifetime.Persistent) .WithEndpointProxySupport(false) - .WithContainerName("Exceptionless-Kibana") - .WithParentRelationship(ownedElastic)); + .WithContainerName(kibanaContainerName) + .WithParentRelationship(ownedElastic), + port: kibanaPort); } var ownedCache = cache; @@ -242,3 +252,15 @@ await builder.Build().RunAsync(); bool HasArgument(string name) => args.Any(arg => StringComparer.OrdinalIgnoreCase.Equals(arg, name) || StringComparer.OrdinalIgnoreCase.Equals(arg, name.TrimStart('-'))); + +int GetPort(string key, int defaultValue) +{ + string? value = builder.Configuration[key]; + if (String.IsNullOrWhiteSpace(value)) + return defaultValue; + + if (!Int32.TryParse(value, out int port) || port is < 1 or > 65535) + throw new InvalidOperationException($"Configuration value '{key}' must be a valid TCP port."); + + return port; +} diff --git a/tests/Exceptionless.Tests/AppHostConfigurationTests.cs b/tests/Exceptionless.Tests/AppHostConfigurationTests.cs new file mode 100644 index 0000000000..3f5d49b9ee --- /dev/null +++ b/tests/Exceptionless.Tests/AppHostConfigurationTests.cs @@ -0,0 +1,30 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Testing; +using Xunit; + +namespace Exceptionless.Tests; + +public class AppHostConfigurationTests +{ + [Fact] + public async Task CreateAsync_WithSeparateElasticsearchAndKibanaOverrides_UsesIndependentImageTags() + { + const string elasticsearchImageTag = "8.19.21-sha256-candidate"; + const string kibanaImageTag = "8.19.21"; + var appHost = await DistributedApplicationTestingBuilder.CreateAsync( + [ + $"--Elasticsearch:ImageTag={elasticsearchImageTag}", + $"--Elasticsearch:KibanaImageTag={kibanaImageTag}" + ], + TestContext.Current.CancellationToken); + + var elasticsearch = Assert.Single(appHost.Resources.OfType()); + var kibana = Assert.Single(appHost.Resources.OfType()); + var elasticsearchImage = Assert.Single(elasticsearch.Annotations.OfType()); + var kibanaImage = Assert.Single(kibana.Annotations.OfType()); + + Assert.Equal(elasticsearchImageTag, elasticsearchImage.Tag); + Assert.Equal(kibanaImageTag, kibanaImage.Tag); + } +} diff --git a/tests/Exceptionless.Tests/AppWebHostFactory.cs b/tests/Exceptionless.Tests/AppWebHostFactory.cs index c08f21687f..8f6fbdbd75 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactory.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactory.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Net; +using System.Text.Json; using Aspire.Hosting; using Aspire.Hosting.Testing; using Exceptionless.Core; @@ -21,7 +22,7 @@ namespace Exceptionless.Tests; public class AppWebHostFactory : WebApplicationFactory, IAsyncLifetime { - private const string SharedElasticsearchUrl = "http://localhost:9200"; + private static readonly string SharedElasticsearchUrl = GetSharedElasticsearchUrl(); private static readonly TimeSpan SharedElasticsearchStartupTimeout = TimeSpan.FromMinutes(3); private static int s_counter = -1; private static readonly Lazy> s_sharedAppHost = new(StartSharedAppHostAsync, LazyThreadSafetyMode.ExecutionAndPublication); @@ -58,22 +59,40 @@ private static async Task StartSharedAppHostAsync() return app; } + private static string GetSharedElasticsearchUrl() + { + const int defaultPort = 9200; + string? configuredPort = Environment.GetEnvironmentVariable("Elasticsearch__Port"); + if (String.IsNullOrWhiteSpace(configuredPort)) + return $"http://localhost:{defaultPort}"; + + if (!Int32.TryParse(configuredPort, out int port) || port is < 1 or > 65535) + throw new InvalidOperationException("Environment variable 'Elasticsearch__Port' must be a valid TCP port."); + + return $"http://localhost:{port}"; + } + private static async Task WaitForElasticsearchAsync(Uri elasticsearchUri) { - using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(1) }; + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; var deadline = TimeProvider.System.GetUtcNow() + SharedElasticsearchStartupTimeout; + var healthUri = new Uri(elasticsearchUri, "/_cluster/health?wait_for_status=yellow&timeout=1s"); while (TimeProvider.System.GetUtcNow() < deadline) { try { - using var response = await client.GetAsync(elasticsearchUri); - if (response.StatusCode == HttpStatusCode.OK) + using var response = await client.GetAsync(healthUri); + using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + if (IsElasticsearchReady(response.StatusCode, document.RootElement)) return; } catch (HttpRequestException) { } + catch (JsonException) + { + } catch (TaskCanceledException) { } @@ -84,6 +103,20 @@ private static async Task WaitForElasticsearchAsync(Uri elasticsearchUri) throw new TimeoutException("Timed out waiting for the shared Elasticsearch container to be ready."); } + internal static bool IsElasticsearchReady(HttpStatusCode statusCode, JsonElement health) + { + if (statusCode != HttpStatusCode.OK) + return false; + + bool requestCompleted = health.TryGetProperty("timed_out", out var timedOut) + && timedOut.ValueKind == JsonValueKind.False; + bool clusterReady = health.TryGetProperty("status", out var status) + && status.ValueKind == JsonValueKind.String + && status.GetString() is "yellow" or "green"; + + return requestCompleted && clusterReady; + } + protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment(Environments.Development); diff --git a/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs b/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs index 7fef487faa..539785b681 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs @@ -1,4 +1,6 @@ +using System.Net; using System.Text; +using System.Text.Json; using Foundatio.Storage; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -7,6 +9,22 @@ namespace Exceptionless.Tests; public sealed class AppWebHostFactoryTests { + [Theory] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"yellow"}""", true)] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"green"}""", true)] + [InlineData(HttpStatusCode.OK, """{"timed_out":true,"status":"yellow"}""", false)] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"red"}""", false)] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":1}""", false)] + [InlineData(HttpStatusCode.ServiceUnavailable, """{"timed_out":false,"status":"yellow"}""", false)] + public void IsElasticsearchReady_ClusterHealthResponse_ReturnsExpectedResult(HttpStatusCode statusCode, string json, bool expected) + { + using var document = JsonDocument.Parse(json); + + bool isReady = AppWebHostFactory.IsElasticsearchReady(statusCode, document.RootElement); + + Assert.Equal(expected, isReady); + } + [Fact] public async Task ConfigureWebHost_MultipleFactories_IsolatesFileStorageByAppScope() { From e5077a557b0bcede4e7451524833315a64c88fc4 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Sun, 6 Sep 2026 23:53:32 -0500 Subject: [PATCH 2/2] Keep Elasticsearch 8 upgrade limited to version updates --- .github/workflows/build.yaml | 72 ------------------- .github/workflows/elasticsearch-docker-8.yml | 12 +--- .github/workflows/preview-command.yml | 6 +- build/docker/elasticsearch/8.x/Dockerfile | 1 + samples/docker-compose.all-in-one.yml | 4 +- .../Extensions/ElasticsearchExtensions.cs | 24 ++----- src/Exceptionless.AppHost/Program.cs | 32 ++------- .../AppHostConfigurationTests.cs | 30 -------- .../Exceptionless.Tests/AppWebHostFactory.cs | 41 ++--------- .../AppWebHostFactoryTests.cs | 18 ----- 10 files changed, 21 insertions(+), 219 deletions(-) delete mode 100644 tests/Exceptionless.Tests/AppHostConfigurationTests.cs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 087c72f58f..1d0725d975 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -83,8 +83,6 @@ jobs: timeout-minutes: 30 outputs: version: ${{ steps.version.outputs.version }} - elasticsearch_image_tag: ${{ steps.elasticsearch_image.outputs.tag }} - build_elasticsearch_image: ${{ steps.elasticsearch_image.outputs.build_locally }} should_publish: ${{ (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'dev-preview')) && secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }} is_prod_deploy: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} is_dev_deploy: ${{ (github.event_name == 'repository_dispatch' && github.event.action == 'preview') || (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'dev-preview')) }} @@ -131,66 +129,9 @@ jobs: echo "version=$version" >> $GITHUB_OUTPUT echo "### $version" >> $GITHUB_STEP_SUMMARY - - name: Resolve Elasticsearch image - id: elasticsearch_image - env: - PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} - PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} - PREVIEW_BASE_SHA: ${{ github.event.client_payload.base_sha }} - PUSH_BEFORE_SHA: ${{ github.event.before }} - run: | - version=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/8.x/Dockerfile) - tag=$version - image_changed=false - build_locally=false - - if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] && - ! git diff --quiet "$PR_BASE_SHA"...HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then - image_changed=true - elif [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]] && - ! git diff --quiet "$PUSH_BEFORE_SHA"..HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then - image_changed=true - elif [[ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]] && - ! git diff --quiet "${PREVIEW_BASE_SHA:-origin/main}"...HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then - image_changed=true - elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]] && - ! git diff --quiet origin/main...HEAD -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml; then - image_changed=true - fi - - if [[ "$image_changed" == "true" ]]; then - image_sha=$(git ls-files -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml | sort | xargs sha256sum | sha256sum | cut -d " " -f 1) - tag="$version-sha256-$image_sha" - image="exceptionless/elasticsearch:$tag" - - if [[ "$GITHUB_EVENT_NAME" == "pull_request" && "$PR_HEAD_REPOSITORY" != "$GITHUB_REPOSITORY" ]]; then - build_locally=true - else - for attempt in {1..150}; do - if docker manifest inspect "$image" > /dev/null 2>&1; then - break - fi - - if [[ "$attempt" -eq 150 ]]; then - echo "::error::Timed out waiting for $image to be published." - exit 1 - fi - - sleep 10 - done - fi - fi - - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "build_locally=$build_locally" >> "$GITHUB_OUTPUT" - echo "### Elasticsearch image: exceptionless/elasticsearch:$tag" >> "$GITHUB_STEP_SUMMARY" - test-api: - needs: version runs-on: ubuntu-latest timeout-minutes: 30 - env: - Elasticsearch__ImageTag: ${{ needs.version.outputs.elasticsearch_image_tag }} steps: - name: Checkout @@ -198,10 +139,6 @@ jobs: with: ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} - - name: Build fork Elasticsearch candidate locally - if: ${{ needs.version.outputs.build_elasticsearch_image == 'true' }} - run: docker build --tag "exceptionless/elasticsearch:${{ needs.version.outputs.elasticsearch_image_tag }}" --file build/docker/elasticsearch/8.x/Dockerfile build/docker/elasticsearch/8.x - - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: @@ -286,21 +223,12 @@ jobs: run: echo "npm run test:integration" test-e2e: - needs: version runs-on: ubuntu-latest timeout-minutes: 45 - env: - Elasticsearch__ImageTag: ${{ needs.version.outputs.elasticsearch_image_tag }} steps: - name: Checkout uses: actions/checkout@v6 - with: - ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} - - - name: Build fork Elasticsearch candidate locally - if: ${{ needs.version.outputs.build_elasticsearch_image == 'true' }} - run: docker build --tag "exceptionless/elasticsearch:${{ needs.version.outputs.elasticsearch_image_tag }}" --file build/docker/elasticsearch/8.x/Dockerfile build/docker/elasticsearch/8.x - name: Setup .NET Core uses: actions/setup-dotnet@v6 diff --git a/.github/workflows/elasticsearch-docker-8.yml b/.github/workflows/elasticsearch-docker-8.yml index fdfb667602..ef74055e42 100644 --- a/.github/workflows/elasticsearch-docker-8.yml +++ b/.github/workflows/elasticsearch-docker-8.yml @@ -40,13 +40,7 @@ jobs: with: platforms: linux/amd64,linux/arm64 - name: Build custom Elasticsearch 8.x docker image - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + working-directory: build/docker/elasticsearch/8.x run: | - VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/8.x/Dockerfile) - IMAGE_SHA=$(git ls-files -- build/docker/elasticsearch/8.x .github/workflows/elasticsearch-docker-8.yml | sort | xargs sha256sum | sha256sum | cut -d " " -f 1) - TAGS=(--tag "exceptionless/elasticsearch:$VERSION-sha256-$IMAGE_SHA") - if [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]; then - TAGS+=(--tag "exceptionless/elasticsearch:$VERSION" --tag "exceptionless/elasticsearch:latest") - fi - docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file build/docker/elasticsearch/8.x/Dockerfile build/docker/elasticsearch/8.x "${TAGS[@]}" + VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) + docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . --tag exceptionless/elasticsearch:$VERSION --tag exceptionless/elasticsearch:latest diff --git a/.github/workflows/preview-command.yml b/.github/workflows/preview-command.yml index e4dd285c7f..fe94875393 100644 --- a/.github/workflows/preview-command.yml +++ b/.github/workflows/preview-command.yml @@ -77,7 +77,6 @@ jobs: const headRepository = pullRequest.head.repo.full_name; const headRef = pullRequest.head.ref; const headSha = pullRequest.head.sha; - const baseSha = pullRequest.base.sha; const headLabel = pullRequest.head.label; const headShortSha = headSha.slice(0, 12); @@ -96,7 +95,6 @@ jobs: core.setOutput("head-ref", headRef); core.setOutput("head-label", headLabel); core.setOutput("head-sha", headSha); - core.setOutput("base-sha", baseSha); core.setOutput("head-short-sha", headShortSha); const previewLabel = "dev-preview"; @@ -144,7 +142,6 @@ jobs: HEAD_REF: ${{ steps.preview.outputs.head-ref }} HEAD_LABEL: ${{ steps.preview.outputs.head-label }} HEAD_SHA: ${{ steps.preview.outputs.head-sha }} - BASE_SHA: ${{ steps.preview.outputs.base-sha }} with: script: | await github.rest.repos.createDispatchEvent({ @@ -155,8 +152,7 @@ jobs: pr_number: Number(process.env.PR_NUMBER), head_ref: process.env.HEAD_REF, head_label: process.env.HEAD_LABEL, - head_sha: process.env.HEAD_SHA, - base_sha: process.env.BASE_SHA + head_sha: process.env.HEAD_SHA } }); diff --git a/build/docker/elasticsearch/8.x/Dockerfile b/build/docker/elasticsearch/8.x/Dockerfile index 3be7362ff5..c2dc4e0d49 100644 --- a/build/docker/elasticsearch/8.x/Dockerfile +++ b/build/docker/elasticsearch/8.x/Dockerfile @@ -2,3 +2,4 @@ FROM docker.elastic.co/elasticsearch/elasticsearch:8.19.21 RUN elasticsearch-plugin install -b mapper-size + diff --git a/samples/docker-compose.all-in-one.yml b/samples/docker-compose.all-in-one.yml index 1ab2a5c820..f604d357f2 100644 --- a/samples/docker-compose.all-in-one.yml +++ b/samples/docker-compose.all-in-one.yml @@ -19,12 +19,10 @@ services: # Runs Kibana for working with Elasticsearch data directly. This is normally not needed and takes up resources when running. kibana: depends_on: - - exceptionless + - elasticsearch image: docker.elastic.co/kibana/kibana:8.19.21 ports: - 5601:5601 - environment: - ELASTICSEARCH_HOSTS: http://exceptionless:9200 volumes: ex_esdata: diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index 2185c9140b..689fb8411c 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -60,11 +60,7 @@ public static IResourceBuilder AddElasticsearch(this IDis .PublishAsConnectionString(); } - public static IResourceBuilder WithKibana( - this IResourceBuilder builder, - Action>? configureContainer = null, - string? containerName = null, - int? port = null) + public static IResourceBuilder WithKibana(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) { ArgumentNullException.ThrowIfNull(builder); @@ -83,7 +79,7 @@ public static IResourceBuilder WithKibana( var resourceBuilder = builder.ApplicationBuilder.AddResource(resource) .WithImage(ElasticsearchContainerImageTags.KibanaImage, ElasticsearchContainerImageTags.Tag) .WithImageRegistry(ElasticsearchContainerImageTags.KibanaRegistry) - .WithHttpEndpoint(targetPort: KibanaPort, port: port, name: containerName) + .WithHttpEndpoint(targetPort: KibanaPort, name: containerName) .WithUrlForEndpoint(containerName, u => u.DisplayText = "Kibana") .WithEnvironment("xpack.security.enabled", "false") .WithEnvironment(ctx => @@ -138,17 +134,9 @@ public async Task CheckHealthAsync(HealthCheckContext context using var settings = new ElasticsearchClientSettings(new Uri(connectionString)); var client = new ElasticsearchClient(settings); - var response = await client.Cluster.HealthAsync( - request => request.WaitForStatus(Elastic.Clients.Elasticsearch.HealthStatus.Yellow), - cancellationToken); - bool isReady = response.IsValidResponse - && !response.TimedOut - && response.Status is Elastic.Clients.Elasticsearch.HealthStatus.Yellow or Elastic.Clients.Elasticsearch.HealthStatus.Green; - if (isReady) - return HealthCheckResult.Healthy(); - - return new HealthCheckResult( - context.Registration.FailureStatus, - $"Elasticsearch cluster health check failed. Timed out: {response.TimedOut}; status: {response.Status}. {response.DebugInformation}"); + var response = await client.PingAsync(cancellationToken); + return response.IsValidResponse + ? HealthCheckResult.Healthy() + : new HealthCheckResult(context.Registration.FailureStatus, $"Elasticsearch ping failed: {response.DebugInformation}"); } } diff --git a/src/Exceptionless.AppHost/Program.cs b/src/Exceptionless.AppHost/Program.cs index ef33ee8333..b12b48aefe 100644 --- a/src/Exceptionless.AppHost/Program.cs +++ b/src/Exceptionless.AppHost/Program.cs @@ -13,13 +13,6 @@ bool servicesOnly = HasArgument("--services-only"); bool ciE2E = HasArgument("--ci-e2e"); bool includeDevTools = !ciE2E; -int elasticsearchPort = GetPort("Elasticsearch:Port", 9200); -string elasticsearchImageTag = builder.Configuration["Elasticsearch:ImageTag"] ?? ElasticsearchContainerImageTags.Tag; -string kibanaImageTag = builder.Configuration["Elasticsearch:KibanaImageTag"] ?? ElasticsearchContainerImageTags.Tag; -string elasticsearchContainerName = builder.Configuration["Elasticsearch:ContainerName"] ?? "Exceptionless-Elasticsearch"; -string elasticsearchDataVolume = builder.Configuration["Elasticsearch:DataVolume"] ?? "exceptionless.data.v1"; -string kibanaContainerName = builder.Configuration["Elasticsearch:KibanaContainerName"] ?? "Exceptionless-Kibana"; -int kibanaPort = GetPort("Elasticsearch:KibanaPort", 5601); int oldAppHttpPort = worktreePorts?.OldAppHttp ?? 7120; int oldAppPort = worktreePorts?.OldAppHttps ?? 7121; int oldAppLiveReloadPort = worktreePorts?.OldAppLiveReload ?? 35729; @@ -31,9 +24,8 @@ string exceptionlessServerUrl = worktreePorts?.ApiHttpsUrl ?? $"https://api-ex.dev.localhost:{DefaultApiHttpsPort}"; const string SharedEmailConnectionString = "smtp://localhost:1026"; -var elastic = builder.AddElasticsearch("Elasticsearch", port: elasticsearchPort) - .WithImageTag(elasticsearchImageTag) - .WithDataVolume(elasticsearchDataVolume) +var elastic = builder.AddElasticsearch("Elasticsearch", port: 9200) + .WithDataVolume("exceptionless.data.v1") .WithEndpointProxySupport(false); var storage = builder.AddAzureStorage("Storage") @@ -78,17 +70,15 @@ var ownedElastic = elastic; elastic = ownedElastic .WithLifetime(ContainerLifetime.Persistent) - .WithContainerName(elasticsearchContainerName); + .WithContainerName("Exceptionless-Elasticsearch"); if (!servicesOnly && includeDevTools) { elastic = elastic.WithKibana(b => b - .WithImageTag(kibanaImageTag) .WithLifetime(ContainerLifetime.Persistent) .WithEndpointProxySupport(false) - .WithContainerName(kibanaContainerName) - .WithParentRelationship(ownedElastic), - port: kibanaPort); + .WithContainerName("Exceptionless-Kibana") + .WithParentRelationship(ownedElastic)); } var ownedCache = cache; @@ -252,15 +242,3 @@ await builder.Build().RunAsync(); bool HasArgument(string name) => args.Any(arg => StringComparer.OrdinalIgnoreCase.Equals(arg, name) || StringComparer.OrdinalIgnoreCase.Equals(arg, name.TrimStart('-'))); - -int GetPort(string key, int defaultValue) -{ - string? value = builder.Configuration[key]; - if (String.IsNullOrWhiteSpace(value)) - return defaultValue; - - if (!Int32.TryParse(value, out int port) || port is < 1 or > 65535) - throw new InvalidOperationException($"Configuration value '{key}' must be a valid TCP port."); - - return port; -} diff --git a/tests/Exceptionless.Tests/AppHostConfigurationTests.cs b/tests/Exceptionless.Tests/AppHostConfigurationTests.cs deleted file mode 100644 index 3f5d49b9ee..0000000000 --- a/tests/Exceptionless.Tests/AppHostConfigurationTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Aspire.Hosting; -using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Testing; -using Xunit; - -namespace Exceptionless.Tests; - -public class AppHostConfigurationTests -{ - [Fact] - public async Task CreateAsync_WithSeparateElasticsearchAndKibanaOverrides_UsesIndependentImageTags() - { - const string elasticsearchImageTag = "8.19.21-sha256-candidate"; - const string kibanaImageTag = "8.19.21"; - var appHost = await DistributedApplicationTestingBuilder.CreateAsync( - [ - $"--Elasticsearch:ImageTag={elasticsearchImageTag}", - $"--Elasticsearch:KibanaImageTag={kibanaImageTag}" - ], - TestContext.Current.CancellationToken); - - var elasticsearch = Assert.Single(appHost.Resources.OfType()); - var kibana = Assert.Single(appHost.Resources.OfType()); - var elasticsearchImage = Assert.Single(elasticsearch.Annotations.OfType()); - var kibanaImage = Assert.Single(kibana.Annotations.OfType()); - - Assert.Equal(elasticsearchImageTag, elasticsearchImage.Tag); - Assert.Equal(kibanaImageTag, kibanaImage.Tag); - } -} diff --git a/tests/Exceptionless.Tests/AppWebHostFactory.cs b/tests/Exceptionless.Tests/AppWebHostFactory.cs index 8f6fbdbd75..c08f21687f 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactory.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactory.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Net; -using System.Text.Json; using Aspire.Hosting; using Aspire.Hosting.Testing; using Exceptionless.Core; @@ -22,7 +21,7 @@ namespace Exceptionless.Tests; public class AppWebHostFactory : WebApplicationFactory, IAsyncLifetime { - private static readonly string SharedElasticsearchUrl = GetSharedElasticsearchUrl(); + private const string SharedElasticsearchUrl = "http://localhost:9200"; private static readonly TimeSpan SharedElasticsearchStartupTimeout = TimeSpan.FromMinutes(3); private static int s_counter = -1; private static readonly Lazy> s_sharedAppHost = new(StartSharedAppHostAsync, LazyThreadSafetyMode.ExecutionAndPublication); @@ -59,40 +58,22 @@ private static async Task StartSharedAppHostAsync() return app; } - private static string GetSharedElasticsearchUrl() - { - const int defaultPort = 9200; - string? configuredPort = Environment.GetEnvironmentVariable("Elasticsearch__Port"); - if (String.IsNullOrWhiteSpace(configuredPort)) - return $"http://localhost:{defaultPort}"; - - if (!Int32.TryParse(configuredPort, out int port) || port is < 1 or > 65535) - throw new InvalidOperationException("Environment variable 'Elasticsearch__Port' must be a valid TCP port."); - - return $"http://localhost:{port}"; - } - private static async Task WaitForElasticsearchAsync(Uri elasticsearchUri) { - using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(1) }; var deadline = TimeProvider.System.GetUtcNow() + SharedElasticsearchStartupTimeout; - var healthUri = new Uri(elasticsearchUri, "/_cluster/health?wait_for_status=yellow&timeout=1s"); while (TimeProvider.System.GetUtcNow() < deadline) { try { - using var response = await client.GetAsync(healthUri); - using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); - if (IsElasticsearchReady(response.StatusCode, document.RootElement)) + using var response = await client.GetAsync(elasticsearchUri); + if (response.StatusCode == HttpStatusCode.OK) return; } catch (HttpRequestException) { } - catch (JsonException) - { - } catch (TaskCanceledException) { } @@ -103,20 +84,6 @@ private static async Task WaitForElasticsearchAsync(Uri elasticsearchUri) throw new TimeoutException("Timed out waiting for the shared Elasticsearch container to be ready."); } - internal static bool IsElasticsearchReady(HttpStatusCode statusCode, JsonElement health) - { - if (statusCode != HttpStatusCode.OK) - return false; - - bool requestCompleted = health.TryGetProperty("timed_out", out var timedOut) - && timedOut.ValueKind == JsonValueKind.False; - bool clusterReady = health.TryGetProperty("status", out var status) - && status.ValueKind == JsonValueKind.String - && status.GetString() is "yellow" or "green"; - - return requestCompleted && clusterReady; - } - protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment(Environments.Development); diff --git a/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs b/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs index 539785b681..7fef487faa 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs @@ -1,6 +1,4 @@ -using System.Net; using System.Text; -using System.Text.Json; using Foundatio.Storage; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -9,22 +7,6 @@ namespace Exceptionless.Tests; public sealed class AppWebHostFactoryTests { - [Theory] - [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"yellow"}""", true)] - [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"green"}""", true)] - [InlineData(HttpStatusCode.OK, """{"timed_out":true,"status":"yellow"}""", false)] - [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"red"}""", false)] - [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":1}""", false)] - [InlineData(HttpStatusCode.ServiceUnavailable, """{"timed_out":false,"status":"yellow"}""", false)] - public void IsElasticsearchReady_ClusterHealthResponse_ReturnsExpectedResult(HttpStatusCode statusCode, string json, bool expected) - { - using var document = JsonDocument.Parse(json); - - bool isReady = AppWebHostFactory.IsElasticsearchReady(statusCode, document.RootElement); - - Assert.Equal(expected, isReady); - } - [Fact] public async Task ConfigureWebHost_MultipleFactories_IsolatesFileStorageByAppScope() {