Compare commits

...
Author SHA1 Message Date
KyuubiYoru dc2de76963 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).
2026-08-31 03:55:17 +02:00
6 changed files with 80 additions and 0 deletions
+4
View File
@@ -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"]
+6
View File
@@ -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.