feat(observability): add diagnostic dashboards (#27)
quality-gate / quality (push) Failing after 1m31s
quality-gate / container (push) Has been skipped

This commit is contained in:
KyuubiYoru
2026-07-17 00:22:46 +02:00
parent 06c4ecf8f3
commit 99885f8c8c
30 changed files with 3173 additions and 18 deletions
@@ -0,0 +1,111 @@
using System.Reflection;
using FinalFactory.Rendezvous.Contracts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace FinalFactory.Rendezvous.Server.Diagnostics;
internal static class DiagnosticDashboardEndpoints
{
private const string ContentSecurityPolicy =
"default-src 'none'; base-uri 'none'; connect-src 'self'; "
+ "font-src 'self'; form-action 'none'; frame-ancestors 'none'; "
+ "img-src 'self'; manifest-src 'none'; object-src 'none'; "
+ "script-src 'self'; style-src 'self'";
private static readonly byte[] Index = ReadAsset("index.html");
private static readonly byte[] Script = ReadAsset("app.mjs");
private static readonly byte[] Styles = ReadAsset("styles.css");
public static IEndpointRouteBuilder MapDiagnosticDashboardEndpoints(
this IEndpointRouteBuilder endpoints)
{
RouteGroupBuilder dashboard = endpoints.MapGroup("/diagnostics")
.ExcludeFromDescription();
dashboard.MapGet("", ServeIndex);
dashboard.MapGet("/app.mjs", ServeScript);
dashboard.MapGet("/styles.css", ServeStyles);
dashboard.MapGet("/config.json", ServeConfiguration);
return endpoints;
}
private static IResult ServeIndex(
HttpContext context,
[FromServices] IOptions<DiagnosticDashboardOptions> configured) =>
ServeAsset(context, configured.Value, Index, "text/html; charset=utf-8");
private static IResult ServeScript(
HttpContext context,
[FromServices] IOptions<DiagnosticDashboardOptions> configured) =>
ServeAsset(context, configured.Value, Script, "text/javascript; charset=utf-8");
private static IResult ServeStyles(
HttpContext context,
[FromServices] IOptions<DiagnosticDashboardOptions> configured) =>
ServeAsset(context, configured.Value, Styles, "text/css; charset=utf-8");
private static IResult ServeConfiguration(
HttpContext context,
[FromServices] IOptions<DiagnosticDashboardOptions> configured)
{
DiagnosticDashboardOptions options = configured.Value;
if (!options.Enabled)
{
return Results.NotFound();
}
ApplySecurityHeaders(context.Response);
return Results.Json(new
{
contractVersion = ContractLimits.ContractVersion,
pollIntervalSeconds = options.PollIntervalSeconds,
maximumRenderedSessions = options.MaximumRenderedSessions,
scopes = options.Scopes.Select(static scope => new
{
gameId = scope.GameId,
environmentId = scope.EnvironmentId,
protocolVersions = scope.ProtocolVersions,
regions = scope.Regions,
visibility = "public",
}),
});
}
private static IResult ServeAsset(
HttpContext context,
DiagnosticDashboardOptions options,
byte[] content,
string contentType)
{
if (!options.Enabled)
{
return Results.NotFound();
}
ApplySecurityHeaders(context.Response);
return Results.Bytes(content, contentType);
}
private static void ApplySecurityHeaders(HttpResponse response)
{
response.Headers.CacheControl = "no-store";
response.Headers["Content-Security-Policy"] = ContentSecurityPolicy;
response.Headers["X-Content-Type-Options"] = "nosniff";
response.Headers["X-Frame-Options"] = "DENY";
response.Headers["Cross-Origin-Opener-Policy"] = "same-origin";
response.Headers["Cross-Origin-Resource-Policy"] = "same-origin";
response.Headers["Permissions-Policy"] =
"camera=(), geolocation=(), microphone=(), payment=(), usb=()";
response.Headers["Referrer-Policy"] = "no-referrer";
}
private static byte[] ReadAsset(string fileName)
{
Assembly assembly = typeof(DiagnosticDashboardEndpoints).Assembly;
string resourceName = $"FinalFactory.Rendezvous.Server.Diagnostics.Assets.{fileName}";
using Stream source = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Missing embedded dashboard asset {fileName}.");
using MemoryStream destination = new();
source.CopyTo(destination);
return destination.ToArray();
}
}