Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
50808ed
WIP.
mitchdenny Sep 1, 2024
f7d199b
WIP
mitchdenny Sep 2, 2024
231e93f
WIP
mitchdenny Sep 2, 2024
5cc25e1
Moved over to using RNS to detecting health.
mitchdenny Sep 2, 2024
50252a9
Connection strings without config.
mitchdenny Sep 2, 2024
d9189b0
Add playground for WaitFor.
mitchdenny Sep 3, 2024
65d4eed
Tidy up capture of connection string in Redis.
mitchdenny Sep 3, 2024
8b46ab0
Progress on WaitFor on Azure resource.
mitchdenny Sep 3, 2024
f98ccb9
Make HealthStatus null by default.
mitchdenny Sep 3, 2024
ee4e611
Remove WaitFor in testshop.
mitchdenny Sep 4, 2024
bd3b710
Put project based dashboard back in.
mitchdenny Sep 4, 2024
e8d8a42
Added ConnectionStringAvailableEvent and fire from within AppExecutor…
mitchdenny Sep 4, 2024
a801683
Forgot Redis extensions.
mitchdenny Sep 4, 2024
f115faf
Fix up manifest.
mitchdenny Sep 4, 2024
d57f7a3
Health check name.
mitchdenny Sep 4, 2024
4c93885
Keep track of resources and surrogate resources to avoid searches.
mitchdenny Sep 5, 2024
bff081e
Get child resources updating correcting in Azure provisioner.
mitchdenny Sep 5, 2024
dc6675e
Clean-up playgrounds.
mitchdenny Sep 5, 2024
4a639e6
PR feedback.
mitchdenny Sep 5, 2024
4d95831
WaitFor works on Postgres database resource.
mitchdenny Sep 6, 2024
7973a2c
Add WithHealthCheck.
mitchdenny Sep 6, 2024
86b5f6a
Update RedisBuilderExtensions.cs
mitchdenny Sep 6, 2024
e0aceae
Remove whitespace change.
mitchdenny Sep 6, 2024
007c2b6
Merge branch 'mitchdenny/healthcheckintegration' of https://github.co…
mitchdenny Sep 6, 2024
0587773
Throw when connection string not available.
mitchdenny Sep 6, 2024
6652a18
Functional tests.
mitchdenny Sep 6, 2024
c55f588
Ignore health check service errors in app host logs for now.
mitchdenny Sep 6, 2024
1f83066
Add some XML doc comments.
mitchdenny Sep 6, 2024
572bcc9
Add remarks to APIs.
mitchdenny Sep 6, 2024
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
12 changes: 7 additions & 5 deletions playground/TestShop/TestShop.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
var builder = DistributedApplication.CreateBuilder(args);

var catalogDb = builder.AddPostgres("postgres")
.WithDataVolume()
.WithPgAdmin()
.AddDatabase("catalogdb");
var postgres = builder.AddPostgres("postgres")
.WithDataVolume()
.WithPgAdmin();

var catalogDb = postgres.AddDatabase("catalogdb");

var basketCache = builder.AddRedis("basketcache")
.WithDataVolume();
Expand All @@ -16,7 +17,8 @@
#endif

var catalogDbApp = builder.AddProject<Projects.CatalogDb>("catalogdbapp")
.WithReference(catalogDb);
.WithReference(catalogDb)
.WaitFor(postgres);

var catalogService = builder.AddProject<Projects.CatalogService>("catalogservice")
.WithReference(catalogDb)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" />
</ItemGroup>
Expand Down
15 changes: 14 additions & 1 deletion src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Postgres;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -38,6 +39,17 @@ public static IResourceBuilder<PostgresServerResource> AddPostgres(this IDistrib
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password");

var postgresServer = new PostgresServerResource(name, userName?.Resource, passwordParameter);

string? connectionString = null;

builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>(async (@event, ct) =>
{
connectionString = await postgresServer.GetConnectionStringAsync(ct).ConfigureAwait(false);
});

builder.Services.AddHealthChecks()
.AddNpgSql(sp => connectionString!, name: $"{name}_check");

return builder.AddResource(postgresServer)
.WithEndpoint(port: port, targetPort: 5432, name: PostgresServerResource.PrimaryEndpointName) // Internal port is always 5432.
.WithImage(PostgresContainerImageTags.Image, PostgresContainerImageTags.Tag)
Expand All @@ -48,7 +60,8 @@ public static IResourceBuilder<PostgresServerResource> AddPostgres(this IDistrib
{
context.EnvironmentVariables[UserEnvVarName] = postgresServer.UserNameReference;
context.EnvironmentVariables[PasswordEnvVarName] = postgresServer.PasswordParameter;
});
})
.WithAnnotation(new HealthCheckAnnotation($"{postgresServer.Name}_check"));
}

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.Redis/Aspire.Hosting.Redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.Redis" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" />
</ItemGroup>
Expand Down
18 changes: 17 additions & 1 deletion src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Redis;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

