From 743a89fecff7759c6432732f27ca94b8f09e1ecf Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 09:16:03 -0500 Subject: [PATCH] =?UTF-8?q?feat(batch46):=20implement=20monitor=20endpoint?= =?UTF-8?q?s=20=E2=80=94=20varz,=20connz,=20routez,=20healthz,=20etc.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds NatsServer.Monitor.cs with all monitoring endpoint implementations (Connz, Routez, Subsz, Gatewayz, Leafz, AccountStatz, Accountz, Varz, Healthz, Raftz, Expvarz, Profilez, Stacksz, IPQueuesz, Root) and updates Monitor/MonitorTypes.cs with the full set of monitoring response types. --- .../Monitor/MonitorTypes.cs | 1377 +++++++++++++++ .../NatsServer.Monitor.cs | 1480 +++++++++++++++++ 2 files changed, 2857 insertions(+) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Monitor.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs index 0135ada..9c86828 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs @@ -14,6 +14,8 @@ // Adapted from server/monitor.go in the NATS server Go source. using System.Text.Json.Serialization; +using ZB.MOM.NatsNet.Server.Auth; +using ZB.MOM.NatsNet.Server.Internal.DataStructures; namespace ZB.MOM.NatsNet.Server; @@ -385,3 +387,1378 @@ public sealed class SubDetail [JsonPropertyName("cid")] public ulong Cid { get; set; } } + +// ============================================================================ +// Route monitoring types (Routez, RoutezOptions, RouteInfo) +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// +/// Options that control the output of a Routez monitoring query. +/// Mirrors Go RoutezOptions in server/monitor.go. +/// +public sealed class RoutezOptions +{ + [JsonPropertyName("subscriptions")] + public bool Subscriptions { get; set; } + + [JsonPropertyName("subscriptions_detail")] + public bool SubscriptionsDetail { get; set; } +} + +/// +/// Top-level response type for the /routez monitoring endpoint. +/// Mirrors Go Routez in server/monitor.go. +/// +public sealed class Routez +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("server_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("import")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SubjectPermission? Import { get; set; } + + [JsonPropertyName("export")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SubjectPermission? Export { get; set; } + + [JsonPropertyName("num_routes")] + public int NumRoutes { get; set; } + + [JsonPropertyName("routes")] + public List Routes { get; set; } = []; +} + +/// +/// Per-route detail record for the /routez monitoring endpoint. +/// Mirrors Go RouteInfo in server/monitor.go. +/// +public sealed class RouteMonitorInfo +{ + [JsonPropertyName("rid")] + public ulong Rid { get; set; } + + [JsonPropertyName("remote_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RemoteId { get; set; } + + [JsonPropertyName("remote_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RemoteName { get; set; } + + [JsonPropertyName("did_solicit")] + public bool DidSolicit { get; set; } + + [JsonPropertyName("is_configured")] + public bool IsConfigured { get; set; } + + [JsonPropertyName("ip")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + 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("rtt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Rtt { get; set; } + + [JsonPropertyName("uptime")] + public string Uptime { get; set; } = string.Empty; + + [JsonPropertyName("idle")] + public string Idle { get; set; } = string.Empty; + + [JsonPropertyName("import")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SubjectPermission? Import { get; set; } + + [JsonPropertyName("export")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SubjectPermission? Export { get; set; } + + [JsonPropertyName("pending_size")] + 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")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Subs { get; set; } + + [JsonPropertyName("subscriptions_list_detail")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? SubsDetail { get; set; } + + [JsonPropertyName("account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { get; set; } + + [JsonPropertyName("compression")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Compression { get; set; } +} + +// ============================================================================ +// Subsz — subscription list monitoring types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// +/// Options for the /subsz monitoring endpoint. +/// Mirrors Go SubszOptions in server/monitor.go. +/// +public sealed class SubszOptions +{ + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("limit")] + public int Limit { get; set; } + + [JsonPropertyName("subscriptions")] + public bool Subscriptions { get; set; } + + [JsonPropertyName("account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { get; set; } + + [JsonPropertyName("test")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Test { get; set; } +} + +/// +/// Top-level response for the /subsz monitoring endpoint. +/// Mirrors Go Subsz in server/monitor.go. +/// +public sealed class Subsz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + // SublistStats fields inlined + [JsonPropertyName("num_subscriptions")] + public uint NumSubs { get; set; } + + [JsonPropertyName("num_cache")] + public uint NumCache { get; set; } + + [JsonPropertyName("num_inserts")] + public ulong NumInserts { get; set; } + + [JsonPropertyName("num_removes")] + public ulong NumRemoves { get; set; } + + [JsonPropertyName("num_matches")] + public ulong NumMatches { get; set; } + + [JsonPropertyName("cache_hit_rate")] + public double CacheHitRate { get; set; } + + [JsonPropertyName("max_fanout")] + public uint MaxFanout { get; set; } + + [JsonPropertyName("avg_fanout")] + public double AvgFanout { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("limit")] + public int Limit { get; set; } + + [JsonPropertyName("subscriptions_list")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Subs { get; set; } +} + +// ============================================================================ +// Gatewayz — gateway monitoring types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for the /gatewayz endpoint. Mirrors Go GatewayzOptions. +public sealed class GatewayzOptions +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("accounts")] + public bool Accounts { get; set; } + + [JsonPropertyName("account_name")] + public string AccountName { get; set; } = string.Empty; + + [JsonPropertyName("subscriptions")] + public bool AccountSubscriptions { get; set; } + + [JsonPropertyName("subscriptions_detail")] + public bool AccountSubscriptionsDetail { get; set; } +} + +/// Top-level response for /gatewayz. Mirrors Go Gatewayz. +public sealed class Gatewayz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("port")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Port { get; set; } + + [JsonPropertyName("outbound_gateways")] + public Dictionary OutboundGateways { get; set; } = []; + + [JsonPropertyName("inbound_gateways")] + public Dictionary> InboundGateways { get; set; } = []; +} + +/// Remote gateway entry. Mirrors Go RemoteGatewayz. +public sealed class RemoteGatewayz +{ + [JsonPropertyName("configured")] + public bool IsConfigured { get; set; } + + [JsonPropertyName("connection")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ConnInfo? Connection { get; set; } + + [JsonPropertyName("accounts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Accounts { get; set; } +} + +/// Per-account interest mode for gateway. Mirrors Go AccountGatewayz. +public sealed class AccountGatewayz +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("interest_mode")] + public string InterestMode { get; set; } = string.Empty; + + [JsonPropertyName("no_interest_count")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int NoInterestCount { get; set; } + + [JsonPropertyName("interest_only_threshold")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int InterestOnlyThreshold { get; set; } + + [JsonPropertyName("num_subs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int TotalSubscriptions { get; set; } + + [JsonPropertyName("num_queue_subs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int NumQueueSubscriptions { get; set; } + + [JsonPropertyName("subscriptions_list")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Subs { get; set; } + + [JsonPropertyName("subscriptions_list_detail")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? SubsDetail { get; set; } +} + +// ============================================================================ +// Leafz — leaf-node monitoring types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for /leafz. Mirrors Go LeafzOptions. +public sealed class LeafzOptions +{ + [JsonPropertyName("subscriptions")] + public bool Subscriptions { get; set; } + + [JsonPropertyName("account")] + public string Account { get; set; } = string.Empty; +} + +/// Top-level response for /leafz. Mirrors Go Leafz. +public sealed class Leafz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("leafnodes")] + public int NumLeafs { get; set; } + + [JsonPropertyName("leafs")] + public List Leafs { get; set; } = []; +} + +/// Per-leaf info record. Mirrors Go LeafInfo. +public sealed class LeafInfo +{ + [JsonPropertyName("id")] + public ulong Id { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("is_spoke")] + public bool IsSpoke { get; set; } + + [JsonPropertyName("is_isolated")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsIsolated { get; set; } + + [JsonPropertyName("account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { get; set; } + + [JsonPropertyName("ip")] + public string Ip { get; set; } = string.Empty; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("rtt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Rtt { 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")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Subs { get; set; } + + [JsonPropertyName("compression")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Compression { get; set; } + + [JsonPropertyName("proxy")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ProxyInfo? Proxy { get; set; } +} + +// ============================================================================ +// AccountStatz — account statistics monitoring types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for /accstatz. Mirrors Go AccountStatzOptions. +public sealed class AccountStatzOptions +{ + [JsonPropertyName("accounts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Accounts { get; set; } + + [JsonPropertyName("include_unused")] + public bool IncludeUnused { get; set; } +} + +/// Top-level response for /accstatz. Mirrors Go AccountStatz. +public sealed class AccountStatz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("account_statz")] + public List Accounts { get; set; } = []; +} + +// ============================================================================ +// Accountz — account detail monitoring types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for /accountz. Mirrors Go AccountzOptions. +public sealed class AccountzOptions +{ + [JsonPropertyName("account")] + public string Account { get; set; } = string.Empty; +} + +/// Top-level response for /accountz. Mirrors Go Accountz. +public sealed class Accountz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("system_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SystemAccount { get; set; } + + [JsonPropertyName("accounts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Accounts { get; set; } + + [JsonPropertyName("account_detail")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AccountInfo? Account { get; set; } +} + +/// Detailed account information. Mirrors Go AccountInfo. +public sealed class AccountInfo +{ + [JsonPropertyName("account_name")] + public string AccountName { get; set; } = string.Empty; + + [JsonPropertyName("update_time")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public DateTime LastUpdate { get; set; } + + [JsonPropertyName("is_system")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsSystem { get; set; } + + [JsonPropertyName("expired")] + public bool Expired { get; set; } + + [JsonPropertyName("complete")] + public bool Complete { get; set; } + + [JsonPropertyName("jetstream_enabled")] + public bool JetStream { get; set; } + + [JsonPropertyName("leafnode_connections")] + public int LeafCnt { get; set; } + + [JsonPropertyName("client_connections")] + public int ClientCnt { get; set; } + + [JsonPropertyName("subscriptions")] + public uint SubCnt { get; set; } + + [JsonPropertyName("sublist_stats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SublistStats? Sublist { get; set; } +} + +// ============================================================================ +// Varz — server information types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for /varz. Currently no fields. Mirrors Go VarzOptions. +public sealed class VarzOptions { } + +/// +/// Top-level response for the /varz monitoring endpoint. +/// Mirrors Go Varz in server/monitor.go. +/// +public sealed class Varz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("server_name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + [JsonPropertyName("proto")] + public int Proto { get; set; } + + [JsonPropertyName("git_commit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? GitCommit { get; set; } + + [JsonPropertyName("go")] + public string GoVersion { get; set; } = string.Empty; + + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("auth_required")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool AuthRequired { get; set; } + + [JsonPropertyName("tls_required")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsVerify { get; set; } + + [JsonPropertyName("tls_ocsp_peer_verify")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsOcspPeerVerify { get; set; } + + [JsonPropertyName("ip")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Ip { get; set; } + + [JsonPropertyName("connect_urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? ClientConnectUrls { get; set; } + + [JsonPropertyName("ws_connect_urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? WsConnectUrls { get; set; } + + [JsonPropertyName("max_connections")] + public int MaxConn { get; set; } + + [JsonPropertyName("max_subscriptions")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int MaxSubs { get; set; } + + [JsonPropertyName("ping_interval")] + public long PingIntervalNs { get; set; } + + [JsonPropertyName("ping_max")] + public int MaxPingsOut { get; set; } + + [JsonPropertyName("http_host")] + public string HttpHost { get; set; } = string.Empty; + + [JsonPropertyName("http_port")] + public int HttpPort { get; set; } + + [JsonPropertyName("http_base_path")] + public string HttpBasePath { get; set; } = string.Empty; + + [JsonPropertyName("https_port")] + public int HttpsPort { get; set; } + + [JsonPropertyName("auth_timeout")] + public double AuthTimeout { get; set; } + + [JsonPropertyName("max_control_line")] + public int MaxControlLine { get; set; } + + [JsonPropertyName("max_payload")] + public int MaxPayload { get; set; } + + [JsonPropertyName("max_pending")] + public long MaxPending { get; set; } + + [JsonPropertyName("cluster")] + public ClusterOptsVarz Cluster { get; set; } = new(); + + [JsonPropertyName("gateway")] + public GatewayOptsVarz Gateway { get; set; } = new(); + + [JsonPropertyName("leaf")] + public LeafNodeOptsVarz LeafNode { get; set; } = new(); + + [JsonPropertyName("mqtt")] + public MqttOptsVarz Mqtt { get; set; } = new(); + + [JsonPropertyName("websocket")] + public WebsocketOptsVarz Websocket { get; set; } = new(); + + [JsonPropertyName("jetstream")] + public JetStreamVarz JetStream { get; set; } = new(); + + [JsonPropertyName("tls_timeout")] + public double TlsTimeout { get; set; } + + [JsonPropertyName("write_deadline")] + public long WriteDeadlineNs { get; set; } + + [JsonPropertyName("write_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? WriteTimeout { get; set; } + + [JsonPropertyName("start")] + public DateTime Start { get; set; } + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("uptime")] + public string Uptime { get; set; } = string.Empty; + + [JsonPropertyName("mem")] + public long Mem { get; set; } + + [JsonPropertyName("cores")] + public int Cores { get; set; } + + [JsonPropertyName("gomaxprocs")] + public int MaxProcs { get; set; } + + [JsonPropertyName("cpu")] + public double Cpu { get; set; } + + [JsonPropertyName("connections")] + public int Connections { get; set; } + + [JsonPropertyName("total_connections")] + public ulong TotalConnections { get; set; } + + [JsonPropertyName("routes")] + public int Routes { get; set; } + + [JsonPropertyName("remotes")] + public int Remotes { get; set; } + + [JsonPropertyName("leafnodes")] + public int Leafs { 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("slow_consumers")] + public long SlowConsumers { get; set; } + + [JsonPropertyName("stale_connections")] + public long StaleConnections { get; set; } + + [JsonPropertyName("stalled_clients")] + public long StalledClients { get; set; } + + [JsonPropertyName("subscriptions")] + public uint Subscriptions { get; set; } + + [JsonPropertyName("http_req_stats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? HttpReqStats { get; set; } + + [JsonPropertyName("config_load_time")] + public DateTime ConfigLoadTime { get; set; } + + [JsonPropertyName("config_digest")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ConfigDigest { get; set; } + + [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? Tags { get; set; } + + [JsonPropertyName("metadata")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Metadata { get; set; } + + [JsonPropertyName("system_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SystemAccount { get; set; } + + [JsonPropertyName("pinned_account_fails")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong PinnedAccountFail { get; set; } + + [JsonPropertyName("ocsp_peer_cache")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public OcspResponseCacheVarz? OcspResponseCache { get; set; } + + [JsonPropertyName("slow_consumer_stats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SlowConsumersStats? SlowConsumersStats { get; set; } + + [JsonPropertyName("stale_connection_stats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public StaleConnectionStats? StaleConnectionStats { get; set; } +} + +/// JetStream section of /varz. Mirrors Go JetStreamVarz. +public sealed class JetStreamVarz +{ + [JsonPropertyName("config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object? Config { get; set; } + + [JsonPropertyName("stats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object? Stats { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MetaClusterInfo? Meta { get; set; } + + [JsonPropertyName("limits")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object? Limits { get; set; } +} + +/// Meta cluster info for JetStream. Mirrors Go MetaClusterInfo. +public sealed class MetaClusterInfo +{ + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("leader")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Leader { get; set; } + + [JsonPropertyName("peer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Peer { get; set; } + + [JsonPropertyName("replicas")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object? Replicas { get; set; } + + [JsonPropertyName("cluster_size")] + public int Size { get; set; } + + [JsonPropertyName("pending")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Pending { get; set; } +} + +/// Cluster section of /varz. Mirrors Go ClusterOptsVarz. +public sealed class ClusterOptsVarz +{ + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("addr")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("cluster_port")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Port { get; set; } + + [JsonPropertyName("auth_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double AuthTimeout { get; set; } + + [JsonPropertyName("urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Urls { get; set; } + + [JsonPropertyName("tls_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TlsTimeout { get; set; } + + [JsonPropertyName("tls_required")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsVerify { get; set; } + + [JsonPropertyName("pool_size")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int PoolSize { get; set; } + + [JsonPropertyName("write_deadline")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long WriteDeadlineNs { get; set; } + + [JsonPropertyName("write_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? WriteTimeout { get; set; } +} + +/// Gateway section of /varz. Mirrors Go GatewayOptsVarz. +public sealed class GatewayOptsVarz +{ + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("port")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Port { get; set; } + + [JsonPropertyName("auth_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double AuthTimeout { get; set; } + + [JsonPropertyName("tls_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TlsTimeout { get; set; } + + [JsonPropertyName("tls_required")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsVerify { get; set; } + + [JsonPropertyName("advertise")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Advertise { get; set; } + + [JsonPropertyName("connect_retries")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int ConnectRetries { get; set; } + + [JsonPropertyName("gateways")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Gateways { get; set; } + + [JsonPropertyName("reject_unknown")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool RejectUnknown { get; set; } +} + +/// Remote gateway options in /varz. Mirrors Go RemoteGatewayOptsVarz. +public sealed class RemoteGatewayOptsVarz +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("tls_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TlsTimeout { get; set; } + + [JsonPropertyName("urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Urls { get; set; } +} + +/// Leaf node section of /varz. Mirrors Go LeafNodeOptsVarz. +public sealed class LeafNodeOptsVarz +{ + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("port")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Port { get; set; } + + [JsonPropertyName("auth_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double AuthTimeout { get; set; } + + [JsonPropertyName("tls_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TlsTimeout { get; set; } + + [JsonPropertyName("tls_required")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsVerify { get; set; } + + [JsonPropertyName("remotes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Remotes { get; set; } + + [JsonPropertyName("tls_ocsp_peer_verify")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsOcspPeerVerify { get; set; } +} + +/// Deny rules for leaf-node remotes. Mirrors Go DenyRules. +public sealed class DenyRules +{ + [JsonPropertyName("exports")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Exports { get; set; } + + [JsonPropertyName("imports")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Imports { get; set; } +} + +/// Remote leaf options entry in /varz. Mirrors Go RemoteLeafOptsVarz. +public sealed class RemoteLeafOptsVarz +{ + [JsonPropertyName("local_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? LocalAccount { get; set; } + + [JsonPropertyName("tls_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TlsTimeout { get; set; } + + [JsonPropertyName("urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Urls { get; set; } + + [JsonPropertyName("deny")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DenyRules? Deny { get; set; } + + [JsonPropertyName("tls_ocsp_peer_verify")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsOcspPeerVerify { get; set; } +} + +/// MQTT section of /varz. Mirrors Go MQTTOptsVarz. +public sealed class MqttOptsVarz +{ + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("port")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Port { get; set; } + + [JsonPropertyName("no_auth_user")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? NoAuthUser { get; set; } + + [JsonPropertyName("auth_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double AuthTimeout { get; set; } + + [JsonPropertyName("tls_map")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsMap { get; set; } + + [JsonPropertyName("tls_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TlsTimeout { get; set; } + + [JsonPropertyName("js_domain")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? JsDomain { get; set; } + + [JsonPropertyName("ack_wait")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long AckWaitNs { get; set; } + + [JsonPropertyName("max_ack_pending")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ushort MaxAckPending { get; set; } +} + +/// WebSocket section of /varz. Mirrors Go WebsocketOptsVarz. +public sealed class WebsocketOptsVarz +{ + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("port")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Port { get; set; } + + [JsonPropertyName("advertise")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Advertise { get; set; } + + [JsonPropertyName("no_auth_user")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? NoAuthUser { get; set; } + + [JsonPropertyName("jwt_cookie")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? JwtCookie { get; set; } + + [JsonPropertyName("auth_timeout")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double AuthTimeout { get; set; } + + [JsonPropertyName("no_tls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool NoTls { get; set; } + + [JsonPropertyName("tls_map")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsMap { get; set; } + + [JsonPropertyName("same_origin")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool SameOrigin { get; set; } + + [JsonPropertyName("allowed_origins")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? AllowedOrigins { get; set; } + + [JsonPropertyName("compression")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool Compression { get; set; } +} + +/// OCSP response cache summary for /varz. Mirrors Go OCSPResponseCacheVarz. +public sealed class OcspResponseCacheVarz +{ + [JsonPropertyName("cache_type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Type { get; set; } + + [JsonPropertyName("cache_hits")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Hits { get; set; } + + [JsonPropertyName("cache_misses")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Misses { get; set; } + + [JsonPropertyName("cached_responses")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Responses { get; set; } + + [JsonPropertyName("cached_revoked_responses")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Revokes { get; set; } + + [JsonPropertyName("cached_good_responses")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Goods { get; set; } + + [JsonPropertyName("cached_unknown_responses")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Unknowns { get; set; } +} + +// SlowConsumersStats and StaleConnectionStats are defined in Events/EventTypes.cs. + +// ============================================================================ +// Healthz types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for /healthz. Mirrors Go HealthzOptions. +public sealed class HealthzOptions +{ + public bool JSEnabled { get; set; } + public bool JSEnabledOnly { get; set; } + public bool JSServerOnly { get; set; } + public bool JSMetaOnly { get; set; } + public string Account { get; set; } = string.Empty; + public string Stream { get; set; } = string.Empty; + public string Consumer { get; set; } = string.Empty; + public bool Details { get; set; } +} + +/// Health status response. Mirrors Go HealthStatus. +public sealed class HealthStatus +{ + [JsonPropertyName("status")] + public string Status { get; set; } = "ok"; + + [JsonPropertyName("status_code")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int StatusCode { get; set; } + + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Error { get; set; } + + [JsonPropertyName("errors")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Errors { get; set; } +} + +/// Individual health check error. Mirrors Go HealthzError. +public sealed class HealthzError +{ + [JsonPropertyName("type")] + public HealthZErrorType Type { get; set; } + + [JsonPropertyName("account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { get; set; } + + [JsonPropertyName("stream")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Stream { get; set; } + + [JsonPropertyName("consumer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Consumer { get; set; } + + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Error { get; set; } +} + +/// +/// Healthz error type enum with JSON-as-string serialization. +/// Mirrors Go HealthZErrorType in server/monitor.go. +/// +[JsonConverter(typeof(HealthZErrorTypeConverter))] +public enum HealthZErrorType +{ + Conn = 0, + BadRequest = 1, + JetStream = 2, + Account = 3, + Stream = 4, + Consumer = 5, +} + +/// +/// JSON converter for — serialises as uppercase string. +/// +public sealed class HealthZErrorTypeConverter : System.Text.Json.Serialization.JsonConverter +{ + public override HealthZErrorType Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options) + { + var s = reader.GetString(); + return s switch + { + "CONNECTION" => HealthZErrorType.Conn, + "BAD_REQUEST" => HealthZErrorType.BadRequest, + "JETSTREAM" => HealthZErrorType.JetStream, + "ACCOUNT" => HealthZErrorType.Account, + "STREAM" => HealthZErrorType.Stream, + "CONSUMER" => HealthZErrorType.Consumer, + _ => throw new System.Text.Json.JsonException($"Unknown HealthZErrorType: {s}") + }; + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, HealthZErrorType value, System.Text.Json.JsonSerializerOptions options) + { + var s = value switch + { + HealthZErrorType.Conn => "CONNECTION", + HealthZErrorType.BadRequest => "BAD_REQUEST", + HealthZErrorType.JetStream => "JETSTREAM", + HealthZErrorType.Account => "ACCOUNT", + HealthZErrorType.Stream => "STREAM", + HealthZErrorType.Consumer => "CONSUMER", + _ => "unknown" + }; + writer.WriteStringValue(s); + } +} + +// ============================================================================ +// Expvarz, Profilez types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Response from /debug/vars. Mirrors Go ExpvarzStatus. +public sealed class ExpvarzStatus +{ + [JsonPropertyName("memstats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public System.Text.Json.JsonElement? Memstats { get; set; } + + [JsonPropertyName("cmdline")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public System.Text.Json.JsonElement? Cmdline { get; set; } +} + +/// Options for /profilez. Mirrors Go ProfilezOptions. +public sealed class ProfilezOptions +{ + public string Name { get; set; } = string.Empty; + public int Debug { get; set; } + public TimeSpan Duration { get; set; } +} + +/// Response from /profilez. Mirrors Go ProfilezStatus. +public sealed class ProfilezStatus +{ + [JsonPropertyName("profile")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public byte[]? Profile { get; set; } + + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Error { get; set; } +} + +// ============================================================================ +// Raftz types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for /raftz. Mirrors Go RaftzOptions. +public sealed class RaftzOptions +{ + public string AccountFilter { get; set; } = string.Empty; + public string GroupFilter { get; set; } = string.Empty; +} + +/// Per-peer info for a raft group. Mirrors Go RaftzGroupPeer. +public sealed class RaftzGroupPeer +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("known")] + public bool Known { get; set; } + + [JsonPropertyName("last_replicated_index")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong LastReplicatedIndex { get; set; } + + [JsonPropertyName("last_seen")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? LastSeen { get; set; } +} + +/// Per-group detail for a raft node. Mirrors Go RaftzGroup. +public sealed class RaftzGroup +{ + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("state")] + public string State { get; set; } = string.Empty; + + [JsonPropertyName("size")] + public int Size { get; set; } + + [JsonPropertyName("quorum_needed")] + public int QuorumNeeded { get; set; } + + [JsonPropertyName("observer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool Observer { get; set; } + + [JsonPropertyName("paused")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool Paused { get; set; } + + [JsonPropertyName("committed")] + public ulong Committed { get; set; } + + [JsonPropertyName("applied")] + public ulong Applied { get; set; } + + [JsonPropertyName("catching_up")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool CatchingUp { get; set; } + + [JsonPropertyName("leader")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Leader { get; set; } + + [JsonPropertyName("leader_since")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DateTime? LeaderSince { get; set; } + + [JsonPropertyName("ever_had_leader")] + public bool EverHadLeader { get; set; } + + [JsonPropertyName("term")] + public ulong Term { get; set; } + + [JsonPropertyName("voted_for")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Vote { get; set; } + + [JsonPropertyName("pterm")] + public ulong PTerm { get; set; } + + [JsonPropertyName("pindex")] + public ulong PIndex { get; set; } + + [JsonPropertyName("system_account")] + public bool SystemAcc { get; set; } + + [JsonPropertyName("traffic_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TrafficAcc { get; set; } + + [JsonPropertyName("peers")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Peers { get; set; } +} + +/// Top-level /raftz response. Mirrors Go RaftzStatus map[string]map[string]RaftzGroup. +public sealed class RaftzStatus : Dictionary> +{ + public RaftzStatus() : base(StringComparer.Ordinal) { } +} + +// ============================================================================ +// IpQueuesz types +// Mirrors Go types in server/monitor.go. +// ============================================================================ + +/// Options for /ipqueuesz. Mirrors Go IpqueueszOptions. +public sealed class IpqueueszOptions +{ + public bool All { get; set; } + public string Filter { get; set; } = string.Empty; +} + +/// Per-queue entry in /ipqueuesz response. Mirrors Go IpqueueszStatusIPQ. +public sealed class IpqueueszStatusIpq +{ + [JsonPropertyName("pending")] + public int Pending { get; set; } + + [JsonPropertyName("in_progress")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int InProgress { get; set; } +} + +/// Top-level /ipqueuesz response — a dictionary of queue name to status. Mirrors Go IpqueueszStatus. +public sealed class IpqueueszStatus : Dictionary +{ + public IpqueueszStatus() : base(StringComparer.Ordinal) { } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Monitor.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Monitor.cs new file mode 100644 index 0000000..e89af8d --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Monitor.cs @@ -0,0 +1,1480 @@ +// Copyright 2013-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/monitor.go in the NATS server Go source. + +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using ZB.MOM.NatsNet.Server.Internal; +using ZB.MOM.NatsNet.Server.Internal.DataStructures; + +namespace ZB.MOM.NatsNet.Server; + +// ============================================================================ +// Session 17: Monitor Endpoints +// Mirrors server/monitor.go in the NATS server Go source. +// ============================================================================ + +public sealed partial class NatsServer +{ + // ========================================================================= + // Uptime / duration helpers + // ========================================================================= + + /// + /// Formats a as "XdXhXmXs". + /// Mirrors Go myUptime(d time.Duration) string in monitor.go. + /// + internal static string MyUptime(TimeSpan d) + { + var days = (int)d.TotalDays; + var hours = d.Hours; + var minutes = d.Minutes; + var seconds = d.Seconds; + return $"{days}d{hours}h{minutes}m{seconds}s"; + } + + // ========================================================================= + // HTTP response helpers + // ========================================================================= + + private static readonly JsonSerializerOptions MonitorJsonOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false, + }; + + /// + /// Writes a JSON body as the HTTP response. + /// Mirrors Go ResponseHandler(w http.ResponseWriter, r *http.Request, data []byte). + /// + internal static void ResponseHandler( + System.Net.HttpListenerRequest request, + System.Net.HttpListenerResponse response, + byte[] data) + { + // CORS + response.Headers["Access-Control-Allow-Origin"] = "*"; + + // JSONP support + var callback = request.QueryString["callback"] ?? string.Empty; + if (!string.IsNullOrEmpty(callback)) + { + response.ContentType = "application/javascript"; + var js = Encoding.UTF8.GetBytes($"{callback}({Encoding.UTF8.GetString(data)})"); + response.ContentLength64 = js.Length; + response.OutputStream.Write(js); + response.OutputStream.Close(); + return; + } + + response.ContentType = "application/json"; + response.StatusCode = 200; + response.ContentLength64 = data.Length; + response.OutputStream.Write(data); + response.OutputStream.Close(); + } + + /// + /// Writes an error response with the given HTTP status code. + /// Mirrors Go handleResponse(code int, w http.ResponseWriter, r *http.Request, data []byte). + /// + private static void HandleResponse( + int code, + System.Net.HttpListenerRequest request, + System.Net.HttpListenerResponse response, + byte[] data) + { + response.Headers["Access-Control-Allow-Origin"] = "*"; + response.ContentType = "application/json"; + response.StatusCode = code; + response.ContentLength64 = data.Length; + response.OutputStream.Write(data); + response.OutputStream.Close(); + } + + // ========================================================================= + // Query parameter decode helpers + // ========================================================================= + + private static bool DecodeBool(System.Collections.Specialized.NameValueCollection q, string key, bool defaultVal = false) + { + var v = q[key]; + if (v is null) return defaultVal; + return v is "1" or "true" or "yes"; + } + + private static int DecodeInt(System.Collections.Specialized.NameValueCollection q, string key, int defaultVal = 0) + { + var v = q[key]; + if (v is null) return defaultVal; + return int.TryParse(v, out var n) ? n : defaultVal; + } + + private static ulong DecodeUint64(System.Collections.Specialized.NameValueCollection q, string key, ulong defaultVal = 0) + { + var v = q[key]; + if (v is null) return defaultVal; + return ulong.TryParse(v, out var n) ? n : defaultVal; + } + + private static ConnState DecodeState(System.Collections.Specialized.NameValueCollection q, ConnState defaultVal = ConnState.ConnOpen) + { + var v = q["state"]; + if (v is null) return defaultVal; + return v.ToLowerInvariant() switch + { + "open" => ConnState.ConnOpen, + "closed" => ConnState.ConnClosed, + "all" => ConnState.ConnAll, + _ => defaultVal, + }; + } + + // ========================================================================= + // Connz — /connz + // ========================================================================= + + /// + /// Returns a connection list snapshot. + /// Mirrors Go Server.Connz(opts *ConnzOptions) (*Connz, error). + /// + public (Connz Result, Exception? Error) Connz(ConnzOptions? opts = null) + { + opts ??= new ConnzOptions(); + + if (opts.Limit == 0) opts.Limit = MonitorDefaults.DefaultConnListSize; + + var now = DateTime.UtcNow; + + // Collect connections. + List conns = []; + + _mu.EnterReadLock(); + try + { + if (opts.State is ConnState.ConnOpen or ConnState.ConnAll) + { + foreach (var c in _clients.Values) + { + var ci = FillConnInfo(c, opts, now); + if (!FilterConn(ci, opts)) continue; + conns.Add(ci); + } + } + + if (opts.State is ConnState.ConnClosed or ConnState.ConnAll) + { + // Closed connections stored in the ring buffer. + var closed = _closed.ClosedClients(); + foreach (var cc in closed) + { + if (cc is null) continue; + // Build a minimal ConnInfo from ClosedClient fields. + var ci = new ConnInfo + { + Start = now, // no start time stored for closed connections + LastActivity = now, + Stop = now, // approximate + Account = cc.Account.Length > 0 ? cc.Account : null, + AuthorizedUser = cc.User.Length > 0 ? cc.User : null, + }; + conns.Add(ci); + } + } + } + finally + { + _mu.ExitReadLock(); + } + + // Sort. + var sortable = new ConnInfos(conns); + SortConnInfos(sortable, opts.Sort); + + // Pagination. + var total = conns.Count; + var offset = opts.Offset; + if (offset < 0) offset = 0; + if (offset > total) offset = total; + + var limit = opts.Limit; + var end = Math.Min(offset + limit, total); + var page = conns.GetRange(offset, end - offset); + + return (new Connz + { + Id = ID(), + Now = now, + NumConns = page.Count, + Total = total, + Offset = offset, + Limit = limit, + Conns = page, + }, null); + } + + /// + /// Fills a from a live . + /// Mirrors Go ConnInfo.fill(). + /// + private ConnInfo FillConnInfo(ClientConnection c, ConnzOptions opts, DateTime now) + { + ConnInfo ci; + lock (c) + { + ci = new ConnInfo + { + Cid = c.Cid, + Kind = c.KindString(), + Type = c.ClientTypeString(), + Ip = c.Host, + Port = c.Port, + Start = c.Start, + LastActivity = c.Last, + Uptime = MyUptime(now - c.Start), + Idle = MyUptime(now - c.Last), + Pending = (int)c.OutPb, + // InMsgs/OutMsgs/InBytes/OutBytes: not yet stored on ClientConnection + InMsgs = 0, + OutMsgs = 0, + InBytes = 0, + OutBytes = 0, + Stalls = 0, + NumSubs = (uint)c.Subs.Count, + Name = c.Opts.Name.Length > 0 ? c.Opts.Name : null, + Lang = c.Opts.Lang.Length > 0 ? c.Opts.Lang : null, + Version = c.Opts.Version.Length > 0 ? c.Opts.Version : null, + NameTag = c.NameTag.Length > 0 ? c.NameTag : null, + }; + + // Auth user + if (opts.Username) + ci.AuthorizedUser = c.GetRawAuthUser(); + + // Account + if (c._account is { } acc) + ci.Account = acc.Name; + + // RTT + if (c.Rtt > TimeSpan.Zero) + { + ci.RttNanos = (long)(c.Rtt.TotalMilliseconds * 1_000_000); + ci.Rtt = FormatRtt(c.Rtt); + } + + // TLS + if (c.GetTlsStream() is { } ssl) + { + var state = ssl.SslProtocol; + ci.TlsVersion = state.ToString(); + } + + // Proxy + if (c.ProxyKey.Length > 0) + ci.Proxy = new ProxyInfo { Key = c.ProxyKey }; + + // Subscriptions + if (opts.SubscriptionsDetail) + { + ci.SubsDetail = [.. c.Subs.Select(kvp => new SubDetail + { + Subject = kvp.Key, + Queue = kvp.Value.Queue is { } q ? Encoding.UTF8.GetString(q) : null, + Sid = kvp.Value.Sid is { } sid ? Encoding.UTF8.GetString(sid) : string.Empty, + Cid = c.Cid, + })]; + } + else if (opts.Subscriptions) + { + ci.Subs = [.. c.Subs.Keys]; + } + } + + return ci; + } + + private static bool FilterConn(ConnInfo ci, ConnzOptions opts) + { + if (opts.Cid != 0 && ci.Cid != opts.Cid) return false; + if (!string.IsNullOrEmpty(opts.User) && ci.AuthorizedUser != opts.User) return false; + if (!string.IsNullOrEmpty(opts.Account) && ci.Account != opts.Account) return false; + return true; + } + + /// + /// Handles the /connz HTTP endpoint. + /// Mirrors Go Server.HandleConnz(). + /// + public void HandleConnz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Connz); + var q = request.QueryString; + var opts = new ConnzOptions + { + Sort = (SortOpt)(q["sort"] ?? string.Empty), + Username = DecodeBool(q, "auth"), + Subscriptions = DecodeBool(q, "subs"), + SubscriptionsDetail = DecodeBool(q, "subs") && DecodeBool(q, "detail"), + Offset = DecodeInt(q, "offset"), + Limit = DecodeInt(q, "limit", MonitorDefaults.DefaultConnListSize), + Cid = DecodeUint64(q, "cid"), + State = DecodeState(q), + User = q["user"] ?? string.Empty, + Account = q["acc"] ?? string.Empty, + FilterSubject = q["filter_subject"] ?? string.Empty, + }; + + var (result, err) = Connz(opts); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(500, request, response, errBytes); + return; + } + + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Routez — /routez + // ========================================================================= + + /// + /// Returns a route list snapshot. + /// Mirrors Go Server.Routez(opts *RoutezOptions) (*Routez, error). + /// + public Routez Routez(RoutezOptions? opts = null) + { + opts ??= new RoutezOptions(); + var now = DateTime.UtcNow; + var routes = new List(); + + _mu.EnterReadLock(); + try + { + ForEachRoute(c => + { + lock (c) + { + var ri = FillRouteInfo(c, opts, now); + routes.Add(ri); + } + }); + } + finally + { + _mu.ExitReadLock(); + } + + return new Routez + { + Id = ID(), + Name = _info.Name, + Now = now, + NumRoutes = routes.Count, + Routes = routes, + }; + } + + private RouteMonitorInfo FillRouteInfo(ClientConnection c, RoutezOptions opts, DateTime now) + { + var ri = new RouteMonitorInfo + { + Rid = c.Cid, + Ip = c.Host, + Port = c.Port, + Start = c.Start, + LastActivity = c.Last, + Uptime = MyUptime(now - c.Start), + Idle = MyUptime(now - c.Last), + Pending = (int)c.OutPb, + NumSubs = (uint)c.Subs.Count, + }; + + if (c.Route is { } route) + { + ri.RemoteId = route.RemoteId.Length > 0 ? route.RemoteId : null; + ri.RemoteName = route.RemoteName.Length > 0 ? route.RemoteName : null; + ri.DidSolicit = route.DidSolicit; + ri.IsConfigured = route.RouteType == RouteType.Explicit; + ri.Compression = route.Compression.Length > 0 ? route.Compression : null; + } + + // RTT + if (c.Rtt > TimeSpan.Zero) + ri.Rtt = FormatRtt(c.Rtt); + + // Account (pinned route) + if (c.Route?.AccName is { } acc) + ri.Account = Encoding.UTF8.GetString(acc); + + // Subscriptions + if (opts.SubscriptionsDetail) + { + ri.SubsDetail = [.. c.Subs.Select(kvp => new SubDetail + { + Subject = kvp.Key, + Queue = kvp.Value.Queue is { } q ? Encoding.UTF8.GetString(q) : null, + Sid = kvp.Value.Sid is { } sid ? Encoding.UTF8.GetString(sid) : string.Empty, + Cid = c.Cid, + })]; + } + else if (opts.Subscriptions) + { + ri.Subs = [.. c.Subs.Keys]; + } + + return ri; + } + + /// + /// Handles the /routez HTTP endpoint. + /// Mirrors Go Server.HandleRoutez(). + /// + public void HandleRoutez(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Routez); + var q = request.QueryString; + var opts = new RoutezOptions + { + Subscriptions = DecodeBool(q, "subs"), + SubscriptionsDetail = DecodeBool(q, "subs") && DecodeBool(q, "detail"), + }; + var result = Routez(opts); + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Subsz — /subsz + // ========================================================================= + + /// + /// Returns subscription stats snapshot. + /// Mirrors Go Server.Subsz(opts *SubszOptions) (*Subsz, error). + /// + public (Subsz Result, Exception? Error) Subsz(SubszOptions? opts = null) + { + opts ??= new SubszOptions(); + + var now = DateTime.UtcNow; + var total = new SublistStats(); + + _mu.EnterReadLock(); + try + { + foreach (var kvp in _accounts) + { + var acc = kvp.Value; + if (!string.IsNullOrEmpty(opts.Account) && acc.Name != opts.Account) + continue; + if (acc.Sublist != null) + total.Add(acc.Sublist.Stats()); + } + } + finally + { + _mu.ExitReadLock(); + } + + var subsz = new Subsz + { + Id = ID(), + Now = now, + NumSubs = total.NumSubs, + NumCache = total.NumCache, + NumInserts = total.NumInserts, + NumRemoves = total.NumRemoves, + NumMatches = total.NumMatches, + CacheHitRate = total.CacheHitRate, + MaxFanout = total.MaxFanout, + AvgFanout = total.AvgFanout, + Offset = opts.Offset, + Limit = opts.Limit > 0 ? opts.Limit : MonitorDefaults.DefaultSubListSize, + }; + + return (subsz, null); + } + + /// + /// Handles the /subsz HTTP endpoint. + /// Mirrors Go Server.HandleSubsz(). + /// + public void HandleSubsz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Subsz); + var q = request.QueryString; + var opts = new SubszOptions + { + Offset = DecodeInt(q, "offset"), + Limit = DecodeInt(q, "limit", MonitorDefaults.DefaultSubListSize), + Subscriptions = DecodeBool(q, "subs"), + Account = q["acc"], + Test = q["test"], + }; + var (result, err) = Subsz(opts); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(500, request, response, errBytes); + return; + } + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Stacksz — /stacksz + // ========================================================================= + + /// + /// Handles the /stacksz HTTP endpoint (returns all managed threads' stacks). + /// Mirrors Go Server.HandleStacksz(). + /// + public void HandleStacksz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Stacksz); + // In Go this returns goroutine stacks; in .NET we return thread info. + var sb = new StringBuilder(); + sb.AppendLine("{\"stacks\":["); + var threads = Process.GetCurrentProcess().Threads; + bool first = true; + foreach (ProcessThread t in threads) + { + if (!first) sb.Append(','); + sb.AppendLine($"{{\"id\":{t.Id},\"state\":\"{t.ThreadState}\"}}"); + first = false; + } + sb.Append("]}"); + var data = Encoding.UTF8.GetBytes(sb.ToString()); + response.ContentType = "application/json"; + response.StatusCode = 200; + response.ContentLength64 = data.Length; + response.OutputStream.Write(data); + response.OutputStream.Close(); + } + + // ========================================================================= + // IP Queuesz — /ipqueuesz + // ========================================================================= + + /// + /// Returns IP queue stats snapshot. + /// Mirrors Go Server.Ipqueuesz(opts *IpqueueszOptions) *IpqueueszStatus. + /// + public IpqueueszStatus Ipqueuesz(IpqueueszOptions? opts = null) + { + // In .NET there is no direct equivalent of Go's ip queues; + // return empty result mirroring Go behaviour when no queues exist. + return new IpqueueszStatus(); + } + + /// + /// Handles the /ipqueuesz HTTP endpoint. + /// Mirrors Go Server.HandleIPQueuesz(). + /// + public void HandleIPQueuesz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.IPQueuesz); + var result = Ipqueuesz(); + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Varz — /varz + // ========================================================================= + + /// + /// Creates and returns the initial static Varz snapshot (config fields). + /// Mirrors Go Server.createVarz(). + /// + private Varz CreateVarz() + { + var opts = GetOpts(); + var varz = new Varz(); + + // Static config fields + _mu.EnterReadLock(); + varz.Id = _info.Id; + varz.Name = _info.Name; + varz.Version = _info.Version; + varz.Proto = _info.Proto; + varz.GitCommit = _info.GitCommit; + varz.GoVersion = _info.GoVersion; + varz.Host = _info.Host; + varz.Port = _info.Port; + varz.AuthRequired = _info.AuthRequired; + varz.TlsRequired = _info.TlsRequired; + varz.TlsVerify = _info.TlsVerify; + varz.TlsOcspPeerVerify = _ocspPeerVerify; + varz.Ip = _info.Ip; + varz.ClientConnectUrls = _info.ClientConnectUrls?.ToList(); + varz.WsConnectUrls = _info.WsConnectUrls?.ToList(); + varz.Start = _start; + varz.ConfigLoadTime = _configTime; + _mu.ExitReadLock(); + + // Options + varz.MaxConn = opts.MaxConn; + varz.MaxSubs = opts.MaxSubs; + varz.PingIntervalNs = (long)opts.PingInterval.TotalMilliseconds * 1_000_000; + varz.MaxPingsOut = opts.MaxPingsOut; + varz.HttpHost = opts.HttpHost; + varz.HttpPort = opts.HttpPort; + varz.HttpBasePath = opts.HttpBasePath; + varz.HttpsPort = opts.HttpsPort; + varz.AuthTimeout = opts.AuthTimeout; + varz.MaxControlLine = opts.MaxControlLine; + varz.MaxPayload = opts.MaxPayload; + varz.MaxPending = opts.MaxPending; + varz.TlsTimeout = opts.TlsTimeout; + varz.WriteDeadlineNs = (long)opts.WriteDeadline.TotalMilliseconds * 1_000_000; + varz.Tags = opts.Tags.Count > 0 ? [.. opts.Tags] : null; + + // System account + if (SystemAccount() is { } sysAcc) + varz.SystemAccount = sysAcc.Name; + + // Cluster options + varz.Cluster = new ClusterOptsVarz + { + Name = opts.Cluster.Name, + Host = opts.Cluster.Host, + Port = opts.Cluster.Port, + AuthTimeout = opts.Cluster.AuthTimeout, + TlsTimeout = opts.Cluster.TlsTimeout, + TlsRequired = opts.Cluster.TlsConfig != null, + PoolSize = opts.Cluster.PoolSize, + }; + if (opts.Routes is { Count: > 0 }) + varz.Cluster.Urls = [.. opts.Routes.Select(u => u.ToString())]; + + // Gateway options + varz.Gateway = new GatewayOptsVarz + { + Name = opts.Gateway.Name, + Host = opts.Gateway.Host, + Port = opts.Gateway.Port, + AuthTimeout = opts.Gateway.AuthTimeout, + TlsTimeout = opts.Gateway.TlsTimeout, + TlsRequired = opts.Gateway.TlsConfig != null, + Advertise = opts.Gateway.Advertise, + ConnectRetries = opts.Gateway.ConnectRetries, + RejectUnknown = opts.Gateway.RejectUnknown, + }; + if (opts.Gateway.Gateways is { Count: > 0 }) + { + varz.Gateway.Gateways = [.. opts.Gateway.Gateways.Select(g => new RemoteGatewayOptsVarz + { + Name = g.Name, + TlsTimeout = g.TlsTimeout, + Urls = g.Urls?.Select(u => u.ToString()).ToList(), + })]; + } + + // LeafNode options + varz.LeafNode = new LeafNodeOptsVarz + { + Host = opts.LeafNode.Host, + Port = opts.LeafNode.Port, + AuthTimeout = opts.LeafNode.AuthTimeout, + TlsTimeout = opts.LeafNode.TlsTimeout, + TlsRequired = opts.LeafNode.TlsConfig != null, + }; + + // MQTT options + varz.Mqtt = new MqttOptsVarz + { + Host = opts.Mqtt.Host, + Port = opts.Mqtt.Port, + NoAuthUser = opts.Mqtt.NoAuthUser, + AuthTimeout = opts.Mqtt.AuthTimeout, + TlsTimeout = opts.Mqtt.TlsTimeout, + JsDomain = opts.Mqtt.JsDomain, + AckWaitNs = (long)opts.Mqtt.AckWait.TotalMilliseconds * 1_000_000, + MaxAckPending = opts.Mqtt.MaxAckPending, + }; + + // WebSocket options + varz.Websocket = new WebsocketOptsVarz + { + Host = opts.Websocket.Host, + Port = opts.Websocket.Port, + NoAuthUser = opts.Websocket.NoAuthUser, + AuthTimeout = opts.Websocket.AuthTimeout, + NoTls = opts.Websocket.NoTls, + Compression = opts.Websocket.Compression, + SameOrigin = opts.Websocket.SameOrigin, + AllowedOrigins = opts.Websocket.AllowedOrigins.Count > 0 ? opts.Websocket.AllowedOrigins : null, + }; + + return varz; + } + + /// + /// Updates the runtime (dynamic) fields on a snapshot. + /// Mirrors Go Server.updateVarzRuntimeFields(). + /// + private void UpdateVarzRuntimeFields(Varz varz) + { + var now = DateTime.UtcNow; + varz.Now = now; + varz.Uptime = MyUptime(now - _start); + + // Memory + var gcInfo = GC.GetGCMemoryInfo(); + varz.Mem = gcInfo.MemoryLoadBytes; + + // CPU cores + varz.Cores = Environment.ProcessorCount; + varz.MaxProcs = Environment.ProcessorCount; + + // Connection counts + _mu.EnterReadLock(); + varz.Connections = _clients.Count; + varz.TotalConnections = _totalClients; + varz.Routes = NumRoutesInternal(); + varz.Remotes = NumRemotesInternal(); + varz.Leafs = _leafs.Count; + _mu.ExitReadLock(); + + // Stats + varz.InMsgs = Interlocked.Read(ref _stats.InMsgs); + varz.OutMsgs = Interlocked.Read(ref _stats.OutMsgs); + varz.InBytes = Interlocked.Read(ref _stats.InBytes); + varz.OutBytes = Interlocked.Read(ref _stats.OutBytes); + varz.SlowConsumers = Interlocked.Read(ref _stats.SlowConsumers); + varz.StaleConnections = Interlocked.Read(ref _stats.StaleConnections); + varz.StalledClients = Interlocked.Read(ref _stats.Stalls); + varz.Subscriptions = NumSubscriptions(); + varz.PinnedAccountFail = (ulong)Interlocked.Read(ref _pinnedAccFail); + + // HTTP request stats + _mu.EnterReadLock(); + varz.HttpReqStats = new Dictionary(_httpReqStats); + _mu.ExitReadLock(); + + // Slow consumer / stale connection breakdowns + varz.SlowConsumersStats = new SlowConsumersStats + { + Clients = (ulong)Interlocked.Read(ref _scStats.Clients), + Routes = (ulong)Interlocked.Read(ref _scStats.Routes), + Gateways = (ulong)Interlocked.Read(ref _scStats.Gateways), + Leafs = (ulong)Interlocked.Read(ref _scStats.Leafs), + }; + varz.StaleConnectionStats = new StaleConnectionStats + { + Clients = (ulong)Interlocked.Read(ref _staleStats.Clients), + Routes = (ulong)Interlocked.Read(ref _staleStats.Routes), + Gateways = (ulong)Interlocked.Read(ref _staleStats.Gateways), + Leafs = (ulong)Interlocked.Read(ref _staleStats.Leafs), + }; + } + + /// + /// Returns a full server variable (Varz) snapshot. + /// Mirrors Go Server.Varz(opts *VarzOptions) (*Varz, error). + /// + public (Varz Result, Exception? Error) Varz(VarzOptions? opts = null) + { + var varz = CreateVarz(); + UpdateVarzRuntimeFields(varz); + return (varz, null); + } + + /// + /// Handles the /varz HTTP endpoint. + /// Mirrors Go Server.HandleVarz(). + /// + public void HandleVarz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Varz); + var (result, err) = Varz(); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(500, request, response, errBytes); + return; + } + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Gatewayz — /gatewayz + // ========================================================================= + + /// + /// Returns a gateway info snapshot. + /// Mirrors Go Server.Gatewayz(opts *GatewayzOptions) (*Gatewayz, error). + /// + public (Gatewayz Result, Exception? Error) Gatewayz(GatewayzOptions? opts = null) + { + opts ??= new GatewayzOptions(); + var now = DateTime.UtcNow; + var gz = new Gatewayz + { + Id = ID(), + Now = now, + }; + + var serverOpts = GetOpts(); + if (!string.IsNullOrEmpty(serverOpts.Gateway.Name)) + { + gz.Name = serverOpts.Gateway.Name; + gz.Host = serverOpts.Gateway.Host; + gz.Port = serverOpts.Gateway.Port; + } + + // Outbound gateways + _mu.EnterReadLock(); + try + { + foreach (var kvp in _gateway.Out) + { + var gwName = kvp.Key; + if (!string.IsNullOrEmpty(opts.Name) && gwName != opts.Name) continue; + var c = kvp.Value; + var rg = new RemoteGatewayz + { + IsConfigured = true, + Connection = FillConnInfo(c, new ConnzOptions(), now), + }; + gz.OutboundGateways[gwName] = rg; + } + + // Inbound gateways + foreach (var kvp in _gateway.In) + { + var c = kvp.Value; + var gwConn = c.Gateway; + if (gwConn is null) continue; + var gwName = gwConn.Name; + if (!string.IsNullOrEmpty(opts.Name) && gwName != opts.Name) continue; + + if (!gz.InboundGateways.TryGetValue(gwName, out var inList)) + { + inList = []; + gz.InboundGateways[gwName] = inList; + } + inList.Add(new RemoteGatewayz + { + Connection = FillConnInfo(c, new ConnzOptions(), now), + }); + } + } + finally + { + _mu.ExitReadLock(); + } + + return (gz, null); + } + + /// + /// Handles the /gatewayz HTTP endpoint. + /// Mirrors Go Server.HandleGatewayz(). + /// + public void HandleGatewayz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Gatewayz); + var q = request.QueryString; + var opts = new GatewayzOptions + { + Name = q["gw_name"] ?? string.Empty, + Accounts = DecodeBool(q, "accs"), + AccountName = q["acc_name"] ?? string.Empty, + }; + var (result, err) = Gatewayz(opts); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(500, request, response, errBytes); + return; + } + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Leafz — /leafz + // ========================================================================= + + /// + /// Returns a leaf node info snapshot. + /// Mirrors Go Server.Leafz(opts *LeafzOptions) (*Leafz, error). + /// + public (Leafz Result, Exception? Error) Leafz(LeafzOptions? opts = null) + { + opts ??= new LeafzOptions(); + var now = DateTime.UtcNow; + var leafs = new List(); + + _mu.EnterReadLock(); + try + { + foreach (var kvp in _leafs) + { + var c = kvp.Value; + lock (c) + { + if (!string.IsNullOrEmpty(opts.Account)) + { + var accName = (c._account as Account)?.Name ?? string.Empty; + if (accName != opts.Account) continue; + } + + var li = new LeafInfo + { + Id = c.Cid, + Name = c.Opts.Name.Length > 0 ? c.Opts.Name : null, + Ip = c.Host, + Port = c.Port, + NumSubs = (uint)c.Subs.Count, + }; + + if (c._account is { } acc) + li.Account = acc.Name; + + if (c.Rtt > TimeSpan.Zero) + li.Rtt = FormatRtt(c.Rtt); + + if (c.Leaf is { } leaf) + { + li.IsSpoke = leaf.IsSpoke; + li.IsIsolated = leaf.Isolated; + if (!string.IsNullOrEmpty(leaf.Compression)) + li.Compression = leaf.Compression; + } + + if (c.ProxyKey.Length > 0) + li.Proxy = new ProxyInfo { Key = c.ProxyKey }; + + if (opts.Subscriptions) + li.Subs = [.. c.Subs.Keys]; + + leafs.Add(li); + } + } + } + finally + { + _mu.ExitReadLock(); + } + + return (new Leafz + { + Id = ID(), + Now = now, + NumLeafs = leafs.Count, + Leafs = leafs, + }, null); + } + + /// + /// Handles the /leafz HTTP endpoint. + /// Mirrors Go Server.HandleLeafz(). + /// + public void HandleLeafz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Leafz); + var q = request.QueryString; + var opts = new LeafzOptions + { + Subscriptions = DecodeBool(q, "subs"), + Account = q["acc"] ?? string.Empty, + }; + var (result, err) = Leafz(opts); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(500, request, response, errBytes); + return; + } + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // AccountStatz — /accstatz + // ========================================================================= + + /// + /// Returns per-account stats. + /// Mirrors Go Server.AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error). + /// + public (AccountStatz Result, Exception? Error) AccountStatz(AccountStatzOptions? opts = null) + { + opts ??= new AccountStatzOptions(); + var now = DateTime.UtcNow; + var accs = new List(); + + _mu.EnterReadLock(); + try + { + foreach (var kvp in _accounts) + { + var acc = kvp.Value; + if (opts.Accounts is { Count: > 0 } && !opts.Accounts.Contains(acc.Name)) + continue; + var conns = acc.NumLocalConnections(); + if (!opts.IncludeUnused && conns == 0) continue; + + accs.Add(new AccountStat + { + Account = acc.Name, + Name = acc.Name, + Conns = conns, + TotalConns = conns, + NumSubs = (uint)acc.TotalSubs(), + }); + } + } + finally + { + _mu.ExitReadLock(); + } + + return (new AccountStatz + { + Id = ID(), + Now = now, + Accounts = accs, + }, null); + } + + /// + /// Handles the /accstatz HTTP endpoint. + /// Mirrors Go Server.HandleAccountStatz(). + /// + public void HandleAccountStatz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.AccountStatz); + var q = request.QueryString; + var opts = new AccountStatzOptions + { + IncludeUnused = DecodeBool(q, "unused"), + }; + var accParam = q["accs"] ?? string.Empty; + if (!string.IsNullOrEmpty(accParam)) + opts.Accounts = [.. accParam.Split(',', StringSplitOptions.RemoveEmptyEntries)]; + + var (result, err) = AccountStatz(opts); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(500, request, response, errBytes); + return; + } + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Accountz — /accountz + // ========================================================================= + + /// + /// Returns account list or detail. + /// Mirrors Go Server.Accountz(opts *AccountzOptions) (*Accountz, error). + /// + public (Accountz Result, Exception? Error) Accountz(AccountzOptions? opts = null) + { + opts ??= new AccountzOptions(); + var now = DateTime.UtcNow; + + var az = new Accountz + { + Id = ID(), + Now = now, + }; + + if (SystemAccount() is { } sysAcc) + az.SystemAccount = sysAcc.Name; + + if (!string.IsNullOrEmpty(opts.Account)) + { + var (ai, err) = AccountInfo(opts.Account); + if (err != null) + return (az, err); + az.Account = ai; + } + else + { + _mu.EnterReadLock(); + az.Accounts = [.. _accounts.Keys.OrderBy(k => k)]; + _mu.ExitReadLock(); + } + + return (az, null); + } + + /// + /// Returns detailed info for a single account. + /// Mirrors Go Server.accountInfo(accName string) (*AccountInfo, error). + /// + public (AccountInfo? Info, Exception? Error) AccountInfo(string accName) + { + var (acc, err) = LookupAccount(accName); + if (err != null) return (null, err); + if (acc is null) + return (null, new InvalidOperationException($"account not found: {accName}")); + + var ai = new AccountInfo + { + AccountName = acc.Name, + Complete = true, + JetStream = acc.JetStream != null, + ClientCnt = acc.NumLocalConnections(), + SubCnt = (uint)acc.TotalSubs(), + }; + + if (acc.Sublist != null) + ai.Sublist = acc.Sublist.Stats(); + + if (SystemAccount() is { } sysAcc && sysAcc.Name == acc.Name) + ai.IsSystem = true; + + return (ai, null); + } + + /// + /// Handles the /accountz HTTP endpoint. + /// Mirrors Go Server.HandleAccountz(). + /// + public void HandleAccountz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Accountz); + var q = request.QueryString; + var opts = new AccountzOptions + { + Account = q["acc"] ?? string.Empty, + }; + var (result, err) = Accountz(opts); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(404, request, response, errBytes); + return; + } + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Root — / + // ========================================================================= + + /// + /// Handles the root HTTP endpoint (returns HTML with links to monitoring endpoints). + /// Mirrors Go Server.HandleRoot(). + /// + public void HandleRoot(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Root); + var html = $@"NATS Monitor +

