using Microsoft.AspNetCore.Http; namespace NATS.Server.Monitoring; /// /// Handles /connz endpoint requests, returning detailed connection information. /// Corresponds to Go server/monitor.go handleConnz function. /// public sealed class ConnzHandler(NatsServer server) { public Connz HandleConnz(HttpContext ctx) { var opts = ParseQueryParams(ctx); var now = DateTime.UtcNow; var connInfos = new List(); // Collect open connections if (opts.State is ConnState.Open or ConnState.All) { var clients = server.GetClients().ToArray(); connInfos.AddRange(clients.Select(c => BuildConnInfo(c, now, opts))); } // Collect closed connections if (opts.State is ConnState.Closed or ConnState.All) { connInfos.AddRange(server.GetClosedClients().Select(c => BuildClosedConnInfo(c, now, opts))); } // Validate sort options that require closed state if (opts.Sort is SortOpt.ByStop or SortOpt.ByReason && opts.State == ConnState.Open) opts.Sort = SortOpt.ByCid; // Fallback // Sort connInfos = opts.Sort switch { SortOpt.ByCid => connInfos.OrderBy(c => c.Cid).ToList(), SortOpt.ByStart => connInfos.OrderBy(c => c.Start).ToList(), SortOpt.BySubs => connInfos.OrderByDescending(c => c.NumSubs).ToList(), SortOpt.ByPending => connInfos.OrderByDescending(c => c.Pending).ToList(), SortOpt.ByMsgsTo => connInfos.OrderByDescending(c => c.OutMsgs).ToList(), SortOpt.ByMsgsFrom => connInfos.OrderByDescending(c => c.InMsgs).ToList(), SortOpt.ByBytesTo => connInfos.OrderByDescending(c => c.OutBytes).ToList(), SortOpt.ByBytesFrom => connInfos.OrderByDescending(c => c.InBytes).ToList(), SortOpt.ByLast => connInfos.OrderByDescending(c => c.LastActivity).ToList(), SortOpt.ByIdle => connInfos.OrderByDescending(c => now - c.LastActivity).ToList(), SortOpt.ByUptime => connInfos.OrderByDescending(c => now - c.Start).ToList(), SortOpt.ByStop => connInfos.OrderByDescending(c => c.Stop ?? DateTime.MinValue).ToList(), SortOpt.ByReason => connInfos.OrderBy(c => c.Reason).ToList(), SortOpt.ByRtt => connInfos.OrderBy(c => c.Rtt).ToList(), _ => connInfos.OrderBy(c => c.Cid).ToList(), }; var total = connInfos.Count; var paged = connInfos.Skip(opts.Offset).Take(opts.Limit).ToArray(); return new Connz { Id = server.ServerId, Now = now, NumConns = paged.Length, Total = total, Offset = opts.Offset, Limit = opts.Limit, Conns = paged, }; } private static ConnInfo BuildConnInfo(NatsClient client, DateTime now, ConnzOptions opts) { var info = new ConnInfo { Cid = client.Id, Kind = "Client", Type = "Client", Ip = client.RemoteIp ?? "", Port = client.RemotePort, Start = client.StartTime, LastActivity = client.LastActivity, Uptime = FormatDuration(now - client.StartTime), Idle = FormatDuration(now - client.LastActivity), InMsgs = Interlocked.Read(ref client.InMsgs), OutMsgs = Interlocked.Read(ref client.OutMsgs), InBytes = Interlocked.Read(ref client.InBytes), OutBytes = Interlocked.Read(ref client.OutBytes), NumSubs = (uint)client.Subscriptions.Count, Name = client.ClientOpts?.Name ?? "", Lang = client.ClientOpts?.Lang ?? "", Version = client.ClientOpts?.Version ?? "", Pending = (int)client.PendingBytes, Reason = client.CloseReason.ToReasonString(), TlsVersion = client.TlsState?.TlsVersion ?? "", TlsCipherSuite = client.TlsState?.CipherSuite ?? "", Rtt = FormatRtt(client.Rtt), }; if (opts.Subscriptions) { info.Subs = client.Subscriptions.Values.Select(s => s.Subject).ToArray(); } if (opts.SubscriptionsDetail) { info.SubsDetail = client.Subscriptions.Values.Select(s => new SubDetail { Subject = s.Subject, Queue = s.Queue ?? "", Sid = s.Sid, Msgs = Interlocked.Read(ref s.MessageCount), Max = s.MaxMessages, Cid = client.Id, }).ToArray(); } return info; } private static ConnInfo BuildClosedConnInfo(ClosedClient closed, DateTime now, ConnzOptions opts) { return new ConnInfo { Cid = closed.Cid, Kind = "Client", Type = "Client", Ip = closed.Ip, Port = closed.Port, Start = closed.Start, Stop = closed.Stop, LastActivity = closed.Stop, Uptime = FormatDuration(closed.Stop - closed.Start), Idle = FormatDuration(now - closed.Stop), InMsgs = closed.InMsgs, OutMsgs = closed.OutMsgs, InBytes = closed.InBytes, OutBytes = closed.OutBytes, NumSubs = closed.NumSubs, Name = closed.Name, Lang = closed.Lang, Version = closed.Version, Reason = closed.Reason, Rtt = FormatRtt(closed.Rtt), TlsVersion = closed.TlsVersion, TlsCipherSuite = closed.TlsCipherSuite, }; } private static ConnzOptions ParseQueryParams(HttpContext ctx) { var q = ctx.Request.Query; var opts = new ConnzOptions(); if (q.TryGetValue("sort", out var sort)) { opts.Sort = sort.ToString().ToLowerInvariant() switch { "cid" => SortOpt.ByCid, "start" => SortOpt.ByStart, "subs" => SortOpt.BySubs, "pending" => SortOpt.ByPending, "msgs_to" => SortOpt.ByMsgsTo, "msgs_from" => SortOpt.ByMsgsFrom, "bytes_to" => SortOpt.ByBytesTo, "bytes_from" => SortOpt.ByBytesFrom, "last" => SortOpt.ByLast, "idle" => SortOpt.ByIdle, "uptime" => SortOpt.ByUptime, "rtt" => SortOpt.ByRtt, "stop" => SortOpt.ByStop, "reason" => SortOpt.ByReason, _ => SortOpt.ByCid, }; } if (q.TryGetValue("subs", out var subs)) { if (subs == "detail") opts.SubscriptionsDetail = true; else opts.Subscriptions = true; } if (q.TryGetValue("state", out var state)) { opts.State = state.ToString().ToLowerInvariant() switch { "open" => ConnState.Open, "closed" => ConnState.Closed, "all" => ConnState.All, _ => ConnState.Open, }; } if (q.TryGetValue("offset", out var offset) && int.TryParse(offset, out var o)) opts.Offset = o; if (q.TryGetValue("limit", out var limit) && int.TryParse(limit, out var l)) opts.Limit = l; return opts; } private static string FormatRtt(TimeSpan rtt) { if (rtt == TimeSpan.Zero) return ""; if (rtt.TotalMilliseconds < 1) return $"{rtt.TotalMicroseconds:F3}\u00b5s"; if (rtt.TotalSeconds < 1) return $"{rtt.TotalMilliseconds:F3}ms"; return $"{rtt.TotalSeconds:F3}s"; } private static string FormatDuration(TimeSpan ts) { if (ts.TotalDays >= 1) return $"{(int)ts.TotalDays}d{ts.Hours}h{ts.Minutes}m{ts.Seconds}s"; if (ts.TotalHours >= 1) return $"{(int)ts.TotalHours}h{ts.Minutes}m{ts.Seconds}s"; if (ts.TotalMinutes >= 1) return $"{(int)ts.TotalMinutes}m{ts.Seconds}s"; return $"{(int)ts.TotalSeconds}s"; } }