Ir al contenido

Monitorización completa con Prometheus, Grafana y Loki: métricas, logs y contenedores Docker

Rogelio Guerra Riverón
Autor
Rogelio Guerra Riverón
Construyendo mi propia infraestructura web desde cero. Aquí documento cada paso: servidores, redes, contenedores y lo que vaya surgiendo.

Un servidor sin monitorización es un servidor ciego. No sabes cuándo se llena el disco, qué contenedor está consumiendo demasiada RAM, o cuántas peticiones 404 está generando tu web. Este artículo documenta cómo configuré el stack completo: Prometheus + Node Exporter + Grafana + Loki + Promtail.

La arquitectura
#

[Servidor Italia]
  ├── node-exporter        → métricas del sistema (CPU, RAM, disco, red)
  ├── docker-stats-        → métricas de contenedores (textfile collector)
  │   collector
  ├── prometheus           → recolecta y almacena métricas
  ├── loki                 → agrega y almacena logs
  ├── promtail             → envía logs de Nginx y syslog a Loki
  └── grafana              → dashboards de todo lo anterior

Todos los servicios corren en Docker, coordinados por el mismo docker-compose.yml.

Métricas del sistema: Node Exporter
#

Node Exporter expone métricas del hardware y del SO. El truco: tiene que correr con network_mode: host para ver las interfaces de red reales del servidor. Si corre en red de Docker, solo ve la interfaz eth0 del contenedor.

  node-exporter:
    image: prom/node-exporter:v1.8.2
    network_mode: host
    pid: host
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
      - ./textfile-collector:/textfile:ro
    command:
      - --path.procfs=/host/proc
      - --path.sysfs=/host/sys
      - --path.rootfs=/rootfs
      - --web.listen-address=127.0.0.1:9100
      - --collector.textfile.directory=/textfile

Escucha en 127.0.0.1:9100. Prometheus lo alcanza por 172.17.0.1:9100 (la IP del host desde la red Docker).

Métricas de contenedores: docker stats + textfile collector
#

El problema con cAdvisor es que no funciona con Docker 29 y el driver de almacenamiento overlayfs en cgroupv2 — falla con “failed to identify read-write layer ID”.

La solución: un contenedor ligero que ejecuta docker stats cada 30 segundos y escribe el resultado en formato Prometheus en un archivo que Node Exporter lee.

#!/bin/bash
# docker_stats.sh
OUTFILE="/textfile/docker_stats.prom"
TMPFILE="${OUTFILE}.tmp"

{
echo "# HELP docker_container_cpu_percent CPU usage percentage per container"
echo "# TYPE docker_container_cpu_percent gauge"
# ... más definiciones ...

docker stats --no-stream --format \
  '{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}' 2>/dev/null | \
while IFS='|' read -r name cpu mem net; do
    cpu_val=$(echo "$cpu" | tr -d '%' | tr ',' '.')
    # ... conversión de unidades ...
    echo "docker_container_cpu_percent{name=\"${name}\"} ${cpu_val}"
    echo "docker_container_memory_bytes{name=\"${name}\"} ${mem_used_bytes}"
    echo "docker_container_running{name=\"${name}\"} 1"
done

# Contenedores parados
docker ps -a --filter "status=exited" --format '{{.Names}}' 2>/dev/null | \
while read -r name; do
    echo "docker_container_running{name=\"${name}\"} 0"
done

} > "$TMPFILE" && mv "$TMPFILE" "$OUTFILE"

La escritura atómica (tmp → final) evita que Prometheus lea un archivo a medias.

  docker-stats-collector:
    image: docker:27-cli
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./textfile-collector:/textfile
      - ./docker_stats.sh:/docker_stats.sh:ro
    entrypoint: sh -c "apk add --no-cache bc > /dev/null 2>&1; while true; do sh /docker_stats.sh; sleep 30; done"

Prometheus: recolectar y retener
#

  prometheus:
    image: prom/prometheus:v2.51.2
    networks:
      - monitoring
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=30d
      - --web.enable-lifecycle

Configuración de scraping:

global:
  scrape_interval: 15s

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

  - job_name: node
    static_configs:
      - targets: [172.17.0.1:9100]
    relabel_configs:
      - target_label: host
        replacement: italia

172.17.0.1 es la IP del host accesible desde la red Docker bridge. Los datos se retienen 30 días.

Logs: Loki + Promtail
#

Loki almacena logs sin indexar el contenido completo — solo las etiquetas (labels). Promtail los recoge y los envía con etiquetas como job, host, filename.

  promtail:
    image: grafana/promtail:3.3.2
    user: root
    networks:
      - monitoring
    volumes:
      - ./promtail-config.yml:/etc/promtail/config.yml:ro
      - ./promtail-data:/tmp/promtail
      - /home/tellme/CLAUDE/web/logs:/logs/nginx:ro
      - /var/log:/logs/host:ro

Necesita correr como root para leer /var/log.

Grafana: dashboards
#

Grafana se conecta a Prometheus y Loki como data sources. Los dashboards más útiles:

Sistema (Node Exporter):

  • CPU total y por núcleo
  • RAM usada / libre / cache
  • Disco: uso por partición, IOPS, throughput
  • Red: tráfico de entrada/salida por interfaz

Contenedores (docker stats):

  • CPU % por contenedor
  • RAM por contenedor vs límite
  • Estado (running/stopped)
  • Tráfico de red por contenedor

Logs (Loki):

  • Logs de Nginx en tiempo real
  • Peticiones por código de estado (200, 301, 404, 500)
  • Top de IPs con más peticiones
  • Top de rutas más accedidas

Problema: [$__range] en consultas instantáneas de Loki
#

Al usar paneles de tipo “stat” o “piechart” con Loki, la variable [$__range] no se resuelve — Grafana devuelve “empty duration string”. La solución es usar una duración fija:

# MAL (en paneles stat/piechart):
sum by(status) (count_over_time({job="nginx"} | pattern ... [$__range]))

# BIEN:
sum by(status) (count_over_time({job="nginx"} | pattern ... [24h]))

Los paneles de tipo “time series” sí admiten [$__interval] correctamente.

Seguridad del stack
#

  • Prometheus y Loki no tienen acceso externo — solo en la red interna monitoring
  • Grafana es el único punto de acceso, protegido con Traefik y Let’s Encrypt
  • GF_AUTH_ANONYMOUS_ENABLED=false y GF_USERS_ALLOW_SIGN_UP=false en Grafana
  • Node Exporter escucha solo en 127.0.0.1, no expuesto en todas las interfaces

Resultado
#

Con este stack tienes visibilidad completa del servidor: qué procesos consumen recursos, qué contenedores fallan, qué peticiones recibe tu web y qué errores genera. Todo en dashboards accesibles desde monitor.serviciosrogeliowar.com.