Expand All @@ -29,10 +30,25 @@ public static IResourceBuilder<RedisResource> AddRedis(this IDistributedApplicat
ArgumentNullException.ThrowIfNull(builder);

var redis = new RedisResource(name);

builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>(async (@event, ct) =>
{
var connectionString = await redis.GetConnectionStringAsync(ct).ConfigureAwait(false);
builder.Configuration["hackedinconnectionstring"] = connectionString;
});

var hcb = builder.Services.AddHealthChecks().AddRedis(sp =>
{
return builder.Configuration["hackedinconnectionstring"]!;
},
name: $"{redis.Name}_check"
);

return builder.AddResource(redis)
.WithEndpoint(port: port, targetPort: 6379, name: RedisResource.PrimaryEndpointName)
.WithImage(RedisContainerImageTags.Image, RedisContainerImageTags.Tag)
.WithImageRegistry(RedisContainerImageTags.Registry);
.WithImageRegistry(RedisContainerImageTags.Registry)
.WithAnnotation(new HealthCheckAnnotation($"{redis.Name}_check"));
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Aspire.Hosting.ApplicationModel;

Expand Down Expand Up @@ -35,6 +36,11 @@ public sealed record CustomResourceSnapshot
/// </summary>
public int? ExitCode { get; init; }

/// <summary>
/// The health status of the resource.
/// </summary>
public HealthStatus HealthStatus { get; init; } = HealthStatus.Unhealthy;

/// <summary>
/// The environment variables that should show up in the dashboard for this resource.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/HealthCheckAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// An annotation which tracks the name of the health check used to detect to health of a resource.
/// </summary>
/// <param name="key">TODO </param>
public class HealthCheckAnnotation(string key) : IResourceAnnotation
{
/// <summary>
/// TODO
/// </summary>
public string Key => key;
}
4 changes: 4 additions & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
using Aspire.Hosting.Dashboard;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Health;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -159,8 +161,10 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Services.AddHostedService<DistributedApplicationRunner>();
_innerBuilder.Services.AddSingleton(options);
_innerBuilder.Services.AddSingleton<ResourceNotificationService>();
_innerBuilder.Services.AddSingleton<IHealthCheckPublisher, ResourceNotificationHealthCheckPublisher>();
_innerBuilder.Services.AddSingleton<ResourceLoggerService>();
_innerBuilder.Services.AddSingleton<IDistributedApplicationEventing>(Eventing);
_innerBuilder.Services.AddHealthChecks();

if (ExecutionContext.IsRunMode)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Aspire.Hosting.Health;

internal class ResourceNotificationHealthCheckPublisher(DistributedApplicationModel model, ResourceNotificationService resourceNotificationService) : IHealthCheckPublisher
{
public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
_ = model;
_ = resourceNotificationService;

foreach (var resource in model.Resources)
{
if (resource.TryGetAnnotationsOfType<HealthCheckAnnotation>(out var annotations))
{
var resourceEntries = report.Entries.Where(e => annotations.Any(a => a.Key == e.Key));
var status = resourceEntries.All(e => e.Value.Status == HealthStatus.Healthy) ? HealthStatus.Healthy : HealthStatus.Unhealthy;

await resourceNotificationService.PublishUpdateAsync(resource, s => s with
{
HealthStatus = status
}).ConfigureAwait(false);
}
else
{
await resourceNotificationService.PublishUpdateAsync(resource, s => s with
{
HealthStatus = HealthStatus.Healthy
}).ConfigureAwait(false);
}
}
}
}
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.LifetimeType.set ->
Aspire.Hosting.ApplicationModel.ContainerLifetimeType
Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Default = 0 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Persistent = 1 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.get -> Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.init -> void
Aspire.Hosting.ApplicationModel.HealthCheckAnnotation
Aspire.Hosting.ApplicationModel.HealthCheckAnnotation.HealthCheckAnnotation(string! key) -> void
Aspire.Hosting.ApplicationModel.HealthCheckAnnotation.Key.get -> string!
Aspire.Hosting.ApplicationModel.ResourceNotificationService.ResourceNotificationService(Microsoft.Extensions.Logging.ILogger<Aspire.Hosting.ApplicationModel.ResourceNotificationService!>! logger, Microsoft.Extensions.Hosting.IHostApplicationLifetime! hostApplicationLifetime) -> void
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Func<Aspire.Hosting.ApplicationModel.ResourceEvent!, bool>! predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Aspire.Hosting.ApplicationModel.ResourceEvent!>!
Aspire.Hosting.DistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eventing.IDistributedApplicationEventing!
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;
Expand Down Expand Up @@ -607,6 +608,9 @@ public static IResourceBuilder<T> WaitFor<T>(this IResourceBuilder<T> builder, I
$"Resource '{dependency.Resource.Name}' has entered the '{snapshot.State.Text}' state prematurely."
);
}

// If we get to here the resource is running so we just need to check on whether it is healthy or not.
await rns.WaitForResourceAsync(dependency.Resource.Name, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cancellationToken: ct).ConfigureAwait(false);
});

return builder;
Expand Down