Add ConnzConnectionInfo, ConnzFilterResult, ConnzFilterOptions, and ConnzFilter static class to Connz.cs, providing a pure unit-testable layer for account-scoped filtering and pagination that mirrors the Go server's /connz ?acc= query-parameter behaviour. Ten new tests in ConnzAccountFilterTests.cs cover FilterByAccount (match, no-match, case-insensitive), ConnzFilterOptions.Parse (acc param, defaults, offset/limit), and ApplyFilters (account filter, offset, limit, no-filter pass-through).
374 lines
10 KiB
C#
374 lines
10 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace NATS.Server.Monitoring;
|
|
|
|
/// <summary>
|
|
/// Connection information response. Corresponds to Go server/monitor.go Connz struct.
|
|
/// </summary>
|
|
public sealed class Connz
|
|
{
|
|
[JsonPropertyName("server_id")]
|
|
public string Id { get; set; } = "";
|
|
|
|
[JsonPropertyName("now")]
|
|
public DateTime Now { get; set; }
|
|
|
|
[JsonPropertyName("num_connections")]
|
|
public int NumConns { get; set; }
|
|
|
|
[JsonPropertyName("total")]
|
|
public int Total { get; set; }
|
|
|
|
[JsonPropertyName("offset")]
|
|
public int Offset { get; set; }
|
|
|
|
[JsonPropertyName("limit")]
|
|
public int Limit { get; set; }
|
|
|
|
[JsonPropertyName("connections")]
|
|
public ConnInfo[] Conns { get; set; } = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detailed information on a per-connection basis.
|
|
/// Corresponds to Go server/monitor.go ConnInfo struct.
|
|
/// </summary>
|
|
public sealed class ConnInfo
|
|
{
|
|
[JsonPropertyName("cid")]
|
|
public ulong Cid { get; set; }
|
|
|
|
[JsonPropertyName("kind")]
|
|
public string Kind { get; set; } = "";
|
|
|
|
[JsonPropertyName("type")]
|
|
public string Type { get; set; } = "";
|
|
|
|
[JsonPropertyName("ip")]
|
|
public string Ip { get; set; } = "";
|
|
|
|
[JsonPropertyName("port")]
|
|
public int Port { get; set; }
|
|
|
|
[JsonPropertyName("start")]
|
|
public DateTime Start { get; set; }
|
|
|
|
[JsonPropertyName("last_activity")]
|
|
public DateTime LastActivity { get; set; }
|
|
|
|
[JsonPropertyName("stop")]
|
|
public DateTime? Stop { get; set; }
|
|
|
|
[JsonPropertyName("reason")]
|
|
public string Reason { get; set; } = "";
|
|
|
|
[JsonPropertyName("rtt")]
|
|
public string Rtt { get; set; } = "";
|
|
|
|
[JsonPropertyName("uptime")]
|
|
public string Uptime { get; set; } = "";
|
|
|
|
[JsonPropertyName("idle")]
|
|
public string Idle { get; set; } = "";
|
|
|
|
[JsonPropertyName("pending_bytes")]
|
|
public int Pending { get; set; }
|
|
|
|
[JsonPropertyName("in_msgs")]
|
|
public long InMsgs { get; set; }
|
|
|
|
[JsonPropertyName("out_msgs")]
|
|
public long OutMsgs { get; set; }
|
|
|
|
[JsonPropertyName("in_bytes")]
|
|
public long InBytes { get; set; }
|
|
|
|
[JsonPropertyName("out_bytes")]
|
|
public long OutBytes { get; set; }
|
|
|
|
[JsonPropertyName("subscriptions")]
|
|
public uint NumSubs { get; set; }
|
|
|
|
[JsonPropertyName("subscriptions_list")]
|
|
public string[] Subs { get; set; } = [];
|
|
|
|
[JsonPropertyName("subscriptions_list_detail")]
|
|
public SubDetail[] SubsDetail { get; set; } = [];
|
|
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; set; } = "";
|
|
|
|
[JsonPropertyName("lang")]
|
|
public string Lang { get; set; } = "";
|
|
|
|
[JsonPropertyName("version")]
|
|
public string Version { get; set; } = "";
|
|
|
|
[JsonPropertyName("authorized_user")]
|
|
public string AuthorizedUser { get; set; } = "";
|
|
|
|
[JsonPropertyName("account")]
|
|
public string Account { get; set; } = "";
|
|
|
|
[JsonPropertyName("tls_version")]
|
|
public string TlsVersion { get; set; } = "";
|
|
|
|
[JsonPropertyName("tls_cipher_suite")]
|
|
public string TlsCipherSuite { get; set; } = "";
|
|
|
|
[JsonPropertyName("tls_peer_cert_subject")]
|
|
public string TlsPeerCertSubject { get; set; } = "";
|
|
|
|
[JsonPropertyName("tls_first")]
|
|
public bool TlsFirst { get; set; }
|
|
|
|
[JsonPropertyName("mqtt_client")]
|
|
public string MqttClient { get; set; } = "";
|
|
|
|
[JsonPropertyName("jwt_issuer_key")]
|
|
public string JwtIssuerKey { get; set; } = "";
|
|
|
|
[JsonPropertyName("jwt_tags")]
|
|
public string JwtTags { get; set; } = "";
|
|
|
|
[JsonPropertyName("proxy")]
|
|
public string Proxy { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscription detail information.
|
|
/// Corresponds to Go server/monitor.go SubDetail struct.
|
|
/// </summary>
|
|
public sealed class SubDetail
|
|
{
|
|
[JsonPropertyName("account")]
|
|
public string Account { get; set; } = "";
|
|
|
|
[JsonPropertyName("subject")]
|
|
public string Subject { get; set; } = "";
|
|
|
|
[JsonPropertyName("qgroup")]
|
|
public string Queue { get; set; } = "";
|
|
|
|
[JsonPropertyName("sid")]
|
|
public string Sid { get; set; } = "";
|
|
|
|
[JsonPropertyName("msgs")]
|
|
public long Msgs { get; set; }
|
|
|
|
[JsonPropertyName("max")]
|
|
public long Max { get; set; }
|
|
|
|
[JsonPropertyName("cid")]
|
|
public ulong Cid { get; set; }
|
|
}
|
|
|
|
/// <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.
|
|
/// Go reference: monitor.go ConnInfo (subset of fields relevant to account filtering).
|
|
/// </summary>
|
|
public sealed record ConnzConnectionInfo(
|
|
ulong ClientId,
|
|
string RemoteAddress,
|
|
string? AccountName,
|
|
string? Name,
|
|
DateTime ConnectedAt,
|
|
long InMsgs,
|
|
long OutMsgs,
|
|
long InBytes,
|
|
long OutBytes);
|
|
|
|
/// <summary>
|
|
/// Paginated result returned by <see cref="ConnzFilter.ApplyFilters"/>.
|
|
/// </summary>
|
|
public sealed record ConnzFilterResult(
|
|
IReadOnlyList<ConnzConnectionInfo> Connections,
|
|
int Total,
|
|
int Offset,
|
|
int Limit);
|
|
|
|
/// <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.
|
|
/// Go reference: monitor.go ConnzOptions / Connz().
|
|
/// </summary>
|
|
public sealed class ConnzFilterOptions
|
|
{
|
|
public string? AccountFilter { get; init; }
|
|
|
|
/// <summary>"open", "closed", or "any" (default: "open")</summary>
|
|
public string? StateFilter { get; init; }
|
|
|
|
public int Offset { get; init; }
|
|
|
|
public int Limit { get; init; } = 1024;
|
|
|
|
/// <summary>
|
|
/// Parses a raw query string (e.g. "?acc=ACCOUNT&state=open&offset=0&limit=100")
|
|
/// into a <see cref="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;
|
|
string? stateFilter = null;
|
|
int offset = 0;
|
|
int limit = 1024;
|
|
|
|
foreach (var pair in qs.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var eqIdx = pair.IndexOf('=');
|
|
if (eqIdx < 0) continue;
|
|
|
|
var key = Uri.UnescapeDataString(pair[..eqIdx]).ToLowerInvariant();
|
|
var value = Uri.UnescapeDataString(pair[(eqIdx + 1)..]);
|
|
|
|
switch (key)
|
|
{
|
|
case "acc":
|
|
accountFilter = value;
|
|
break;
|
|
case "state":
|
|
stateFilter = value.ToLowerInvariant();
|
|
break;
|
|
case "offset" when int.TryParse(value, out var o):
|
|
offset = o;
|
|
break;
|
|
case "limit" when int.TryParse(value, out var l):
|
|
limit = l;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return new ConnzFilterOptions
|
|
{
|
|
AccountFilter = accountFilter,
|
|
StateFilter = stateFilter,
|
|
Offset = offset,
|
|
Limit = limit,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pure filtering helper for <see cref="ConnzConnectionInfo"/> collections.
|
|
/// Provides account-scoped filtering and pagination that mirror the Go server's
|
|
/// /connz ?acc= behavior.
|
|
/// 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"/>
|
|
/// using a case-insensitive ordinal comparison.
|
|
/// </summary>
|
|
public static IReadOnlyList<ConnzConnectionInfo> FilterByAccount(
|
|
IEnumerable<ConnzConnectionInfo> connections,
|
|
string accountName)
|
|
{
|
|
return 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.
|
|
/// </summary>
|
|
public static ConnzFilterResult ApplyFilters(
|
|
IEnumerable<ConnzConnectionInfo> connections,
|
|
ConnzFilterOptions options)
|
|
{
|
|
IEnumerable<ConnzConnectionInfo> filtered = connections;
|
|
|
|
if (!string.IsNullOrEmpty(options.AccountFilter))
|
|
filtered = filtered.Where(c =>
|
|
string.Equals(c.AccountName, options.AccountFilter, StringComparison.OrdinalIgnoreCase));
|
|
|
|
var list = filtered.ToList();
|
|
var total = list.Count;
|
|
var paged = list.Skip(options.Offset).Take(options.Limit).ToList();
|
|
|
|
return new ConnzFilterResult(paged, total, options.Offset, options.Limit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sort options for connection listing.
|
|
/// Corresponds to Go server/monitor_sort_opts.go SortOpt type.
|
|
/// </summary>
|
|
public enum SortOpt
|
|
{
|
|
ByCid,
|
|
ByStart,
|
|
BySubs,
|
|
ByPending,
|
|
ByMsgsTo,
|
|
ByMsgsFrom,
|
|
ByBytesTo,
|
|
ByBytesFrom,
|
|
ByLast,
|
|
ByIdle,
|
|
ByUptime,
|
|
ByRtt,
|
|
ByStop,
|
|
ByReason,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connection state filter.
|
|
/// Corresponds to Go server/monitor.go ConnState type.
|
|
/// </summary>
|
|
public enum ConnState
|
|
{
|
|
Open,
|
|
Closed,
|
|
All,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Options passed to Connz() for filtering and sorting.
|
|
/// Corresponds to Go server/monitor.go ConnzOptions struct.
|
|
/// </summary>
|
|
public sealed class ConnzOptions
|
|
{
|
|
public SortOpt Sort { get; set; } = SortOpt.ByCid;
|
|
|
|
public bool Subscriptions { get; set; }
|
|
|
|
public bool SubscriptionsDetail { get; set; }
|
|
|
|
public ConnState State { get; set; } = ConnState.Open;
|
|
|
|
public string User { get; set; } = "";
|
|
|
|
public string Account { get; set; } = "";
|
|
|
|
public string FilterSubject { get; set; } = "";
|
|
|
|
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;
|
|
}
|