Skip to content

Commit

Permalink
Make resource HealthStatus computed from HealthReports (#6368) (#6458)
Browse files Browse the repository at this point in the history
* Make resource HealthStatus computed from HealthReports
* Update health reports when they have changed but the status has not

---------

Co-authored-by: Adam Ratzman <[email protected]>
Co-authored-by: Mitch Denny <[email protected]>

(cherry picked from commit a4ef97a)
  • Loading branch information
adamint authored Oct 24, 2024
1 parent 8f0bda6 commit b1e81d7
Show file tree
Hide file tree
Showing 36 changed files with 249 additions and 213 deletions.
40 changes: 24 additions & 16 deletions playground/HealthChecks/HealthChecksSandbox.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = DistributedApplication.CreateBuilder(args);

builder.Services.TryAddLifecycleHook<TestResourceLifecycleHook>();

AddTestResource("healthy", HealthStatus.Healthy, "I'm fine, thanks for asking.");
AddTestResource("unhealthy", HealthStatus.Unhealthy, "I can't do that, Dave.", exception: GetException("Feeling unhealthy."));
AddTestResource("degraded", HealthStatus.Degraded, "Had better days.", exception: GetException("Feeling degraded."));
AddTestResource("unhealthy", HealthStatus.Unhealthy, "I can't do that, Dave.", exceptionMessage: "Feeling unhealthy.");
AddTestResource("degraded", HealthStatus.Degraded, "Had better days.", exceptionMessage: "Feeling degraded.");

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
Expand All @@ -24,30 +25,37 @@

builder.Build().Run();

static string GetException(string message)
void AddTestResource(string name, HealthStatus status, string? description = null, string? exceptionMessage = null)
{
try
{
throw new InvalidOperationException(message);
}
catch (InvalidOperationException ex)
{
return ex.ToString();
}
}
var hasHealthyAfterFirstRunCheckRun = false;
builder.Services.AddHealthChecks()
.AddCheck(
$"{name}_check",
() => new HealthCheckResult(status, description, new InvalidOperationException(exceptionMessage))
)
.AddCheck($"{name}_resource_healthy_after_first_run_check", () =>
{
if (!hasHealthyAfterFirstRunCheckRun)
{
hasHealthyAfterFirstRunCheckRun = true;
return new HealthCheckResult(HealthStatus.Unhealthy, "Initial failure state.");
}
IResourceBuilder<TestResource> AddTestResource(string name, HealthStatus status, string? description = null, string? exception = null)
{
return builder
return new HealthCheckResult(HealthStatus.Healthy, "Healthy beginning second health check run.");
});

builder
.AddResource(new TestResource(name))
.WithHealthCheck($"{name}_check")
.WithHealthCheck($"{name}_resource_healthy_after_first_run_check")
.WithInitialState(new()
{
ResourceType = "Test Resource",
State = "Starting",
Properties = [],
HealthReports = [new HealthReportSnapshot($"{name}_check", status, description, exception)]
})
.ExcludeFromManifest();
return;
}

internal sealed class TestResource(string name) : Resource(name);
Expand Down
6 changes: 0 additions & 6 deletions src/Aspire.Dashboard/Model/ResourceStateViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ internal static ResourceStateViewModel GetStateViewModel(ResourceViewModel resou
icon = new Icons.Filled.Size16.Circle();
color = Color.Info;
}
else if (resource.HealthStatus is null)
{
// If we are waiting for a health check, show a progress bar and consider the resource unhealthy
icon = new Icons.Filled.Size16.CheckmarkCircleWarning();
color = Color.Warning;
}
else if (resource.HealthStatus is not HealthStatus.Healthy)
{
icon = new Icons.Filled.Size16.CheckmarkCircleWarning();
Expand Down
43 changes: 40 additions & 3 deletions src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ namespace Aspire.Dashboard.Model;
[DebuggerDisplay("Name = {Name}, ResourceType = {ResourceType}, State = {State}, Properties = {Properties.Count}")]
public sealed class ResourceViewModel
{
private readonly ImmutableArray<HealthReportViewModel> _healthReports = [];
private readonly KnownResourceState? _knownState;

public required string Name { get; init; }
public required string ResourceType { get; init; }
public required string DisplayName { get; init; }
Expand All @@ -35,16 +38,50 @@ public sealed class ResourceViewModel
public required FrozenDictionary<string, ResourcePropertyViewModel> Properties { get; init; }
public required ImmutableArray<CommandViewModel> Commands { get; init; }
/// <summary>The health status of the resource. <see langword="null"/> indicates that health status is expected but not yet available.</summary>
public required HealthStatus? HealthStatus { get; init; }
public required ImmutableArray<HealthReportViewModel> HealthReports { get; init; }
public KnownResourceState? KnownState { get; init; }
public HealthStatus? HealthStatus { get; private set; }

public required ImmutableArray<HealthReportViewModel> HealthReports
{
get => _healthReports;
init
{
_healthReports = value;
HealthStatus = ComputeHealthStatus(value, KnownState);
}
}

public KnownResourceState? KnownState
{
get => _knownState;
init
{
_knownState = value;
HealthStatus = ComputeHealthStatus(_healthReports, value);
}
}

internal bool MatchesFilter(string filter)
{
// TODO let ResourceType define the additional data values we include in searches
return Name.Contains(filter, StringComparisons.UserTextSearch);
}

internal static HealthStatus? ComputeHealthStatus(ImmutableArray<HealthReportViewModel> healthReports, KnownResourceState? state)
{
if (state != KnownResourceState.Running)
{
return null;
}

return healthReports.Length == 0
// If there are no health reports and the resource is running, assume it's healthy.
? Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Healthy
// If there are health reports, the health status is the minimum of the health status of the reports.
// If any of the reports is null (first health check has not returned), the health status is unhealthy.
: healthReports.MinBy(r => r.HealthStatus)?.HealthStatus
?? Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy;
}

public static string GetResourceName(ResourceViewModel resource, IDictionary<string, ResourceViewModel> allResources)
{
var count = 0;
Expand Down
1 change: 0 additions & 1 deletion src/Aspire.Dashboard/ResourceService/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public ResourceViewModel ToViewModel(BrowserTimeProvider timeProvider, IKnownPro
KnownState = HasState ? Enum.TryParse(State, out KnownResourceState knownState) ? knownState : null : null,
StateStyle = HasStateStyle ? StateStyle : null,
Commands = GetCommands(),
HealthStatus = HasHealthStatus ? MapHealthStatus(HealthStatus) : null,
HealthReports = HealthReports.Select(ToHealthReportViewModel).OrderBy(vm => vm.Name).ToImmutableArray(),
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Resources/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
<value>View console logs</value>
</data>
<data name="ResourcesDetailsHealthStateProperty" xml:space="preserve">
<value>Health State</value>
<value>Health state</value>
</data>
<data name="ResourcesDetailsStopTimeProperty" xml:space="preserve">
<value>Stop time</value>
Expand Down Expand Up @@ -262,4 +262,4 @@
<data name="ResourceActionTelemetryTooltip" xml:space="preserve">
<value>No telemetry found for this resource.</value>
</data>
</root>
</root>
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b1e81d7

Please sign in to comment.