Imagina un Raspberry Pi que lleva meses encendido en el taller, junto a la CNC o la impresora 3D. Un día empieza a andar lento, y al siguiente se cuelga justo a mitad de un trabajo. Lo que nunca viste fue el deterioro: la temperatura que subía porque el disipador se llenó de polvo, la tarjeta que se iba llenando semana a semana, las caídas de voltaje de tres segundos cada vez que arrancaban los motores. Un Pi que funciona no te cuenta nada de sí mismo: responde o no responde.

Esta guía convierte todo eso en gráficos con historial y, lo más importante, en un aviso en tu celular antes de que el problema te detenga. Vas a montar Prometheus y Grafana en el propio Pi con tres contenedores de Docker, importar un dashboard listo y crear cuatro alertas que valen la pena. Al final vas a saber:

  • Qué hace cada pieza del stack y por qué el modelo de "ir a buscar" los datos simplifica todo.
  • El ajuste escondido de Raspberry Pi OS sin el cual la mitad de los gráficos queda vacía.
  • Cómo medir el subvoltaje del Pi, el fallo más típico y el que ningún dashboard estándar muestra.
  • Cómo leer bien la temperatura (el valor más alto casi nunca es la CPU).
  • Cómo recibir las alertas en el celular con ntfy, o en Telegram.

Es un montaje pequeño, no una solución empresarial: funciona con un solo Pi y crece sin drama al puñado de equipos que la mayoría tiene en casa o en el taller.

Concepto: qué vas a medir y con qué

Lo que ves cuando todo está andando

Abres una página y tienes las últimas dos semanas de un vistazo: uso y temperatura de la CPU, memoria libre, espacio en disco con su tendencia, tráfico de red y una fila por cada contenedor de Docker. El valor no está en el número de este momento (eso ya te lo da htop), está en la curva. Un Pi que hoy anda a 45 °C y en marzo andaba a 58 °C tiene un problema de refrigeración, y eso solo lo muestra un historial.

Encima de los gráficos van las alertas. Cuatro cubren casi todo lo que de verdad falla en un servidor casero: el equipo se calienta, el disco se llena, la fuente se queda corta o el equipo deja de responder.

Las cuatro piezas, cada una con una sola tarea

En internet los nombres se mezclan y eso hace que el tema parezca más complicado de lo que es:

Pieza Qué hace Dónde corre
Node Exporter Lee los valores del equipo (CPU, RAM, discos, red, temperaturas) y los publica como una página de texto simple En cada equipo que quieras vigilar
cAdvisor Lo mismo, pero para los contenedores de Docker: CPU, memoria y tráfico por contenedor En cada equipo con Docker
Prometheus Visita esas páginas cada pocos segundos y guarda los valores con fecha y hora (una base de datos de series de tiempo) Una vez, en un equipo
Grafana Le hace preguntas a Prometheus, dibuja las respuestas y maneja las alertas Una vez, en un equipo

La clave que conviene entender antes de sumar un segundo equipo: Prometheus va a buscar los datos, nadie se los envía. Por eso agregar un equipo es solo levantar un exporter ahí y sumar una línea a la configuración. Sin agentes que registrar ni claves que repartir.

Este modelo "pull" tiene otra ventaja que se nota en el taller: si un equipo muere, Prometheus lo sabe de inmediato porque la visita falla. En un sistema donde cada equipo "empuja" sus datos, un equipo muerto simplemente se queda callado, y el silencio es difícil de distinguir de "todo bien".

Hardware: qué necesitas

  • Raspberry Pi 4 o 5 con al menos 2 GB de RAM. Un Pi Zero 2 W con 512 MB no alcanza: el stack en reposo ocupa unos 360 MB, casi no le queda nada al sistema y un dashboard abierto lo pasa del límite. Con los dashboards abiertos el consumo sube a unos 670 MB, y la mayor parte se la lleva Grafana, no Prometheus.
  • Raspberry Pi OS de 64 bits con Docker.
  • Almacenamiento que aguante muchas escrituras pequeñas. Prometheus escribe todo el tiempo. Una SSD NVMe o una SSD por USB es el lugar correcto; con microSD también funciona, pero la tarjeta pasa a ser la pieza que se desgasta primero.
  • Una fuente como corresponde. Parece un detalle, pero es exactamente lo que vas a vigilar en el paso de subvoltaje.

Tiempo total: unos 45 minutos, diez de ellos en un reinicio y en esperar los primeros datos.

Si todavía no tienes Docker en el Pi, esta es la versión corta de la instalación:

Bash
curl -sSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Después cierra la sesión y vuelve a entrar para que el cambio de grupo tenga efecto.

Software, parte 1: el ajuste que casi todas las guías olvidan

La mayoría de las guías de monitoreo se escriben en un PC, por eso se saltan este paso. En Raspberry Pi OS el kernel trae desactivado el controlador de memoria de los cgroups. Docker funciona igual, pero nadie puede medir cuánta RAM usa cada contenedor.

Revísalo así:

Bash
cat /sys/fs/cgroup/cgroup.controllers

Si la lista dice cpuset cpu io pids y no aparece memory, entonces docker stats muestra 0B en todos los contenedores, cAdvisor no entrega métricas container_memory_* y cualquier dashboard de Docker que importes va a tener los gráficos de memoria vacíos. Lo típico es culpar a cAdvisor y perder una tarde buscando en el lugar equivocado, cuando solo falta un parámetro de arranque.

Respalda el archivo y ábrelo:

Bash
sudo cp /boot/firmware/cmdline.txt /boot/firmware/cmdline.txt.bak
sudo nano /boot/firmware/cmdline.txt

Agrega esto al final de la línea que ya existe, separado por un espacio:

Text
cgroup_enable=memory cgroup_memory=1

No crees una segunda línea. cmdline.txt es una sola línea sin salto al final, y el bootloader solo lee la primera. Un salto de línea ahí significa un Pi que no vuelve a arrancar; por eso el respaldo va antes. Si igual te pasa, saca la tarjeta o la SSD, abre la partición de arranque en otro computador y restaura el .bak.

Reinicia y revisa de nuevo:

Bash
sudo reboot
# nach dem Neustart:
cat /sys/fs/cgroup/cgroup.controllers

Ahora debería aparecer memory. Desde aquí docker stats muestra valores reales, y Grafana también.

Software, parte 2: el stack con Docker Compose

Todo vive en una carpeta. Créala junto con la estructura de configuración:

Bash
mkdir -p ~/monitoring/prometheus
mkdir -p ~/monitoring/grafana/provisioning/datasources
mkdir -p ~/monitoring/grafana/provisioning/alerting
sudo mkdir -p /var/lib/node_exporter
sudo chown $USER:$USER /var/lib/node_exporter
cd ~/monitoring

El archivo compose.yaml completo:

YAML
services:
  prometheus:
    image: prom/prometheus:v3.14.0
    container_name: prometheus
    restart: unless-stopped
    user: "65534:65534"
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=15d
      - --storage.tsdb.retention.size=2GB
      - --web.enable-lifecycle
    volumes:
      - ./prometheus:/etc/prometheus:ro
      - prometheus-data:/prometheus
    extra_hosts:
      - "host.docker.internal:host-gateway"
    ports:
      - "9090:9090"

  node-exporter:
    image: prom/node-exporter:v1.12.1
    container_name: node-exporter
    restart: unless-stopped
    pid: host
    network_mode: host
    command:
      - --path.procfs=/host/proc
      - --path.sysfs=/host/sys
      - --path.rootfs=/host/root
      - --collector.textfile.directory=/var/lib/node_exporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/host/root:ro,rslave
      - /var/lib/node_exporter:/var/lib/node_exporter:ro

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.55.1
    container_name: cadvisor
    restart: unless-stopped
    privileged: true
    devices:
      - /dev/kmsg
    command:
      - --docker_only=true
      - --housekeeping_interval=30s
      - --store_container_labels=false
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
      - /dev/disk:/dev/disk:ro
    ports:
      - "8080:8080"

  grafana:
    image: grafana/grafana:13.2.2
    container_name: grafana
    restart: unless-stopped
    user: "472:472"
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: bitte-aendern
      GF_ANALYTICS_REPORTING_ENABLED: "false"
      GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

volumes:
  prometheus-data:
  grafana-data:

Antes de levantarlo, cambia la contraseña de Grafana: el valor bitte-aendern del ejemplo significa, literalmente, "por favor cámbiame" en alemán.

Hay dos decisiones en este archivo que no son obvias y que el autor original justifica con mediciones:

Por qué Node Exporter va en la red del host. Casi todas las guías lo ponen en la red bridge normal de Docker, y entonces los gráficos de red miden otra cosa. /proc/net depende del espacio de nombres de red, y --path.procfs=/host/proc no lo cambia: el exporter sigue leyendo la interfaz de su propio contenedor. En el Pi de prueba eso significaba que Node Exporter reportaba 1,2 MB recibidos en eth0 mientras la interfaz real, end0, llevaba 556 MB. Cada gráfico de tráfico mostraba al contenedor de monitoreo hablando solo. network_mode: host lo resuelve, y es lo que recomienda el propio proyecto Node Exporter. Por eso no hay puerto publicado (el exporter ocupa el 9100 directo en el Pi) y Prometheus lo alcanza vía host.docker.internal.

Gráficos de red en Grafana con las interfaces reales del Raspberry Pi, entre ellas end0 y wlan0

Por qué cAdvisor queda fijo en v0.55.1. El proyecto publica versiones más nuevas en GitHub, pero en el registro gcr.io/cadvisor/cadvisor la última imagen para arm64 es la v0.55.1. Si pides una etiqueta más nueva o :latest, obtienes no such manifest.

Ahora la configuración de Prometheus, prometheus/prometheus.yml:

YAML
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]

  - job_name: node
    static_configs:
      - targets: ["host.docker.internal:9100"]

  - job_name: cadvisor
    static_configs:
      - targets: ["cadvisor:8080"]

Resiste la tentación de ponerle una etiqueta instance propia al objetivo para que los gráficos queden con un nombre bonito. El dashboard que vas a importar filtra por instance con la dirección completa host.docker.internal:9100; si la reemplazas, todos los gráficos quedan vacíos y parece una instalación rota. Más abajo verás la forma correcta de nombrar equipos.

Y la fuente de datos de Grafana, grafana/provisioning/datasources/datasource.yml:

YAML
apiVersion: 1

datasources:
  - name: Prometheus
    uid: prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false

Fija el uid desde ya. Muchos ejemplos lo omiten y Grafana inventa uno, pero las alertas de más adelante apuntan a la fuente de datos por su uid. Si agregas esa línea después, sobre una fuente ya creada, Grafana deja de arrancar con Datasource provisioning error: data source not found, interfaz web incluida, y la única salida es borrar el volumen de Grafana.

Levántalo:

Bash
docker compose up -d
docker compose ps

Dale un minuto y abre http://<ip-del-pi>:9090/targets. Los tres jobs deben estar en UP. Si node o cadvisor están caídos, el mensaje de error en esa misma página dice por qué; casi siempre es un error de tipeo en el nombre del objetivo.

Página Target health de Prometheus con los objetivos cadvisor, node y prometheus en estado UP

Software, parte 3: el subvoltaje, lo que el Pi no le cuenta a nadie

Node Exporter es genérico: no sabe qué es vcgencmd ni conoce el subvoltaje. Y el throttling (bajar la frecuencia para protegerse) es el fallo más típico del Pi. Basta una fuente débil o un cable USB largo y delgado para que el firmware baje el reloj; el Pi se pone lento y ningún dashboard común te dice por qué.

Cómo funciona get_throttled por dentro

vcgencmd get_throttled devuelve un número en hexadecimal, por ejemplo throttled=0x50005. Cada bit es una bandera:

Bit Significado Tipo
0 Subvoltaje detectado ahora Estado actual
1 Frecuencia del ARM limitada ahora Estado actual
2 Throttling activo ahora Estado actual
3 Límite suave de temperatura activo ahora Estado actual
16 Hubo subvoltaje desde el arranque Memoria
17 Hubo limitación de frecuencia desde el arranque Memoria
18 Hubo throttling desde el arranque Memoria
19 Se alcanzó el límite suave de temperatura desde el arranque Memoria

