Skip to content

Commit f7577cd

Browse files
committed
Add health check UI page and cache tag support
1 parent e89d48b commit f7577cd

9 files changed

Lines changed: 193 additions & 3 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<PackageVersion Include="Equatable.Generator" Version="3.0.0" />
2424
<PackageVersion Include="FluentValidation" Version="12.1.1" />
2525
<PackageVersion Include="Fluid.Core" Version="2.31.0" />
26+
<PackageVersion Include="Humanizer" Version="3.0.10" />
2627
<PackageVersion Include="Injectio" Version="6.1.0" />
2728
<PackageVersion Include="LoreSoft.Blazor.Controls" Version="13.9.4" />
2829
<PackageVersion Include="MailKit" Version="4.17.0" />
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
@switch (Name.ToLowerInvariant())
2+
{
3+
case "warning":
4+
case "degraded":
5+
case "caution":
6+
<i class="bi bi-exclamation-triangle-fill text-warning"
7+
@attributes="Attributes"></i>
8+
break;
9+
case "error":
10+
case "critical":
11+
case "fatal":
12+
case "failed":
13+
case "unhealthy":
14+
case "false":
15+
<i class="bi bi-x-circle-fill text-danger"
16+
@attributes="Attributes"></i>
17+
break;
18+
case "information":
19+
<i class="bi bi-info-circle-fill text-primary"
20+
@attributes="Attributes"></i>
21+
break;
22+
case "debug":
23+
case "verbose":
24+
case "trace":
25+
<i class="bi bi-info-circle-fill text-secondary"
26+
@attributes="Attributes"></i>
27+
break;
28+
case "success":
29+
case "healthy":
30+
case "true":
31+
<i class="bi bi-check-circle-fill text-success"
32+
@attributes="Attributes"></i>
33+
break;
34+
case "credit":
35+
<i class="bi bi-arrow-down-circle text-success"></i>
36+
break;
37+
case "debit":
38+
<i class="bi bi-arrow-up-circle text-danger"></i>
39+
break;
40+
case "status":
41+
<i class="bi bi-arrow-left-right text-primary"></i>
42+
break;
43+
default:
44+
<i class="bi bi-question-circle-fill text-secondary"
45+
@attributes="Attributes"></i>
46+
break;
47+
}
48+
49+
@code {
50+
[Parameter, EditorRequired]
51+
public required string Name { get; set; }
52+
53+
[Parameter(CaptureUnmatchedValues = true)]
54+
public IReadOnlyDictionary<string, object>? Attributes { get; set; }
55+
}

samples/EntityFramework/src/Tracker.Client/Layout/ApplicationBar.razor

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@
7373
Users
7474
</a>
7575
</li>
76+
<li><hr class="dropdown-divider"></li>
77+
<li>
78+
<a id="navbar-item-administrative-healthCheck-link"
79+
class="dropdown-item"
80+
title="Health Check"
81+
href="/health">
82+
Health Check
83+
</a>
84+
</li>
85+
7686
</ul>
7787
</li>
7888
</ul>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
@page "/health"
2+
3+
@using Arbiter.CommandQuery.Commands
4+
@using Arbiter.CommandQuery.Models
5+
@using Arbiter.Dispatcher.Client
6+
@using Humanizer
7+
8+
<PageTitle>Health Check</PageTitle>
9+
10+
<div class="container-fluid">
11+
<LoadingBlock IsLoading="HealthReport == null">
12+
<table class="table table-bordered">
13+
<tbody>
14+
<tr>
15+
<th class="table-secondary">Overall Status</th>
16+
<td>
17+
<IconSelector Name="@HealthReport?.Status" />
18+
@HealthReport?.Status
19+
</td>
20+
<th class="table-secondary">Total Duration</th>
21+
<td>@HealthReport?.TotalDuration?.Humanize()</td>
22+
</tr>
23+
</tbody>
24+
</table>
25+
26+
<table class="table table-bordered table-hover">
27+
<colgroup>
28+
<col style="width: 30px" />
29+
<col style="" />
30+
<col style="width: 120px" />
31+
<col style="width: 180px" />
32+
<col style="" />
33+
<col style="width: 170px" />
34+
</colgroup>
35+
<thead>
36+
<tr class="table-secondary">
37+
<th scope="col"></th>
38+
<th scope="col">Name</th>
39+
<th scope="col">Status</th>
40+
<th scope="col">Tags</th>
41+
<th scope="col">Description</th>
42+
<th scope="col">Duration</th>
43+
</tr>
44+
</thead>
45+
<tbody>
46+
@foreach (var healthEntiry in HealthReport?.Entries ?? [])
47+
{
48+
<tr>
49+
<td class="text-center">
50+
<IconSelector Name="@healthEntiry.Status" />
51+
</td>
52+
<td>@healthEntiry.Key</td>
53+
<td>@healthEntiry.Status</td>
54+
<td>@healthEntiry.Tags?.Humanize(",")</td>
55+
<td>
56+
@healthEntiry.Description
57+
@healthEntiry.Message
58+
</td>
59+
<td>
60+
@healthEntiry.Duration?.Humanize()
61+
</td>
62+
</tr>
63+
}
64+
</tbody>
65+
</table>
66+
</LoadingBlock>
67+
</div>
68+
69+
@code {
70+
[Inject]
71+
public NotificationService NotificationService { get; set; } = null!;
72+
73+
[Inject]
74+
public IDispatcher Dispatcher { get; set; } = null!;
75+
76+
protected HealthReportModel? HealthReport { get; set; }
77+
78+
protected override async Task OnInitializedAsync()
79+
{
80+
try
81+
{
82+
var command = new HealthCheckCommand(null);
83+
var result = await Dispatcher.Send(command);
84+
85+
HealthReport = result;
86+
}
87+
catch (Exception ex)
88+
{
89+
NotificationService.ShowError(ex);
90+
}
91+
finally
92+
{
93+
await InvokeAsync(StateHasChanged);
94+
}
95+
}
96+
}

