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 }