From 045c12cce7fae2412b0c3b24c77c1d77a75b8763 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Feb 2026 22:13:50 -0500 Subject: [PATCH] feat: add Varz and Connz monitoring JSON models with Go field name parity --- src/NATS.Server/Monitoring/Connz.cs | 207 +++++++++ src/NATS.Server/Monitoring/Varz.cs | 415 +++++++++++++++++++ tests/NATS.Server.Tests/MonitorModelTests.cs | 51 +++ 3 files changed, 673 insertions(+) create mode 100644 src/NATS.Server/Monitoring/Connz.cs create mode 100644 src/NATS.Server/Monitoring/Varz.cs create mode 100644 tests/NATS.Server.Tests/MonitorModelTests.cs diff --git a/src/NATS.Server/Monitoring/Connz.cs b/src/NATS.Server/Monitoring/Connz.cs new file mode 100644 index 0000000..d2a6f49 --- /dev/null +++ b/src/NATS.Server/Monitoring/Connz.cs @@ -0,0 +1,207 @@ +using System.Text.Json.Serialization; + +namespace NATS.Server.Monitoring; + +/// +/// Connection information response. Corresponds to Go server/monitor.go Connz struct. +/// +public sealed class Connz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("num_connections")] + public int NumConns { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("limit")] + public int Limit { get; set; } + + [JsonPropertyName("connections")] + public ConnInfo[] Conns { get; set; } = []; +} + +/// +/// Detailed information on a per-connection basis. +/// Corresponds to Go server/monitor.go ConnInfo struct. +/// +public sealed class ConnInfo +{ + [JsonPropertyName("cid")] + public ulong Cid { get; set; } + + [JsonPropertyName("kind")] + public string Kind { get; set; } = ""; + + [JsonPropertyName("type")] + public string Type { get; set; } = ""; + + [JsonPropertyName("ip")] + public string Ip { get; set; } = ""; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("start")] + public DateTime Start { get; set; } + + [JsonPropertyName("last_activity")] + public DateTime LastActivity { get; set; } + + [JsonPropertyName("stop")] + public DateTime? Stop { get; set; } + + [JsonPropertyName("reason")] + public string Reason { get; set; } = ""; + + [JsonPropertyName("rtt")] + public string Rtt { get; set; } = ""; + + [JsonPropertyName("uptime")] + public string Uptime { get; set; } = ""; + + [JsonPropertyName("idle")] + public string Idle { get; set; } = ""; + + [JsonPropertyName("pending_bytes")] + public int Pending { get; set; } + + [JsonPropertyName("in_msgs")] + public long InMsgs { get; set; } + + [JsonPropertyName("out_msgs")] + public long OutMsgs { get; set; } + + [JsonPropertyName("in_bytes")] + public long InBytes { get; set; } + + [JsonPropertyName("out_bytes")] + public long OutBytes { get; set; } + + [JsonPropertyName("subscriptions")] + public uint NumSubs { get; set; } + + [JsonPropertyName("subscriptions_list")] + public string[] Subs { get; set; } = []; + + [JsonPropertyName("subscriptions_list_detail")] + public SubDetail[] SubsDetail { get; set; } = []; + + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("lang")] + public string Lang { get; set; } = ""; + + [JsonPropertyName("version")] + public string Version { get; set; } = ""; + + [JsonPropertyName("authorized_user")] + public string AuthorizedUser { get; set; } = ""; + + [JsonPropertyName("account")] + public string Account { get; set; } = ""; + + [JsonPropertyName("tls_version")] + public string TlsVersion { get; set; } = ""; + + [JsonPropertyName("tls_cipher_suite")] + public string TlsCipherSuite { get; set; } = ""; + + [JsonPropertyName("tls_first")] + public bool TlsFirst { get; set; } + + [JsonPropertyName("mqtt_client")] + public string MqttClient { get; set; } = ""; +} + +/// +/// Subscription detail information. +/// Corresponds to Go server/monitor.go SubDetail struct. +/// +public sealed class SubDetail +{ + [JsonPropertyName("account")] + public string Account { get; set; } = ""; + + [JsonPropertyName("subject")] + public string Subject { get; set; } = ""; + + [JsonPropertyName("qgroup")] + public string Queue { get; set; } = ""; + + [JsonPropertyName("sid")] + public string Sid { get; set; } = ""; + + [JsonPropertyName("msgs")] + public long Msgs { get; set; } + + [JsonPropertyName("max")] + public long Max { get; set; } + + [JsonPropertyName("cid")] + public ulong Cid { get; set; } +} + +/// +/// Sort options for connection listing. +/// Corresponds to Go server/monitor_sort_opts.go SortOpt type. +/// +public enum SortOpt +{ + ByCid, + ByStart, + BySubs, + ByPending, + ByMsgsTo, + ByMsgsFrom, + ByBytesTo, + ByBytesFrom, + ByLast, + ByIdle, + ByUptime, +} + +/// +/// Connection state filter. +/// Corresponds to Go server/monitor.go ConnState type. +/// +public enum ConnState +{ + Open, + Closed, + All, +} + +/// +/// Options passed to Connz() for filtering and sorting. +/// Corresponds to Go server/monitor.go ConnzOptions struct. +/// +public sealed class ConnzOptions +{ + public SortOpt Sort { get; set; } = SortOpt.ByCid; + + public bool Subscriptions { get; set; } + + public bool SubscriptionsDetail { get; set; } + + public ConnState State { get; set; } = ConnState.Open; + + public string User { get; set; } = ""; + + public string Account { get; set; } = ""; + + public string FilterSubject { get; set; } = ""; + + public int Offset { get; set; } + + public int Limit { get; set; } = 1024; +} diff --git a/src/NATS.Server/Monitoring/Varz.cs b/src/NATS.Server/Monitoring/Varz.cs new file mode 100644 index 0000000..847bdc2 --- /dev/null +++ b/src/NATS.Server/Monitoring/Varz.cs @@ -0,0 +1,415 @@ +using System.Text.Json.Serialization; + +namespace NATS.Server.Monitoring; + +/// +/// Server general information. Corresponds to Go server/monitor.go Varz struct. +/// +public sealed class Varz +{ + // Identity + [JsonPropertyName("server_id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("server_name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("version")] + public string Version { get; set; } = ""; + + [JsonPropertyName("proto")] + public int Proto { get; set; } + + [JsonPropertyName("git_commit")] + public string GitCommit { get; set; } = ""; + + [JsonPropertyName("go")] + public string GoVersion { get; set; } = ""; + + [JsonPropertyName("host")] + public string Host { get; set; } = ""; + + [JsonPropertyName("port")] + public int Port { get; set; } + + // Network + [JsonPropertyName("ip")] + public string Ip { get; set; } = ""; + + [JsonPropertyName("connect_urls")] + public string[] ConnectUrls { get; set; } = []; + + [JsonPropertyName("ws_connect_urls")] + public string[] WsConnectUrls { get; set; } = []; + + [JsonPropertyName("http_host")] + public string HttpHost { get; set; } = ""; + + [JsonPropertyName("http_port")] + public int HttpPort { get; set; } + + [JsonPropertyName("http_base_path")] + public string HttpBasePath { get; set; } = ""; + + [JsonPropertyName("https_port")] + public int HttpsPort { get; set; } + + // Security + [JsonPropertyName("auth_required")] + public bool AuthRequired { get; set; } + + [JsonPropertyName("tls_required")] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + public bool TlsVerify { get; set; } + + [JsonPropertyName("tls_ocsp_peer_verify")] + public bool TlsOcspPeerVerify { get; set; } + + [JsonPropertyName("auth_timeout")] + public double AuthTimeout { get; set; } + + [JsonPropertyName("tls_timeout")] + public double TlsTimeout { get; set; } + + // Limits + [JsonPropertyName("max_connections")] + public int MaxConnections { get; set; } + + [JsonPropertyName("max_subscriptions")] + public int MaxSubscriptions { get; set; } + + [JsonPropertyName("max_payload")] + public int MaxPayload { get; set; } + + [JsonPropertyName("max_pending")] + public long MaxPending { get; set; } + + [JsonPropertyName("max_control_line")] + public int MaxControlLine { get; set; } + + [JsonPropertyName("ping_max")] + public int MaxPingsOut { get; set; } + + // Timing + [JsonPropertyName("ping_interval")] + public long PingInterval { get; set; } + + [JsonPropertyName("write_deadline")] + public long WriteDeadline { get; set; } + + [JsonPropertyName("start")] + public DateTime Start { get; set; } + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("uptime")] + public string Uptime { get; set; } = ""; + + // Runtime + [JsonPropertyName("mem")] + public long Mem { get; set; } + + [JsonPropertyName("cpu")] + public double Cpu { get; set; } + + [JsonPropertyName("cores")] + public int Cores { get; set; } + + [JsonPropertyName("gomaxprocs")] + public int MaxProcs { get; set; } + + // Connections + [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 Leafnodes { get; set; } + + // Messages + [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; } + + // Health + [JsonPropertyName("slow_consumers")] + public long SlowConsumers { get; set; } + + [JsonPropertyName("slow_consumer_stats")] + public SlowConsumersStats SlowConsumerStats { get; set; } = new(); + + [JsonPropertyName("subscriptions")] + public uint Subscriptions { get; set; } + + // Config + [JsonPropertyName("config_load_time")] + public DateTime ConfigLoadTime { get; set; } + + [JsonPropertyName("tags")] + public string[] Tags { get; set; } = []; + + [JsonPropertyName("system_account")] + public string SystemAccount { get; set; } = ""; + + [JsonPropertyName("pinned_account_fails")] + public ulong PinnedAccountFail { get; set; } + + [JsonPropertyName("tls_cert_not_after")] + public DateTime TlsCertNotAfter { get; set; } + + // HTTP + [JsonPropertyName("http_req_stats")] + public Dictionary HttpReqStats { get; set; } = new(); + + // Subsystems + [JsonPropertyName("cluster")] + public ClusterOptsVarz Cluster { get; set; } = new(); + + [JsonPropertyName("gateway")] + public GatewayOptsVarz Gateway { get; set; } = new(); + + [JsonPropertyName("leaf")] + public LeafNodeOptsVarz Leaf { 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(); +} + +/// +/// Statistics about slow consumers by connection type. +/// Corresponds to Go server/monitor.go SlowConsumersStats struct. +/// +public sealed class SlowConsumersStats +{ + [JsonPropertyName("clients")] + public ulong Clients { get; set; } + + [JsonPropertyName("routes")] + public ulong Routes { get; set; } + + [JsonPropertyName("gateways")] + public ulong Gateways { get; set; } + + [JsonPropertyName("leafs")] + public ulong Leafs { get; set; } +} + +/// +/// Cluster configuration monitoring information. +/// Corresponds to Go server/monitor.go ClusterOptsVarz struct. +/// +public sealed class ClusterOptsVarz +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("addr")] + public string Host { get; set; } = ""; + + [JsonPropertyName("cluster_port")] + public int Port { get; set; } + + [JsonPropertyName("auth_timeout")] + public double AuthTimeout { get; set; } + + [JsonPropertyName("tls_timeout")] + public double TlsTimeout { get; set; } + + [JsonPropertyName("tls_required")] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + public bool TlsVerify { get; set; } + + [JsonPropertyName("pool_size")] + public int PoolSize { get; set; } + + [JsonPropertyName("urls")] + public string[] Urls { get; set; } = []; +} + +/// +/// Gateway configuration monitoring information. +/// Corresponds to Go server/monitor.go GatewayOptsVarz struct. +/// +public sealed class GatewayOptsVarz +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("host")] + public string Host { get; set; } = ""; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("auth_timeout")] + public double AuthTimeout { get; set; } + + [JsonPropertyName("tls_timeout")] + public double TlsTimeout { get; set; } + + [JsonPropertyName("tls_required")] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + public bool TlsVerify { get; set; } + + [JsonPropertyName("advertise")] + public string Advertise { get; set; } = ""; + + [JsonPropertyName("connect_retries")] + public int ConnectRetries { get; set; } + + [JsonPropertyName("reject_unknown")] + public bool RejectUnknown { get; set; } +} + +/// +/// Leaf node configuration monitoring information. +/// Corresponds to Go server/monitor.go LeafNodeOptsVarz struct. +/// +public sealed class LeafNodeOptsVarz +{ + [JsonPropertyName("host")] + public string Host { get; set; } = ""; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("auth_timeout")] + public double AuthTimeout { get; set; } + + [JsonPropertyName("tls_timeout")] + public double TlsTimeout { get; set; } + + [JsonPropertyName("tls_required")] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + public bool TlsVerify { get; set; } + + [JsonPropertyName("tls_ocsp_peer_verify")] + public bool TlsOcspPeerVerify { get; set; } +} + +/// +/// MQTT configuration monitoring information. +/// Corresponds to Go server/monitor.go MQTTOptsVarz struct. +/// +public sealed class MqttOptsVarz +{ + [JsonPropertyName("host")] + public string Host { get; set; } = ""; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("tls_timeout")] + public double TlsTimeout { get; set; } +} + +/// +/// Websocket configuration monitoring information. +/// Corresponds to Go server/monitor.go WebsocketOptsVarz struct. +/// +public sealed class WebsocketOptsVarz +{ + [JsonPropertyName("host")] + public string Host { get; set; } = ""; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("tls_timeout")] + public double TlsTimeout { get; set; } +} + +/// +/// JetStream runtime information. +/// Corresponds to Go server/monitor.go JetStreamVarz struct. +/// +public sealed class JetStreamVarz +{ + [JsonPropertyName("config")] + public JetStreamConfig Config { get; set; } = new(); + + [JsonPropertyName("stats")] + public JetStreamStats Stats { get; set; } = new(); +} + +/// +/// JetStream configuration. +/// Corresponds to Go server/jetstream.go JetStreamConfig struct. +/// +public sealed class JetStreamConfig +{ + [JsonPropertyName("max_memory")] + public long MaxMemory { get; set; } + + [JsonPropertyName("max_storage")] + public long MaxStorage { get; set; } + + [JsonPropertyName("store_dir")] + public string StoreDir { get; set; } = ""; +} + +/// +/// JetStream statistics. +/// Corresponds to Go server/jetstream.go JetStreamStats struct. +/// +public sealed class JetStreamStats +{ + [JsonPropertyName("memory")] + public ulong Memory { get; set; } + + [JsonPropertyName("storage")] + public ulong Storage { get; set; } + + [JsonPropertyName("accounts")] + public int Accounts { get; set; } + + [JsonPropertyName("ha_assets")] + public int HaAssets { get; set; } + + [JsonPropertyName("api")] + public JetStreamApiStats Api { get; set; } = new(); +} + +/// +/// JetStream API statistics. +/// Corresponds to Go server/jetstream.go JetStreamAPIStats struct. +/// +public sealed class JetStreamApiStats +{ + [JsonPropertyName("total")] + public ulong Total { get; set; } + + [JsonPropertyName("errors")] + public ulong Errors { get; set; } +} diff --git a/tests/NATS.Server.Tests/MonitorModelTests.cs b/tests/NATS.Server.Tests/MonitorModelTests.cs new file mode 100644 index 0000000..690afd5 --- /dev/null +++ b/tests/NATS.Server.Tests/MonitorModelTests.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using NATS.Server.Monitoring; + +namespace NATS.Server.Tests; + +public class MonitorModelTests +{ + [Fact] + public void Varz_serializes_with_go_field_names() + { + var varz = new Varz + { + Id = "TESTID", Name = "test-server", Version = "0.1.0", + Host = "0.0.0.0", Port = 4222, InMsgs = 100, OutMsgs = 200, + }; + var json = JsonSerializer.Serialize(varz); + json.ShouldContain("\"server_id\":"); + json.ShouldContain("\"server_name\":"); + json.ShouldContain("\"in_msgs\":"); + json.ShouldContain("\"out_msgs\":"); + json.ShouldNotContain("\"InMsgs\""); + } + + [Fact] + public void Connz_serializes_with_go_field_names() + { + var connz = new Connz + { + Id = "TESTID", Now = DateTime.UtcNow, NumConns = 1, Total = 1, Limit = 1024, + Conns = [new ConnInfo { Cid = 1, Ip = "127.0.0.1", Port = 5555, + InMsgs = 10, Uptime = "1s", Idle = "0s", + Start = DateTime.UtcNow, LastActivity = DateTime.UtcNow }], + }; + var json = JsonSerializer.Serialize(connz); + json.ShouldContain("\"server_id\":"); + json.ShouldContain("\"num_connections\":"); + json.ShouldContain("\"in_msgs\":"); + json.ShouldContain("\"pending_bytes\":"); + } + + [Fact] + public void Varz_includes_nested_config_stubs() + { + var varz = new Varz { Id = "X", Name = "X", Version = "X", Host = "X" }; + var json = JsonSerializer.Serialize(varz); + json.ShouldContain("\"cluster\":"); + json.ShouldContain("\"gateway\":"); + json.ShouldContain("\"leaf\":"); + json.ShouldContain("\"jetstream\":"); + } +}