Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ jobs:
- 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 }}
Expand All @@ -145,7 +144,7 @@ jobs:
build_locally=false

if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] &&
! git diff --quiet "$PR_BASE_SHA"...HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then
! git diff --quiet origin/main...HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.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/9.x .github/workflows/elasticsearch-docker-9.yml; then
Expand Down Expand Up @@ -362,8 +361,8 @@ jobs:
- name: Wait for Aspire Resources
run: |
for attempt in {1..60}; do
if curl -fksS https://web-ex.dev.localhost:7131/api/v2/about > /dev/null &&
curl -fksS https://web-ex.dev.localhost:7131/next/login > /dev/null; then
if curl -fksS --connect-timeout 5 --max-time 10 https://web-ex.dev.localhost:7131/api/v2/about > /dev/null &&
curl -fksS --connect-timeout 5 --max-time 10 https://web-ex.dev.localhost:7131/next/login > /dev/null; then
break
fi

Expand All @@ -382,8 +381,8 @@ jobs:

- name: Verify E2E Endpoints
run: |
curl -fksS https://web-ex.dev.localhost:7131/api/v2/about > /dev/null
curl -fksS https://web-ex.dev.localhost:7131/next/login > /dev/null
curl -fksS --connect-timeout 5 --max-time 10 https://web-ex.dev.localhost:7131/api/v2/about > /dev/null
curl -fksS --connect-timeout 5 --max-time 10 https://web-ex.dev.localhost:7131/next/login > /dev/null

- name: Run Playwright E2E Tests
working-directory: src/Exceptionless.Web/ClientApp
Expand Down
1 change: 0 additions & 1 deletion src/Exceptionless.AppHost/Exceptionless.AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<PackageReference Include="Aspire.Hosting.Browsers" Version="13.4.6-preview.1.26319.6" />
<PackageReference Include="Aspire.Hosting.JavaScript" Version="13.4.6" />
<PackageReference Include="Aspire.Hosting.Redis" Version="13.4.6" />
<PackageReference Include="AspNetCore.HealthChecks.Elasticsearch" Version="9.0.0" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.Deno" Version="13.4.0" />
</ItemGroup>

Expand Down
36 changes: 21 additions & 15 deletions src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Elastic.Clients.Elasticsearch;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

Expand Down Expand Up @@ -136,19 +136,25 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
if (string.IsNullOrEmpty(connectionString))
return new HealthCheckResult(context.Registration.FailureStatus, "Connection string not available.");

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}");
try
{
using var client = new HttpClient { BaseAddress = new Uri(connectionString), Timeout = TimeSpan.FromSeconds(10) };
using var response = await client.GetAsync("_cluster/health?wait_for_status=yellow&timeout=5s", cancellationToken);
if (!response.IsSuccessStatusCode)
return new HealthCheckResult(context.Registration.FailureStatus, $"Elasticsearch cluster health returned HTTP {(int)response.StatusCode}.");

await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var document = await JsonDocument.ParseAsync(responseStream, cancellationToken: cancellationToken);
bool timedOut = document.RootElement.TryGetProperty("timed_out", out var timedOutElement) && timedOutElement.GetBoolean();
string? status = document.RootElement.TryGetProperty("status", out var statusElement) ? statusElement.GetString() : null;
if (!timedOut && status is "yellow" or "green")
return HealthCheckResult.Healthy();

return new HealthCheckResult(context.Registration.FailureStatus, $"Elasticsearch cluster health check failed. Timed out: {timedOut}; status: {status ?? "unknown"}.");
}
catch (Exception ex) when (ex is HttpRequestException or JsonException or TaskCanceledException)
{
return new HealthCheckResult(context.Registration.FailureStatus, "Elasticsearch cluster health check request failed.", ex);
}
}
}
7 changes: 7 additions & 0 deletions src/Exceptionless.AppHost/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"Elasticsearch": {
"Port": 9215,
"ContainerName": "Exceptionless-Elasticsearch-9-5-Lookup",
"DataVolume": "exceptionless.elasticsearch-9-5-lookup.data.v1",
"KibanaPort": 5615,
"KibanaContainerName": "Exceptionless-Kibana-9-5-Lookup"
},
"Logging": {
"LogLevel": {
"Default": "Information",
Expand Down
1 change: 1 addition & 0 deletions src/Exceptionless.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO

services.AddSingleton<ExceptionlessElasticConfiguration>();
services.AddSingleton<ElasticsearchClient>(s => s.GetRequiredService<ExceptionlessElasticConfiguration>().Client);
services.AddSingleton<IStackRollupSearchService, StackRollupSearchService>();
services.AddSingleton<IElasticConfiguration>(s => s.GetRequiredService<ExceptionlessElasticConfiguration>());
services.AddStartupAction<ExceptionlessElasticConfiguration>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed class StackIndex : VersionedIndex<Stack>

private readonly ExceptionlessElasticConfiguration _configuration;

public StackIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "stacks", 1)
public StackIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "stacks", 2)
{
_configuration = configuration;
}
Expand All @@ -27,11 +27,28 @@ public override void ConfigureIndex(CreateIndexRequestDescriptor idx)
base.ConfigureIndex(idx);
idx.Settings(s => s
.Analysis(a => BuildAnalysis(a))
.NumberOfShards(_configuration.Options.NumberOfShards)
.Mode("lookup")
.NumberOfShards(1)
.NumberOfReplicas(_configuration.Options.NumberOfReplicas)
.Priority(5));
}

