Skip to content

Commit 60839c0

Browse files
ejsmithniemyjski
andauthored
Switch to using Aspire (#1784)
* Working on migrating to aspire * Progress * Some aspire progress * Fix bad merge * Update to Aspire 9 * Fix duplicate project refs * Update elasticsearch to 8.16.1 * Add storage to Aspire * Update Elasticsearch * Fix tests * Revert some changes. Fix linting. * Revert more changes * Cleanup * Use the right Elasticsearch docker image * Use explicit minio version * Fixed launch setting * Removed start and stop services * Use fixed web client ports * Use S3 storage when running local * Fixed an issue where code could throw due to CurrentUser * Fix S3 * [BREAKING] Remove scope prefix from bucket names and instead use scoped file storage for app scopes * Only poll queue metrics in the same process that is running the stack event count job * Reverted some of the breaking changes around storage. --------- Co-authored-by: Blake Niemyjski <[email protected]>
1 parent 77bfc5e commit 60839c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+924
-210
lines changed

Dockerfile

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ COPY ./*.sln ./NuGet.Config ./
55
COPY ./src/*.props ./src/
66
COPY ./tests/*.props ./tests/
77
COPY ./build/packages/* ./build/packages/
8-
COPY ./docker/docker-compose.dcproj ./docker/
98

109
# Copy the main source project files
1110
COPY src/*/*.csproj ./

Exceptionless.sln

+6-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1111
.github\workflows\build.yaml = .github\workflows\build.yaml
1212
CONTRIBUTING.md = CONTRIBUTING.md
1313
src\Directory.Build.props = src\Directory.Build.props
14-
docker\docker-compose.yml = docker\docker-compose.yml
1514
Dockerfile = Dockerfile
1615
exceptionless.http = exceptionless.http
1716
global.json = global.json
@@ -28,8 +27,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.Tests", "test
2827
EndProject
2928
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.Job", "src\Exceptionless.Job\Exceptionless.Job.csproj", "{788BA00C-FFBE-42A9-92A3-89E24FC137B5}"
3029
EndProject
31-
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker\docker-compose.dcproj", "{9F933018-9E8B-4649-8C9A-D217B5E1C184}"
32-
EndProject
3330
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "http", "http", "{97ED03A0-8C49-4B15-8D93-C56AF4DDC30F}"
3431
ProjectSection(SolutionItems) = preProject
3532
tests\http\admin.http = tests\http\admin.http
@@ -44,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "http", "http", "{97ED03A0-8
4441
tests\http\webhooks.http = tests\http\webhooks.http
4542
EndProjectSection
4643
EndProject
44+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exceptionless.AppHost", "src\Exceptionless.AppHost\Exceptionless.AppHost.csproj", "{EB1AF004-A00D-4016-BA97-5E89177B0074}"
45+
EndProject
4746
Global
4847
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4948
Debug|Any CPU = Debug|Any CPU
@@ -70,10 +69,10 @@ Global
7069
{788BA00C-FFBE-42A9-92A3-89E24FC137B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
7170
{788BA00C-FFBE-42A9-92A3-89E24FC137B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
7271
{788BA00C-FFBE-42A9-92A3-89E24FC137B5}.Release|Any CPU.Build.0 = Release|Any CPU
73-
{9F933018-9E8B-4649-8C9A-D217B5E1C184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
74-
{9F933018-9E8B-4649-8C9A-D217B5E1C184}.Debug|Any CPU.Build.0 = Debug|Any CPU
75-
{9F933018-9E8B-4649-8C9A-D217B5E1C184}.Release|Any CPU.ActiveCfg = Release|Any CPU
76-
{9F933018-9E8B-4649-8C9A-D217B5E1C184}.Release|Any CPU.Build.0 = Release|Any CPU
72+
{EB1AF004-A00D-4016-BA97-5E89177B0074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
73+
{EB1AF004-A00D-4016-BA97-5E89177B0074}.Debug|Any CPU.Build.0 = Debug|Any CPU
74+
{EB1AF004-A00D-4016-BA97-5E89177B0074}.Release|Any CPU.ActiveCfg = Release|Any CPU
75+
{EB1AF004-A00D-4016-BA97-5E89177B0074}.Release|Any CPU.Build.0 = Release|Any CPU
7776
EndGlobalSection
7877
GlobalSection(SolutionProperties) = preSolution
7978
HideSolutionNode = FALSE

docker/docker-compose.dcproj

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
4+
5+
<PropertyGroup>
6+
<OutputType>Exe</OutputType>
7+
<TargetFramework>net9.0</TargetFramework>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<Nullable>enable</Nullable>
10+
<IsAspireHost>true</IsAspireHost>
11+
<UserSecretsId>a9c2ddcc-e51d-4cd1-9782-96e1d74eec87</UserSecretsId>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
16+
<PackageReference Include="Aspire.Hosting.NodeJs" Version="9.0.0" />
17+
<PackageReference Include="Aspire.Hosting.Redis" Version="9.0.0" />
18+
<PackageReference Include="AspNetCore.HealthChecks.Elasticsearch" Version="8.0.1" />
19+
<PackageReference Include="Foundatio.AWS" Version="11.0.6" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\Exceptionless.Job\Exceptionless.Job.csproj" />
24+
<ProjectReference Include="..\Exceptionless.Web\Exceptionless.Web.csproj" />
25+
</ItemGroup>
26+
27+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using Aspire.Hosting.Lifecycle;
2+
using Aspire.Hosting.Utils;
3+
using HealthChecks.Elasticsearch;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Diagnostics.HealthChecks;
6+
7+
namespace Aspire.Hosting;
8+
9+
/// <summary>
10+
/// Provides extension methods for adding Elasticsearch resources to the application model.
11+
/// </summary>
12+
public static class ElasticsearchBuilderExtensions
13+
{
14+
private const int ElasticsearchPort = 9200;
15+
private const int ElasticsearchInternalPort = 9300;
16+
private const int KibanaPort = 5601;
17+
18+
/// <summary>
19+
/// 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.17.0 tag of the Elasticsearch container image
20+
/// </summary>
21+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
22+
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
23+
/// <param name="port">The host port to bind the underlying container to.</param>
24+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
25+
public static IResourceBuilder<ElasticsearchResource> AddElasticsearch(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null)
26+
{
27+
ArgumentNullException.ThrowIfNull(builder);
28+
ArgumentNullException.ThrowIfNull(name);
29+
30+
var elasticsearch = new ElasticsearchResource(name);
31+
32+
string? connectionString = null;
33+
ElasticsearchOptions? options = null;
34+
35+
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(elasticsearch, async (@event, ct) =>
36+
{
37+
connectionString = await elasticsearch.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
38+
if (connectionString is null)
39+
{
40+
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{elasticsearch.Name}' resource but the connection string was null.");
41+
}
42+
43+
options = new ElasticsearchOptions();
44+
options.UseServer(connectionString);
45+
});
46+
47+
var healthCheckKey = $"{name}_check";
48+
builder.Services.AddHealthChecks()
49+
.Add(new HealthCheckRegistration(
50+
healthCheckKey,
51+
sp => new ElasticsearchHealthCheck(options!),
52+
failureStatus: default,
53+
tags: default,
54+
timeout: default));
55+
56+
return builder.AddResource(elasticsearch)
57+
.WithImage(ElasticsearchContainerImageTags.Image, ElasticsearchContainerImageTags.Tag)
58+
.WithImageRegistry(ElasticsearchContainerImageTags.ElasticsearchRegistry)
59+
.WithHttpEndpoint(targetPort: ElasticsearchPort, port: port, name: ElasticsearchResource.PrimaryEndpointName)
60+
.WithEndpoint(targetPort: ElasticsearchInternalPort, name: ElasticsearchResource.InternalEndpointName)
61+
.WithEnvironment("discovery.type", "single-node")
62+
.WithEnvironment("xpack.security.enabled", "false")
63+
.WithEnvironment("action.destructive_requires_name", "false")
64+
.WithEnvironment("ES_JAVA_OPTS", "-Xms1g -Xmx1g")
65+
.WithHealthCheck(healthCheckKey)
66+
.PublishAsConnectionString();
67+
}
68+
69+
public static IResourceBuilder<ElasticsearchResource> WithKibana(this IResourceBuilder<ElasticsearchResource> builder, Action<IResourceBuilder<KibanaResource>>? configureContainer = null, string? containerName = null)
70+
{
71+
ArgumentNullException.ThrowIfNull(builder);
72+
73+
if (builder.ApplicationBuilder.Resources.OfType<KibanaResource>().SingleOrDefault() is { } existingKibanaResource)
74+
{
75+
var builderForExistingResource = builder.ApplicationBuilder.CreateResourceBuilder(existingKibanaResource);
76+
configureContainer?.Invoke(builderForExistingResource);
77+
return builder;
78+
}
79+
else
80+
{
81+
containerName ??= $"{builder.Resource.Name}-kibana";
82+
83+
builder.ApplicationBuilder.Services.TryAddLifecycleHook<KibanaConfigWriterHook>();
84+
85+
var resource = new KibanaResource(containerName);
86+
var resourceBuilder = builder.ApplicationBuilder.AddResource(resource)
87+
.WithImage(ElasticsearchContainerImageTags.KibanaImage, ElasticsearchContainerImageTags.Tag)
88+
.WithImageRegistry(ElasticsearchContainerImageTags.KibanaRegistry)
89+
.WithHttpEndpoint(targetPort: KibanaPort, name: containerName)
90+
.WithEnvironment("xpack.security.enabled", "false")
91+
.ExcludeFromManifest();
92+
93+
configureContainer?.Invoke(resourceBuilder);
94+
95+
return builder;
96+
}
97+
}
98+
99+
public static IResourceBuilder<ElasticsearchResource> WithDataVolume(this IResourceBuilder<ElasticsearchResource> builder, string? name = null)
100+
{
101+
ArgumentNullException.ThrowIfNull(builder);
102+
103+
return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/usr/share/elasticsearch/data");
104+
}
105+
106+
public static IResourceBuilder<ElasticsearchResource> WithDataBindMount(this IResourceBuilder<ElasticsearchResource> builder, string source)
107+
{
108+
ArgumentNullException.ThrowIfNull(builder);
109+
ArgumentNullException.ThrowIfNull(source);
110+
111+
return builder.WithBindMount(source, "/usr/share/elasticsearch/data");
112+
}
113+
}
114+
115+
internal static class ElasticsearchContainerImageTags
116+
{
117+
public const string ElasticsearchRegistry = "docker.io";
118+
public const string Image = "exceptionless/elasticsearch";
119+
public const string KibanaRegistry = "docker.elastic.co";
120+
public const string KibanaImage = "kibana/kibana";
121+
public const string Tag = "8.17.0";
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
namespace Aspire.Hosting;
2+
3+
/// <summary>
4+
/// A resource that represents a Elasticsearch resource independent of the hosting model.
5+
/// </summary>
6+
public class ElasticsearchResource : ContainerResource, IResourceWithConnectionString
7+
{
8+
// this endpoint is used for all API calls over HTTP.
9+
// This includes search and aggregations, monitoring and anything else that uses a HTTP request.
10+
// All client libraries will use this port to talk to Elasticsearch
11+
internal const string PrimaryEndpointName = "http";
12+
13+
//this endpoint is a custom binary protocol used for communications between nodes in a cluster.
14+
//For things like cluster updates, master elections, nodes joining/leaving, shard allocation
15+
internal const string InternalEndpointName = "internal";
16+
17+
/// <param name="name">The name of the resource.</param>
18+
public ElasticsearchResource(string name) : base(name)
19+
{
20+
}
21+
22+
private EndpointReference? _primaryEndpoint;
23+
private EndpointReference? _internalEndpoint;
24+
25+
/// <summary>
26+
/// Gets the primary endpoint for the Elasticsearch. This endpoint is used for all API calls over HTTP.
27+
/// </summary>
28+
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);
29+
30+
/// <summary>
31+
/// Gets the internal endpoint for the Elasticsearch. This endpoint used for communications between nodes in a cluster
32+
/// </summary>
33+
public EndpointReference InternalEndpoint => _internalEndpoint ??= new(this, InternalEndpointName);
34+
35+
/// <summary>
36+
/// Gets the connection string expression for the Elasticsearch
37+
/// </summary>
38+
public ReferenceExpression ConnectionString =>
39+
ReferenceExpression.Create($"http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
40+
41+
42+
/// <summary>
43+
/// Gets the connection string expression for the Elasticsearch server for the manifest.
44+
/// </summary>
45+
public ReferenceExpression ConnectionStringExpression
46+
{
47+
get
48+
{
49+
if (this.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var connectionStringAnnotation))
50+
{
51+
return connectionStringAnnotation.Resource.ConnectionStringExpression;
52+
}
53+
54+
return ConnectionString;
55+
}
56+
}
57+
58+
/// <summary>
59+
/// Gets the connection string for the Elasticsearch server.
60+
/// </summary>
61+
/// <param name="cancellationToken"> A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
62+
/// <returns>A connection string for the Elasticsearch server in the form "http://host:port".</returns>
63+
public ValueTask<string?> GetConnectionStringAsync(CancellationToken cancellationToken = default)
64+
{
65+
if (this.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var connectionStringAnnotation))
66+
{
67+
return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken);
68+
}
69+
70+
return ConnectionString.GetValueAsync(cancellationToken);
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Text;
2+
using Aspire.Hosting.Lifecycle;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace Aspire.Hosting;
6+
7+
internal class KibanaConfigWriterHook : IDistributedApplicationLifecycleHook
8+
{
9+
public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
10+
{
11+
if (appModel.Resources.OfType<KibanaResource>().SingleOrDefault() is not { } kibanaResource)
12+
return;
13+
14+
var elasticsearchInstances = appModel.Resources.OfType<ElasticsearchResource>();
15+
16+
if (!elasticsearchInstances.Any())
17+
return;
18+
19+
var hostsVariableBuilder = new StringBuilder();
20+
21+
foreach (var elasticsearchInstance in elasticsearchInstances)
22+
{
23+
if (elasticsearchInstance.PrimaryEndpoint.IsAllocated)
24+
{
25+
var connectionString = await elasticsearchInstance.GetConnectionStringAsync();
26+
if (hostsVariableBuilder.Length > 0)
27+
hostsVariableBuilder.Append(",");
28+
hostsVariableBuilder.Append(elasticsearchInstance.PrimaryEndpoint.Scheme).Append("://").Append(elasticsearchInstance.PrimaryEndpoint.ContainerHost).Append(":").Append(elasticsearchInstance.PrimaryEndpoint.Port);
29+
}
30+
}
31+
32+
kibanaResource.Annotations.Add(new EnvironmentCallbackAnnotation(context =>
33+
{
34+
context.EnvironmentVariables.Add("ELASTICSEARCH_HOSTS", hostsVariableBuilder.ToString());
35+
}));
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Aspire.Hosting;
2+
3+
/// <summary>
4+
/// A resource that represents a Kibana container.
5+
/// </summary>
6+
/// <param name="name">The name of the resource.</param>
7+
public class KibanaResource(string name) : ContainerResource(name)
8+
{
9+
}

0 commit comments

Comments
 (0)