Skip to content

Commit 1ca2296

Browse files
committed
Starting to play with Aspire
1 parent 883d75b commit 1ca2296

File tree

7 files changed

+234
-0
lines changed

7 files changed

+234
-0
lines changed

Exceptionless.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ 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.dev.yml = docker\docker-compose.dev.yml
1415
docker\docker-compose.yml = docker\docker-compose.yml
1516
Dockerfile = Dockerfile
1617
exceptionless.http = exceptionless.http
@@ -44,6 +45,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "http", "http", "{97ED03A0-8
4445
tests\http\webhooks.http = tests\http\webhooks.http
4546
EndProjectSection
4647
EndProject
48+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exceptionless.AppHost", "src\Exceptionless.AppHost\Exceptionless.AppHost.csproj", "{E730BF07-50D3-48C4-9C39-45D8BCB2A35A}"
49+
EndProject
4750
Global
4851
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4952
Debug|Any CPU = Debug|Any CPU
@@ -74,6 +77,10 @@ Global
7477
{9F933018-9E8B-4649-8C9A-D217B5E1C184}.Debug|Any CPU.Build.0 = Debug|Any CPU
7578
{9F933018-9E8B-4649-8C9A-D217B5E1C184}.Release|Any CPU.ActiveCfg = Release|Any CPU
7679
{9F933018-9E8B-4649-8C9A-D217B5E1C184}.Release|Any CPU.Build.0 = Release|Any CPU
80+
{E730BF07-50D3-48C4-9C39-45D8BCB2A35A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
81+
{E730BF07-50D3-48C4-9C39-45D8BCB2A35A}.Debug|Any CPU.Build.0 = Debug|Any CPU
82+
{E730BF07-50D3-48C4-9C39-45D8BCB2A35A}.Release|Any CPU.ActiveCfg = Release|Any CPU
83+
{E730BF07-50D3-48C4-9C39-45D8BCB2A35A}.Release|Any CPU.Build.0 = Release|Any CPU
7784
EndGlobalSection
7885
GlobalSection(SolutionProperties) = preSolution
7986
HideSolutionNode = FALSE
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using System.Text;
2+
using Aspire.Hosting.Lifecycle;
3+
using Aspire.Hosting.Utils;
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace Aspire.Hosting;
7+
8+
/// <summary>
9+
/// Provides extension methods for adding Elasticsearch resources to the application model.
10+
/// </summary>
11+
public static class ElasticsearchBuilderExtensions
12+
{
13+
/// <summary>
14+
/// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch" and tag is "latest". This version the package defaults to the 8.12.2 tag of the redis container image
15+
/// </summary>
16+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
17+
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
18+
/// <param name="port">The host port to bind the underlying container to.</param>
19+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
20+
public static IResourceBuilder<ElasticsearchResource> AddElasticsearch(this IDistributedApplicationBuilder builder, string name, int? port = null)
21+
{
22+
var redis = new ElasticsearchResource(name);
23+
return builder.AddResource(redis)
24+
.WithHttpEndpoint(containerPort: 9200)
25+
.WithAnnotation(new ContainerImageAnnotation { Image = "docker.elastic.co/elasticsearch/elasticsearch", Tag = "8.12.2" })
26+
.WithEnvironment("discovery.type", "single-node")
27+
//.WithEnvironment("network.host", "0.0.0.0")
28+
.WithEnvironment("XPACK_SECURITY_ENABLED", "false")
29+
.WithEnvironment("action.destructive_requires_name", "false")
30+
.WithEnvironment("ES_JAVA_OPTS", "-Xms1g -Xmx1g")
31+
.PublishAsContainer();
32+
}
33+
34+
/// <summary>
35+
/// TODO: Doc Comments
36+
/// </summary>
37+
/// <param name="builder"></param>
38+
/// <param name="containerName"></param>
39+
/// <param name="hostPort"></param>
40+
/// <returns></returns>
41+
public static IResourceBuilder<ElasticsearchResource> WithKibana(this IResourceBuilder<ElasticsearchResource> builder, string? containerName = null, int? hostPort = null)
42+
{
43+
if (builder.ApplicationBuilder.Resources.OfType<KibanaResource>().Any())
44+
{
45+
return builder;
46+
}
47+
48+
builder.ApplicationBuilder.Services.TryAddLifecycleHook<KibanaConfigWriterHook>();
49+
50+
containerName ??= $"{builder.Resource.Name}-kibana";
51+
52+
var resource = new KibanaResource(containerName);
53+
builder.ApplicationBuilder.AddResource(resource)
54+
.WithAnnotation(new ContainerImageAnnotation { Image = "docker.elastic.co/kibana/kibana", Tag = "8.12.2" })
55+
.WithEnvironment("XPACK_SECURITY_ENABLED", "false")
56+
.WithHttpEndpoint(containerPort: 5601, hostPort: hostPort, name: containerName)
57+
.ExcludeFromManifest();
58+
59+
return builder;
60+
}
61+
}
62+
63+
/// <summary>
64+
/// A resource that represents a Elasticsearch resource independent of the hosting model.
65+
/// </summary>
66+
/// <param name="name">The name of the resource.</param>
67+
public class ElasticsearchResource(string name) : ContainerResource(name), IResourceWithConnectionString
68+
{
69+
/// <summary>
70+
/// Gets the connection string expression for the Elasticsearch server for the manifest.
71+
/// </summary>
72+
public string? ConnectionStringExpression
73+
{
74+
get
75+
{
76+
if (this.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var connectionStringAnnotation))
77+
{
78+
return connectionStringAnnotation.Resource.ConnectionStringExpression;
79+
}
80+
81+
return $"{{{Name}.bindings.tcp.host}}:{{{Name}.bindings.tcp.port}}";
82+
}
83+
}
84+
85+
/// <summary>
86+
/// Gets the connection string for the Elasticsearch server.
87+
/// </summary>
88+
/// <returns>A connection string for the redis server in the form "host:port".</returns>
89+
public string? GetConnectionString()
90+
{
91+
if (this.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var connectionStringAnnotation))
92+
{
93+
return connectionStringAnnotation.Resource.GetConnectionString();
94+
}
95+
96+
if (!this.TryGetAnnotationsOfType<AllocatedEndpointAnnotation>(out var allocatedEndpoints))
97+
{
98+
throw new DistributedApplicationException("Elasticsearch resource does not have endpoint annotation.");
99+
}
100+
101+
// We should only have one endpoint for Elasticsearch for local scenarios.
102+
var endpoint = allocatedEndpoints.Single();
103+
return endpoint.EndPointString;
104+
}
105+
}
106+
107+
/// <summary>
108+
/// A resource that represents a Kibana container.
109+
/// </summary>
110+
/// <param name="name">The name of the resource.</param>
111+
public class KibanaResource(string name) : ContainerResource(name)
112+
{
113+
}
114+
115+
internal class KibanaConfigWriterHook(IConfiguration configuration) : IDistributedApplicationLifecycleHook
116+
{
117+
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
118+
{
119+
if (appModel.Resources.OfType<KibanaResource>().SingleOrDefault() is not { } kibanaResource)
120+
{
121+
// No-op if there is no commander resource (removed after hook added).
122+
return Task.CompletedTask;
123+
}
124+
125+
var elasticsearchInstances = appModel.Resources.OfType<ElasticsearchResource>();
126+
127+
if (!elasticsearchInstances.Any())
128+
{
129+
// No-op if there are no Elasticsearch resources present.
130+
return Task.CompletedTask;
131+
}
132+
133+
var containerHostName = HostNameResolver.ReplaceLocalhostWithContainerHost("localhost", configuration);
134+
135+
var hostsVariableBuilder = new StringBuilder();
136+
137+
foreach (var elasticsearchInstance in elasticsearchInstances)
138+
{
139+
if (elasticsearchInstance.TryGetAllocatedEndPoints(out var allocatedEndpoints))
140+
{
141+
var endpoint = allocatedEndpoints.Where(ae => ae.Name == "http").Single();
142+
143+
var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}http://{containerHostName}:{endpoint.Port}";
144+
hostsVariableBuilder.Append(hostString);
145+
}
146+
}
147+
148+
kibanaResource.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) =>
149+
{
150+
context.EnvironmentVariables.Add("ELASTICSEARCH_HOSTS", hostsVariableBuilder.ToString());
151+
}));
152+
153+
return Task.CompletedTask;
154+
}
155+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsAspireHost>true</IsAspireHost>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Aspire.Hosting" Version="8.0.0-preview.4.24156.9" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\Exceptionless.Job\Exceptionless.Job.csproj" />
17+
<ProjectReference Include="..\Exceptionless.Web\Exceptionless.Web.csproj" />
18+
</ItemGroup>
19+
20+
</Project>

src/Exceptionless.AppHost/Program.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
var builder = DistributedApplication.CreateBuilder(args);
2+
3+
var elastic = builder.AddElasticsearch("Elastic")
4+
.WithKibana();
5+
6+
var cache = builder.AddRedis("Redis")
7+
.WithRedisCommander();
8+
9+
var job = builder.AddProject<Projects.Exceptionless_Job>("Jobs")
10+
.WithReference(cache)
11+
.WithReference(elastic)
12+
.WithLaunchProfile("AllJobs");
13+
14+
var api = builder.AddProject<Projects.Exceptionless_Web>("Api")
15+
.WithReference(cache)
16+
.WithReference(elastic)
17+
.WithLaunchProfile("Exceptionless API");
18+
19+
builder.Build().Run();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "http://localhost:15212",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development",
11+
"DOTNET_ENVIRONMENT": "Development",
12+
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16298"
13+
}
14+
}
15+
}
16+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning",
6+
"Aspire.Hosting.Dcp": "Warning"
7+
}
8+
}
9+
}

0 commit comments

Comments
 (0)