Skip to content

Commit f7a571e

Browse files
authored
Add health endpoint to dashboard and call from app host (#9191)
1 parent 86a4fbe commit f7a571e

File tree

7 files changed

+56
-5
lines changed

7 files changed

+56
-5
lines changed

src/Aspire.Dashboard/DashboardEndpointsBuilder.cs

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ namespace Aspire.Dashboard;
1414

1515
public static class DashboardEndpointsBuilder
1616
{
17+
public static void MapDashboardHealthChecks(this IEndpointRouteBuilder endpoints)
18+
{
19+
endpoints.MapHealthChecks($"/{DashboardUrls.HealthBasePath}").AllowAnonymous();
20+
}
21+
1722
public static void MapDashboardApi(this IEndpointRouteBuilder endpoints, DashboardOptions dashboardOptions)
1823
{
1924
if (dashboardOptions.Frontend.AuthMode == FrontendAuthMode.BrowserToken)

src/Aspire.Dashboard/DashboardWebApplication.cs

+2
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ public DashboardWebApplication(
204204
// See https://learn.microsoft.com/aspnet/core/performance/response-compression#compression-with-https for more information
205205
options.MimeTypes = ["text/javascript", "application/javascript", "text/css", "image/svg+xml"];
206206
});
207+
builder.Services.AddHealthChecks();
207208
if (dashboardOptions.Otlp.Cors.IsCorsEnabled)
208209
{
209210
builder.Services.AddCors(options =>
@@ -441,6 +442,7 @@ public DashboardWebApplication(
441442
_app.MapGrpcService<OtlpGrpcLogsService>();
442443

443444
_app.MapDashboardApi(dashboardOptions);
445+
_app.MapDashboardHealthChecks();
444446
}
445447

446448
private ILogger<DashboardWebApplication> GetLogger()

src/Aspire.Dashboard/Utils/DashboardUrls.cs

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal static class DashboardUrls
1414
public const string StructuredLogsBasePath = "structuredlogs";
1515
public const string TracesBasePath = "traces";
1616
public const string LoginBasePath = "login";
17+
public const string HealthBasePath = "health";
1718

1819
public static string ResourcesUrl(string? resource = null, string? view = null, string? hiddenTypes = null, string? hiddenStates = null, string? hiddenHealthStates = null)
1920
{

src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
179179
dashboardResource.Annotations.Add(new ResourceSnapshotAnnotation(snapshot));
180180

181181
dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(ConfigureEnvironmentVariables));
182-
dashboardResource.Annotations.Add(new HealthCheckAnnotation(KnownHealthCheckNames.DasboardHealthCheck));
182+
dashboardResource.Annotations.Add(new HealthCheckAnnotation(KnownHealthCheckNames.DashboardHealthCheck));
183183
}
184184

185185
internal async Task ConfigureEnvironmentVariables(EnvironmentCallbackContext context)

src/Aspire.Hosting/DistributedApplicationBuilder.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -395,15 +395,19 @@ private void ConfigureDashboardHealthCheck()
395395
var dashboardOptions = sp.GetRequiredService<IOptions<DashboardOptions>>().Value;
396396
if (StringUtils.TryGetUriFromDelimitedString(dashboardOptions.DashboardUrl, ";", out var firstDashboardUrl))
397397
{
398-
return firstDashboardUrl;
398+
// Health checks to the dashboard should go to the /health endpoint. This endpoint allows anonymous requests.
399+
// Sending a request to other dashboard endpoints triggered auth, which the request fails, and is redirected to the login page.
400+
var uriBuilder = new UriBuilder(firstDashboardUrl);
401+
uriBuilder.Path = "/health";
402+
return uriBuilder.Uri;
399403
}
400404
else
401405
{
402406
throw new DistributedApplicationException($"The dashboard resource '{KnownResourceNames.AspireDashboard}' does not have endpoints.");
403407
}
404-
}, KnownHealthCheckNames.DasboardHealthCheck);
408+
}, KnownHealthCheckNames.DashboardHealthCheck);
405409

406-
_innerBuilder.Services.SuppressHealthCheckHttpClientLogging(KnownHealthCheckNames.DasboardHealthCheck);
410+
_innerBuilder.Services.SuppressHealthCheckHttpClientLogging(KnownHealthCheckNames.DashboardHealthCheck);
407411
}
408412

409413
private void ConfigureHealthChecks()

src/Shared/KnownHealthCheckNames.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ internal static class KnownHealthCheckNames
88
/// <summary>
99
/// Common name for dashboard health check.
1010
/// </summary>
11-
public const string DasboardHealthCheck = "aspire_dashboard_check";
11+
public const string DashboardHealthCheck = "aspire_dashboard_check";
1212
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Dashboard.Utils;
5+
using Microsoft.AspNetCore.InternalTesting;
6+
using System.Net;
7+
using Xunit;
8+
9+
namespace Aspire.Dashboard.Tests.Integration;
10+
11+
public class HealthTests(ITestOutputHelper testOutputHelper)
12+
{
13+
[Fact]
14+
public async Task HealthEndpoint_SendRequest_200Response()
15+
{
16+
// Arrange
17+
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper);
18+
await app.StartAsync().DefaultTimeout();
19+
20+
await MakeRequestAndAssert($"http://{app.FrontendSingleEndPointAccessor().EndPoint}", HttpVersion.Version11).DefaultTimeout();
21+
await MakeRequestAndAssert($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}", HttpVersion.Version11).DefaultTimeout();
22+
await MakeRequestAndAssert($"http://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", HttpVersion.Version20).DefaultTimeout();
23+
24+
static async Task MakeRequestAndAssert(string basePath, Version httpVersion)
25+
{
26+
using var httpClientHandler = new HttpClientHandler { AllowAutoRedirect = false };
27+
using var client = new HttpClient(httpClientHandler) { BaseAddress = new Uri(basePath) };
28+
29+
// Act
30+
var request = new HttpRequestMessage(HttpMethod.Get, $"/{DashboardUrls.HealthBasePath}");
31+
request.Version = httpVersion;
32+
request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
33+
var response = await client.SendAsync(request).DefaultTimeout();
34+
35+
// Assert
36+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)