Apuntes monitoreo con Grafana, Loki y Promtail - Ejemplos de Configuración

En la primera parte, se hablo sobre monitoreo de ciertos servicios, el artículo se alargó mucho, agregar las configuraciones de ejemplo hubiera alargado mucho más el atículo, en esta segunda parte dejaremos varios ejemplos de configuración para cada servicio, con una breve descripción.

Antes de entrar a cada servicio, conviene recordar el flujo general. Promtail lee los archivos de log y el journal de systemd, asigna labels y envía todo a Loki, que los almacena e indexa. Grafana consulta a Loki con LogQL para armar dashboards. Los ejemplos de esta parte se enfocan en dos cosas:

  • Qué debe emitir cada servicio para que sus registros sean útiles (formato de logs, driver de logging, rutas).
  • Cómo Promtail recolecta y etiqueta esos registros para poder filtrarlos después en Grafana.

Nota importante: En Debian 13 (Trixie) ya no existe rsyslog por defecto, no hay /var/log/syslog ni /var/log/messages. Todo lo del sistema pasa por systemd-journald, por eso varios servicios se leen con el scraper de tipo journal.

Lista de servicios a monitorear

1. SSH

SSH escribe sus eventos en el journal de systemd a través de la unidad ssh.service (en Debian el binario es sshd). No hace falta configurar nada especial en SSH para monitorearlo, basta con leer el journal filtrando por la unidad correspondiente.

Para asegurar que los intentos de login y las IPs de origen queden registrados con suficiente detalle, conviene revisar el nivel de log en /etc/ssh/sshd_config:

1# /etc/ssh/sshd_config
2LogLevel VERBOSE

Con VERBOSE se registran los fingerprints de las llaves usadas y más detalle de cada intento. Aplicar el cambio:

1sudo systemctl restart ssh

Para verificar qué está emitiendo SSH al journal:

1# Últimos eventos de la unidad ssh
2journalctl -u ssh -n 50 --no-pager
3
4# Seguir intentos fallidos en vivo
5journalctl -u ssh -f | grep -i "failed\|invalid\|accepted"

En Grafana se pueden hacer consultas LogQL como estas:

1# Intentos de login fallidos por SSH
2{unit="ssh.service"} |= "Failed password"
3
4# Logins exitosos
5{unit="ssh.service"} |= "Accepted"
6
7# Extraer la IP de origen de cada intento fallido
8{unit="ssh.service"} |= "Failed password"
9  | regexp "from (?P<ip>\\d+\\.\\d+\\.\\d+\\.\\d+)"

2. Nginx

Para Nginx lo ideal es tener un formato de log predecible y separar los logs por dominio, así Promtail puede etiquetar cada archivo con su domain y distinguir access de error.

Definir un log_format claro en /etc/nginx/nginx.conf dentro del bloque http:

1# /etc/nginx/nginx.conf (bloque http)
2log_format monitor '$remote_addr - $remote_user [$time_local] '
3                   '"$request" $status $body_bytes_sent '
4                   '"$http_referer" "$http_user_agent" '
5                   'rt=$request_time';

Luego, en cada server block, apuntar los logs a rutas por dominio para que el label domain salga limpio:

 1# /etc/nginx/sites-available/ejemplo.com
 2server {
 3    listen 80;
 4    server_name ejemplo.com;
 5
 6    access_log /var/log/nginx/ejemplo.com-access.log monitor;
 7    error_log  /var/log/nginx/ejemplo.com-error.log warn;
 8
 9    location / {
10        proxy_pass http://127.0.0.1:8080;
11        proxy_set_header Host $host;
12        proxy_set_header X-Real-IP $remote_addr;
13        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
14        proxy_set_header X-Forwarded-Proto $scheme;
15    }
16}

Validar y recargar Nginx:

1sudo nginx -t
2sudo systemctl reload nginx

Ejemplos de consultas LogQL para el dashboard:

 1# Requests por segundo
 2sum(rate({job="nginx", log_type="access"}[1m]))
 3
 4# Conteo de códigos de estado HTTP
 5sum by (status) (count_over_time({job="nginx", log_type="access"}[5m]))
 6
 7# Solo errores 5xx
 8{job="nginx", log_type="access"} | status =~ "5.."
 9
10# Errores de Nginx en tiempo real
11{job="nginx", log_type="error"}

3. Fail2ban

Fail2ban escribe sus acciones en /var/log/fail2ban.log, de ahí salen los baneos y las detecciones. Conviene asegurar el nivel de log en su configuración.

En /etc/fail2ban/fail2ban.local (para no tocar el .conf que se sobreescribe en actualizaciones):

1# /etc/fail2ban/fail2ban.local
2[Definition]
3loglevel = INFO
4logtarget = /var/log/fail2ban.log

Un ejemplo mínimo de jail para SSH en /etc/fail2ban/jail.local:

 1# /etc/fail2ban/jail.local
 2[DEFAULT]
 3bantime  = 1h
 4findtime = 10m
 5maxretry = 5
 6
 7[sshd]
 8enabled  = true
 9port     = ssh
10logpath  = %(sshd_log)s
11backend  = systemd

Reiniciar el servicio:

1sudo systemctl restart fail2ban
2sudo fail2ban-client status sshd

Consultas LogQL para el dashboard de defensa:

1# IPs baneadas
2{job="fail2ban"} |= "Ban" != "Unban"
3
4# IPs detectadas (antes del baneo)
5{job="fail2ban"} |= "Found"
6
7# Tendencia de baneos en el tiempo
8sum(count_over_time({job="fail2ban"} |= "Ban" != "Unban" [1h]))

4. Docker

Como se vio en la primera parte, para monitorear contenedores le pedimos a Docker que escriba sus registros en el journal de systemd cambiando el Logging Driver de json-file a journald.

Recordatorio del archivo /etc/docker/daemon.json:

1sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
2{
3 "log-driver": "journald"
4}
5EOF
6sudo systemctl restart docker
7docker info --format '{{.LoggingDriver}}'   # Salida: journald

Con journald, Docker etiqueta cada línea con metadatos como el nombre del contenedor y su imagen. Promtail los expone como labels con relabel_configs sobre el mismo scraper de tipo journal:

 1# scrape_configs de Promtail (fragmento) - Docker vía journal
 2- job_name: docker
 3  journal:
 4    path: /var/log/journal
 5    max_age: 12h
 6    labels:
 7      job: docker
 8      host: vps-01
 9  relabel_configs:
10    # Solo líneas que provienen del driver journald de Docker
11    - source_labels: ['__journal_container_name']
12      target_label: container
13    - source_labels: ['__journal_image_name']
14      target_label: image
15    # Descarta entradas que no sean de contenedores
16    - source_labels: ['__journal_container_name']
17      regex: '^$'
18      action: drop

Consultas LogQL para el dashboard de contenedores:

1# Logs de un contenedor específico
2{job="docker", container="mi-app"}
3
4# Volumen de logs por contenedor
5sum by (container) (rate({job="docker"}[5m]))
6
7# Buscar errores en todos los contenedores
8{job="docker"} |~ "(?i)error|fatal|panic"

Aplicar la configuración de Promtail

Cada vez que se editan los scrape_configs, hay que validar y reiniciar el agente:

1# Reiniciar Promtail
2sudo systemctl restart promtail
3
4# Revisar que no haya errores de arranque
5sudo journalctl -u promtail -n 50 --no-pager
6
7# Verificar los targets activos desde la API de Promtail
8curl -s http://localhost:9080/targets | head

Si Loki está recibiendo datos, en Grafana → Explore ya deberían aparecer los labels (job, unit, domain, container) para filtrar.

Resumen de labels por servicio

ServicioFuenteJob en PromtailLabels clave
SSHjournal (ssh.service)journalunit, level, host
Nginx/var/log/nginx/*-access.lognginx-accessdomain, status, log_type
Nginx/var/log/nginx/*-error.lognginx-errordomain, log_type=error
Fail2ban/var/log/fail2ban.logfail2banservice=fail2ban
Dockerjournal (journald driver)dockercontainer, image, host

Conclusión

La clave de un buen monitoreo basado en logs no está solo en el stack, sino en que cada servicio emita registros con formato predecible y en que Promtail los etiquete de forma consistente. Con labels bien definidos (unit, domain, status, container) las consultas LogQL se vuelven simples y los dashboards se arman rápido.

En esta parte quedaron los ejemplos de configuración de cada servicio monitoreado. Las configuraciones propias del stack de Grafana (Loki, grafana.ini y el config.yml completo de Promtail) las dejaré por mi lado para complementar estos apuntes.

Referencias

Traducciones: