Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3b86ce7
Add health state enum modelled on Azure Monitor
james-gould Aug 3, 2026
d9eb9cb
Add dependency rollup configuration types
james-gould Aug 3, 2026
9989eca
Add entity impact for child-side health propagation
james-gould Aug 4, 2026
4c579ae
Add health state severity ordering and impact helpers
james-gould Aug 4, 2026
e1511b0
Add signals with threshold evaluation rules
james-gould Aug 5, 2026
0562f02
Add entity, relationship and model definition types
james-gould Aug 6, 2026
25205f3
Add evaluated snapshot types for rendering
james-gould Aug 6, 2026
cfe184f
Add evaluator implementing the Azure rollup pipeline
james-gould Aug 7, 2026
d3c49d6
Add tests for rollup, impact and threshold semantics
james-gould Aug 8, 2026
b042ed0
Project Aspire resources into a sample health model
james-gould Aug 9, 2026
ca15092
Add tests for the Aspire resource projection
james-gould Aug 9, 2026
4a054e9
Include custom resource types and external services in the model
james-gould Aug 10, 2026
27c1cdf
Add health model route and localized page strings
james-gould Aug 11, 2026
556371d
Add health state icon helper
james-gould Aug 11, 2026
2c98865
Add health model page and entity details pane
james-gould Aug 12, 2026
fedf651
Add health model to desktop and mobile navigation
james-gould Aug 13, 2026
2abf5d8
Add page render tests and fix the data grid queryable crash
james-gould Aug 13, 2026
3d23760
Surface a single unhealthy container on the application entity
james-gould Aug 14, 2026
17d043e
Fix subscription disposal ordering and back navigation
james-gould Aug 14, 2026
ab38c25
Document the Bicep translation path
james-gould Aug 14, 2026
f094f75
entity relationship is now expressed, but poor
james-gould Sep 7, 2026
b86cfb7
entity relationship expressed better, connecting health state lines a…
james-gould Sep 7, 2026
95b51ac
added explicit playground
james-gould Sep 7, 2026
439792e
broken commit
james-gould Sep 7, 2026
61c414f
improvements to topology parsing and wiring between entities
james-gould Sep 13, 2026
3dec255
experimental aspire publishing
james-gould Sep 13, 2026
974a24a
Merge branch 'ahm-graph-view' into main
james-gould Sep 15, 2026
22f46c6
Merge pull request #1 from james-gould/main
james-gould Sep 15, 2026
369e80b
fixed merge issues
james-gould Sep 15, 2026
902e40f
fixed broken colours and removed old artefact hm
james-gould Sep 15, 2026
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
5 changes: 5 additions & 0 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
<Project Path="src/Aspire.Hosting.Azure.EventHubs/Aspire.Hosting.Azure.EventHubs.csproj" />
<Project Path="src/Aspire.Hosting.Azure.FrontDoor/Aspire.Hosting.Azure.FrontDoor.csproj" />
<Project Path="src/Aspire.Hosting.Azure.Functions/Aspire.Hosting.Azure.Functions.csproj" />
<Project Path="src/Aspire.Hosting.Azure.HealthModels/Aspire.Hosting.Azure.HealthModels.csproj" />
<Project Path="src/Aspire.Hosting.Azure.KeyVault/Aspire.Hosting.Azure.KeyVault.csproj" />
<Project Path="src/Aspire.Hosting.Azure.Kubernetes/Aspire.Hosting.Azure.Kubernetes.csproj" />
<Project Path="src/Aspire.Hosting.Azure.Kusto/Aspire.Hosting.Azure.Kusto.csproj" />
Expand Down Expand Up @@ -286,6 +287,10 @@
<Folder Name="/playground/HealthChecks/">
<Project Path="playground/HealthChecks/HealthChecksSandbox.AppHost/HealthChecksSandbox.AppHost.csproj" />
</Folder>
<Folder Name="/playground/HealthModel/">
<Project Path="playground/HealthModel/HealthModel.Metrics/HealthModel.Metrics.csproj" />
<Project Path="playground/HealthModel/HealthModelSandbox.AppHost/HealthModelSandbox.AppHost.csproj" />
</Folder>
<Folder Name="/playground/kafka/">
<Project Path="playground/kafka/Consumer/Consumer.csproj" />
<Project Path="playground/kafka/KafkaBasic.AppHost/KafkaBasic.AppHost.csproj" />
Expand Down
11 changes: 11 additions & 0 deletions playground/HealthModel/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run AppHost",
"type": "aspire",
"request": "launch",
"program": "${workspaceFolder}"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\HealthModelScenario.cs" Link="HealthModelScenario.cs" />
<!-- The linked scenario also compiles in non-web projects with explicit DI imports. -->
<Using Remove="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions playground/HealthModel/HealthModel.Metrics/HealthModelMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Text;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace HealthModelPlayground;

