# 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://: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 -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.