d43ffae3ae
- 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>
227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# 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):
|
||
|
||
```go
|
||
// 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**.
|
||
|
||
1. **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.
|
||
2. **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/2` en los 6 KV buckets) y
|
||
`healthz` verde en el nodo reiniciado antes de pasar al siguiente (comandos en
|
||
`deploy/cluster/README.md` § "NATS server metrics").
|
||
3. **Desplegar el scraper por nodo** (una vez que su 8222 está abierto):
|
||
`./hub/deploy_unibus_nats_exporter.sh magnus om` (y `homer homer`, `datardos dd`).
|
||
4. **Provisionar el dashboard:** copiar `hub/dashboards/unibus-nats.json` a
|
||
`/var/lib/grafana/dashboards` en magnus (mismo mecanismo que `unibus-cluster.json`; el provider
|
||
`fleet` lo recoge).
|
||
5. **Verificar:** series `nats_*`/`kv_bucket_msgs` en VictoriaMetrics y el dashboard `unibus-nats`
|
||
en Grafana.
|
||
|
||
## Gaps / pendientes
|
||
|
||
- **Función del registry sin commitear (dependencia de integración):** `parse_nats_monitor` está
|
||
creada, indexada y testeada en el working tree del padre `fn_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_exporter` sin remote Gitea:** el sub-repo aún no está sincronizado a `dataforge/`. El
|
||
commit `dcdae92` quedó **local** en la rama `quick/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-flag` en unibus, `quick/nats-deep-metrics` en 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_MONITOR` en 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.
|
||
- **`NRestarts` por proceso/stream no lo expone NATS 8222:** se emite `nats_server_start_seconds`
|
||
como proxy (un cambio de su valor = el nats-server reinició; el panel "Reinicios (1h)" usa
|
||
`changes()`). El contador de reinicios del proceso systemd (`NRestarts`) vendría de otra fuente.
|
||
- **Nit de linter (cosmético):** `parse_nats_monitor.go` usa `HasPrefix`+`TrimPrefix` donde el
|
||
linter sugiere `CutPrefix`. 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) |
|