internal static class HealthModelMetrics
{
public const string ContentType = "text/plain; version=0.0.4; charset=utf-8";

public static string Format(HealthReport report, IEnumerable<HealthModelScenarioResource> resources)
{
var output = new StringBuilder();
output.Append("# HELP aspire_health_status Simulated resource health: 0 = Unhealthy, 1 = Degraded, 2 = Healthy.\n");
output.Append("# TYPE aspire_health_status gauge\n");

foreach (var resource in resources.OrderBy(resource => resource.Name, StringComparer.Ordinal))
{
// An absent check is no data, even if the scenario's expected status is Healthy.
// In particular, do not use the aggregate report status for an individual resource.
if (report.Entries.TryGetValue(resource.HealthCheckName, out var entry))
{
AppendSample(output, resource.Name, resource.HealthCheckName, entry.Status);
}

// These entities run inside this serving process, rather than representing real databases.
AppendSample(output, resource.Name, "resource-state", HealthStatus.Healthy);
}

return output.ToString();
}

private static void AppendSample(StringBuilder output, string resourceName, string healthCheck, HealthStatus status)
{
output.Append("aspire_health_status{resource_name=\"");
AppendLabelValue(output, resourceName);
output.Append("\",health_check=\"");
AppendLabelValue(output, healthCheck);
output.Append("\",replica_index=\"1\"} ");
output.Append(((int)status).ToString(CultureInfo.InvariantCulture));
output.Append('\n');
}

private static void AppendLabelValue(StringBuilder output, string value)
{
// A label such as api"\name<LF>next is written as api\"\\name\nnext.
// Prometheus text format escapes backslashes, quotes, and line feeds:
// https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details
foreach (var character in value)
{
output.Append(character switch
{
'\\' => "\\\\",
'"' => "\\\"",
'\n' => "\\n",
_ => character.ToString()
});
}
}
}
21 changes: 21 additions & 0 deletions playground/HealthModel/HealthModel.Metrics/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using HealthModelPlayground;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);
HealthModelScenario.AddHealthChecks(builder.Services);

var app = builder.Build();

app.MapGet("/metrics", async (HealthCheckService healthChecks, CancellationToken cancellationToken) =>
{
var report = await healthChecks.CheckHealthAsync(cancellationToken);
return Results.Text(HealthModelMetrics.Format(report, HealthModelScenario.Resources), HealthModelMetrics.ContentType);
});

// Process liveness is independent of the intentionally degraded and unhealthy simulated resources.
app.MapGet("/alive", () => Results.Text("Alive\n", "text/plain"));

app.Run();
131 changes: 131 additions & 0 deletions playground/HealthModel/HealthModelSandbox.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREAZUREHEALTH001

using Aspire.Hosting.Eventing;
using Aspire.Hosting.Lifecycle;
using HealthModelPlayground;

// Playground for the health model and the resource graph.
//
// The topology below is a strict tree: every resource has exactly one parent, and nothing is shared between
// branches. That is deliberate. A shared dependency turns the model into a graph, which is legitimate but
// makes it much harder to see at a glance whether health is rolling up correctly, because a single unhealthy
// leaf lights up several unrelated-looking paths at once.
//
// storefront (Healthy) web front end
// ├── checkout-api (Healthy)
// │ ├── orders-db-server (Healthy)
// │ │ └── orders-db (Healthy) child of its server
// │ └── payments-gateway (Degraded) <- degrades the checkout branch
// ├── catalog-api (Healthy)
// │ ├── catalog-db-server (Healthy)
// │ │ └── catalog-db (Healthy) child of its server
// │ └── search-index (Unhealthy) <- fails the catalog branch
// └── identity-api (Healthy) fully healthy branch
// └── identity-cache (Healthy)
//
// Only two resources are anything other than healthy, so every other colour in the graph has been
// inherited. The expected result once everything has started:
//
// storefront Unhealthy (worst of its branches, via catalog-api -> search-index)
// checkout-api Degraded (via payments-gateway)
// catalog-api Unhealthy (via search-index)
// identity-api Healthy (nothing unhealthy underneath it)

