Skip to content

[WIP] Show time alongside health check status #9591

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 12 additions & 1 deletion src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@
EnableHighlighting="@(!string.IsNullOrEmpty(_filter))"
HighlightText="@_filter" />
</AspireTemplateColumn>
<AspireTemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.StateColumnHeader)]" Class="stateColumn">
<AspireTemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.StateColumnHeader)]" Class="stateColumn" Tooltip="true" TooltipText="@(c => GetHealthCheckTooltip(c))">
<GridValue ValueDescription="@ControlStringsLoc[nameof(ControlsStrings.StateColumnHeader)]"
Value="@(context.HealthStatus?.Humanize() ?? Loc[nameof(Resources.WaitingHealthDataStatusMessage)])"
EnableHighlighting="@(!string.IsNullOrEmpty(_filter))"
Expand Down Expand Up @@ -329,4 +329,15 @@
return @<a href="@vm.Url" title="@vm.Url" target="_blank">@vm.DisplayName</a>;
}
}

private string? GetHealthCheckTooltip(HealthReportViewModel healthReport)
{
if (healthReport.LastRun.HasValue)
{
// Convert UTC to local time zone
var lastRunLocal = TimeZoneInfo.ConvertTimeFromUtc(healthReport.LastRun.Value, TimeZoneInfo.Local);
return $"Last check: {lastRunLocal:yyyy-MM-dd HH:mm:ss}";
}
return null;
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ public bool MatchesFilter(string filter) =>
Target?.Contains(filter, StringComparison.CurrentCultureIgnoreCase) == true;
}