protected override Task UpdateIndexAsync(string name, Action<PutIndicesSettingsRequestDescriptor>? descriptor = null)
{
if (descriptor is not null)
return base.UpdateIndexAsync(name, descriptor);

// index.mode is a final creation-only setting. Foundatio derives updates from
// ConfigureIndex, so keep the mutable settings explicit for existing indexes.
return base.UpdateIndexAsync(name, update => update
.Reopen(true)
.Settings(new IndexSettings
{
NumberOfReplicas = _configuration.Options.NumberOfReplicas,
Priority = 5
}));
}

public override void ConfigureIndexMapping(TypeMappingDescriptor<Stack> map)
{
map
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ public EventStackFilter()
_stackQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(f => !stackFields.Contains(f)));
_stackQueryVisitor.AddVisitor(new CleanupQueryVisitor());
// handles stack special fields and changing event field names to their stack equivalent
_stackQueryVisitor.AddVisitor(new StackFilterQueryVisitor());
_stackQueryVisitor.AddVisitor(new StackFilterQueryVisitor(_stackOnlyFields.Union(_stackOnlySpecialFields)));
_stackQueryVisitor.AddVisitor(new CleanupQueryVisitor());

_invertedStackQueryVisitor = new ChainedQueryVisitor();
// remove everything not in the stack fields list
_invertedStackQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(f => !stackFields.Contains(f)));
_invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor());
// handles stack special fields and changing event field names to their stack equivalent
_invertedStackQueryVisitor.AddVisitor(new StackFilterQueryVisitor());
_invertedStackQueryVisitor.AddVisitor(new StackFilterQueryVisitor(_stackOnlyFields.Union(_stackOnlySpecialFields)));
_invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor());
// inverts the filter
_invertedStackQueryVisitor.AddVisitor(new InvertQueryVisitor(_stackNonInvertedFields));
Expand Down Expand Up @@ -109,13 +109,21 @@ public EventStackFilter()
InvertedFilter = invertedResult?.ToString(),
HasStatus = context.GetBoolean(nameof(StackFilter.HasStatus)),
HasStackIds = context.GetBoolean(nameof(StackFilter.HasStackIds)),
HasStatusOpen = context.GetBoolean(nameof(StackFilter.HasStatusOpen))
HasStatusOpen = context.GetBoolean(nameof(StackFilter.HasStatusOpen)),
HasStackOnlyCriteria = context.GetBoolean(nameof(StackFilter.HasStackOnlyCriteria))
};
}
}

public class StackFilterQueryVisitor : ChainableQueryVisitor
{
private readonly ISet<string> _stackOnlyFields;

public StackFilterQueryVisitor(IEnumerable<string>? stackOnlyFields = null)
{
_stackOnlyFields = new HashSet<string>(stackOnlyFields ?? [], StringComparer.OrdinalIgnoreCase);
}

public override Task<IQueryNode?> VisitAsync(TermNode node, IQueryVisitorContext context)
{
IQueryNode result = node;
Expand All @@ -127,6 +135,9 @@ public class StackFilterQueryVisitor : ChainableQueryVisitor
return Task.FromResult<IQueryNode?>(null);
}

if (_stackOnlyFields.Contains(node.Field))
context.SetValue(nameof(StackFilter.HasStackOnlyCriteria), true);

// process special stack fields
switch (node.Field?.ToLowerInvariant())
{
Expand Down Expand Up @@ -216,4 +227,5 @@ public record StackFilter
public required bool HasStatus { get; set; }
public required bool HasStatusOpen { get; set; }
public required bool HasStackIds { get; set; }
public required bool HasStackOnlyCriteria { get; set; }
}
Loading
Loading