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();
+}