- reports/0001-2026-06-07-unibus-grafana-monitoring.md - reports/0008-2026-06-07-unibus-admin-users-wired.md - reports/0008-2026-06-07-unibus-decentralization-audit.md - reports/0009-2026-06-07-unibus-cluster-hardening.md - reports/0010-2026-06-07-unibus-android-native.md - reports/0011-2026-06-07-unibus-cluster-deploy.md - reports/0012-2026-06-07-unibus-deploy-gaps-closed.md - reports/0013-2026-06-07-unibus-admin-panel.md - reports/0014-2026-06-07-unibus-users-http-admin-api.md - reports/0015-2026-06-07-unibus-web-wired.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Report 0016 — unibus: métricas profundas de NATS/JetStream (limpio, sin debug-log)
- Fecha: 07/06/2026
- Autor: Claude (agente NATS metrics)
- Ámbito:
projects/message_bus/apps/unibus(flag),functions/infra(parser del registry),projects/fleet_monitoring/apps/unibus_exporter(scraper),projects/fleet_monitoring/hub(dashboard + deploy) - Estado: done (código + dashboard listos y probados; rollout al cluster pendiente, por diseño)
Resumen
Se añadieron las métricas server-level de NATS/JetStream (msgs/s, conexiones, slow consumers,
memoria, KV bucket msgs, RAFT leader por stream, reinicios) a la monitorización del cluster
unibus, de forma limpia: desacoplando el endpoint de monitoring del nats-server (puerto 8222)
del debug-log verboso al que estaba acoplado.
El problema original: el monitoring 8222 solo se abría con UNIBUS_NATS_DEBUG=1, que enciende el
log verboso del nats-server (rutas/RAFT/subjects de rooms a journald en claro), incompatible con el
endurecimiento del issue 0007. La causa era únicamente cómo estaba escrito el toggle: monitoring y
debug-log estaban acoplados sin necesidad.
La solución es un flag dedicado UNIBUS_NATS_MONITOR=1 que abre el monitoring loopback sin tocar
el nivel de log, un scraper local por nodo (modo nuevo del unibus_exporter) que lee /varz,
/connz, /jsz por loopback y empuja a VictoriaMetrics reusando las funciones del grupo
fleet-metrics, y un dashboard Grafana unibus-nats. No se reinició el cluster (eso es el
rollout consolidado del binario 0.11.0).
Cambios
Parte 1 — flag desacoplado en unibus (rama quick/nats-monitor-flag, pusheada)
Repo: dataforge/unibus. Commit 1c93251.
| Archivo | Cambio |
|---|---|
pkg/embeddednats/embeddednats.go |
Función pura nueva natsLogOpts(debugEnv, monitorEnv) (noLog, debug, trace, monitor); StartServer la usa. El monitoring se abre con monitor (no con debug). |
pkg/embeddednats/monitor_test.go |
Tests del desacoplamiento (tabla pura) + server real con MONITOR=1 (8222 loopback) + server sin flag (cerrado). |
deploy/cluster/membershipd-cluster.service.d/nats-monitor.conf |
Drop-in systemd aditivo (Environment=UNIBUS_NATS_MONITOR=1). |
deploy/cluster/README.md |
Sección "NATS server metrics" con runbook de activación rolling + gate R3. |
app.md |
Bump 0.10.0 → 0.11.0 + growth log. |
Diff esencial (lógica):
// natsLogOpts: pura, testeable. MONITOR=1 abre el endpoint dejando el log en silencio.
func natsLogOpts(debugEnv, monitorEnv string) (noLog, debug, trace, monitor bool) {
debug = debugEnv == "1" || debugEnv == "2"
trace = debugEnv == "2"
monitor = monitorEnv == "1" || debug // debug sigue implicando monitor (compat)
noLog = !debug
return noLog, debug, trace, monitor
}
// ...
opts.NoLog, opts.Debug, opts.Trace = noLog, debugNATS, traceNATS
if monitorNATS {
opts.HTTPHost = "127.0.0.1" // loopback hardcoded — el monitoring NUNCA es público
opts.HTTPPort = 8222
}
El bind 127.0.0.1 se mantiene hardcoded: el endpoint no tiene auth y nunca debe ser alcanzable
por red. El || debugNATS preserva el comportamiento viejo (DEBUG sigue abriendo el monitoring).
Función nueva del registry — parse_nats_monitor_go_infra
Creada vía fn-constructor en el repo padre (functions/infra/). Análoga a parse_unibus_health,
mismo paquete infra, reusa el tipo PromSample.
func ParseNatsMonitor(node string, varz, connz, jsz []byte) ([]PromSample, error)
Convierte /varz + /connz + /jsz?streams=1 en []PromSample con labels node/instance.
Robusta: /varz es el core (error si no parsea); /connz y /jsz best-effort. Series: ver tabla
del dashboard abajo. Tag de grupo fleet-metrics (+ nats, jetstream, monitoring).
Parte 2a — modo NATS-local en unibus_exporter (rama quick/nats-deep-metrics, commit local)
Repo: dataforge/unibus_exporter (sub-repo sin remote aún). Commit dcdae92.
Segundo modo de scrape, componible con el modo healthz existente:
| Archivo | Cambio |
|---|---|
config.go |
Bloque nats_monitor {enabled, node, base_url} + overrides UNIBUS_NODE/UNIBUS_NATS_URL; validación relajada (healthz, NATS o ambos). |
main.go |
scrapeNATS() lee loopback 8222 (/varz,/connz,/jsz?streams=1), llama ParseNatsMonitor, reusa FormatPromExposition + PushPromRemote. getJSON genérico. |
nats_mode_test.go |
scrapeNATS contra fixtures reales + varz caído + validación de config. |
testdata/nats_{varz,connz,jsz}.json |
Fixtures reales capturados de un nats-server 2.11.15. |
systemd/unibus-exporter-nats.service |
Unit del modo NATS-local (por nodo). |
unibus.nats.example.json |
Config de ejemplo nats-only. |
app.md |
Bump 0.1.0 → 0.2.0 + tabla de métricas NATS + growth log. |
El modo healthz no se tocó. Como el monitoring de NATS es loopback-only y sin auth, este modo corre en cada nodo (un exporter local por nodo), distinto del exporter healthz central de magnus.
Parte 2b — dashboard + deploy (rama quick/nats-deep-metrics, pusheada)
Repo: dataforge/fleet_monitoring. Commit dfd55dc.
| Archivo | Cambio |
|---|---|
hub/dashboards/unibus-nats.json |
Dashboard unibus-nats (uid), datasource victoriametrics, 12 paneles. Lo recoge el provider fleet existente (escanea el path de dashboards). |
hub/deploy_unibus_nats_exporter.sh |
Deploy del exporter en modo NATS-local por nodo. magnus → VM local sin auth; homer/datardos → ingesta pública con basic auth (pass fleet/ingest-pass, nunca en argv). Instala unibus-exporter-nats.service y sondea 8222 para avisar si el monitoring no está abierto. NO reinicia membershipd. |
Verificación (evidencia ejecutable)
P1 — flag (unibus, CGO_ENABLED=0)
$ go build ./... → BUILD_EXIT=0
$ go test ./pkg/embeddednats/ -run 'TestNatsLogOpts|TestMonitor' -v
--- PASS: TestNatsLogOptsDecoupled (7 subcases: default off, monitor-only ⇒ log quiet,
debug⇒monitor, trace⇒debug+monitor, both, garbage ignorado ×2)
--- PASS: TestMonitorEndpointLoopback (server real MONITOR=1: /varz 200 en 127.0.0.1:8222)
--- PASS: TestMonitorDisabledByDefault (sin flag: MonitorAddr nil, endpoint cerrado)
$ go test ./pkg/embeddednats/ → ok 5.953s (suite completa, cluster_test incluido)
Función del registry (CGO_ENABLED=1 -tags fts5)
$ go test ./functions/infra/ -run 'ParseNatsMonitor|Battery' → ok 0.004s
Golden verifica: nats_msgs_in_total=17, nats_msgs_out_total=17, nats_mem_bytes=18288640,
nats_jetstream_streams=3, kv_bucket_msgs{UNIBUS_users|rooms|members}=2,
nats_jetstream_raft_leader{KV_UNIBUS_users}=1.
$ ./fn index → Indexed 1451 functions (sin error en la función)
$ ./fn show parse_nats_monitor_go_infra → indexada OK
P2a — exporter (CGO_ENABLED=0)
$ go build ./... → BUILD_OK
$ go vet ./... → VET_OK
$ go test ./... → ok 0.005s
TestScrapeNATSFromFixtures, TestScrapeNATSVarzDown, TestConfigNatsModeValidation
E2E real — scraper contra un nats-server embebido vivo
Se levantó un nats-server embebido con el binario 0.11.0 real (UNIBUS_NATS_MONITOR=1, 8222
abierto, 3 KV buckets creados), se corrió el exporter en modo NATS-local apuntando a 127.0.0.1:8222
y empujando a un VictoriaMetrics simulado (receptor HTTP local). El exporter empujó 29 series
reales, recibidas y verificadas:
unibus_exporter starting: nodes=0 nats="http://127.0.0.1:8222 (node=probe)" ...
pushed 29 samples (nodes=0 nats=true)
# series recibidas en el VM simulado (extracto):
nats_msgs_in_total{instance="probe",node="probe"} 17
nats_msgs_out_total{instance="probe",node="probe"} 17
nats_connections{instance="probe",node="probe"} 1
nats_mem_bytes{instance="probe",node="probe"} 1.855488e+07
nats_slow_consumers{...} 0
kv_bucket_msgs{bucket="UNIBUS_users",...} 2
kv_bucket_msgs{bucket="UNIBUS_rooms",...} 2
kv_bucket_msgs{bucket="UNIBUS_members",...} 2
nats_jetstream_raft_leader{stream="KV_UNIBUS_users",...} 1
nats_jetstream_streams{...} 3
nats_server_start_seconds{...} 1.78085962e+09
nats_up{...} 1 nats_scrape_error{...} 0
Esto valida toda la cadena: nats real → scrape loopback 8222 → ParseNatsMonitor → formato
Prometheus → push a VictoriaMetrics. El harness y los temporales se limpiaron tras la prueba.
Dashboard
$ python3 -c "json.load(...)" → OK uid=unibus-nats title='unibus — NATS server' panels=12
Paneles: NATS up · conexiones · msgs/s in · slow consumers · JetStream msgs · reinicios (1h); msgs/s por nodo (in/out) · conexiones por nodo · KV bucket msgs por bucket · memoria por nodo; tabla "Leader RAFT por stream" · tabla "JetStream por nodo".
Runbook de activación (consolidada, HUMANO/orquestador — tras el rollout 0.11.0)
El monitoring requiere reiniciar membershipd (reinicia el miembro RAFT del nodo). Se hace dentro
del rollout consolidado del binario 0.11.0, nunca dos nodos abajo a la vez.
- Dependencia previa (registry): commitear la función nueva al registry y push directo a master
(es aditivo, exento de TBD):
functions/infra/parse_nats_monitor.{go,md,_test.go}+functions/infra/testdata/nats_*.json. Sin esto el exporter no compila en otro entorno. - Rollout del binario 0.11.0 a los 3 nodos en orden magnus → homer → datardos, instalando
en cada uno el drop-in
nats-monitor.conf+daemon-reload+restart membershipd-cluster. Entre cada nodo, esperar reconvergencia R3 (followers_current=2/2en los 6 KV buckets) yhealthzverde en el nodo reiniciado antes de pasar al siguiente (comandos endeploy/cluster/README.md§ "NATS server metrics"). - Desplegar el scraper por nodo (una vez que su 8222 está abierto):
./hub/deploy_unibus_nats_exporter.sh magnus om(yhomer homer,datardos dd). - Provisionar el dashboard: copiar
hub/dashboards/unibus-nats.jsona/var/lib/grafana/dashboardsen magnus (mismo mecanismo queunibus-cluster.json; el providerfleetlo recoge). - Verificar: series
nats_*/kv_bucket_msgsen VictoriaMetrics y el dashboardunibus-natsen Grafana.
Gaps / pendientes
- Función del registry sin commitear (dependencia de integración):
parse_nats_monitorestá creada, indexada y testeada en el working tree del padrefn_registry, pero NO se commiteó (aislamiento: el prompt prohíbe tocar el padre; el orquestador la integra). Es el paso 1 del runbook. El exporter la importa, así que debe ir al registry antes de integrar el exporter. unibus_exportersin remote Gitea: el sub-repo aún no está sincronizado adataforge/. El commitdcdae92quedó local en la ramaquick/nats-deep-metrics; el push se hará vía/full-git-push(que crea el repo) o configurando el remote. Las otras dos ramas (quick/nats-monitor-flagen unibus,quick/nats-deep-metricsen fleet) sí están pusheadas.- Validación contra el cluster vivo no realizada (por diseño): el scraper se validó e2e contra
un nats-server embebido local con el binario 0.11.0 real, no contra magnus/homer/datardos.
Activar
UNIBUS_NATS_MONITORen datardos habría exigido reiniciar un nodo RAFT de producción con otros agentes trabajando contra el cluster vivo — riesgo no justificado. Queda para el rollout. NRestartspor proceso/stream no lo expone NATS 8222: se emitenats_server_start_secondscomo proxy (un cambio de su valor = el nats-server reinició; el panel "Reinicios (1h)" usachanges()). El contador de reinicios del proceso systemd (NRestarts) vendría de otra fuente.- Nit de linter (cosmético):
parse_nats_monitor.gousaHasPrefix+TrimPrefixdonde el linter sugiereCutPrefix. No afecta corrección; opcional de pulir. - Cluster NO reiniciado: magnus/homer/datardos intactos (8222 cerrado, healthz OK). Correcto según el encargo.
Ramas
| Repo | Rama | Estado |
|---|---|---|
dataforge/unibus |
quick/nats-monitor-flag |
commit 1c93251, pusheada (NO merge) |
dataforge/fleet_monitoring |
quick/nats-deep-metrics |
commit dfd55dc, pusheada (NO merge) |
dataforge/unibus_exporter |
quick/nats-deep-metrics |
commit dcdae92, local (sub-repo sin remote) |
fn_registry (padre) |
working tree | función parse_nats_monitor sin commitear (paso 1 del runbook) |