public sealed record class HealthReportViewModel(string Name, HealthStatus? HealthStatus, string? Description, string? ExceptionText)
public sealed record class HealthReportViewModel(string Name, HealthStatus? HealthStatus, string? Description, string? ExceptionText, DateTime? LastRun)
{
private readonly string? _humanizedHealthStatus = HealthStatus?.Humanize();

Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/ResourceService/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public ResourceViewModel ToViewModel(IKnownPropertyLookup knownPropertyLookup, I

HealthReportViewModel ToHealthReportViewModel(HealthReport healthReport)
{
return new HealthReportViewModel(healthReport.Key, healthReport.HasStatus ? MapHealthStatus(healthReport.Status) : null, healthReport.Description, healthReport.Exception);
DateTime? lastRun = healthReport.LastRun?.ToDateTime();
return new HealthReportViewModel(healthReport.Key, healthReport.HasStatus ? MapHealthStatus(healthReport.Status) : null, healthReport.Description, healthReport.Exception, lastRun);
}

Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus MapHealthStatus(HealthStatus healthStatus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,9 @@ public sealed record ResourceCommandSnapshot(string Name, ResourceCommandState S
/// <param name="Status">The state of the resource, according to the report, or <see langword="null"/> if a health report has not yet been received for this health check.</param>
/// <param name="Description">An optional description of the report, for display.</param>
/// <param name="ExceptionText">An optional string containing exception details.</param>
/// <param name="LastRun">The timestamp when this health check was last executed, or <see langword="null"/> if it has never run.</param>
[DebuggerDisplay("{Status}", Name = "{Name}")]
public sealed record HealthReportSnapshot(string Name, HealthStatus? Status, string? Description, string? ExceptionText);
public sealed record HealthReportSnapshot(string Name, HealthStatus? Status, string? Description, string? ExceptionText, DateTime? LastRun);

/// <summary>
/// The state of a resource command.
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot)
healthReport.Status = MapHealthStatus(report.Status.Value);
}

if (report.LastRun.HasValue)
{
healthReport.LastRun = Timestamp.FromDateTime(report.LastRun.Value.ToUniversalTime());
}

resource.HealthReports.Add(healthReport);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/resource_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ message HealthReport {
string description = 3;
// Any exception details.
string exception = 4;
// The timestamp when this health check was last executed.
optional google.protobuf.Timestamp last_run = 5;
}

enum HealthStatus {
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Hosting/Health/ResourceHealthCheckService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,11 @@ await resourceNotificationService.PublishUpdateAsync(resource, s => s with
private static ImmutableArray<HealthReportSnapshot> MergeHealthReports(ImmutableArray<HealthReportSnapshot> healthReports, HealthReport report)
{
var builder = healthReports.ToBuilder();
var now = DateTime.UtcNow;

foreach (var (key, entry) in report.Entries)
{
var snapshot = new HealthReportSnapshot(key, entry.Status, entry.Description, entry.Exception?.ToString());
var snapshot = new HealthReportSnapshot(key, entry.Status, entry.Description, entry.Exception?.ToString(), now);

var found = false;
for (var i = 0; i < builder.Count; i++)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ private static ImmutableArray<HealthReportSnapshot> GetInitialHealthReports(IRes
return [];
}

var reports = annotations.Select(annotation => new HealthReportSnapshot(annotation.Key, null, null, null));
var reports = annotations.Select(annotation => new HealthReportSnapshot(annotation.Key, null, null, null, null));
return [.. reports];
}

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/api/Aspire.Hosting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ public HealthCheckAnnotation(string key) { }
}

[System.Diagnostics.DebuggerDisplay("{Status}", Name = "{Name}")]
public sealed partial record HealthReportSnapshot(string Name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? Status, string? Description, string? ExceptionText)
public sealed partial record HealthReportSnapshot(string Name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? Status, string? Description, string? ExceptionText, System.DateTime? LastRun)
{
}

Expand Down
12 changes: 6 additions & 6 deletions tests/Aspire.Dashboard.Components.Tests/Pages/ResourcesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void UpdateResources_FiltersUpdated()
"Resource1",
"Type1",
"Running",
ImmutableArray.Create(new HealthReportViewModel("Null", null, "Description1", null))),
ImmutableArray.Create(new HealthReportViewModel("Null", null, "Description1", null, null))),
};
var channel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
var dashboardClient = new TestDashboardClient(isEnabled: true, initialResources: initialResources, resourceChannelProvider: () => channel);
Expand Down Expand Up @@ -73,7 +73,7 @@ public void UpdateResources_FiltersUpdated()
"Resource2",
"Type2",
"Running",
ImmutableArray.Create(new HealthReportViewModel("Healthy", HealthStatus.Healthy, "Description2", null))))
ImmutableArray.Create(new HealthReportViewModel("Healthy", HealthStatus.Healthy, "Description2", null, null))))
]);