samples/EntityFramework/src/Tracker.Client/Tracker.Client.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Blazilla" />
14+
<PackageReference Include="Humanizer" />
1415
<PackageReference Include="LoreSoft.Blazor.Controls" />
1516
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
1617
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />

samples/EntityFramework/src/Tracker.Core/Health/HealthRegistration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Arbiter.Health;
2+
13
using Microsoft.Extensions.DependencyInjection;
24
using Microsoft.Extensions.Diagnostics.HealthChecks;
35

@@ -10,6 +12,8 @@ public static class HealthRegistration
1012
[RegisterServices]
1113
public static void Register(IServiceCollection services)
1214
{
15+
services.AddHealthServices();
16+
1317
var health = services.AddHealthChecks();
1418

1519
//default liveness check to ensure app is responsive

samples/EntityFramework/src/Tracker.Core/Tracker.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
<ItemGroup>
2020
<ProjectReference Include="..\..\..\..\src\Arbiter.CommandQuery.EntityFramework\Arbiter.CommandQuery.EntityFramework.csproj" />
21+
<ProjectReference Include="..\..\..\..\src\Arbiter.Health\Arbiter.Health.csproj" />
2122
<ProjectReference Include="..\..\..\..\src\Arbiter.Mapping.Generators\Arbiter.Mapping.Generators.csproj">
2223
<OutputItemType>Analyzer</OutputItemType>
2324
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>

samples/EntityFramework/src/Tracker.Service/Tracker.Service.http

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,15 @@ X-API-KEY: {{apiKey}}
3737
"name": "Updated Priority",
3838
"description": "Updated description for the priority"
3939
}
40+
41+
### Health Check
42+
43+
GET {{baseUrl}}/health-check
44+
Accept: application/json
45+
X-API-KEY: {{apiKey}}
46+
47+
### Configuration Debugger
48+
49+
GET {{baseUrl}}/config-debugger
50+
Accept: application/json
51+
X-API-KEY: {{apiKey}}

src/Arbiter.CommandQuery.Endpoints/DiagnosticsEndpoint.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,23 @@ private async Task<Results<Ok<List<ClaimType>>, ProblemHttpResult>> ClaimsCheck(
180180
}
181181

182182
/// <summary>
183-
/// Clears cached entries by invalidating all cache tags.
183+
/// Clears cached entries by invalidating the specified cache tags, or all tags when none are specified.
184184
/// </summary>
185185
/// <param name="hybridCache">The configured hybrid cache instance.</param>
186+
/// <param name="tags">The cache tags to invalidate. When empty, all cache entries are invalidated.</param>
186187
/// <param name="user">The current user principal.</param>
187188
/// <param name="cancellationToken">The cancellation token.</param>
188189
/// <returns>A result indicating whether the cache was cleared.</returns>
189190
private async Task<IResult> CacheClear(
190191
[FromServices] HybridCache? hybridCache = null,
192+
[FromQuery(Name = "tag")] string[]? tags = null,
191193
ClaimsPrincipal? user = default,
192194
CancellationToken cancellationToken = default)
193195
{
194-
_logger.LogInformation("Cache clear requested by {UserName}", user?.Identity?.Name ?? "anonymous");
196+
_logger.LogInformation(
197+
"Cache clear requested by {UserName} for tags: {Tags}",
198+
user?.Identity?.Name ?? "anonymous",
199+
tags != null ? string.Join(", ", tags) : "all");
195200

196201
try
197202
{
@@ -201,7 +206,12 @@ private async Task<IResult> CacheClear(
201206
return TypedResults.NoContent();
202207
}
203208

204-
await hybridCache.RemoveByTagAsync("*", cancellationToken).ConfigureAwait(false);
209+
// no tags specified, clear everything using the wildcard tag
210+
var cacheTags = tags?.Where(t => !string.IsNullOrWhiteSpace(t)).ToArray();
211+
if (cacheTags is null || cacheTags.Length == 0)
212+
cacheTags = ["*"];
213+
214+
await hybridCache.RemoveByTagAsync(cacheTags, cancellationToken).ConfigureAwait(false);
205215

206216
return TypedResults.Ok("Cache cleared by request");
207217
}

0 commit comments

Comments
 (0)