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:
420
tests/NATS.Server.Tests/Monitoring/ConnzFilterTests.cs
Normal file
420
tests/NATS.Server.Tests/Monitoring/ConnzFilterTests.cs
Normal file
@@ -0,0 +1,420 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Monitoring;
|
||||
|
||||
namespace NATS.Server.Tests.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ConnzHandler filtering, sorting, pagination, and closed connection
|
||||
/// ring buffer behavior.
|
||||
/// Go reference: monitor_test.go — TestConnz, TestConnzSortedByCid, TestConnzSortedByBytesTo,
|
||||
/// TestConnzFilter, TestConnzWithCID, TestConnzOffsetAndLimit.
|
||||
/// </summary>
|
||||
public class ConnzFilterTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly NatsOptions _opts;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly List<Socket> _sockets = [];
|
||||
|
||||
public ConnzFilterTests()
|
||||
{
|
||||
_opts = new NatsOptions
|
||||
{
|
||||
Port = GetFreePort(),
|
||||
MaxClosedClients = 100,
|
||||
Users =
|
||||
[
|
||||
new User { Username = "alice", Password = "pw", Account = "acctA" },
|
||||
new User { Username = "bob", Password = "pw", Account = "acctB" },
|
||||
],
|
||||
};
|
||||
_server = new NatsServer(_opts, NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ = _server.StartAsync(_cts.Token);
|
||||
await _server.WaitForReadyAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
foreach (var s in _sockets)
|
||||
{
|
||||
try { s.Shutdown(SocketShutdown.Both); } catch { }
|
||||
s.Dispose();
|
||||
}
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
|
||||
private async Task<Socket> ConnectAsync(string user, string? subjectToSubscribe = null)
|
||||
{
|
||||
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
_sockets.Add(sock);
|
||||
await sock.ConnectAsync(IPAddress.Loopback, _opts.Port);
|
||||
|
||||
var buf = new byte[4096];
|
||||
await sock.ReceiveAsync(buf, SocketFlags.None); // INFO
|
||||
|
||||
var connect = $"CONNECT {{\"user\":\"{user}\",\"pass\":\"pw\"}}\r\n";
|
||||
await sock.SendAsync(Encoding.ASCII.GetBytes(connect));
|
||||
|
||||
if (subjectToSubscribe != null)
|
||||
{
|
||||
await sock.SendAsync(Encoding.ASCII.GetBytes($"SUB {subjectToSubscribe} sid1\r\n"));
|
||||
}
|
||||
|
||||
await sock.SendAsync("PING\r\n"u8.ToArray());
|
||||
await ReadUntilAsync(sock, "PONG");
|
||||
return sock;
|
||||
}
|
||||
|
||||
private Connz GetConnz(string queryString = "")
|
||||
{
|
||||
var ctx = new DefaultHttpContext();
|
||||
ctx.Request.QueryString = new QueryString(queryString);
|
||||
return new ConnzHandler(_server).HandleConnz(ctx);
|
||||
}
|
||||
|
||||
// --- Sort tests ---
|
||||
|
||||
[Fact]
|
||||
public async Task Sort_by_cid_returns_ascending_order()
|
||||
{
|
||||
await ConnectAsync("alice");
|
||||
await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
var connz = GetConnz("?sort=cid");
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
|
||||
for (int i = 1; i < connz.Conns.Length; i++)
|
||||
{
|
||||
connz.Conns[i].Cid.ShouldBeGreaterThan(connz.Conns[i - 1].Cid);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sort_by_bytes_to_returns_descending_order()
|
||||
{
|
||||
var sock1 = await ConnectAsync("alice");
|
||||
var sock2 = await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
// Publish some data through sock1 to accumulate bytes
|
||||
await sock1.SendAsync(Encoding.ASCII.GetBytes("SUB test 1\r\nPUB test 10\r\n1234567890\r\n"));
|
||||
await Task.Delay(100);
|
||||
|
||||
var connz = GetConnz("?sort=bytes_to");
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
|
||||
for (int i = 1; i < connz.Conns.Length; i++)
|
||||
{
|
||||
connz.Conns[i].OutBytes.ShouldBeLessThanOrEqualTo(connz.Conns[i - 1].OutBytes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sort_by_msgs_from_returns_descending_order()
|
||||
{
|
||||
var sock1 = await ConnectAsync("alice");
|
||||
await Task.Delay(50);
|
||||
|
||||
// Send a PUB to increment InMsgs
|
||||
await sock1.SendAsync(Encoding.ASCII.GetBytes("PUB test 3\r\nabc\r\n"));
|
||||
await Task.Delay(100);
|
||||
|
||||
var connz = GetConnz("?sort=msgs_from");
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
|
||||
for (int i = 1; i < connz.Conns.Length; i++)
|
||||
{
|
||||
connz.Conns[i].InMsgs.ShouldBeLessThanOrEqualTo(connz.Conns[i - 1].InMsgs);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sort_by_subs_returns_descending_order()
|
||||
{
|
||||
// Alice has 2 subs, Bob has 1
|
||||
var sock1 = await ConnectAsync("alice", "test.a");
|
||||
await sock1.SendAsync(Encoding.ASCII.GetBytes("SUB test.b sid2\r\n"));
|
||||
var sock2 = await ConnectAsync("bob", "test.c");
|
||||
await Task.Delay(100);
|
||||
|
||||
var connz = GetConnz("?sort=subs");
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
|
||||
for (int i = 1; i < connz.Conns.Length; i++)
|
||||
{
|
||||
connz.Conns[i].NumSubs.ShouldBeLessThanOrEqualTo(connz.Conns[i - 1].NumSubs);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sort_by_start_returns_ascending_order()
|
||||
{
|
||||
await ConnectAsync("alice");
|
||||
await Task.Delay(20);
|
||||
await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
var connz = GetConnz("?sort=start");
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
|
||||
for (int i = 1; i < connz.Conns.Length; i++)
|
||||
{
|
||||
connz.Conns[i].Start.ShouldBeGreaterThanOrEqualTo(connz.Conns[i - 1].Start);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Filter tests ---
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_by_account_returns_only_matching_connections()
|
||||
{
|
||||
await ConnectAsync("alice");
|
||||
await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
var connz = GetConnz("?acc=acctA");
|
||||
connz.Conns.ShouldAllBe(c => c.Account == "acctA");
|
||||
connz.Conns.ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_by_user_returns_only_matching_connections()
|
||||
{
|
||||
await ConnectAsync("alice");
|
||||
await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
var connz = GetConnz("?user=bob");
|
||||
connz.Conns.ShouldAllBe(c => c.AuthorizedUser == "bob");
|
||||
connz.Conns.ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_by_subject_returns_matching_subscribers()
|
||||
{
|
||||
await ConnectAsync("alice", "orders.>");
|
||||
await ConnectAsync("bob", "payments.>");
|
||||
await Task.Delay(50);
|
||||
|
||||
var connz = GetConnz("?filter_subject=orders.new&subs=1");
|
||||
connz.Conns.ShouldNotBeEmpty();
|
||||
connz.Conns.ShouldAllBe(c => c.Subs.Any(s => s.Contains("orders")));
|
||||
}
|
||||
|
||||
// --- Pagination tests ---
|
||||
|
||||
[Fact]
|
||||
public async Task Offset_and_limit_paginates_results()
|
||||
{
|
||||
await ConnectAsync("alice");
|
||||
await ConnectAsync("bob");
|
||||
await ConnectAsync("alice");
|
||||
await Task.Delay(50);
|
||||
|
||||
var page1 = GetConnz("?sort=cid&limit=2&offset=0");
|
||||
page1.Conns.Length.ShouldBe(2);
|
||||
page1.Total.ShouldBeGreaterThanOrEqualTo(3);
|
||||
page1.Offset.ShouldBe(0);
|
||||
page1.Limit.ShouldBe(2);
|
||||
|
||||
var page2 = GetConnz("?sort=cid&limit=2&offset=2");
|
||||
page2.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
page2.Offset.ShouldBe(2);
|
||||
|
||||
// Ensure no overlap between pages
|
||||
var page1Cids = page1.Conns.Select(c => c.Cid).ToHashSet();
|
||||
var page2Cids = page2.Conns.Select(c => c.Cid).ToHashSet();
|
||||
page1Cids.Overlaps(page2Cids).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// --- CID lookup test ---
|
||||
|
||||
[Fact]
|
||||
public async Task Cid_lookup_returns_single_connection()
|
||||
{
|
||||
await ConnectAsync("alice");
|
||||
await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
// Get all connections to find a known CID
|
||||
var all = GetConnz("?sort=cid");
|
||||
all.Conns.ShouldNotBeEmpty();
|
||||
var targetCid = all.Conns[0].Cid;
|
||||
|
||||
var single = GetConnz($"?cid={targetCid}");
|
||||
single.Conns.Length.ShouldBe(1);
|
||||
single.Conns[0].Cid.ShouldBe(targetCid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cid_lookup_nonexistent_returns_empty()
|
||||
{
|
||||
var result = GetConnz("?cid=99999999");
|
||||
result.Conns.Length.ShouldBe(0);
|
||||
result.Total.ShouldBe(0);
|
||||
}
|
||||
|
||||
// --- Closed connection tests ---
|
||||
|
||||
[Fact]
|
||||
public async Task Closed_state_shows_disconnected_clients()
|
||||
{
|
||||
var sock = await ConnectAsync("alice");
|
||||
await Task.Delay(50);
|
||||
|
||||
// Close the connection
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Close();
|
||||
_sockets.Remove(sock);
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = GetConnz("?state=closed");
|
||||
connz.Conns.ShouldNotBeEmpty();
|
||||
connz.Conns.ShouldAllBe(c => c.Stop != null);
|
||||
connz.Conns.ShouldAllBe(c => !string.IsNullOrEmpty(c.Reason));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task All_state_shows_both_open_and_closed()
|
||||
{
|
||||
var sock1 = await ConnectAsync("alice");
|
||||
var sock2 = await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
// Close one connection
|
||||
sock1.Shutdown(SocketShutdown.Both);
|
||||
sock1.Close();
|
||||
_sockets.Remove(sock1);
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = GetConnz("?state=all");
|
||||
connz.Total.ShouldBeGreaterThanOrEqualTo(2);
|
||||
// Should have at least one open (bob) and one closed (alice)
|
||||
connz.Conns.Any(c => c.Stop == null).ShouldBeTrue("expected at least one open connection");
|
||||
connz.Conns.Any(c => c.Stop != null).ShouldBeTrue("expected at least one closed connection");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Closed_ring_buffer_caps_at_max()
|
||||
{
|
||||
// MaxClosedClients is 100, create and close 5 connections
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var sock = await ConnectAsync("alice");
|
||||
await Task.Delay(20);
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Close();
|
||||
_sockets.Remove(sock);
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
var connz = GetConnz("?state=closed");
|
||||
connz.Total.ShouldBeLessThanOrEqualTo(_opts.MaxClosedClients);
|
||||
}
|
||||
|
||||
// --- Sort fallback tests ---
|
||||
|
||||
[Fact]
|
||||
public async Task Sort_by_stop_with_open_state_falls_back_to_cid()
|
||||
{
|
||||
await ConnectAsync("alice");
|
||||
await ConnectAsync("bob");
|
||||
await Task.Delay(50);
|
||||
|
||||
// sort=stop with state=open should fall back to cid sorting
|
||||
var connz = GetConnz("?sort=stop&state=open");
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
|
||||
for (int i = 1; i < connz.Conns.Length; i++)
|
||||
{
|
||||
connz.Conns[i].Cid.ShouldBeGreaterThan(connz.Conns[i - 1].Cid);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Combined filter + sort test ---
|
||||
|
||||
[Fact]
|
||||
public async Task Account_filter_with_bytes_sort_and_limit()
|
||||
{
|
||||
// Connect multiple alice clients
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var sock = await ConnectAsync("alice");
|
||||
// Send varying amounts of data
|
||||
var data = new string('x', (i + 1) * 100);
|
||||
await sock.SendAsync(Encoding.ASCII.GetBytes($"SUB test 1\r\nPUB test {data.Length}\r\n{data}\r\n"));
|
||||
}
|
||||
await ConnectAsync("bob");
|
||||
await Task.Delay(100);
|
||||
|
||||
var connz = GetConnz("?acc=acctA&sort=bytes_to&limit=2");
|
||||
connz.Conns.Length.ShouldBeLessThanOrEqualTo(2);
|
||||
connz.Conns.ShouldAllBe(c => c.Account == "acctA");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Closed_cid_lookup_returns_from_ring_buffer()
|
||||
{
|
||||
var sock = await ConnectAsync("alice");
|
||||
await Task.Delay(50);
|
||||
|
||||
// Get the CID before closing
|
||||
var all = GetConnz("?sort=cid");
|
||||
all.Conns.ShouldNotBeEmpty();
|
||||
var targetCid = all.Conns.Last().Cid;
|
||||
|
||||
// Close the socket
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Close();
|
||||
_sockets.Remove(sock);
|
||||
await Task.Delay(200);
|
||||
|
||||
// Look up closed connection by CID
|
||||
var single = GetConnz($"?cid={targetCid}");
|
||||
single.Conns.Length.ShouldBe(1);
|
||||
single.Conns[0].Cid.ShouldBe(targetCid);
|
||||
single.Conns[0].Stop.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
private static async Task ReadUntilAsync(Socket sock, string expected)
|
||||
{
|
||||
var buf = new byte[4096];
|
||||
var all = new StringBuilder();
|
||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (sock.Available > 0)
|
||||
{
|
||||
var n = await sock.ReceiveAsync(buf, SocketFlags.None);
|
||||
all.Append(Encoding.ASCII.GetString(buf, 0, n));
|
||||
if (all.ToString().Contains(expected))
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
throw new TimeoutException($"Did not receive '{expected}' within 5 seconds. Got: {all}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user