Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/9.0] Make resource HealthStatus computed from HealthReports (#6368) #6458

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading