Un Grafana para toda mi infraestructura: cómo lo monté
ContenidoContents
Hace unos días conté cómo le puse OpenTelemetry a mi Moodle y mandé las trazas a un Jaeger en casa. Al final de aquel post dejé caer que el siguiente paso natural era Grafana: pasar del árbol (una traza concreta) al bosque (las tendencias). Pues bien, lo monté. Y se me fue de las manos en el buen sentido: acabó siendo el panel único de toda mi infraestructura, no solo de Moodle.
Este post no es un paso a paso para copiar y pegar. Es el mapa: la arquitectura, el patrón que lo une todo, y los sustos de cada capa —que es lo que de verdad enseña, porque cada pieza me guardaba una sorpresa—. Si te autoalojas cosas, creo que te va a servir. Y si quieres más detalle de cualquier parte, estaré encantado de contártelo: escríbeme sin problema.
La idea: un patrón, repetido cuatro veces
Toda la observabilidad self-hosted moderna gira alrededor de una arquitectura tozudamente simple:
[algo que produce datos] → [Prometheus los almacena] → [Grafana los pinta]
En el medio de la izquierda va un exporter: un proceso que expone métricas en un formato de texto plano que Prometheus entiende. Hay exporters oficiales para casi todo (el host, bases de datos, colas…), y cuando no lo hay, te lo escribes tú en veinte líneas —esto último resultó ser la clave de medio proyecto, ya lo verás—.
Yo ya tenía Prometheus y Grafana corriendo del post anterior (alimentando las métricas de Moodle). Así que “meter toda la infra” fue, en realidad, repetir ese patrón cuatro veces, una por cada cosa que quería ver:
- Las trazas de mi Moodle convertidas en métricas.
- El host y los contenedores: CPU, RAM, disco.
- Uptime y certificados de todos mis sitios.
- El tráfico real que ve Cloudflare.
Y encima de todo, alertas a Telegram. Vamos capa por capa.
Capa 1 — De trazas a métricas (y el susto de los spans)
Del post anterior me llegaban las trazas de Moodle a un OpenTelemetry Collector. El Collector tiene un truco precioso: un connector llamado spanmetrics que, sin que la aplicación emita una sola métrica, fabrica métricas RED (peticiones, errores, latencia) a partir de las propias trazas.
1connectors:
2 spanmetrics:
3 histogram:
4 explicit:
5 buckets: [5ms, 25ms, 100ms, 250ms, 1s, 5s, 10s]
El Collector manda las trazas a Jaeger y las métricas derivadas a Prometheus. Dashboard de latencia p95/p99 gratis. Precioso… hasta que miré el p99.
El susto: el p99 estaba clavado en 10 segundos, en rojo. Pánico. ¿Mis páginas tardan diez segundos? No. Dos cosas: 10 s era el techo de mi histograma, y ahí arriba estaban las tareas de cron de Moodle (que corren en segundo plano y sí tardan segundos), mezcladas con las peticiones de usuario. Intenté filtrar por tipo de span (SERVER) y… nada. Resulta que Moodle marca el span de una página como INTERNAL, no SERVER (reserva SERVER para las llamadas a webservices). El filtro bueno era por nombre:
1histogram_quantile(0.95,
2 sum by (le) (rate(duration_bucket{span_name=~"(GET|POST) .*"}[5m])))
Filtrando así, el p99 real de mis páginas bajó de 10 s a 99 ms. El cron, a su propio panel. Lección: una métrica sin contexto miente; hasta que no separas “lo que sufre un usuario” de “el trabajo de fondo”, el número te engaña.
Capa 2 — El host y los contenedores (dos trampas de macOS)
Para las métricas de la máquina, el estándar es node_exporter. Y aquí, la primera trampa de correr esto en un Mac: node_exporter en un contenedor solo ve la máquina virtual de Docker, no el macOS de verdad. Así que lo instalé nativo, con Homebrew, para que reporte la CPU, la RAM y el disco reales del Mac mini. Prometheus lo alcanza por host.docker.internal:9100.
Y la segunda trampa: quería ver cuánto consume cada contenedor (Jitsi vs Jaeger vs Grafana…). El estándar es cAdvisor, pero sobre OrbStack cAdvisor no expone el nombre del contenedor, solo un id de cgroup ilegible. Callejón sin salida con la herramienta “correcta”.
Aquí es donde entra el patrón que me salvó medio proyecto: el textfile collector de node_exporter. Le das un directorio, y node_exporter publica como métricas cualquier fichero de texto que dejes ahí. O sea: si puedo generar el dato con un script, ya es una fuente de Prometheus, sin escribir un exporter de verdad.
1# Un script que corre cada 30 s y escribe métricas por contenedor:
2docker stats --no-stream --format '{{.Name}} {{.CPUPerc}} {{.MemUsage}}' \
3 | ... convertir a: docker_container_mem_bytes{name="jaeger"} 275251200
node_exporter lo recoge, Prometheus lo scrapea, y de repente tengo CPU y memoria de cada contenedor con su nombre. Cuando la herramienta canónica no encaja, el textfile collector es tu comodín.
Capa 3 — ¿Está todo arriba? (uptime + certificados)
Para saber si mis sitios responden desde fuera, blackbox_exporter: sondea una URL y expone si respondió, cuánto tardó y cuándo caduca su certificado TLS. Prometheus le pasa la lista de sitios y él los prueba uno a uno, saliendo a internet y volviendo por el túnel —así que es un chequeo de extremo a extremo del camino real—.
El susto: dos sitios salían “caídos” que no lo estaban. Uno redirige a una página protegida que devuelve 401; otro devuelve 302. El módulo por defecto de blackbox solo acepta 2xx como “vivo”. Pero un 401 significa que el servidor respondió (está arriba, solo protegido). La solución fue un módulo más permisivo:
1http_uptime:
2 http:
3 valid_status_codes: [200, 301, 302, 303, 307, 308, 401, 403]
4 follow_redirects: false
Lección de andar por casa: “vivo” y “sano” no son lo mismo. Para un uptime honesto quieres “¿respondió algo?”, no “¿respondió un 200?”.
Capa 4 — El tráfico real (y un dataset deprecado)
Todo lo anterior mide mi origen: lo que llega al Mac mini. Pero delante tengo Cloudflare, que sirve mucho desde su caché y ve todo el tráfico —bots incluidos— que mi servidor nunca llega a ver. Esa foto la da la API de analítica de Cloudflare.
Probé dos exporters de Cloudflare de la comunidad. Los dos fallaban con un error de permisos… que me hizo perder un rato culpando a mi token. El token estaba perfecto. El problema: los exporters consultan httpRequests1mGroups, un dataset que Cloudflare ha deprecado y que el plan Free ya no sirve. Lo confirmé preguntándole a la API a mano hasta que di con el dataset actual, httpRequestsAdaptiveGroups, que sí funciona.
Así que volví al comodín: un script de veinte líneas que consulta el dataset bueno por GraphQL y escribe el resultado en el textfile collector.
1{ viewer { zones(filter: {zoneTag: "..."}) {
2 httpRequestsAdaptiveGroups(limit: 5000, filter: {datetime_geq: "..."}) {
3 count sum { edgeResponseBytes } dimensions { cacheStatus edgeResponseStatus }
4} } } }
Y con eso, un dashboard de tráfico real por dominio: peticiones, ratio de caché, ancho de banda y códigos de estado. Sin un contenedor nuevo. Lección: antes de pelearte con la herramienta ajena, comprueba si tú puedes hablar directamente con la fuente. Muchas veces son veinte líneas.
La guinda — Alertas a Telegram (sin duplicar)
Gráficas bonitas está bien, pero lo que convierte esto en útil son las alertas. Grafana alerta de forma nativa: defines reglas (disco > 90 %, un sitio caído, un certificado a menos de 14 días, latencia disparada) y las enrutas a un contacto. Como ya tenía un bot de Telegram avisándome de cosas del servidor, reutilicé ese mismo bot: cero credenciales nuevas.
Y aquí un detalle que casi se me pasa: mis scripts antiguos de monitorización ya avisaban de algunas de esas cosas (disco lleno, web caída). Si no hacía nada, tendría el mismo aviso por duplicado. La solución no fue apagar los scripts —hacen más cosas útiles— sino desviar quirúrgicamente solo las alertas que Grafana ahora cubre, dejando el resto intacto. Que cada aviso llegue una vez, desde el sitio que más contexto da.
El punto ciego que me queda
Escribiendo esto me di cuenta de una grieta incómoda: Grafana, Prometheus, los scripts de monitorización y el propio bot de Telegram que dispara las alertas corren todos en el Mac mini. Es decir, en la misma máquina que vigilan. Y ahí hay un principio que conviene tener grabado: un sistema no puede avisar de forma fiable de su propia muerte. Si el Mac mini cae del todo —un corte de luz, la red de casa, un kernel panic, el túnel— no me llega ninguna alerta, porque quien tendría que avisarme se ha caído con él.
La tentación es decir “pues muevo todo esto a otra máquina”. Pero creo que sería pasarse de frenada. La mayor parte de lo que mido —métricas del host, de cada contenedor, trazas de Moodle, tráfico de origen— solo tiene sentido corriendo junto a lo que observa; sacarlo fuera significaría abrir puertos, exponer métricas por la red y mantener un segundo servidor 24/7 para observar cosas que ya observo bien en local. El 95 % del valor (rendimiento, tendencias, degradaciones) se cubre perfectamente desde dentro.
Lo único que de verdad pide un observador externo e independiente es la pregunta binaria: ¿está vivo el Mac mini entero?. Y para eso no hace falta replicar el stack, basta un vigía pequeño fuera de casa. Las dos formas que estoy sopesando: un dead man’s switch (el Mac mini manda un latido cada pocos minutos a un servicio externo, y es ese servicio quien avisa si el latido deja de llegar) o un chequeo de uptime desde fuera que sondee el sitio cada pocos minutos. Cloudflare ya me sirve una copia archivada si el origen cae, pero no me avisa; ese aviso es justo el agujero que queda.
Todavía no he decidido cuál montar —ni siquiera si de momento me compensa—. Pero tenía que dejarlo escrito, porque es la lección más honesta de todo esto: por muy bonito que quede el panel, sigue mirándose a sí mismo desde dentro. La última milla de la observabilidad, la que confirma que la máquina no ha muerto, tiene que vivir en otro sitio.
Lo que me llevo
Tres cosas, después de haber tocado las cuatro capas.
La primera: el patrón es siempre el mismo —exporter → Prometheus → Grafana— y en cuanto lo interiorizas, “añadir una fuente nueva” deja de dar pereza. La segunda, y la más práctica: el textfile collector es un comodín infravalorado. La mitad de este montaje (contenedores por nombre, tráfico de Cloudflare) no salió con la herramienta “oficial”, sino con un script de veinte líneas volcado a un fichero de texto. Si puedes generar el dato, ya es observable.
Y la tercera, la de siempre cuando uno se autoaloja: cada capa te guarda un susto. El p99 que mentía, cAdvisor que no da nombres, el 401 que parece caída, el dataset deprecado. Ninguno estaba en la documentación feliz. Pero cada susto, una vez entendido, es justo lo que separa “seguí un tutorial” de “sé lo que estoy haciendo”.
Ahora tengo un solo sitio —con su login— donde veo Moodle, el host, cada contenedor, si mis webs están arriba, cuánto tráfico real recibo y cuándo caduca un certificado, y donde me salta un aviso a Telegram si algo se tuerce. Justo el “bosque” que echaba de menos. Del árbol al bosque, sí, pero pasando por unos cuantos sustos por el camino.
Actualización (11 jul) — el depósito sin desagüe
Un día después de publicar esto, mirando el propio Grafana, me fijé en algo: la memoria del contenedor de Jaeger solo subía, nunca bajaba. Y tenía toda la lógica: Jaeger all-in-one guarda las trazas en memoria por defecto, sin límite ni caducidad. Cada traza que entra se queda hasta que reinicias el contenedor. Un depósito con el grifo abierto y sin desagüe: tarde o temprano se lo come todo.
Lo arreglé cambiándolo a Badger, la base de datos embebida de Jaeger, que escribe en disco con un TTL de 72 horas. Ahora las trazas persisten entre reinicios —antes un docker restart las borraba todas— y caducan solas pasados tres días, así que ni la RAM ni el disco crecen sin fin.
La lección va muy en la línea del resto del post: los valores por defecto están pensados para probar, no para dejar corriendo semanas. all-in-one con almacenamiento en memoria es perfecto para un rato; para algo que vive de continuo, hay que ponerle un desagüe. Y fíjate en la ironía: fue el propio panel de Grafana el que me delató el problema. Justo para eso lo monté.