feat(monitoring+events): add connz filtering, event payloads, and message trace context (E12+E13+E14)

- Add ConnzHandler with sorting, filtering, pagination, CID lookup, and closed connection ring buffer
- Add full Go events.go parity types (ConnectEventMsg, DisconnectEventMsg, ServerStatsMsg, etc.)
- Add MessageTraceContext for per-message trace propagation with header parsing
- 74 new tests (17 ConnzFilter + 16 EventPayload + 41 MessageTraceContext)
This commit is contained in:
Joseph Doherty
2026-02-24 16:17:21 -05:00
parent 37d3cc29ea
commit 94878d3dcc
10 changed files with 2595 additions and 15 deletions

View File

@@ -218,6 +218,18 @@ public sealed class ConnzOptions
public string MqttClient { get; set; } = "";
/// <summary>
/// When non-zero, returns only the connection with this CID.
/// Go reference: monitor.go ConnzOptions.CID.
/// </summary>
public ulong Cid { get; set; }
/// <summary>
/// Whether to include authorized user info.
/// Go reference: monitor.go ConnzOptions.Username.
/// </summary>
public bool Auth { get; set; }
public int Offset { get; set; }
public int Limit { get; set; } = 1024;

View File

@@ -16,6 +16,13 @@ public sealed class ConnzHandler(NatsServer server)
var connInfos = new List<ConnInfo>();
// If a specific CID is requested, search for that single connection
// Go reference: monitor.go Connz() — CID fast path
if (opts.Cid > 0)
{
return HandleSingleCid(opts, now);
}
// Collect open connections
if (opts.State is ConnState.Open or ConnState.All)
{
@@ -23,7 +30,7 @@ public sealed class ConnzHandler(NatsServer server)
connInfos.AddRange(clients.Select(c => BuildConnInfo(c, now, opts)));
}
// Collect closed connections
// Collect closed connections from the ring buffer
if (opts.State is ConnState.Closed or ConnState.All)
{
connInfos.AddRange(server.GetClosedClients().Select(c => BuildClosedConnInfo(c, now, opts)));
@@ -81,6 +88,59 @@ public sealed class ConnzHandler(NatsServer server)
};
}
/// <summary>
/// Handles a request for a single connection by CID.
/// Go reference: monitor.go Connz() — CID-specific path.
/// </summary>
private Connz HandleSingleCid(ConnzOptions opts, DateTime now)
{
// Search open connections first
var client = server.GetClients().FirstOrDefault(c => c.Id == opts.Cid);
if (client != null)
{
var info = BuildConnInfo(client, now, opts);
return new Connz
{
Id = server.ServerId,
Now = now,
NumConns = 1,
Total = 1,
Offset = 0,
Limit = 1,
Conns = [info],
};
}
// Search closed connections ring buffer
var closed = server.GetClosedClients().FirstOrDefault(c => c.Cid == opts.Cid);
if (closed != null)
{
var info = BuildClosedConnInfo(closed, now, opts);
return new Connz
{
Id = server.ServerId,
Now = now,
NumConns = 1,
Total = 1,
Offset = 0,
Limit = 1,
Conns = [info],
};
}
// Not found — return empty result
return new Connz
{
Id = server.ServerId,
Now = now,
NumConns = 0,
Total = 0,
Offset = 0,
Limit = 0,
Conns = [],
};
}
private static ConnInfo BuildConnInfo(NatsClient client, DateTime now, ConnzOptions opts)
{
var info = new ConnInfo
@@ -228,6 +288,12 @@ public sealed class ConnzHandler(NatsServer server)
if (q.TryGetValue("limit", out var limit) && int.TryParse(limit, out var l))
opts.Limit = l;
if (q.TryGetValue("cid", out var cid) && ulong.TryParse(cid, out var cidValue))
opts.Cid = cidValue;
if (q.TryGetValue("auth", out var auth))
opts.Auth = auth.ToString().ToLowerInvariant() is "1" or "true";
if (q.TryGetValue("mqtt_client", out var mqttClient))
opts.MqttClient = mqttClient.ToString();
@@ -243,10 +309,13 @@ public sealed class ConnzHandler(NatsServer server)
private static bool MatchesSubjectFilter(ConnInfo info, string filterSubject)
{
if (info.Subs.Any(s => SubjectMatch.MatchLiteral(s, filterSubject)))
// Go reference: monitor.go — matchLiteral(testSub, string(sub.subject))
// The filter subject is the literal, the subscription subject is the pattern
// (subscriptions may contain wildcards like orders.> that match the filter orders.new)
if (info.Subs.Any(s => SubjectMatch.MatchLiteral(filterSubject, s)))
return true;
return info.SubsDetail.Any(s => SubjectMatch.MatchLiteral(s.Subject, filterSubject));
return info.SubsDetail.Any(s => SubjectMatch.MatchLiteral(filterSubject, s.Subject));
}
private static string FormatRtt(TimeSpan rtt)