using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace NATS.Server.Monitoring; /// /// HTTP monitoring server providing /healthz, /varz, and other monitoring endpoints. /// Corresponds to Go server/monitor.go HTTP server setup. /// public sealed class MonitorServer : IAsyncDisposable { private readonly WebApplication _app; private readonly ILogger _logger; private readonly VarzHandler _varzHandler; private readonly ConnzHandler _connzHandler; private readonly SubszHandler _subszHandler; private readonly JszHandler _jszHandler; private readonly RoutezHandler _routezHandler; private readonly GatewayzHandler _gatewayzHandler; private readonly LeafzHandler _leafzHandler; private readonly AccountzHandler _accountzHandler; private readonly PprofHandler _pprofHandler; public MonitorServer(NatsServer server, NatsOptions options, ServerStats stats, ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); var builder = WebApplication.CreateSlimBuilder(); builder.WebHost.UseUrls($"http://{options.MonitorHost}:{options.MonitorPort}"); builder.Logging.ClearProviders(); _app = builder.Build(); var basePath = options.MonitorBasePath ?? ""; _varzHandler = new VarzHandler(server, options); _connzHandler = new ConnzHandler(server); _subszHandler = new SubszHandler(server); _jszHandler = new JszHandler(server, options); _routezHandler = new RoutezHandler(server); _gatewayzHandler = new GatewayzHandler(server); _leafzHandler = new LeafzHandler(server); _accountzHandler = new AccountzHandler(server); _pprofHandler = new PprofHandler(); _app.MapGet(basePath + "/", () => { stats.HttpReqStats.AddOrUpdate("/", 1, (_, v) => v + 1); return Results.Ok(new { endpoints = new[] { "/varz", "/connz", "/healthz", "/routez", "/gatewayz", "/leafz", "/subz", "/accountz", "/jsz", }, }); }); _app.MapGet(basePath + "/healthz", () => { stats.HttpReqStats.AddOrUpdate("/healthz", 1, (_, v) => v + 1); return Results.Ok("ok"); }); _app.MapGet(basePath + "/varz", async (HttpContext ctx) => { stats.HttpReqStats.AddOrUpdate("/varz", 1, (_, v) => v + 1); return Results.Ok(await _varzHandler.HandleVarzAsync(ctx.RequestAborted)); }); _app.MapGet(basePath + "/connz", (HttpContext ctx) => { stats.HttpReqStats.AddOrUpdate("/connz", 1, (_, v) => v + 1); return Results.Ok(_connzHandler.HandleConnz(ctx)); }); _app.MapGet(basePath + "/routez", () => { stats.HttpReqStats.AddOrUpdate("/routez", 1, (_, v) => v + 1); return Results.Ok(_routezHandler.Build()); }); _app.MapGet(basePath + "/gatewayz", () => { stats.HttpReqStats.AddOrUpdate("/gatewayz", 1, (_, v) => v + 1); return Results.Ok(_gatewayzHandler.Build()); }); _app.MapGet(basePath + "/leafz", () => { stats.HttpReqStats.AddOrUpdate("/leafz", 1, (_, v) => v + 1); return Results.Ok(_leafzHandler.Build()); }); _app.MapGet(basePath + "/subz", (HttpContext ctx) => { stats.HttpReqStats.AddOrUpdate("/subz", 1, (_, v) => v + 1); return Results.Ok(_subszHandler.HandleSubsz(ctx)); }); _app.MapGet(basePath + "/subscriptionsz", (HttpContext ctx) => { stats.HttpReqStats.AddOrUpdate("/subscriptionsz", 1, (_, v) => v + 1); return Results.Ok(_subszHandler.HandleSubsz(ctx)); }); _app.MapGet(basePath + "/accountz", () => { stats.HttpReqStats.AddOrUpdate("/accountz", 1, (_, v) => v + 1); return Results.Ok(_accountzHandler.Build()); }); _app.MapGet(basePath + "/accstatz", () => { stats.HttpReqStats.AddOrUpdate("/accstatz", 1, (_, v) => v + 1); return Results.Ok(_accountzHandler.BuildStats()); }); _app.MapGet(basePath + "/jsz", () => { stats.HttpReqStats.AddOrUpdate("/jsz", 1, (_, v) => v + 1); return Results.Ok(_jszHandler.Build()); }); if (options.ProfPort > 0) { _app.MapGet(basePath + "/debug/pprof", () => { stats.HttpReqStats.AddOrUpdate("/debug/pprof", 1, (_, v) => v + 1); return Results.Text(_pprofHandler.Index(), "text/plain"); }); _app.MapGet(basePath + "/debug/pprof/profile", (HttpContext ctx) => { stats.HttpReqStats.AddOrUpdate("/debug/pprof/profile", 1, (_, v) => v + 1); var seconds = 30; if (ctx.Request.Query.TryGetValue("seconds", out var values) && int.TryParse(values.ToString(), out var parsed)) { seconds = parsed; } return Results.File(_pprofHandler.CaptureCpuProfile(seconds), "application/json"); }); } } public async Task StartAsync(CancellationToken ct) { await _app.StartAsync(ct); _logger.LogInformation("Monitoring listening on {Urls}", string.Join(", ", _app.Urls)); } public async ValueTask DisposeAsync() { await _app.StopAsync(); await _app.DisposeAsync(); _varzHandler.Dispose(); } }