diff --git a/functions/infra/parse_nats_monitor.go b/functions/infra/parse_nats_monitor.go new file mode 100644 index 00000000..44494c82 --- /dev/null +++ b/functions/infra/parse_nats_monitor.go @@ -0,0 +1,150 @@ +package infra + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +// natsVarz refleja los campos relevantes de la respuesta JSON del endpoint +// /varz del monitoring HTTP embebido de un nats-server (puerto 8222, loopback). +// Solo se mapean los campos que producen series; el resto se ignora. +type natsVarz struct { + InMsgs int64 `json:"in_msgs"` + OutMsgs int64 `json:"out_msgs"` + InBytes int64 `json:"in_bytes"` + OutBytes int64 `json:"out_bytes"` + Connections int `json:"connections"` + SlowConsumers int `json:"slow_consumers"` + Subscriptions int `json:"subscriptions"` + Mem int64 `json:"mem"` + Start string `json:"start"` +} + +// natsConnz refleja los campos relevantes de /connz. +type natsConnz struct { + NumConnections int `json:"num_connections"` +} + +// natsStreamDetail refleja un stream dentro de account_details[].stream_detail[]. +type natsStreamDetail struct { + Name string `json:"name"` + Cluster struct { + Leader string `json:"leader"` + } `json:"cluster"` + State struct { + Messages int64 `json:"messages"` + Bytes int64 `json:"bytes"` + } `json:"state"` +} + +// natsJsz refleja los campos relevantes de /jsz?streams=1. +type natsJsz struct { + Streams int64 `json:"streams"` + Messages int64 `json:"messages"` + Bytes int64 `json:"bytes"` + Memory int64 `json:"memory"` + Storage int64 `json:"storage"` + AccountDetails []struct { + StreamDetail []natsStreamDetail `json:"stream_detail"` + } `json:"account_details"` +} + +// ParseNatsMonitor convierte las respuestas JSON del endpoint de monitoring HTTP +// embebido de un nats-server (puerto 8222, loopback) en una serie de PromSample +// lista para empujar a VictoriaMetrics. Es la hermana de ParseUnibusHealth para +// las métricas server-level de NATS/JetStream (msgs/s, conexiones, KV bucket +// msgs, RAFT leader por stream, memoria). La consume el unibus_exporter de +// fleet_monitoring en modo scraper local por nodo. +// +// node es el nombre lógico del nodo (p.ej. "magnus"); se adjunta a CADA serie +// como las labels "node" e "instance" para distinguir los nodos cuando un único +// exporter scrapea varios. +// +// varz, connz y jsz son los cuerpos crudos de GET /varz, GET /connz y +// GET /jsz?streams=1 respectivamente: +// - varz es el core: si NO parsea como JSON válido devuelve (nil, error). +// - connz y jsz son best-effort: si vienen vacíos o no parsean, sus series se +// omiten sin abortar (no error), para que el scraper resista que un endpoint +// falle. nats_connections cae a varz.connections cuando connz no parsea. +func ParseNatsMonitor(node string, varz, connz, jsz []byte) ([]PromSample, error) { + var v natsVarz + if err := json.Unmarshal(varz, &v); err != nil { + return nil, fmt.Errorf("parse nats varz for node %q: %w", node, err) + } + + // mk construye un PromSample con las labels base {node, instance} más, de + // forma opcional, labels extra (clave/valor alternados). Las labels base no + // se pueden sobreescribir desde extra. + mk := func(name string, val float64, extra ...string) PromSample { + labels := map[string]string{"node": node, "instance": node} + for i := 0; i+1 < len(extra); i += 2 { + labels[extra[i]] = extra[i+1] + } + return PromSample{Name: name, Labels: labels, Value: val} + } + + out := []PromSample{ + mk("nats_msgs_in_total", float64(v.InMsgs)), + mk("nats_msgs_out_total", float64(v.OutMsgs)), + mk("nats_bytes_in_total", float64(v.InBytes)), + mk("nats_bytes_out_total", float64(v.OutBytes)), + } + + // nats_connections: prefiere connz.num_connections; si connz no parsea, cae + // a varz.connections para no perder la serie. + connections := float64(v.Connections) + if len(connz) > 0 { + var c natsConnz + if err := json.Unmarshal(connz, &c); err == nil { + connections = float64(c.NumConnections) + } + } + out = append(out, + mk("nats_connections", connections), + mk("nats_slow_consumers", float64(v.SlowConsumers)), + mk("nats_mem_bytes", float64(v.Mem)), + mk("nats_subscriptions", float64(v.Subscriptions)), + ) + + // nats_server_start_seconds: epoch (segundos Unix) del campo start (RFC3339). + // Proxy de reinicios del nats-server: un cambio de este valor = el server + // reinició. Si el parse de la fecha falla, se omite la serie (no se aborta). + if t, err := time.Parse(time.RFC3339, v.Start); err == nil { + out = append(out, mk("nats_server_start_seconds", float64(t.Unix()))) + } + + // jsz es best-effort: si vacío o inválido, se omiten todas sus series. + if len(jsz) > 0 { + var j natsJsz + if err := json.Unmarshal(jsz, &j); err == nil { + out = append(out, + mk("nats_jetstream_streams", float64(j.Streams)), + mk("nats_jetstream_messages", float64(j.Messages)), + mk("nats_jetstream_bytes", float64(j.Bytes)), + mk("nats_jetstream_memory_bytes", float64(j.Memory)), + mk("nats_jetstream_storage_bytes", float64(j.Storage)), + ) + for _, acc := range j.AccountDetails { + for _, sd := range acc.StreamDetail { + out = append(out, + mk("nats_stream_messages", float64(sd.State.Messages), "stream", sd.Name), + mk("nats_stream_bytes", float64(sd.State.Bytes), "stream", sd.Name), + ) + leader := 0.0 + if sd.Cluster.Leader == node { + leader = 1 + } + out = append(out, mk("nats_jetstream_raft_leader", leader, "stream", sd.Name)) + + if bucket, ok := strings.CutPrefix(sd.Name, "KV_"); ok { + out = append(out, mk("kv_bucket_msgs", float64(sd.State.Messages), "bucket", bucket)) + } + } + } + } + } + + return out, nil +} diff --git a/functions/infra/parse_nats_monitor.md b/functions/infra/parse_nats_monitor.md new file mode 100644 index 00000000..4d031c05 --- /dev/null +++ b/functions/infra/parse_nats_monitor.md @@ -0,0 +1,119 @@ +--- +name: parse_nats_monitor +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func ParseNatsMonitor(node string, varz, connz, jsz []byte) ([]PromSample, error)" +description: "Convierte las respuestas JSON del endpoint de monitoring HTTP embebido de un nats-server (puerto 8222, loopback) en una serie de PromSample lista para empujar a VictoriaMetrics. Hermana de ParseUnibusHealth pero para las métricas server-level de NATS/JetStream: msgs/s, bytes, conexiones, slow consumers, memoria RSS, start epoch (proxy de reinicios), streams/messages/bytes/memory/storage de JetStream, y por stream nats_stream_messages/bytes, nats_jetstream_raft_leader y kv_bucket_msgs para los buckets KV_. Adjunta labels node e instance a cada serie. varz es el core (error si no parsea); connz y jsz son best-effort (se omiten sin abortar). La consume el unibus_exporter de fleet_monitoring como scraper local por nodo." +tags: [prometheus, metrics, nats, jetstream, monitoring, varz, connz, jsz, kv, raft, fleet-metrics, infra] +uses_functions: [] +uses_types: ["PromSample_go_infra"] +returns: [] +returns_optional: true +error_type: "error_go_core" +imports: ["encoding/json", "fmt", "strings", "time"] +params: + - name: node + desc: "nombre lógico del nodo (p.ej. \"magnus\"); se adjunta como labels node e instance a CADA serie y se compara con cluster.leader de cada stream para nats_jetstream_raft_leader" + - name: varz + desc: "cuerpo JSON crudo de GET http://127.0.0.1:8222/varz; core de la función (in_msgs, out_msgs, in_bytes, out_bytes, connections, slow_consumers, subscriptions, mem, start). Si no parsea, la función devuelve error" + - name: connz + desc: "cuerpo JSON crudo de GET http://127.0.0.1:8222/connz; best-effort (num_connections). Si vacío o inválido, nats_connections cae a varz.connections sin abortar" + - name: jsz + desc: "cuerpo JSON crudo de GET http://127.0.0.1:8222/jsz?streams=1; best-effort (streams, messages, bytes, memory, storage y account_details[].stream_detail[]). Si vacío o inválido, se omiten sus series sin abortar. Necesita ?streams=1 para traer stream_detail" +output: "slice de PromSample con labels base {node,instance}: nats_msgs_in/out_total, nats_bytes_in/out_total, nats_connections, nats_slow_consumers, nats_mem_bytes, nats_subscriptions, nats_server_start_seconds (omitida si start no parsea), nats_jetstream_streams/messages/bytes/memory_bytes/storage_bytes; y por stream nats_stream_messages{stream}, nats_stream_bytes{stream}, nats_jetstream_raft_leader{stream} (1 si cluster.leader==node) y, para streams KV_, kv_bucket_msgs{bucket} con el prefijo KV_ recortado. Error solo si varz no es JSON válido." +tested: true +test_file_path: "functions/infra/parse_nats_monitor_test.go" +tests: + - "TestParseNatsMonitorGolden" + - "TestParseNatsMonitorEmptyJsz" + - "TestParseNatsMonitorInvalidConnz" + - "TestParseNatsMonitorInvalidVarz" +--- + +# parse_nats_monitor + +Función de transformación (clasificada `impure` porque devuelve `error` al fallar el +unmarshal del core; no hace I/O ni red por sí misma) que traduce las métricas +server-level de un **nats-server** a series Prometheus. Es la hermana de +`parse_unibus_health_go_infra`: aquella lee el `/healthz` de `membershipd` (posture), +esta lee el monitoring embebido de NATS (puerto 8222) para las métricas profundas que +`/healthz` no expone: msgs/s, conexiones, RAFT leader por stream, memoria, KV buckets. + +Pertenece al grupo de capacidad `fleet-metrics`: se compone con +`format_prom_exposition_go_infra` (serializar) y `push_prom_remote_go_infra` (empujar a +VictoriaMetrics). La consume el `unibus_exporter` de `fleet_monitoring` en modo scraper +local por nodo, que hace los tres GET y le pasa los cuerpos crudos. + +## Ejemplo + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "time" + + "fn-registry/functions/infra" +) + +func get(url string) []byte { + resp, err := http.Get(url) + if err != nil { + return nil // best-effort: connz/jsz pueden faltar + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return b +} + +func main() { + base := "http://127.0.0.1:8222" + varz := get(base + "/varz") + connz := get(base + "/connz") + jsz := get(base + "/jsz?streams=1") + + samples, err := infra.ParseNatsMonitor("magnus", varz, connz, jsz) + if err != nil { + panic(err) // varz es el core: sin él no hay métricas + } + fmt.Print(infra.FormatPromExposition(samples, time.Now().UnixMilli())) + // nats_msgs_in_total{instance="magnus",node="magnus"} 17 ... + // kv_bucket_msgs{bucket="UNIBUS_users",instance="magnus",node="magnus"} 2 ... + // nats_jetstream_raft_leader{instance="magnus",node="magnus",stream="KV_UNIBUS_users"} 1 ... +} +``` + +## Cuando usarla + +Úsala dentro de un exporter que monitoriza un nats-server con el monitoring HTTP +embebido activado (`http: 127.0.0.1:8222` en la config de NATS): tras hacer +`GET /varz`, `GET /connz` y `GET /jsz?streams=1` contra loopback, pasa los tres cuerpos +crudos a esta función para obtener todas las series server-level del nodo. Llámala como +scraper local por nodo (cada nodo expone su 8222 solo en loopback), no centralizado. + +## Gotchas + +- **Impura por contrato**: solo devuelve `error` si `varz` no es JSON válido (es el core). + `connz` y `jsz` son **best-effort**: si vienen vacíos o no parsean, sus series se omiten + sin abortar. Esto hace al scraper resistente a que un endpoint falle de forma puntual. +- **Monitoring loopback-only sin auth**: el puerto 8222 de NATS no tiene autenticación; por + eso debe bindearse a `127.0.0.1` y scrapearse localmente en cada nodo, nunca exponerse a + la red. El push agregado a VictoriaMetrics lo hace el exporter, no esta función. +- **`/jsz` necesita `?streams=1`** para traer `account_details[].stream_detail[]`. Sin ese + parámetro el cuerpo trae los totales pero no el detalle por stream, y entonces no salen + `nats_stream_*`, `nats_jetstream_raft_leader` ni `kv_bucket_msgs`. +- **`nats_connections`**: prefiere `connz.num_connections`; si `connz` no parsea, cae a + `varz.connections` para no perder la serie. +- **RAFT leader en standalone**: en un nats-server sin clúster, el objeto `cluster` puede + faltar o `leader` venir vacío; en ese caso `nats_jetstream_raft_leader` sale 0 salvo que + `cluster.leader == node`. Es esperado: en standalone no hay quorum RAFT real. +- **`kv_bucket_msgs`** solo se emite para streams cuyo nombre empieza por `KV_`, recortando + el prefijo (stream `KV_UNIBUS_users` → bucket `UNIBUS_users`). +- **`nats_server_start_seconds`** es el epoch Unix del campo `start` (RFC3339): sirve como + proxy de reinicios (un cambio de valor = el server reinició). Si el campo no parsea como + fecha válida, la serie se omite en lugar de abortar. diff --git a/functions/infra/parse_nats_monitor_test.go b/functions/infra/parse_nats_monitor_test.go new file mode 100644 index 00000000..b609858b --- /dev/null +++ b/functions/infra/parse_nats_monitor_test.go @@ -0,0 +1,160 @@ +package infra + +import ( + "os" + "testing" +) + +// findNatsSample devuelve el primer PromSample cuyo Name coincide y cuyos labels +// extra (clave/valor alternados) están todos presentes con el valor esperado. +// El segundo retorno indica si se encontró. +func findNatsSample(samples []PromSample, name string, labels ...string) (PromSample, bool) { + for _, s := range samples { + if s.Name != name { + continue + } + match := true + for i := 0; i+1 < len(labels); i += 2 { + if s.Labels[labels[i]] != labels[i+1] { + match = false + break + } + } + if match { + return s, true + } + } + return PromSample{}, false +} + +func mustRead(t *testing.T, path string) []byte { + t.Helper() + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read fixture %s: %v", path, err) + } + return b +} + +// golden: fixtures reales de un nats-server 2.11.15, node="probe" (== el leader +// de los streams), valores concretos verificados a mano. +func TestParseNatsMonitorGolden(t *testing.T) { + varz := mustRead(t, "testdata/nats_varz.json") + connz := mustRead(t, "testdata/nats_connz.json") + jsz := mustRead(t, "testdata/nats_jsz.json") + + got, err := ParseNatsMonitor("probe", varz, connz, jsz) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := map[string]float64{ + "nats_msgs_in_total": 17, + "nats_msgs_out_total": 17, + "nats_mem_bytes": 18288640, + "nats_jetstream_streams": 3, + "nats_connections": 1, + "nats_jetstream_messages": 6, + } + for name, w := range want { + s, ok := findNatsSample(got, name) + if !ok { + t.Errorf("missing sample %q", name) + continue + } + if s.Value != w { + t.Errorf("%s = %v, want %v", name, s.Value, w) + } + if s.Labels["node"] != "probe" || s.Labels["instance"] != "probe" { + t.Errorf("%s labels = %v, want node=instance=probe", name, s.Labels) + } + } + + // kv_bucket_msgs por cada KV bucket (prefijo KV_ recortado). + for bucket, w := range map[string]float64{ + "UNIBUS_users": 2, + "UNIBUS_rooms": 2, + "UNIBUS_members": 2, + } { + s, ok := findNatsSample(got, "kv_bucket_msgs", "bucket", bucket) + if !ok { + t.Errorf("missing kv_bucket_msgs{bucket=%q}", bucket) + continue + } + if s.Value != w { + t.Errorf("kv_bucket_msgs{bucket=%q} = %v, want %v", bucket, s.Value, w) + } + } + + // raft leader: probe == node, así que el stream KV_UNIBUS_users tiene leader=1. + s, ok := findNatsSample(got, "nats_jetstream_raft_leader", "stream", "KV_UNIBUS_users") + if !ok { + t.Fatal("missing nats_jetstream_raft_leader{stream=KV_UNIBUS_users}") + } + if s.Value != 1 { + t.Errorf("nats_jetstream_raft_leader{stream=KV_UNIBUS_users} = %v, want 1", s.Value) + } + + // stream_detail también emite nats_stream_messages con label stream completo. + if s, ok := findNatsSample(got, "nats_stream_messages", "stream", "KV_UNIBUS_users"); !ok || s.Value != 2 { + t.Errorf("nats_stream_messages{stream=KV_UNIBUS_users} = %v ok=%v, want 2", s.Value, ok) + } + + // nats_server_start_seconds presente (start es RFC3339 válido). + if _, ok := findNatsSample(got, "nats_server_start_seconds"); !ok { + t.Error("missing nats_server_start_seconds (start is a valid RFC3339)") + } +} + +// edge: jsz sin streams ni account_details. No produce series kv_bucket_msgs ni +// nats_stream_*, pero sí las de varz/connz y las jetstream top-level (en 0). +func TestParseNatsMonitorEmptyJsz(t *testing.T) { + varz := mustRead(t, "testdata/nats_varz.json") + connz := mustRead(t, "testdata/nats_connz.json") + jsz := []byte(`{"streams":0,"account_details":[]}`) + + got, err := ParseNatsMonitor("probe", varz, connz, jsz) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := findNatsSample(got, "kv_bucket_msgs", "bucket", "UNIBUS_users"); ok { + t.Error("did not expect kv_bucket_msgs with empty account_details") + } + if _, ok := findNatsSample(got, "nats_stream_messages"); ok { + t.Error("did not expect nats_stream_messages with empty account_details") + } + // varz/connz siguen presentes. + if s, ok := findNatsSample(got, "nats_msgs_in_total"); !ok || s.Value != 17 { + t.Errorf("nats_msgs_in_total = %v ok=%v, want 17", s.Value, ok) + } + if s, ok := findNatsSample(got, "nats_connections"); !ok || s.Value != 1 { + t.Errorf("nats_connections = %v ok=%v, want 1", s.Value, ok) + } +} + +// edge: connz inválido. No es error; nats_connections cae a varz.connections (1). +// varz/jsz siguen produciendo sus series. +func TestParseNatsMonitorInvalidConnz(t *testing.T) { + varz := mustRead(t, "testdata/nats_varz.json") + jsz := mustRead(t, "testdata/nats_jsz.json") + + got, err := ParseNatsMonitor("probe", varz, []byte("not json"), jsz) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // fallback a varz.connections (= 1). + if s, ok := findNatsSample(got, "nats_connections"); !ok || s.Value != 1 { + t.Errorf("nats_connections = %v ok=%v, want 1 (fallback varz.connections)", s.Value, ok) + } + // jsz sigue vivo. + if s, ok := findNatsSample(got, "nats_jetstream_streams"); !ok || s.Value != 3 { + t.Errorf("nats_jetstream_streams = %v ok=%v, want 3", s.Value, ok) + } +} + +// error path: varz inválido devuelve error no-nil (es el core, sin él no hay nada). +func TestParseNatsMonitorInvalidVarz(t *testing.T) { + if _, err := ParseNatsMonitor("probe", []byte("{{{"), nil, nil); err == nil { + t.Fatal("expected error for invalid varz, got nil") + } +} diff --git a/functions/infra/testdata/nats_connz.json b/functions/infra/testdata/nats_connz.json new file mode 100644 index 00000000..7c81a64d --- /dev/null +++ b/functions/infra/testdata/nats_connz.json @@ -0,0 +1,30 @@ +{ + "server_id": "NC23B47RQSJYPX5AIUC5CA3ND5RLCYREKSAFLM65MLBY5PBRIXPAFL7O", + "now": "2026-06-07T19:02:25.326833943Z", + "num_connections": 1, + "total": 1, + "offset": 0, + "limit": 1024, + "connections": [ + { + "cid": 5, + "kind": "Client", + "type": "nats", + "ip": "127.0.0.1", + "port": 52734, + "start": "2026-06-07T21:02:24.812382826+02:00", + "last_activity": "2026-06-07T21:02:24.821005187+02:00", + "rtt": "623µs", + "uptime": "0s", + "idle": "0s", + "pending_bytes": 0, + "in_msgs": 17, + "out_msgs": 17, + "in_bytes": 1304, + "out_bytes": 3905, + "subscriptions": 2, + "lang": "go", + "version": "1.49.0" + } + ] +} \ No newline at end of file diff --git a/functions/infra/testdata/nats_jsz.json b/functions/infra/testdata/nats_jsz.json new file mode 100644 index 00000000..c0aa0b24 --- /dev/null +++ b/functions/infra/testdata/nats_jsz.json @@ -0,0 +1,97 @@ +{ + "memory": 0, + "storage": 310, + "reserved_memory": 0, + "reserved_storage": 0, + "accounts": 1, + "ha_assets": 0, + "api": { + "level": 1, + "total": 6, + "errors": 0 + }, + "server_id": "NC23B47RQSJYPX5AIUC5CA3ND5RLCYREKSAFLM65MLBY5PBRIXPAFL7O", + "now": "2026-06-07T19:02:25.327216549Z", + "config": { + "max_memory": 3221225472, + "max_storage": 546399169536, + "store_dir": "/tmp/natsprobe4019469486/jetstream", + "sync_interval": 120000000000 + }, + "limits": {}, + "streams": 3, + "consumers": 0, + "messages": 6, + "bytes": 310, + "account_details": [ + { + "name": "$G", + "id": "$G", + "memory": 0, + "storage": 310, + "reserved_memory": 18446744073709551615, + "reserved_storage": 18446744073709551615, + "accounts": 0, + "ha_assets": 0, + "api": { + "level": 0, + "total": 6, + "errors": 0 + }, + "stream_detail": [ + { + "name": "KV_UNIBUS_rooms", + "created": "2026-06-07T19:02:24.8170934Z", + "cluster": { + "leader": "probe" + }, + "state": { + "messages": 2, + "bytes": 102, + "first_seq": 1, + "first_ts": "2026-06-07T19:02:24.817910599Z", + "last_seq": 2, + "last_ts": "2026-06-07T19:02:24.818011867Z", + "num_subjects": 2, + "consumer_count": 0 + } + }, + { + "name": "KV_UNIBUS_members", + "created": "2026-06-07T19:02:24.818494147Z", + "cluster": { + "leader": "probe" + }, + "state": { + "messages": 2, + "bytes": 106, + "first_seq": 1, + "first_ts": "2026-06-07T19:02:24.81917932Z", + "last_seq": 2, + "last_ts": "2026-06-07T19:02:24.819283444Z", + "num_subjects": 2, + "consumer_count": 0 + } + }, + { + "name": "KV_UNIBUS_users", + "created": "2026-06-07T19:02:24.814500069Z", + "cluster": { + "leader": "probe" + }, + "state": { + "messages": 2, + "bytes": 102, + "first_seq": 1, + "first_ts": "2026-06-07T19:02:24.81638123Z", + "last_seq": 2, + "last_ts": "2026-06-07T19:02:24.816570377Z", + "num_subjects": 2, + "consumer_count": 0 + } + } + ] + } + ], + "total": 1 +} \ No newline at end of file diff --git a/functions/infra/testdata/nats_varz.json b/functions/infra/testdata/nats_varz.json new file mode 100644 index 00000000..6a7605ec --- /dev/null +++ b/functions/infra/testdata/nats_varz.json @@ -0,0 +1,80 @@ +{ + "server_id": "NC23B47RQSJYPX5AIUC5CA3ND5RLCYREKSAFLM65MLBY5PBRIXPAFL7O", + "server_name": "probe", + "version": "2.11.15", + "proto": 1, + "go": "go1.26.4", + "host": "127.0.0.1", + "port": 14260, + "max_connections": 65536, + "ping_interval": 120000000000, + "ping_max": 2, + "http_host": "127.0.0.1", + "http_port": 8222, + "http_base_path": "", + "https_port": 0, + "auth_timeout": 2, + "max_control_line": 4096, + "max_payload": 1048576, + "max_pending": 67108864, + "cluster": {}, + "gateway": {}, + "leaf": {}, + "mqtt": {}, + "websocket": {}, + "jetstream": { + "config": { + "max_memory": 3221225472, + "max_storage": 546399169536, + "store_dir": "/tmp/natsprobe4019469486/jetstream", + "sync_interval": 120000000000 + }, + "stats": { + "memory": 0, + "storage": 310, + "reserved_memory": 0, + "reserved_storage": 0, + "accounts": 1, + "ha_assets": 0, + "api": { + "level": 1, + "total": 6, + "errors": 0 + } + }, + "limits": {} + }, + "tls_timeout": 2, + "write_deadline": 10000000000, + "start": "2026-06-07T19:02:24.785745698Z", + "now": "2026-06-07T19:02:25.325501038Z", + "uptime": "0s", + "mem": 18288640, + "cores": 24, + "gomaxprocs": 24, + "gomemlimit": 4294967296, + "cpu": 0, + "connections": 1, + "total_connections": 1, + "routes": 0, + "remotes": 0, + "leafnodes": 0, + "in_msgs": 17, + "out_msgs": 17, + "in_bytes": 1304, + "out_bytes": 3905, + "slow_consumers": 0, + "subscriptions": 75, + "http_req_stats": { + "/varz": 1 + }, + "config_load_time": "2026-06-07T19:02:24.785745698Z", + "config_digest": "", + "system_account": "$SYS", + "slow_consumer_stats": { + "clients": 0, + "routes": 0, + "gateways": 0, + "leafs": 0 + } +} \ No newline at end of file