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
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
5 changes: 5 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -6,6 +6,11 @@
* Make only high confidence suggestions when reviewing code changes.
* Always use the latest version C#, currently C# 13 features.
* Never change global.json unless explicitly asked to.
* Avoid breaking changes on public APIs. When adding new members to existing public types:
* Add new properties rather than positional parameters to records
* Add new optional parameters at the end of method signatures
* Use overloads instead of changing existing method signatures
* Consider backward compatibility and existing consumers
* Don't update files under `*/api/*.cs` (e.g. src/Aspire.Hosting/api/Aspire.Hosting.cs) as they are generated.

## Formatting
14 changes: 13 additions & 1 deletion src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor
Original file line number Diff line number Diff line change
@@ -233,7 +233,8 @@
Value="@(context.HealthStatus?.Humanize() ?? Loc[nameof(Resources.WaitingHealthDataStatusMessage)])"
EnableHighlighting="@(!string.IsNullOrEmpty(_filter))"
HighlightText="@_filter"
TextVisualizerTitle="@context.Name">
TextVisualizerTitle="@context.Name"
ToolTip="@GetHealthCheckTooltip(context)">
<ContentBeforeValue>
@if (context.HealthStatus is null)
{
@@ -329,4 +330,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
@@ -392,7 +392,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();

3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/ResourceService/Partials.cs
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -292,7 +292,13 @@ public sealed record ResourceCommandSnapshot(string Name, ResourceCommandState S
/// <param name="Description">An optional description of the report, for display.</param>
/// <param name="ExceptionText">An optional string containing exception details.</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)
{
/// <summary>
/// The timestamp when this health check was last executed, or <see langword="null"/> if it has never run.
/// </summary>
public DateTime? LastRun { get; init; }
}

/// <summary>
/// The state of a resource command.
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/Partials.cs
Original file line number Diff line number Diff line change
@@ -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);
}

2 changes: 2 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/resource_service.proto
Original file line number Diff line number Diff line change
@@ -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 {
3 changes: 2 additions & 1 deletion src/Aspire.Hosting/Health/ResourceHealthCheckService.cs
Original file line number Diff line number Diff line change
@@ -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()) { LastRun = now };

var found = false;
for (var i = 0; i < builder.Count; i++)
12 changes: 6 additions & 6 deletions tests/Aspire.Dashboard.Components.Tests/Pages/ResourcesTests.cs
Original file line number Diff line number Diff line change
@@ -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);
@@ -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);
@@ -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(
@@ -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(
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion tests/Shared/DashboardModel/ModelTestHelpers.cs
Original file line number Diff line number Diff line change
@@ -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
Loading
Oops, something went wrong.