var builder = DistributedApplication.CreateBuilder(args);

HealthModelScenario.AddHealthChecks(builder.Services);
var resources = HealthModelScenario.Resources.ToDictionary(resource => resource.Name, AddTestResource, StringComparer.Ordinal);
foreach (var resource in HealthModelScenario.Resources)
{
if (resource.ParentName is { } parent)
{
resources[resource.Name].WithParentRelationship(resources[parent]);
}
foreach (var dependency in resource.Dependencies)
{
resources[resource.Name].WithReferenceRelationship(resources[dependency]);
}
}

if (builder.ExecutionContext.IsPublishMode)
{
builder.AddAzureContainerAppEnvironment("azure");
var health = builder.AddAzureHealthModel("health", "aspire-healthmodel.json");
var metrics = builder.AddProject<Projects.HealthModel_Metrics>("health-metrics")
.WithHttpEndpoint(name: "http", targetPort: 8080)
.WithHttpHealthCheck("/alive")
.PublishAsAzureContainerApp((_, app) =>
{
app.Template.Scale.MinReplicas = 1;
app.Template.Scale.MaxReplicas = 1;
});

builder.AddAzureContainerAppsHealthModelCollector("health-collector", health, metrics.GetEndpoint("http"));
}
else
{
builder.Services.TryAddEventingSubscriber<TestResourceLifecycle>();
}

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
// of the dashboard. It is not required in end developer code. Comment out this code
// or build with `/p:SkipDashboardReference=true`, to test end developer
// dashboard launch experience, Refer to Directory.Build.props for the path to
// the dashboard binary (defaults to the Aspire.Dashboard bin output in the
// artifacts dir).
if (builder.ExecutionContext.IsRunMode)
{
builder.AddProject<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard);
}
#endif

builder.Build().Run();

IResourceBuilder<TestResource> AddTestResource(HealthModelScenarioResource resource)
{
return builder
.AddResource(new TestResource(resource.Name))
.WithHealthCheck(resource.HealthCheckName)
.WithInitialState(new()
{
ResourceType = "Test Resource",
State = "Starting",
Properties = [],
})
.ExcludeFromManifest();
}

internal sealed class TestResource(string name) : Resource(name), IResourceWithEndpoints;

/// <summary>
/// Moves the test resources to the running state shortly after startup.
/// </summary>
/// <remarks>
/// The delay is intentional. It leaves the resources in an unknown state for long enough to see that a
/// starting resource does not colour the graph, which is the behaviour that keeps the model from flashing
/// red every time the app host boots.
/// </remarks>
internal sealed class TestResourceLifecycle(ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber
{
public async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);

// Keep startup work owned by the lifecycle event so cancellation and publication failures are
// observed instead of escaping from fire-and-forget tasks after the AppHost has stopped.
await Task.WhenAll(@event.Model.Resources.OfType<TestResource>().Select(resource =>
notificationService.PublishUpdateAsync(
resource,
state => state with { State = new("Running", "success") })));
}

public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
{
eventing.Subscribe<BeforeStartEvent>(OnBeforeStartAsync);
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>0e5a0d0e-5b1c-4d0a-9d5f-2c4b6a8f1e73</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.HealthModels" />
<ProjectReference Include="..\HealthModel.Metrics\HealthModel.Metrics.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\KnownResourceNames.cs" Link="KnownResourceNames.cs" />
<Compile Include="..\HealthModelScenario.cs" Link="HealthModelScenario.cs" />
<None Update="aspire-healthmodel.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://HealthModel.dev.localhost:17420;http://HealthModel.dev.localhost:15320",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://HealthModel.dev.localhost:15320",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development"
}
},
"generate-manifest": {
"commandName": "Project",
"launchBrowser": true,
"dotnetRunMessages": true,
"commandLineArgs": "--publisher manifest --output-path aspire-manifest.json",
"applicationUrl": "http://HealthModel.dev.localhost:16320",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Loading
Loading