cut.WaitForState(() => cut.Instance.GetFilteredResources().Count() == 2);
Expand Down Expand Up @@ -120,17 +120,17 @@ public void FilterResources()
"Resource1",
"Type1",
"Running",
ImmutableArray.Create(new HealthReportViewModel("Null", null, "Description1", null))),
ImmutableArray.Create(new HealthReportViewModel("Null", null, "Description1", null, null))),
CreateResource(
"Resource2",
"Type2",
"Running",
ImmutableArray.Create(new HealthReportViewModel("Healthy", HealthStatus.Healthy, "Description2", null))),
ImmutableArray.Create(new HealthReportViewModel("Healthy", HealthStatus.Healthy, "Description2", null, null))),
CreateResource(
"Resource3",
"Type3",
"Stopping",
ImmutableArray.Create(new HealthReportViewModel("Degraded", HealthStatus.Degraded, "Description3", null))),
ImmutableArray.Create(new HealthReportViewModel("Degraded", HealthStatus.Degraded, "Description3", null, null))),
};
var dashboardClient = new TestDashboardClient(isEnabled: true, initialResources: initialResources, resourceChannelProvider: Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>);
ResourceSetupHelpers.SetupResourcesPage(
Expand Down Expand Up @@ -182,7 +182,7 @@ public void ResourceGraph_MultipleRenders_InitializeOnce()
"Resource1",
"Type1",
"Running",
ImmutableArray.Create(new HealthReportViewModel("Null", null, "Description1", null))),
ImmutableArray.Create(new HealthReportViewModel("Null", null, "Description1", null, null))),
};
var dashboardClient = new TestDashboardClient(isEnabled: true, initialResources: initialResources, resourceChannelProvider: Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>);
ResourceSetupHelpers.SetupResourcesPage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public sealed class ResourceViewModelTests
[InlineData(KnownResourceState.Running, DiagnosticsHealthStatus.Degraded, new string?[] {"Healthy", "Degraded"})]
public void Resource_WithHealthReportAndState_ReturnsCorrectHealthStatus(KnownResourceState? state, DiagnosticsHealthStatus? expectedStatus, string?[]? healthStatusStrings)
{
var reports = healthStatusStrings?.Select<string?, HealthReportViewModel>((h, i) => new HealthReportViewModel(i.ToString(), h is null ? null : System.Enum.Parse<DiagnosticsHealthStatus>(h), null, null)).ToImmutableArray() ?? [];
var reports = healthStatusStrings?.Select<string?, HealthReportViewModel>((h, i) => new HealthReportViewModel(i.ToString(), h is null ? null : System.Enum.Parse<DiagnosticsHealthStatus>(h), null, null, null)).ToImmutableArray() ?? [];
var actualStatus = ResourceViewModel.ComputeHealthStatus(reports, state);
Assert.Equal(expectedStatus, actualStatus);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Aspire.Hosting.Tests/Health/HealthStatusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class HealthStatusTests
[InlineData(RunningState, HealthStatus.Degraded, new string?[] {"Healthy", "Degraded"})]
public void Resource_WithHealthReportAndState_ReturnsCorrectHealthStatus(string? state, HealthStatus? expectedStatus, string?[]? healthStatusStrings)
{
var reports = healthStatusStrings?.Select<string?, HealthReportSnapshot>((h, i) => new HealthReportSnapshot(i.ToString(), h is null ? null : Enum.Parse<HealthStatus>(h), null, null)).ToImmutableArray() ?? [];
var reports = healthStatusStrings?.Select<string?, HealthReportSnapshot>((h, i) => new HealthReportSnapshot(i.ToString(), h is null ? null : Enum.Parse<HealthStatus>(h), null, null, null)).ToImmutableArray() ?? [];
var actualStatus = CustomResourceSnapshot.ComputeHealthStatus(reports, state);
Assert.Equal(expectedStatus, actualStatus);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ await app.ResourceNotifications.PublishUpdateAsync(resource.Resource, s => s wit
await app.ResourceNotifications.PublishUpdateAsync(resource.Resource, s => s with
{
State = new ResourceStateSnapshot(KnownResourceStates.Running, null),
HealthReports = [new HealthReportSnapshot("healthcheck_a", Status: null, Description: null, ExceptionText: null)]
HealthReports = [new HealthReportSnapshot("healthcheck_a", Status: null, Description: null, ExceptionText: null, LastRun: null)]
});

await app.ResourceNotifications.WaitForResourceHealthyAsync("resource").DefaultTimeout();
Expand Down
2 changes: 1 addition & 1 deletion tests/Shared/DashboardModel/ModelTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static ResourceViewModel CreateResource(
State = state?.ToString(),
KnownState = state,
StateStyle = stateStyle,
HealthReports = reportHealthStatus is null && !createNullHealthReport ? [] : [new HealthReportViewModel("healthcheck", reportHealthStatus, null, null)],
HealthReports = reportHealthStatus is null && !createNullHealthReport ? [] : [new HealthReportViewModel("healthcheck", reportHealthStatus, null, null, null)],
Commands = commands ?? [],
Relationships = relationships ?? [],
IsHidden = hidden
Expand Down
Loading