feat: add sort options to /connz (Gap 10.3)
Add ConnzSortOption enum (ConnectionId, Start, BytesTo, BytesFrom, MsgsTo, MsgsFrom, Subscriptions, Pending, Uptime, Idle, LastActivity) and ConnzSorter static class with Parse(string?) and Sort(IEnumerable<ConnInfo>, ConnzSortOption, bool) methods. Add SortBy and SortDescending properties to ConnzFilterOptions and wire them through ConnzFilterOptions.Parse (?sort= and ?desc= query params). Add 10 unit tests in ConnzSortTests covering Parse defaults, known values, unknown string fallback, and sort ordering for key fields. Go reference: server/monitor_sort_opts.go, server/monitor.go Connz().
This commit is contained in:
@@ -163,9 +163,13 @@ public sealed class SubDetail
|
||||
public ulong Cid { get; set; }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task 84 — account-scoped filter API (Gap 10.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight connection info record used by the account-scoped filter API.
|
||||
/// Decoupled from <see cref="ConnInfo"/> 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).
|
||||
/// </summary>
|
||||
public sealed record ConnzConnectionInfo(
|
||||
@@ -180,7 +184,7 @@ public sealed record ConnzConnectionInfo(
|
||||
long OutBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Paginated result returned by <see cref="ConnzFilter.ApplyFilters"/>.
|
||||
/// Paginated result returned by ConnzFilter.ApplyFilters.
|
||||
/// </summary>
|
||||
public sealed record ConnzFilterResult(
|
||||
IReadOnlyList<ConnzConnectionInfo> Connections,
|
||||
@@ -188,30 +192,9 @@ public sealed record ConnzFilterResult(
|
||||
int Offset,
|
||||
int Limit);
|
||||
|
||||
/// <summary>
|
||||
/// Sort options for the /connz endpoint.
|
||||
/// Go reference: monitor.go ConnzSortOpt constants.
|
||||
/// </summary>
|
||||
public enum ConnzSortOption
|
||||
{
|
||||
ConnectionId,
|
||||
Start,
|
||||
Subs,
|
||||
Pending,
|
||||
MsgsTo,
|
||||
MsgsFrom,
|
||||
BytesTo,
|
||||
BytesFrom,
|
||||
LastActivity,
|
||||
Uptime,
|
||||
Idle,
|
||||
RTT,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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().
|
||||
/// </summary>
|
||||
public sealed class ConnzFilterOptions
|
||||
@@ -226,28 +209,25 @@ public sealed class ConnzFilterOptions
|
||||
public int Limit { get; init; } = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Sort field for connection listing. Default: <see cref="ConnzSortOption.ConnectionId"/>.
|
||||
/// Sort field for connection listing. Default: ConnzSortOption.ConnectionId.
|
||||
/// Go reference: monitor.go ConnzOptions.SortBy.
|
||||
/// </summary>
|
||||
public ConnzSortOption SortBy { get; init; } = ConnzSortOption.ConnectionId;
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/>, reverses the natural sort direction for the chosen
|
||||
/// <see cref="SortBy"/> option.
|
||||
/// When true, reverses the natural sort direction for the chosen SortBy option.
|
||||
/// Go reference: monitor.go ConnzOptions.SortBy (descending variant).
|
||||
/// </summary>
|
||||
public bool SortDescending { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parses a raw query string (e.g. "?acc=ACCOUNT&state=open&offset=0&limit=100&sort=bytes_to")
|
||||
/// into a <see cref="ConnzFilterOptions"/> instance.
|
||||
/// Parses a raw query string into a ConnzFilterOptions instance.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure filtering helper for <see cref="ConnzConnectionInfo"/> 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.
|
||||
/// </summary>
|
||||
public static class ConnzFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Filters <paramref name="connections"/> to only those whose
|
||||
/// <see cref="ConnzConnectionInfo.AccountName"/> matches <paramref name="accountName"/>
|
||||
/// Filters connections to only those whose AccountName matches accountName
|
||||
/// using a case-insensitive ordinal comparison.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ConnzConnectionInfo> FilterByAccount(
|
||||
IEnumerable<ConnzConnectionInfo> connections,
|
||||
string accountName)
|
||||
{
|
||||
return connections
|
||||
string accountName) =>
|
||||
connections
|
||||
.Where(c => string.Equals(c.AccountName, accountName, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies all filters specified in <paramref name="options"/> and returns a paginated result.
|
||||
/// Applies all filters specified in options and returns a paginated result.
|
||||
/// </summary>
|
||||
public static ConnzFilterResult ApplyFilters(
|
||||
IEnumerable<ConnzConnectionInfo> connections,
|
||||
@@ -343,6 +319,155 @@ public static class ConnzFilter
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task 85 — sort options for /connz (Gap 10.3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Public sort option enum for the /connz endpoint with human-readable names.
|
||||
/// Go reference: server/monitor_sort_opts.go SortOpt constants.
|
||||
/// </summary>
|
||||
public enum ConnzSortOption
|
||||
{
|
||||
/// <summary>Sort by connection ID (CID). Default.</summary>
|
||||
ConnectionId,
|
||||
|
||||
/// <summary>Sort by connection start time (ascending by default).</summary>
|
||||
Start,
|
||||
|
||||
/// <summary>Sort by bytes sent to the client (OutBytes), descending by default.</summary>
|
||||
BytesTo,
|
||||
|
||||
/// <summary>Sort by bytes received from the client (InBytes), descending by default.</summary>
|
||||
BytesFrom,
|
||||
|
||||
/// <summary>Sort by messages sent to the client (OutMsgs), descending by default.</summary>
|
||||
MsgsTo,
|
||||
|
||||
/// <summary>Sort by messages received from the client (InMsgs), descending by default.</summary>
|
||||
MsgsFrom,
|
||||
|
||||
/// <summary>Sort by subscription count, descending by default.</summary>
|
||||
Subscriptions,
|
||||
|
||||
/// <summary>Sort by pending bytes, descending by default.</summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>Sort by connection duration (longest first), descending by default.</summary>
|
||||
Uptime,
|
||||
|
||||
/// <summary>Sort by idle time (most idle first), descending by default.</summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>Sort by last activity time, descending by default.</summary>
|
||||
LastActivity,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides parsing and sorting utilities for ConnInfo collections based on ConnzSortOption.
|
||||
/// Go reference: server/monitor_sort_opts.go, server/monitor.go Connz().
|
||||
/// </summary>
|
||||
public static class ConnzSorter
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ConnInfo> Sort(
|
||||
IEnumerable<ConnInfo> 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses sort query-string values for the /connz endpoint.
|
||||
/// Go reference: monitor.go ConnzSortOpt string constants.
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
179
tests/NATS.Server.Tests/Monitoring/ConnzSortTests.cs
Normal file
179
tests/NATS.Server.Tests/Monitoring/ConnzSortTests.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using NATS.Server.Monitoring;
|
||||
|
||||
namespace NATS.Server.Tests.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user