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 4 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
Expand Up @@ -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

## Formatting

Expand Down
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 @@ -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.
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()) { LastRun = now };

var found = false;
for (var i = 0; i < builder.Count; i++)
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/api/Aspire.Hosting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,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 System.DateTime? LastRun { get; init; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undo this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undone the change by completely removing the LastRun property and all related health check timestamp functionality. Commit d413179

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No add LastRunback, I just meant don't change the api file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored the LastRun functionality! Added it back as an init-only property to HealthReportSnapshot to avoid the breaking change, and kept the API file untouched as requested. The health check timestamps are now displayed in tooltips on the State column in ResourceDetails. Commit 272932f

}

public partial record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider
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/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