# Report 0001 — Monitorización de unibus en Grafana/VictoriaMetrics

- **Fecha:** 07/06/2026
- **Autor:** agente (Claude Opus 4.8)
- **Ámbito:** `projects/fleet_monitoring/` (exporter + dashboard + deploy) + 1 función nueva en el registry (`functions/infra/`). Solo lectura de `projects/message_bus/apps/unibus/` (endpoint `/healthz` + CA).
- **Estado:** done (métricas de `/healthz`); gap declarado en métricas profundas NATS/JetStream.

## Resumen

Se añadió monitorización del cluster de mensajería **unibus** (NATS+JetStream, 3 nodos: magnus,
homer, datardos) a la stack de Grafana/VictoriaMetrics que ya corre en magnus, **sin instrumentar
el bus**. Un nuevo daemon `unibus_exporter` sondea el endpoint público de salud de cada nodo
(`GET https://<nodo>:8470/healthz`, TLS verificado con la CA del cluster) en un bucle de 15 s,
traduce la respuesta a métricas Prometheus y las empuja a la VictoriaMetrics local de magnus.
Un dashboard dedicado las visualiza. No se tocó nada del código de unibus ni la stack existente
(Caddy/Gitea/VM/Grafana/fleet-agent intactos).

## Qué métricas se recolectan y cómo

El `/healthz` de cada nodo devuelve, verificado en producción:

```json
{"posture":{"enforce":true,"acl":true,"tls":true,"cluster":true,"store":"kv"},"status":"ok"}
```

De ahí se derivan estas series (labels `node` e `instance` = nombre del nodo, label común
`job=unibus_exporter`):

| Serie | Origen | Significado |
|---|---|---|
| `unibus_up` | exporter | 1 si el nodo respondió `/healthz`, 0 si el GET/parseo falló |
| `unibus_status_ok` | healthz `status` | 1 si `status=="ok"` |
| `unibus_posture_enforce` | healthz `posture.enforce` | enforcement de auth (1/0) |
| `unibus_posture_acl` | healthz `posture.acl` | ACL de subjects (1/0) |
| `unibus_posture_tls` | healthz `posture.tls` | TLS del transporte (1/0) |
| `unibus_posture_cluster` | healthz `posture.cluster` | modo cluster activo (1/0) |
| `unibus_store_kv` | healthz `posture.store` | 1 si el store es `kv` (JetStream KV) |
| `unibus_scrape_error` | exporter | 1 si el scrape de ese nodo falló |
| `unibus_scrape_duration_seconds` | exporter | latencia del GET `/healthz` |
| `unibus_cluster_size` | exporter (config) | nº de nodos configurados (los vivos = `sum(unibus_up)`) |

`unibus_up=0` lo emite el exporter (no el parser) cuando el GET falla, para que un nodo caído sea
**visible** en Grafana, no simplemente ausente.

## Componentes entregados

### Función del registry — `parse_unibus_health_go_infra`
- `functions/infra/parse_unibus_health.go` + `.md` + `_test.go` (grupo `fleet-metrics`, tags `unibus`).
- `func ParseUnibusHealth(node string, body []byte) ([]PromSample, error)` — pura de transformación
  (clasificada `impure` solo por el error de unmarshal). Tests golden/edge/error.
- **Nota de proceso:** el prompt pedía delegar esta función a `fn-constructor`, pero ese
  subagent_type no existe en este entorno. Tras confirmar con el usuario ("crear en el registry"),
  se creó a mano siguiendo el flujo (archivos + tests + `fn index`).

### App — `unibus_exporter` (sub-repo Gitea propio)
- `projects/fleet_monitoring/apps/unibus_exporter/`: `main.go`, `config.go`, `unibus.example.json`,
  `systemd/unibus-exporter.service`, `app.md`, `.gitignore`.
- Compone `parse_unibus_health` + `format_prom_exposition` + `push_prom_remote` del registry (no
  reescribe push ni formato). Config JSON; secretos (CA, basic-auth) fuera de argv. Verifica TLS
  siempre contra la CA del cluster (sin `InsecureSkipVerify`).
- `git init -b master` + commit inicial hecho (apps/* está gitignored en el project; sin sub-repo
  el código se perdería). Falta crear el repo Gitea remoto: lo hará `/full-git-push`.

### Dashboard — `unibus-cluster.json`
- `projects/fleet_monitoring/hub/dashboards/unibus-cluster.json` (formato de los `fleet-*.json`,
  datasource `victoriametrics`, carpeta **Fleet**, uid `unibus-cluster`, 9 paneles): nodos up,
  cluster size, nodos caídos, posture homogénea segura, up/down por nodo, matriz de posture por
  nodo (state-timeline enforce/acl/tls/cluster/store-kv × 3 nodos), latencia de scrape y tabla de
  estado por nodo. Panel "Meta-leader" preparado (muestra n/d sin métricas NATS).

### Deploy — `deploy_unibus_exporter.sh`
- `projects/fleet_monitoring/hub/deploy_unibus_exporter.sh`: compila el binario linux/amd64, sube
  binario + CA del cluster a magnus (`/opt/unibus-exporter`, `/etc/unibus-exporter/ca.crt` chmod
  600 la config) e instala el servicio systemd apuntando a `http://127.0.0.1:8428/...` (VM local,
  sin auth porque corre en el propio hub).

## Verificación (evidencia ejecutable)

**1. Acceso y healthz de los 3 nodos (CA del cluster por path):**
```
$ curl -s --cacert .../deploy/tls/ca.crt https://135.125.201.30:8470/healthz
{"posture":{"enforce":true,"acl":true,"tls":true,"cluster":true,"store":"kv"},"status":"ok"}
   (idéntico en homer 141.94.69.66 y datardos 51.91.100.142)
```

**2. Tests de la función:**
```
$ go test -tags fts5 -run ParseUnibusHealth ./functions/infra/
ok  	fn-registry/functions/infra	0.004s
$ ./fn index   # → "Indexed 1450 functions ..."; ./fn show parse_unibus_health_go_infra → OK
```

**3. Exporter build + scrape/push único de prueba (local → VM):**
```
$ ./unibus_exporter -config <test> -once
unibus_exporter starting: nodes=3 hub="https://metrics-…/api/v1/import/prometheus" interval=15s
pushed 28 samples for 3 nodes        # 1 cluster_size + 3 nodos × 9 series
```

**4. Daemon systemd en magnus:**
```
$ systemctl is-active unibus-exporter   → active
$ systemctl is-enabled unibus-exporter  → enabled
$ journalctl -u unibus-exporter         → "pushed 28 samples for 3 nodes"
```

**5. Series en VictoriaMetrics (magnus, 127.0.0.1:8428):**
```
sum(unibus_up)        = 3
unibus_cluster_size   = 3
count(unibus_up==1)   = 3
unibus_posture_enforce: magnus=1 homer=1 datardos=1  (job=unibus_exporter)
unibus_store_kv:        magnus=1 homer=1 datardos=1
unibus_scrape_duration_seconds: magnus≈4ms homer≈32ms datardos≈19ms
```

**6. Dashboard en Grafana (visto en el navegador):**
- `https://grafana-…/d/unibus-cluster` — carpeta Fleet, 9 paneles renderizando datos reales.
- Nodos up: **3** · Cluster size: **3** · Nodos caídos: **0** · Posture homogénea segura:
  **OK (enforce+acl+tls+cluster+kv)** · matriz de posture: 15 celdas en verde · tabla de estado
  por nodo con ✓ en up/enforce/acl/tls.
- Captura: `unibus-grafana-monitoring-dashboard.png` (junto a este report).
- API: `GET /api/dashboards/uid/unibus-cluster` → `dashboard CARGADO: unibus — Cluster | folder: Fleet | paneles: 9`.

## Gaps / pendientes

- **Métricas profundas NATS/JetStream (msgs/s, conexiones, KV bucket msgs, RAFT leader por
  stream, `NRestarts`) — NO incluidas, gap recomendado.** La vía es el monitoring embebido de
  NATS (puerto 8222), que se confirmó **cerrado** en los 3 nodos en producción
  (`curl 127.0.0.1:8222/varz` → CLOSED en magnus/homer/dd). Activarlo (bindeado a 127.0.0.1 +
  scrape local, o sacar `/jsz` por SSH) exige tocar la config/unit de los nodos del cluster, que
  además están siendo trabajados por otros agentes ahora mismo. Se decidió **no forzarlo** por
  riesgo en producción. Para abordarlo después: añadir `UNIBUS_NATS_DEBUG`/equivalente bindeado a
  loopback en cada nodo (cambio aditivo, coordinado con `unibus/deploy/cluster/`), un scrape local
  del `/jsz`+`/varz` y nuevas series `unibus_jsz_*`. El panel "Meta-leader" del dashboard ya está
  preparado para cuando exista `unibus_meta_leader`.
- **`unibus_cluster_size`** refleja el nº de nodos **configurados** en el exporter (3), no un
  recuento que el bus reporte (healthz no lo expone). Los nodos vivos se ven con `sum(unibus_up)`.
- **Commit en el repo padre `fn_registry` sin pushear (a propósito):** la función nueva quedó
  commiteada en local (`82f1f1bd`, master ahead 1) pero **no se hizo push** del padre, para
  respetar el aislamiento pedido. El humano debería revisarlo y pushearlo (junto con `fn index`
  para regenerar `registry.db`, que está gitignored).
- **Repo Gitea del exporter:** `apps/unibus_exporter` tiene su `git init` + commit local pero aún
  no tiene remoto en Gitea; `/full-git-push` lo creará (`dataforge/unibus_exporter`).
- **Vida útil (DoD capa 3):** validado funcionalmente hoy; falta la ventana de uso real ≥7 días.
