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:
Joseph Doherty
2026-02-25 13:09:11 -05:00
parent 9ece600ebc
commit 619acc3c08
2 changed files with 344 additions and 61 deletions

View File

@@ -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&amp;state=open&amp;offset=0&amp;limit=100&amp;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,
};
}