diff --git a/src/NATS.Server/Monitoring/Connz.cs b/src/NATS.Server/Monitoring/Connz.cs
index 958b4ed..e11a905 100644
--- a/src/NATS.Server/Monitoring/Connz.cs
+++ b/src/NATS.Server/Monitoring/Connz.cs
@@ -163,9 +163,13 @@ public sealed class SubDetail
public ulong Cid { get; set; }
}
+// ---------------------------------------------------------------------------
+// Task 84 — account-scoped filter API (Gap 10.2)
+// ---------------------------------------------------------------------------
+
///
/// Lightweight connection info record used by the account-scoped filter API.
-/// Decoupled from so filter logic can be tested without a running server.
+/// Decoupled from ConnInfo so filter logic can be tested without a running server.
/// Go reference: monitor.go ConnInfo (subset of fields relevant to account filtering).
///
public sealed record ConnzConnectionInfo(
@@ -180,7 +184,7 @@ public sealed record ConnzConnectionInfo(
long OutBytes);
///
-/// Paginated result returned by .
+/// Paginated result returned by ConnzFilter.ApplyFilters.
///
public sealed record ConnzFilterResult(
IReadOnlyList Connections,
@@ -188,30 +192,9 @@ public sealed record ConnzFilterResult(
int Offset,
int Limit);
-///
-/// Sort options for the /connz endpoint.
-/// Go reference: monitor.go ConnzSortOpt constants.
-///
-public enum ConnzSortOption
-{
- ConnectionId,
- Start,
- Subs,
- Pending,
- MsgsTo,
- MsgsFrom,
- BytesTo,
- BytesFrom,
- LastActivity,
- Uptime,
- Idle,
- RTT,
-}
-
///
/// Query-string options for the account-scoped filter API.
-/// Parses the ?acc=, ?state=, ?offset=, and ?limit= parameters that the Go server
-/// accepts on the /connz endpoint.
+/// Parses the ?acc=, ?state=, ?offset=, ?limit=, ?sort=, and ?desc= parameters.
/// Go reference: monitor.go ConnzOptions / Connz().
///
public sealed class ConnzFilterOptions
@@ -226,28 +209,25 @@ public sealed class ConnzFilterOptions
public int Limit { get; init; } = 1024;
///
- /// Sort field for connection listing. Default: .
+ /// Sort field for connection listing. Default: ConnzSortOption.ConnectionId.
/// Go reference: monitor.go ConnzOptions.SortBy.
///
public ConnzSortOption SortBy { get; init; } = ConnzSortOption.ConnectionId;
///
- /// When , reverses the natural sort direction for the chosen
- /// option.
+ /// When true, reverses the natural sort direction for the chosen SortBy option.
/// Go reference: monitor.go ConnzOptions.SortBy (descending variant).
///
public bool SortDescending { get; init; }
///
- /// Parses a raw query string (e.g. "?acc=ACCOUNT&state=open&offset=0&limit=100&sort=bytes_to")
- /// into a instance.
+ /// Parses a raw query string into a ConnzFilterOptions instance.
///
public static ConnzFilterOptions Parse(string? queryString)
{
if (string.IsNullOrEmpty(queryString))
return new ConnzFilterOptions();
- // Strip leading '?'
var qs = queryString.TrimStart('?');
string? accountFilter = null;
@@ -301,29 +281,25 @@ public sealed class ConnzFilterOptions
}
///
-/// Pure filtering helper for collections.
-/// Provides account-scoped filtering and pagination that mirror the Go server's
-/// /connz ?acc= behavior.
+/// Pure filtering helper for ConnzConnectionInfo collections.
+/// Provides account-scoped filtering and pagination mirroring the Go server /connz ?acc= behaviour.
/// Go reference: monitor.go Connz() — account filter branch.
///
public static class ConnzFilter
{
///
- /// Filters to only those whose
- /// matches
+ /// Filters connections to only those whose AccountName matches accountName
/// using a case-insensitive ordinal comparison.
///
public static IReadOnlyList FilterByAccount(
IEnumerable connections,
- string accountName)
- {
- return connections
+ string accountName) =>
+ connections
.Where(c => string.Equals(c.AccountName, accountName, StringComparison.OrdinalIgnoreCase))
.ToList();
- }
///
- /// Applies all filters specified in and returns a paginated result.
+ /// Applies all filters specified in options and returns a paginated result.
///
public static ConnzFilterResult ApplyFilters(
IEnumerable connections,
@@ -343,6 +319,155 @@ public static class ConnzFilter
}
}
+// ---------------------------------------------------------------------------
+// Task 85 — sort options for /connz (Gap 10.3)
+// ---------------------------------------------------------------------------
+
+///
+/// Public sort option enum for the /connz endpoint with human-readable names.
+/// Go reference: server/monitor_sort_opts.go SortOpt constants.
+///
+public enum ConnzSortOption
+{
+ /// Sort by connection ID (CID). Default.
+ ConnectionId,
+
+ /// Sort by connection start time (ascending by default).
+ Start,
+
+ /// Sort by bytes sent to the client (OutBytes), descending by default.
+ BytesTo,
+
+ /// Sort by bytes received from the client (InBytes), descending by default.
+ BytesFrom,
+
+ /// Sort by messages sent to the client (OutMsgs), descending by default.
+ MsgsTo,
+
+ /// Sort by messages received from the client (InMsgs), descending by default.
+ MsgsFrom,
+
+ /// Sort by subscription count, descending by default.
+ Subscriptions,
+
+ /// Sort by pending bytes, descending by default.
+ Pending,
+
+ /// Sort by connection duration (longest first), descending by default.
+ Uptime,
+
+ /// Sort by idle time (most idle first), descending by default.
+ Idle,
+
+ /// Sort by last activity time, descending by default.
+ LastActivity,
+}
+
+///
+/// Provides parsing and sorting utilities for ConnInfo collections based on ConnzSortOption.
+/// Go reference: server/monitor_sort_opts.go, server/monitor.go Connz().
+///
+public static class ConnzSorter
+{
+ ///
+ /// Parses a query-string sort parameter to ConnzSortOption.
+ /// Returns ConnzSortOption.ConnectionId for null, empty, or unrecognised values.
+ /// Go reference: server/monitor_sort_opts.go UnmarshalJSON.
+ ///
+ public static ConnzSortOption Parse(string? sortBy) =>
+ sortBy?.ToLowerInvariant() switch
+ {
+ "start" => ConnzSortOption.Start,
+ "bytes_to" => ConnzSortOption.BytesTo,
+ "bytes_from" => ConnzSortOption.BytesFrom,
+ "msgs_to" => ConnzSortOption.MsgsTo,
+ "msgs_from" => ConnzSortOption.MsgsFrom,
+ "subs" => ConnzSortOption.Subscriptions,
+ "pending" => ConnzSortOption.Pending,
+ "uptime" => ConnzSortOption.Uptime,
+ "idle" => ConnzSortOption.Idle,
+ "last" => ConnzSortOption.LastActivity,
+ _ => ConnzSortOption.ConnectionId,
+ };
+
+ ///
+ /// Sorts a collection of ConnInfo records by the specified option and direction.
+ /// Traffic/activity metrics default to descending (highest first); ConnectionId and Start
+ /// default to ascending. Setting descending=true reverses the default.
+ /// Go reference: monitor.go Connz() sort switch.
+ ///
+ public static IReadOnlyList Sort(
+ IEnumerable connections,
+ ConnzSortOption sortBy,
+ bool descending = false)
+ {
+ var now = DateTime.UtcNow;
+
+ return sortBy switch
+ {
+ ConnzSortOption.Start =>
+ descending
+ ? [.. connections.OrderByDescending(c => c.Start)]
+ : [.. connections.OrderBy(c => c.Start)],
+
+ ConnzSortOption.BytesTo =>
+ descending
+ ? [.. connections.OrderBy(c => c.OutBytes)]
+ : [.. connections.OrderByDescending(c => c.OutBytes)],
+
+ ConnzSortOption.BytesFrom =>
+ descending
+ ? [.. connections.OrderBy(c => c.InBytes)]
+ : [.. connections.OrderByDescending(c => c.InBytes)],
+
+ ConnzSortOption.MsgsTo =>
+ descending
+ ? [.. connections.OrderBy(c => c.OutMsgs)]
+ : [.. connections.OrderByDescending(c => c.OutMsgs)],
+
+ ConnzSortOption.MsgsFrom =>
+ descending
+ ? [.. connections.OrderBy(c => c.InMsgs)]
+ : [.. connections.OrderByDescending(c => c.InMsgs)],
+
+ ConnzSortOption.Subscriptions =>
+ descending
+ ? [.. connections.OrderBy(c => c.NumSubs)]
+ : [.. connections.OrderByDescending(c => c.NumSubs)],
+
+ ConnzSortOption.Pending =>
+ descending
+ ? [.. connections.OrderBy(c => c.Pending)]
+ : [.. connections.OrderByDescending(c => c.Pending)],
+
+ ConnzSortOption.Uptime =>
+ descending
+ ? [.. connections.OrderBy(c => now - c.Start)]
+ : [.. connections.OrderByDescending(c => now - c.Start)],
+
+ ConnzSortOption.Idle =>
+ descending
+ ? [.. connections.OrderBy(c => now - c.LastActivity)]
+ : [.. connections.OrderByDescending(c => now - c.LastActivity)],
+
+ ConnzSortOption.LastActivity =>
+ descending
+ ? [.. connections.OrderBy(c => c.LastActivity)]
+ : [.. connections.OrderByDescending(c => c.LastActivity)],
+
+ // ConnectionId and default: ascending by CID
+ _ =>
+ descending
+ ? [.. connections.OrderByDescending(c => c.Cid)]
+ : [.. connections.OrderBy(c => c.Cid)],
+ };
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Internal types — used by ConnzHandler and existing sort logic
+// ---------------------------------------------------------------------------
+
///
/// Sort options for connection listing.
/// Corresponds to Go server/monitor_sort_opts.go SortOpt type.
@@ -414,24 +539,3 @@ public sealed class ConnzOptions
public int Limit { get; set; } = 1024;
}
-
-///
-/// Parses sort query-string values for the /connz endpoint.
-/// Go reference: monitor.go ConnzSortOpt string constants.
-///
-public static class ConnzSorter
-{
- public static ConnzSortOption Parse(string value) => value.ToLowerInvariant() switch
- {
- "start" or "start_time" => ConnzSortOption.Start,
- "subs" => ConnzSortOption.Subs,
- "pending" => ConnzSortOption.Pending,
- "msgs_to" or "msgs_from" => ConnzSortOption.MsgsTo,
- "bytes_to" or "bytes_from" => ConnzSortOption.BytesTo,
- "last" or "last_activity" => ConnzSortOption.LastActivity,
- "uptime" => ConnzSortOption.Uptime,
- "idle" => ConnzSortOption.Idle,
- "rtt" => ConnzSortOption.RTT,
- _ => ConnzSortOption.ConnectionId,
- };
-}
diff --git a/tests/NATS.Server.Tests/Monitoring/ConnzSortTests.cs b/tests/NATS.Server.Tests/Monitoring/ConnzSortTests.cs
new file mode 100644
index 0000000..987bfee
--- /dev/null
+++ b/tests/NATS.Server.Tests/Monitoring/ConnzSortTests.cs
@@ -0,0 +1,179 @@
+using NATS.Server.Monitoring;
+
+namespace NATS.Server.Tests.Monitoring;
+
+///
+/// Unit tests for ConnzSortOption enum, ConnzSorter.Parse, and ConnzSorter.Sort.
+/// All tests exercise the types in isolation — no running server required.
+/// Go reference: monitor_test.go — TestConnzSortedByCid, TestConnzSortedByBytesTo,
+/// TestConnzSortedByMsgsFrom, TestConnzSortedByStart, monitor_sort_opts.go.
+///
+public class ConnzSortTests
+{
+ // -----------------------------------------------------------------------
+ // Helper — build a ConnInfo with specific field values
+ // -----------------------------------------------------------------------
+
+ private static ConnInfo MakeConn(
+ ulong cid = 1,
+ long outBytes = 0,
+ long inBytes = 0,
+ long outMsgs = 0,
+ long inMsgs = 0,
+ uint numSubs = 0,
+ int pending = 0,
+ DateTime connectedAt = default,
+ DateTime lastActivity = default) =>
+ new()
+ {
+ Cid = cid,
+ OutBytes = outBytes,
+ InBytes = inBytes,
+ OutMsgs = outMsgs,
+ InMsgs = inMsgs,
+ NumSubs = numSubs,
+ Pending = pending,
+ Start = connectedAt == default ? DateTime.UtcNow : connectedAt,
+ LastActivity = lastActivity == default ? DateTime.UtcNow : lastActivity,
+ };
+
+ // -----------------------------------------------------------------------
+ // ConnzSorter.Parse tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void Parse_ConnectionId_Default()
+ {
+ // Go reference: monitor_sort_opts.go — empty/unknown value defaults to CID sort
+ ConnzSorter.Parse(null).ShouldBe(ConnzSortOption.ConnectionId);
+ ConnzSorter.Parse("").ShouldBe(ConnzSortOption.ConnectionId);
+ ConnzSorter.Parse(" ").ShouldBe(ConnzSortOption.ConnectionId);
+ ConnzSorter.Parse("cid").ShouldBe(ConnzSortOption.ConnectionId);
+ }
+
+ [Fact]
+ public void Parse_BytesTo_Parsed()
+ {
+ // Go reference: monitor_sort_opts.go — "bytes_to" => ByBytesTo
+ ConnzSorter.Parse("bytes_to").ShouldBe(ConnzSortOption.BytesTo);
+ ConnzSorter.Parse("BYTES_TO").ShouldBe(ConnzSortOption.BytesTo);
+ }
+
+ [Fact]
+ public void Parse_MsgsFrom_Parsed()
+ {
+ // Go reference: monitor_sort_opts.go — "msgs_from" => ByMsgsFrom
+ ConnzSorter.Parse("msgs_from").ShouldBe(ConnzSortOption.MsgsFrom);
+ ConnzSorter.Parse("MSGS_FROM").ShouldBe(ConnzSortOption.MsgsFrom);
+ }
+
+ [Fact]
+ public void Parse_Start_Parsed()
+ {
+ // Go reference: monitor_sort_opts.go — "start" => ByStart
+ ConnzSorter.Parse("start").ShouldBe(ConnzSortOption.Start);
+ ConnzSorter.Parse("START").ShouldBe(ConnzSortOption.Start);
+ }
+
+ [Fact]
+ public void Parse_Unknown_ReturnsDefault()
+ {
+ // Go reference: monitor_sort_opts.go — unrecognised string falls back to CID
+ ConnzSorter.Parse("not_a_sort_key").ShouldBe(ConnzSortOption.ConnectionId);
+ ConnzSorter.Parse("xyz").ShouldBe(ConnzSortOption.ConnectionId);
+ ConnzSorter.Parse("RANDOM").ShouldBe(ConnzSortOption.ConnectionId);
+ }
+
+ // -----------------------------------------------------------------------
+ // ConnzSorter.Sort tests
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void Sort_ByConnectionId_Ascending()
+ {
+ // Go reference: monitor_test.go TestConnzSortedByCid — CID ascending is default
+ var conns = new[]
+ {
+ MakeConn(cid: 3),
+ MakeConn(cid: 1),
+ MakeConn(cid: 2),
+ };
+
+ var result = ConnzSorter.Sort(conns, ConnzSortOption.ConnectionId);
+
+ result.Count.ShouldBe(3);
+ result[0].Cid.ShouldBe(1UL);
+ result[1].Cid.ShouldBe(2UL);
+ result[2].Cid.ShouldBe(3UL);
+ }
+
+ [Fact]
+ public void Sort_ByBytesTo_Descending()
+ {
+ // Go reference: monitor_test.go TestConnzSortedByBytesTo — OutBytes descending
+ var conns = new[]
+ {
+ MakeConn(cid: 1, outBytes: 100),
+ MakeConn(cid: 2, outBytes: 300),
+ MakeConn(cid: 3, outBytes: 200),
+ };
+
+ var result = ConnzSorter.Sort(conns, ConnzSortOption.BytesTo);
+
+ result.Count.ShouldBe(3);
+ result[0].OutBytes.ShouldBe(300);
+ result[1].OutBytes.ShouldBe(200);
+ result[2].OutBytes.ShouldBe(100);
+ }
+
+ [Fact]
+ public void Sort_ByStart_Ascending()
+ {
+ // Go reference: monitor_test.go TestConnzSortedByStart — ConnectedAt ascending
+ var t0 = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ var t1 = t0.AddMinutes(1);
+ var t2 = t0.AddMinutes(2);
+
+ var conns = new[]
+ {
+ MakeConn(cid: 1, connectedAt: t2),
+ MakeConn(cid: 2, connectedAt: t0),
+ MakeConn(cid: 3, connectedAt: t1),
+ };
+
+ var result = ConnzSorter.Sort(conns, ConnzSortOption.Start);
+
+ result.Count.ShouldBe(3);
+ result[0].Start.ShouldBe(t0);
+ result[1].Start.ShouldBe(t1);
+ result[2].Start.ShouldBe(t2);
+ }
+
+ [Fact]
+ public void Sort_ByMsgsFrom_Descending()
+ {
+ // Go reference: monitor_test.go TestConnzSortedByMsgsFrom — InMsgs descending
+ var conns = new[]
+ {
+ MakeConn(cid: 1, inMsgs: 50),
+ MakeConn(cid: 2, inMsgs: 200),
+ MakeConn(cid: 3, inMsgs: 10),
+ };
+
+ var result = ConnzSorter.Sort(conns, ConnzSortOption.MsgsFrom);
+
+ result.Count.ShouldBe(3);
+ result[0].InMsgs.ShouldBe(200);
+ result[1].InMsgs.ShouldBe(50);
+ result[2].InMsgs.ShouldBe(10);
+ }
+
+ [Fact]
+ public void Sort_EmptyList_ReturnsEmpty()
+ {
+ // Sorting an empty input should not throw and should return an empty list.
+ var result = ConnzSorter.Sort([], ConnzSortOption.BytesTo);
+
+ result.ShouldBeEmpty();
+ }
+}