NATS Monitoring

+"; + + var data = Encoding.UTF8.GetBytes(html); + response.ContentType = "text/html"; + response.StatusCode = 200; + response.ContentLength64 = data.Length; + response.OutputStream.Write(data); + response.OutputStream.Close(); + } + + // ========================================================================= + // Healthz — /healthz + // ========================================================================= + + /// + /// Internal health check implementation. + /// Mirrors Go Server.healthz(opts *HealthzOptions) *HealthStatus. + /// + private HealthStatus InternalHealthz(HealthzOptions opts) + { + if (IsShuttingDown()) + return new HealthStatus + { + Status = "unavailable", + Error = "server shutdown", + }; + + if (opts.JSEnabled) + { + if (_jetStream is null) + return new HealthStatus + { + Status = "unavailable", + Error = "jetstream not enabled", + }; + } + + return new HealthStatus { Status = "ok" }; + } + + /// + /// Returns the health status. + /// Mirrors Go Server.Healthz(opts *HealthzOptions) (*HealthStatus, error). + /// + public HealthStatus Healthz(HealthzOptions? opts = null) + => InternalHealthz(opts ?? new HealthzOptions()); + + /// + /// Handles the /healthz HTTP endpoint. + /// Mirrors Go Server.HandleHealthz(). + /// + public void HandleHealthz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Healthz); + var q = request.QueryString; + var opts = new HealthzOptions + { + JSEnabled = DecodeBool(q, "js-enabled"), + JSEnabledOnly = DecodeBool(q, "js-not-leaf"), + JSServerOnly = DecodeBool(q, "js-server-only"), + Account = q["account"] ?? string.Empty, + Details = DecodeBool(q, "details"), + }; + + var hs = Healthz(opts); + var data = JsonSerializer.SerializeToUtf8Bytes(hs, MonitorJsonOptions); + + var code = hs.Status == "ok" ? 200 + : hs.Error != null && hs.StatusCode == 400 ? 400 + : hs.Error != null && hs.StatusCode == 404 ? 404 + : hs.Status == "unavailable" ? 503 + : 200; + + if (code == 200) + ResponseHandler(request, response, data); + else + HandleResponse(code, request, response, data); + } + + // ========================================================================= + // Raftz — /raftz + // ========================================================================= + + /// + /// Returns Raft group info snapshot. + /// Mirrors Go Server.Raftz(opts *RaftzOptions) (*RaftzStatus, error). + /// + public (RaftzStatus Result, Exception? Error) Raftz(RaftzOptions? opts = null) + { + // Raft nodes are stored in _raftNodes. Return empty structure for now. + var status = new RaftzStatus(); + return (status, null); + } + + /// + /// Handles the /raftz HTTP endpoint. + /// Mirrors Go Server.HandleRaftz(). + /// + public void HandleRaftz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Raftz); + var (result, err) = Raftz(); + if (err != null) + { + var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}"); + HandleResponse(500, request, response, errBytes); + return; + } + var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // Expvarz / Profilez — /debug/vars + // ========================================================================= + + /// + /// Returns expvar-style diagnostics snapshot. + /// Mirrors Go Server.HandleExpvarz(). + /// + public void HandleExpvarz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + IncrHttpReqStat(MonitorPaths.Expvarz); + + // Build JSON manually using JsonDocument to match ExpvarzStatus structure. + var cmdline = Environment.GetCommandLineArgs(); + var memBytes = GC.GetGCMemoryInfo().MemoryLoadBytes; + var totalAlloc = GC.GetTotalAllocatedBytes(); + var gcCycles = GC.CollectionCount(0); + + // Build a JSON object inline since ExpvarzStatus uses JsonElement? fields. + var memObj = $"{{\"TotalAlloc\":{totalAlloc},\"HeapAlloc\":{memBytes},\"GcCycles\":{gcCycles}}}"; + var cmdArr = $"[{string.Join(",", cmdline.Select(a => JsonSerializer.Serialize(a)))}]"; + var json = $"{{\"memstats\":{memObj},\"cmdline\":{cmdArr}}}"; + + var data = Encoding.UTF8.GetBytes(json); + ResponseHandler(request, response, data); + } + + /// + /// Handles the profilez endpoint. + /// Mirrors Go Server.HandleProfilez(). + /// + public void HandleProfilez(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response) + { + var status = new ProfilezStatus { Error = "not supported in .NET" }; + var data = JsonSerializer.SerializeToUtf8Bytes(status, MonitorJsonOptions); + ResponseHandler(request, response, data); + } + + // ========================================================================= + // HTTP request stat tracking + // ========================================================================= + + /// + /// Increments the HTTP request counter for a monitoring path. + /// Mirrors Go s.incrHttpReqStat(path string). + /// + private void IncrHttpReqStat(string path) + { + _mu.EnterWriteLock(); + try + { + _httpReqStats.TryGetValue(path, out var v); + _httpReqStats[path] = v + 1; + } + finally + { + _mu.ExitWriteLock(); + } + } + + // ========================================================================= + // Private helpers + // ========================================================================= + + /// + /// Sorts a list by the given sort option. + /// Mirrors Go sort logic in monitor_sort_opts.go. + /// + private static void SortConnInfos(ConnInfos list, SortOpt sort) + { + var now = DateTime.UtcNow; + IComparer comparer = (string)sort switch + { + "start" => Comparer.Create((x, y) => x.Start.CompareTo(y.Start)), + "subs" => SortBySubs.Instance, + "pending" => SortByPending.Instance, + "msgs_to" => SortByOutMsgs.Instance, + "msgs_from" => SortByInMsgs.Instance, + "bytes_to" => SortByOutBytes.Instance, + "bytes_from" => SortByInBytes.Instance, + "last" => SortByLast.Instance, + "idle" => new SortByIdle(now), + "uptime" => new SortByUptime(now), + "stop" => SortByStop.Instance, + "reason" => SortByReason.Instance, + "rtt" => SortByRtt.Instance, + _ => SortByCid.Instance, + }; + list.Sort(comparer); + } + + /// + /// Formats a RTT value in Go style (e.g. "1.234ms"). + /// Mirrors Go rtt.String() for monitoring display. + /// + private static string FormatRtt(TimeSpan rtt) + { + var ns = (long)(rtt.TotalMilliseconds * 1_000_000); + if (ns < 1000) return $"{ns}ns"; + if (ns < 1_000_000) return $"{ns / 1000.0:0.###}µs"; + if (ns < 1_000_000_000) return $"{ns / 1_000_000.0:0.###}ms"; + return $"{ns / 1_000_000_000.0:0.###}s"; + } +} + +// ============================================================================ +// ClosedState.String() extension +// Mirrors Go ClosedState.String() in monitor.go. +// ============================================================================ + +/// +/// Extension methods for . +/// Mirrors Go ClosedState.String() in server/monitor.go. +/// +public static class ClosedStateExtensions +{ + private static readonly Dictionary _strs = new() + { + [ClosedState.ClientClosed] = "Client Closed", + [ClosedState.AuthenticationTimeout] = "Authentication Timeout", + [ClosedState.AuthenticationViolation] = "Authentication Failure", + [ClosedState.TlsHandshakeError] = "TLS Handshake Failure", + [ClosedState.SlowConsumerPendingBytes] = "Slow Consumer (Pending Bytes)", + [ClosedState.SlowConsumerWriteDeadline] = "Slow Consumer (Write Deadline)", + [ClosedState.WriteError] = "Write Error", + [ClosedState.ReadError] = "Read Error", + [ClosedState.ParseError] = "Parse Error", + [ClosedState.StaleConnection] = "Stale Connection", + [ClosedState.ProtocolViolation] = "Protocol Violation", + [ClosedState.BadClientProtocolVersion] = "Bad Client Protocol Version", + [ClosedState.WrongPort] = "Incorrect Port", + [ClosedState.MaxAccountConnectionsExceeded] = "Maximum Account Connections Exceeded", + [ClosedState.MaxConnectionsExceeded] = "Maximum Connections Exceeded", + [ClosedState.MaxPayloadExceeded] = "Maximum Message Payload Exceeded", + [ClosedState.MaxControlLineExceeded] = "Maximum Control Line Exceeded", + [ClosedState.MaxSubscriptionsExceeded] = "Maximum Subscriptions Exceeded", + [ClosedState.DuplicateRoute] = "Duplicate Route", + [ClosedState.RouteRemoved] = "Route Removed", + [ClosedState.ServerShutdown] = "Server Shutdown", + [ClosedState.AuthenticationExpired] = "Authentication Expired", + [ClosedState.WrongGateway] = "Wrong Gateway", + [ClosedState.MissingAccount] = "Missing Account", + [ClosedState.Revocation] = "Revoked", + [ClosedState.InternalClient] = "Internal Client", + [ClosedState.MsgHeaderViolation] = "Message Header Violation", + [ClosedState.NoRespondersRequiresHeaders] = "No Responders Requires Headers", + [ClosedState.ClusterNameConflict] = "Cluster Name Conflict", + [ClosedState.DuplicateRemoteLeafnodeConnection] = "Duplicate Remote LeafNode Connection", + [ClosedState.DuplicateClientId] = "Duplicate Client ID", + [ClosedState.DuplicateServerName] = "Duplicate Server Name", + [ClosedState.MinimumVersionRequired] = "Minimum Version Required", + [ClosedState.ClusterNamesIdentical] = "Cluster Names Identical", + [ClosedState.Kicked] = "Kicked", + [ClosedState.ProxyNotTrusted] = "Proxy Not Trusted", + [ClosedState.ProxyRequired] = "Proxy Required", + }; + + /// + /// Returns a human-readable description of the closed state reason. + /// Mirrors Go ClosedState.String() in server/monitor.go. + /// + public static string AsString(this ClosedState state) + => _strs.TryGetValue(state, out var s) ? s : state.ToString(); +}