En el ejemplo 0x50005, los bits 0 y 2 están activos (hay subvoltaje y throttling en este momento) y los bits 16 y 18 dicen que ya había pasado antes. El Pi marca subvoltaje cuando la alimentación baja de alrededor de 4,63 V; con una fuente de 5 V justa y un cable con resistencia, un pico de consumo (la CPU a fondo, una SSD arrancando, un periférico USB) basta para cruzar esa línea.

El script

Node Exporter puede leer métricas extra desde una carpeta, y para eso está la opción --collector.textfile.directory del compose.yaml. Crea ~/monitoring/rpi_throttled.sh:

Bash
#!/bin/bash
set -eu

OUT=/var/lib/node_exporter/rpi_throttled.prom
RAW=$(vcgencmd get_throttled)          # z. B. throttled=0x50005
VALUE=$((${RAW#throttled=}))

bit() { echo $(( (VALUE >> $1) & 1 )); }

{
  echo "# HELP rpi_throttled_flags Raw value of vcgencmd get_throttled."
  echo "# TYPE rpi_throttled_flags gauge"
  echo "rpi_throttled_flags $VALUE"
  echo "# HELP rpi_throttled_state Raspberry Pi throttling state, 1 = active."
  echo "# TYPE rpi_throttled_state gauge"
  echo "rpi_throttled_state{state=\"under_voltage\"} $(bit 0)"
  echo "rpi_throttled_state{state=\"frequency_capped\"} $(bit 1)"
  echo "rpi_throttled_state{state=\"throttled\"} $(bit 2)"
  echo "rpi_throttled_state{state=\"soft_temp_limit\"} $(bit 3)"
  echo "# HELP rpi_throttled_since_boot Raspberry Pi throttling events since boot, 1 = occurred."
  echo "# TYPE rpi_throttled_since_boot gauge"
  echo "rpi_throttled_since_boot{state=\"under_voltage\"} $(bit 16)"
  echo "rpi_throttled_since_boot{state=\"frequency_capped\"} $(bit 17)"
  echo "rpi_throttled_since_boot{state=\"throttled\"} $(bit 18)"
  echo "rpi_throttled_since_boot{state=\"soft_temp_limit\"} $(bit 19)"
} > "$OUT.tmp"

mv "$OUT.tmp" "$OUT"

Dale permisos, córrelo una vez para probarlo y agrégalo al cron:

Bash
chmod +x ~/monitoring/rpi_throttled.sh
~/monitoring/rpi_throttled.sh
cat /var/lib/node_exporter/rpi_throttled.prom
crontab -e

Al final del crontab agrega esta línea (si tu usuario no es pi, corrige la ruta):

Text
* * * * * /home/pi/monitoring/rpi_throttled.sh >/dev/null 2>&1

El script genera dos grupos de métricas y la diferencia importa. rpi_throttled_state es el estado en este instante. rpi_throttled_since_boot recuerda si pasó alguna vez desde el último reinicio, y es la que atrapa la caída de voltaje de tres segundos a las cuatro de la mañana, mucho después de que el valor instantáneo volvió a cero. Como el cron corre una vez por minuto, sin esa memoria te perderías casi todos los eventos cortos.

El mv del final tampoco es decorativo. Node Exporter puede leer la carpeta en cualquier momento, y si el script escribiera directo en el archivo final, a veces leería un archivo a medio escribir. Escribir primero con otro nombre y después renombrar hace el cambio atómico.

Tests: el dashboard y cómo leerlo sin engañarte

Importar el dashboard 1860

No tienes que diseñar nada. Node Exporter Full es el dashboard que usa prácticamente todo el mundo; en grafana.com tiene el ID 1860 y más de cien millones de descargas. Abre Grafana en http://<ip-del-pi>:3000, entra y ve a Dashboards → New → Import. Escribe 1860, cárgalo y elige Prometheus como fuente de datos.

Casi todo se llena de inmediato. Una parte no se va a llenar nunca, y es mejor saberlo antes para no buscar un error que no existe: de los 124 gráficos con consulta, 16 quedan vacíos en un Raspberry Pi, por dos razones distintas.

Primero, collectors apagados. Node Exporter trae de fábrica una selección moderada. Estos gráficos funcionan en cuanto activas el collector correspondiente en la sección command de node-exporter en el compose.yaml:

YAML
      - --collector.systemd
      - --collector.processes
      - --collector.interrupts
      - --collector.tcpstat

Con eso aparecen los gráficos de systemd, los contadores de procesos e hilos, las interrupciones y los tres gráficos de estados de conexiones TCP. Ojo: cada collector cuesta algo de CPU en cada lectura y suma series a la base de datos, sobre todo systemd en un equipo con muchas unidades. Activa lo que de verdad vas a mirar.

Segundo, datos que el kernel del Pi no entrega. Aquí no hay collector que ayude:

Gráfico Por qué queda vacío
Pressure / Pressure Stall Information Raspberry Pi OS no compila PSI; no existe /proc/pressure
Memory DirectMap, Memory HugePages Esas líneas no aparecen en /proc/meminfo en arm64
Power Supply El Pi no tiene batería; /sys/class/power_supply/ está vacío

Nada de eso está roto: es la diferencia entre un dashboard pensado para servidores y el equipo que tienes sobre el escritorio.

La trampa de la temperatura de CPU

Es el valor que todos buscan primero y el más fácil de leer mal, porque el Pi no reporta una temperatura sino seis. Si en Explore consultas node_hwmon_temp_celsius, recibes tres chips distintos:

Chip Nombre Qué es en realidad
thermal_thermal_zone0 cpu_thermal El SoC. Esta es la temperatura de CPU que buscas
1000120000_pcie_1f000c8000_adc rp1_adc Un sensor del chip de entradas y salidas RP1. En reposo anda más caliente que la CPU
nvme_nvme0 nvme Tres sensores de la SSD NVMe, si tienes una

El gráfico Hardware Temperature Monitor del dashboard 1860 dibuja las seis curvas juntas, y la de más arriba normalmente no es la CPU: en el equipo de prueba, el sensor del RP1 marcaba 39 °C con la CPU a 31 °C. Si miras solo el valor más alto, vas a creer que tu Pi está siempre más caliente de lo que está.

Seis curvas de temperatura del Raspberry Pi en Grafana, con el sensor de la CPU abajo y el del ADC arriba

Para un gráfico propio que muestre solo la CPU, usa esta consulta:

Promql
node_hwmon_temp_celsius{chip="thermal_thermal_zone0",sensor="temp0"}

Compárala una vez con vcgencmd measure_temp en el propio Pi: deberían diferir en pocas décimas.

Un detalle más: si tu ventilador aparece en 0 RPM aunque esté girando, revisa dónde está conectado. La métrica node_hwmon_fan_rpm{chip="platform_cooling_fan"} solo funciona con un ventilador enchufado al conector de cuatro pines del Pi 5. Las cajas con control propio de ventiladores (el equipo de prueba usa una Pironman 5, que los maneja por un GPIO) dejan esa métrica siempre en cero. Antes de comprar un ventilador de repuesto, mira el cable.

¿Cuánto se calienta un Pi de verdad?

Con las curvas funcionando, el autor original hizo una prueba de carga con stress-ng --cpu 4 --timeout 720s (doce minutos con los cuatro núcleos al máximo) y leyó el resultado desde las series de tiempo:

Momento Temperatura del SoC
Reposo antes de la prueba 27,1 °C
A los dos minutos 36,4 °C
A los seis minutos 43,6 °C
Meseta, minutos 9 a 12 45,8 °C
Máximo 46,9 °C
Dos minutos después de terminar 33,7 °C

vcgencmd get_throttled quedó en 0x0 todo el tiempo: sin throttling, sin subvoltaje, sin tocar siquiera el límite suave de temperatura.

Curva de la temperatura de la CPU del Raspberry Pi durante la prueba de carga, subiendo de 28 a 47 °C

Ese número necesita contexto: el Pi 5 de la prueba estaba en una caja Pironman 5 con heatpipe pasivo y dos ventiladores siempre encendidos, mucha más refrigeración que una placa desnuda o el cooler activo oficial. Lo que demuestra es que un Pi 5 bien refrigerado tiene unos 33 grados de margen antes de que el firmware baje la frecuencia. Lo que no demuestra es cómo se porta un Pi sin refrigeración con la misma carga. Para las alertas eso significa que un umbral de 70 °C está bien elegido aunque en un montaje así nunca se dispare: está ahí para el día en que falle un ventilador o el disipador se llene de polvo, que en un taller con viruta y aserrín llega antes de lo que crees.

Varios equipos: sumar un segundo Pi, un NAS o un mini PC

Como Prometheus va a buscar los datos, sumar un equipo es dejarle algo que buscar. En el segundo equipo (otro Pi, un mini PC, cualquier cosa con Linux) solo levantas el exporter:

Bash
docker run -d --name node-exporter --restart unless-stopped \
  --pid host --net host \
  -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro,rslave \
  prom/node-exporter:v1.12.1 \
  --path.procfs=/host/proc --path.sysfs=/host/sys --path.rootfs=/host/root

Después, en el Pi de monitoreo amplías el job node. Aquí va la forma correcta de nombrar equipos que quedó pendiente: no sobrescribes instance, agregas una etiqueta propia al lado:

YAML
- job_name: node
    static_configs:
      - targets: ["host.docker.internal:9100"]
        labels:
          device: monitoring-pi
      - targets: ["192.168.1.50:9100"]
        labels:
          device: nas
      - targets: ["192.168.1.51:9100"]
        labels:
          device: medien-pi

instance sigue teniendo la dirección, así que el dashboard 1860 funciona normal y muestra los tres equipos en su selector. La etiqueta device es tuya, para gráficos propios y para textos de alerta donde nas se lee mucho mejor que 192.168.1.50:9100.

Una consecuencia que conviene conocer: si después cambias la dirección de un objetivo, Prometheus lo trata como otra serie. El historial no se muda, y un dashboard filtrado por el instance viejo se corta justo en el momento del cambio, lo que parece que la recolección murió. No se perdió nada: elige el valor nuevo en el selector. Buena razón para dejar bien las direcciones (idealmente con IP fija reservada en el router) antes de acumular meses de datos.

Prometheus toma el cambio sin reiniciarse, gracias a --web.enable-lifecycle:

Bash
curl -X POST http://localhost:9090/-/reload

Luego revisa /targets. Recuerda que un exporter escuchando en 0.0.0.0:9100 es legible para cualquiera en la red; en una red en la que no confíes, amárralo a la interfaz por la que llega Prometheus o ponle una regla de firewall.

Alertas: cuatro que valen la pena

Grafana trae su propio sistema de alertas. Para un montaje de este tamaño no necesitas Alertmanager, y te ahorras un quinto contenedor con su consumo de memoria. Las reglas se pueden dejar en un archivo, que es más cómodo que hacer clic y además queda en tu respaldo.

Crea ~/monitoring/grafana/provisioning/alerting/alerts.yml. Esta es la primera regla completa:

YAML
apiVersion: 1

groups:
  - orgId: 1
    name: raspberry-pi
    folder: Raspberry Pi
    interval: 1m
    rules:
      - uid: rpi-cpu-temperature
        title: CPU-Temperatur zu hoch
        condition: C
        for: 5m
        noDataState: NoData
        execErrState: Error
        annotations:
          summary: Der SoC liegt seit fünf Minuten über 70 °C. Kühler und Luftstrom prüfen.
        labels:
          severity: warning
        isPaused: false
        data:
          - refId: A
            relativeTimeRange:
              from: 600
              to: 0
            datasourceUid: prometheus
            model:
              refId: A
              editorMode: code
              instant: true
              expr: node_hwmon_temp_celsius{chip="thermal_thermal_zone0",sensor="temp0"}
          - refId: B
            datasourceUid: __expr__
            model:
              refId: B
              type: reduce
              reducer: last
              expression: A
          - refId: C
            datasourceUid: __expr__
            model:
              refId: C
              type: threshold
              expression: B
              conditions:
                - evaluator:
                    type: gt
                    params: [70]

La regla tiene tres consultas encadenadas: A trae la temperatura, B se queda con el último valor y C compara contra 70. Las otras tres reglas se arman igual; solo cambian la consulta y el umbral:

Regla Qué vigila Se dispara tras
Temperatura de CPU alta node_hwmon_temp_celsius{chip="thermal_thermal_zone0",sensor="temp0"} sobre 70 5 min
Disco raíz llenándose Porcentaje libre de / bajo 15 % 10 min
Equipo vigilado caído up bajo 1 2 min
Subvoltaje detectado rpi_throttled_state{state="under_voltage"} sobre 0 1 min

Para el disco, una consulta que funciona es node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100, con el umbral en 15.

Los tiempos de espera son lo que casi todos configuran mal. Una CPU que toca 71 °C durante ocho segundos en un respaldo no es un problema; una que se queda ahí cinco minutos, sí. Sin la espera te despierta el ruido, dejas de leer los avisos y terminas peor que sin alertas. Al disco se le dan diez minutos porque el espacio se mueve lento, pero ojo: si Prometheus guarda su base en el mismo disco que vigila, una retención mal puesta puede llenarlo más rápido de lo que esperas (más abajo lo dimensionamos).

Después de editar el archivo recarga la configuración sin reiniciar Grafana:

Bash
curl -X POST -u admin:euer-passwort \
  http://localhost:3000/api/admin/provisioning/alerting/reload

Las reglas aparecen en Alerting → Alert rules, cada una en estado Normal. Una regla en Pending ya cruzó el umbral y está esperando su tiempo; Firing significa que avisó.

Reglas de alerta de Grafana para el Raspberry Pi: temperatura de CPU, disco raíz, equipo caído y subvoltaje

El aviso en el celular

Una alerta que solo enciende un dashboard que nadie está mirando es decoración. ntfy es la salida más simple: instalas la app, te suscribes a un topic que inventas tú y todo lo que se publique en ese topic llega como notificación. Sin cuenta y sin token de bot.

Al crear la suscripción en la app, deja apagado el interruptor de usar otro servidor; si lo activas sin tener un servidor propio, la app queda preguntando al vacío.

App ntfy al crear una suscripción, con el interruptor de otro servidor apagado

En Grafana, crea grafana/provisioning/alerting/contactpoints.yml con un contact point de tipo webhook que publica en ntfy.sh. La sección payload es la parte decisiva: arma el JSON que ntfy espera, con título, mensaje, etiquetas de emoji y prioridad distintos para una alerta disparada y para una resuelta:

YAML
apiVersion: 1

contactPoints:
  - orgId: 1
    name: ntfy
    receivers:
      - uid: ntfy-raspberry-pi
        type: webhook
        settings:
          url: https://ntfy.sh
          httpMethod: POST
          payload:
            template: |
              {"topic":"euer-topic-name-hier","title":"{{ if eq .Status "firing" }}{{ .CommonLabels.alertname }}{{ else }}Resolved: {{ .CommonLabels.alertname }}{{ end }}","message":"{{ range .Alerts }}{{ .Annotations.summary }} [{{ .Labels.instance }}] {{ end }}","tags":["{{ if eq .Status "firing" }}rotating_light{{ else }}white_check_mark{{ end }}"],"priority":{{ if eq .Status "firing" }}4{{ else }}3{{ end }}}
        disableResolveMessage: false

Cambia euer-topic-name-hier ("el nombre de tu topic aquí") por el topic que creaste en la app. Como cualquiera que adivine el nombre puede leerlo, usa algo largo y difícil de adivinar, no mi-raspberry. Este archivo solo define el destino; para que las alertas lleguen a él también necesitas una política de notificación que las enrute a ese contact point. El archivo completo, con esa política incluida, está en el repositorio del proyecto (enlace al final). También puedes asignar el contact point por defecto desde Alerting → Notification policies.

Notificación de ntfy en el celular con la alerta disparada y, debajo, el aviso de que se resolvió

Retención: que la base de datos no se coma el disco

En el compose.yaml, Prometheus tiene dos límites: --storage.tsdb.retention.time=15d y --storage.tsdb.retention.size=2GB. Se aplica el que se cumpla primero: borra lo más antiguo al pasar los 15 días o al llegar a 2 GB.

Para tener una idea del orden de magnitud: Prometheus comprime cada muestra a más o menos 1 o 2 bytes. Un Pi con Node Exporter y cAdvisor genera del orden de unas mil series; a una muestra cada 15 segundos son 5.760 muestras por serie al día, es decir, unos 10 MB diarios, y con 15 días quedas bastante bajo los 2 GB. Si sumas equipos o activas collectors extra, la cuenta crece en proporción, y el límite de tamaño es la red de seguridad para que el disco nunca se llene por culpa del monitoreo. Si quieres más historia, sube el tiempo solo después de mirar cuánto ocupa de verdad la carpeta de datos después de una semana.

Y si todavía estás en microSD, este es el mejor argumento para pasar a una SSD: una base de datos de series de tiempo escribiendo cada 15 segundos es justo el patrón que desgasta una tarjeta.

Cuándo no necesitas todo esto

Si tienes un solo Pi que usas de vez en cuando y no te importa el historial, htop y un vcgencmd get_throttled de vez en cuando bastan. El stack se justifica cuando el Pi hace algo que no puede fallar sin aviso: controla una máquina, guarda respaldos, sirve la casa domótica o corre solo por semanas.

Variantes y mejoras

  • Panel de subvoltaje para el Pi del taller. Si tu Pi controla una CNC, una impresora 3D o una estación de soldadura, crea un panel tipo State timeline con rpi_throttled_state{state="under_voltage"} y otro tipo Stat con max_over_time(rpi_throttled_state{state="under_voltage"}[24h]). El primero te dice cuándo cayó el voltaje; cruzándolo con la hora de arranque de los motores sabrás si comparten fuente o cableado con el Pi, la causa más común de cortes en talleres. Suma rpi_throttled_since_boot para no perder los eventos que duran menos que el minuto del cron.
  • Alertas a Telegram en vez de ntfy. Grafana tiene un contact point nativo de Telegram: crea un bot con @BotFather, copia el token, obtén el ID de tu chat y agrégalo en Alerting → Contact points → Add contact point → Telegram. Útil si en el taller ya coordinan todo por un grupo de Telegram: el aviso de subvoltaje o de disco lleno llega a todos.
  • Vigilar el ambiente, no solo el Pi. Conecta un sensor BME280 por I2C al mismo Pi, expón temperatura y humedad del taller con el mismo truco del script en la carpeta de textfile, y crea una alerta de humedad alta para las bobinas de filamento o de sobretemperatura en el gabinete eléctrico.
  • Un UPS para el Pi. Si la alerta de subvoltaje salta seguido por cortes de luz, un HAT UPS con baterías 18650 mantiene el Pi vivo y te permite medir cuánto dura la autonomía con el mismo stack.

Personalización para Chile

En Chile consigues lo necesario para este montaje en MechatronicStore:

  • Raspberry Pi 5 de 4 GB (o un Raspberry Pi 4 con 2 GB o más): el servidor de monitoreo. Con 4 GB sobra espacio para Grafana y otros servicios.
  • Fuente USB-C oficial de 27 W para Raspberry Pi 5: la mejor prevención contra la alerta de subvoltaje. Los cargadores de celular genéricos son la causa número uno del throttling.
  • SSD NVMe con HAT M.2 para Raspberry Pi 5: el lugar correcto para la base de datos de Prometheus.
  • Cooler activo para Raspberry Pi 5: mantiene la CPU lejos de los 70 °C de la alerta y se conecta al conector de cuatro pines, así que la métrica de RPM funciona.
  • Tarjeta microSD de 32 GB: para el primer arranque o si todavía no migras a SSD.

Si en el tutorial original usan una caja Pironman 5 con ventiladores por GPIO, el cooler activo oficial cumple la función de refrigerar, con la ventaja de que su velocidad sí aparece en el dashboard.

Recursos

Versión chilena con componentes en stock local en MechatronicStore.