feat(server): answer the root path and probe container health
A bare GET / previously returned an empty 404, which reads as an outage even while the service is healthy. The root now redirects to /diagnostics when the dashboard is enabled and otherwise returns a small JSON status pointing at /health/live. The route is excluded from the OpenAPI description, so the frozen v1 surface is unchanged. The chiseled runtime image ships no shell or curl, so the container healthcheck re-enters the server assembly with --health-probe, which queries the local liveness endpoint and reports through the exit code. Dockerfile and compose both wire the probe (30s interval, 3 retries).
This commit is contained in:
@@ -32,4 +32,8 @@ COPY --from=build --chown=1654:1654 /out/ ./
|
|||||||
USER 1654:1654
|
USER 1654:1654
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
EXPOSE 9050/udp
|
EXPOSE 9050/udp
|
||||||
|
# The chiseled image has no shell or curl; the probe re-enters the server
|
||||||
|
# assembly in --health-probe mode and reports through the exit code.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD ["dotnet", "FinalFactory.Rendezvous.Server.dll", "--health-probe"]
|
||||||
ENTRYPOINT ["dotnet", "FinalFactory.Rendezvous.Server.dll"]
|
ENTRYPOINT ["dotnet", "FinalFactory.Rendezvous.Server.dll"]
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ services:
|
|||||||
hard: 4096
|
hard: 4096
|
||||||
stop_grace_period: 40s
|
stop_grace_period: 40s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "dotnet", "FinalFactory.Rendezvous.Server.dll", "--health-probe"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
environment:
|
environment:
|
||||||
ASPNETCORE_ENVIRONMENT: Production
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
ASPNETCORE_HTTP_PORTS: "8080"
|
ASPNETCORE_HTTP_PORTS: "8080"
|
||||||
|
|||||||
@@ -57,11 +57,13 @@
|
|||||||
<Compile Include="Diagnostics/DiagnosticDashboardOptions.cs" />
|
<Compile Include="Diagnostics/DiagnosticDashboardOptions.cs" />
|
||||||
<Compile Include="Http/ContractEndpoints.cs" />
|
<Compile Include="Http/ContractEndpoints.cs" />
|
||||||
<Compile Include="Http/RendezvousExceptionHandler.cs" />
|
<Compile Include="Http/RendezvousExceptionHandler.cs" />
|
||||||
|
<Compile Include="Http/RootEndpoints.cs" />
|
||||||
<Compile Include="JoinAttempts/JoinAttemptCursorCodec.cs" />
|
<Compile Include="JoinAttempts/JoinAttemptCursorCodec.cs" />
|
||||||
<Compile Include="JoinAttempts/JoinAttemptService.cs" />
|
<Compile Include="JoinAttempts/JoinAttemptService.cs" />
|
||||||
<Compile Include="Observability/AuditOptions.cs" />
|
<Compile Include="Observability/AuditOptions.cs" />
|
||||||
<Compile Include="Observability/AuditTrail.cs" />
|
<Compile Include="Observability/AuditTrail.cs" />
|
||||||
<Compile Include="Observability/HealthEndpoints.cs" />
|
<Compile Include="Observability/HealthEndpoints.cs" />
|
||||||
|
<Compile Include="Observability/HealthProbe.cs" />
|
||||||
<Compile Include="Observability/PrometheusMetricsEndpoint.cs" />
|
<Compile Include="Observability/PrometheusMetricsEndpoint.cs" />
|
||||||
<Compile Include="Observability/RendezvousReadiness.cs" />
|
<Compile Include="Observability/RendezvousReadiness.cs" />
|
||||||
<Compile Include="Observability/RendezvousTelemetry.cs" />
|
<Compile Include="Observability/RendezvousTelemetry.cs" />
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using FinalFactory.Rendezvous.Contracts;
|
||||||
|
using FinalFactory.Rendezvous.Server.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FinalFactory.Rendezvous.Server.Http;
|
||||||
|
|
||||||
|
internal static class RootEndpoints
|
||||||
|
{
|
||||||
|
public static IEndpointRouteBuilder MapRootEndpoint(
|
||||||
|
this IEndpointRouteBuilder endpoints)
|
||||||
|
{
|
||||||
|
endpoints.MapGet("/", ServeRoot).ExcludeFromDescription();
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IResult ServeRoot(
|
||||||
|
[FromServices] IOptions<DiagnosticDashboardOptions> diagnostics) =>
|
||||||
|
diagnostics.Value.Enabled
|
||||||
|
? Results.Redirect("/diagnostics")
|
||||||
|
: Results.Json(new
|
||||||
|
{
|
||||||
|
contractVersion = ContractLimits.ContractVersion,
|
||||||
|
service = "rendezvous",
|
||||||
|
health = "/health/live",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace FinalFactory.Rendezvous.Server.Observability;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Container healthcheck entry mode (<c>--health-probe</c>). The chiseled
|
||||||
|
/// runtime image ships no shell or curl, so the probe re-enters the server
|
||||||
|
/// assembly, queries the local liveness endpoint, and reports via exit code.
|
||||||
|
/// </summary>
|
||||||
|
internal static class HealthProbe
|
||||||
|
{
|
||||||
|
public const string Argument = "--health-probe";
|
||||||
|
|
||||||
|
public static async Task<int> RunAsync()
|
||||||
|
{
|
||||||
|
string configuredPorts =
|
||||||
|
Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS") ?? "8080";
|
||||||
|
string port = configuredPorts
|
||||||
|
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.FirstOrDefault() ?? "8080";
|
||||||
|
using HttpClient client = new() { Timeout = TimeSpan.FromSeconds(4) };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using HttpResponseMessage response = await client.GetAsync(
|
||||||
|
new Uri($"http://127.0.0.1:{port}/health/live"));
|
||||||
|
return response.IsSuccessStatusCode ? 0 : 1;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
when (exception is HttpRequestException
|
||||||
|
or TaskCanceledException
|
||||||
|
or UriFormatException)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,11 @@ using FinalFactory.Rendezvous.Server.Transport;
|
|||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.OpenApi;
|
using Microsoft.OpenApi;
|
||||||
|
|
||||||
|
if (args.Contains(HealthProbe.Argument, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return await HealthProbe.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Logging.AddFilter(
|
builder.Logging.AddFilter(
|
||||||
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
|
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
|
||||||
@@ -378,6 +383,7 @@ app.UseMiddleware<TelemetryMiddleware>();
|
|||||||
app.UseExceptionHandler();
|
app.UseExceptionHandler();
|
||||||
app.UseMiddleware<HttpAbuseProtectionMiddleware>();
|
app.UseMiddleware<HttpAbuseProtectionMiddleware>();
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
|
app.MapRootEndpoint();
|
||||||
app.MapRendezvousContractEndpoints();
|
app.MapRendezvousContractEndpoints();
|
||||||
app.MapOperatorEndpoints();
|
app.MapOperatorEndpoints();
|
||||||
app.MapRendezvousHealthEndpoints();
|
app.MapRendezvousHealthEndpoints();
|
||||||
@@ -385,6 +391,7 @@ app.MapDiagnosticDashboardEndpoints();
|
|||||||
app.MapPrometheusMetricsEndpoint(metricsOptions, metricsCredential);
|
app.MapPrometheusMetricsEndpoint(metricsOptions, metricsCredential);
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
|
return 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entry point marker used by integration-test hosts.
|
/// Entry point marker used by integration-test hosts.
|
||||||
|
|||||||
Reference in New Issue
Block a user