Promtail y Loki para Agregación de Logs

Loki es un sistema de agregación de logs escalable y económico desarrollado por Grafana Labs que, a diferencia de Elasticsearch, solo indexa los metadatos (etiquetas/labels) y almacena los logs comprimidos. Promtail es el agente que recopila los logs de los ficheros del sistema y los envía a Loki. Juntos forman el stack de logging del ecosistema Grafana, integrándose perfectamente con Prometheus y Grafana para una observabilidad completa.

Requisitos Previos

  • Ubuntu 22.04/20.04, Debian 11, o CentOS/Rocky Linux 8+
  • Grafana instalado para visualización (opcional pero muy recomendado)
  • Al menos 2 GB de RAM para Loki en instalaciones pequeñas
  • Acceso de escritura al directorio de logs del sistema

Instalación de Loki

# Descargar Loki y Promtail desde GitHub
LOKI_VERSION="2.9.4"
wget "https://github.com/grafana/loki/releases/download/v${LOKI_VERSION}/loki-linux-amd64.zip"
wget "https://github.com/grafana/loki/releases/download/v${LOKI_VERSION}/promtail-linux-amd64.zip"

unzip loki-linux-amd64.zip
unzip promtail-linux-amd64.zip

mv loki-linux-amd64 /usr/local/bin/loki
mv promtail-linux-amd64 /usr/local/bin/promtail
chmod +x /usr/local/bin/loki /usr/local/bin/promtail

# Verificar la instalación
loki --version
promtail --version

# Alternativa: instalar con Docker
docker pull grafana/loki:latest
docker pull grafana/promtail:latest
# Crear el usuario del sistema para Loki
useradd --system --home /etc/loki --shell /bin/false loki
mkdir -p /etc/loki /var/lib/loki /var/log/loki
chown -R loki:loki /etc/loki /var/lib/loki /var/log/loki

# Configuración básica de Loki para instalación local
cat > /etc/loki/loki.yaml << 'EOF'
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096
  log_level: info

common:
  instance_addr: 127.0.0.1
  path_prefix: /var/lib/loki
  storage:
    filesystem:
      chunks_directory: /var/lib/loki/chunks
      rules_directory: /var/lib/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v12
      index:
        prefix: index_
        period: 24h

ruler:
  alertmanager_url: http://localhost:9093

limits_config:
  # Retención global de logs (ingestion_rate_mb = 16 por defecto)
  retention_period: 744h    # 31 días
  max_streams_per_user: 10000
  ingestion_rate_mb: 32
  ingestion_burst_size_mb: 64

# Habilitar eliminación de logs por política de retención
compactor:
  working_directory: /var/lib/loki/compactor
  retention_enabled: true
  delete_request_cancel_period: 24h
EOF

# Crear el servicio systemd para Loki
cat > /etc/systemd/system/loki.service << 'EOF'
[Unit]
Description=Loki log aggregation system
After=network.target

[Service]
Type=simple
User=loki
Group=loki
ExecStart=/usr/local/bin/loki --config.file=/etc/loki/loki.yaml
StandardOutput=journal
StandardError=journal
SyslogIdentifier=loki
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable loki && systemctl start loki

# Verificar que Loki está respondiendo
curl -s http://localhost:3100/ready
curl -s http://localhost:3100/metrics | head -20

Instalación y Configuración de Promtail

# Crear el usuario para Promtail
useradd --system --home /etc/promtail --shell /bin/false promtail

# Agregar el usuario promtail al grupo adm para leer logs del sistema
usermod -aG adm promtail

mkdir -p /etc/promtail /var/lib/promtail /var/log/promtail
chown -R promtail:promtail /etc/promtail /var/lib/promtail /var/log/promtail

# Configuración de Promtail
cat > /etc/promtail/promtail.yaml << 'EOF'
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  # Archivo donde Promtail guarda la posición de lectura de cada log
  filename: /var/lib/promtail/positions.yaml

clients:
  - url: http://localhost:3100/loki/api/v1/push

scrape_configs:
  # Recopilar logs del kernel y del sistema
  - job_name: syslog
    static_configs:
      - targets:
          - localhost
        labels:
          job: syslog
          host: __HOSTNAME__
          __path__: /var/log/syslog
    pipeline_stages:
      - regex:
          expression: '^(?P<timestamp>\S+ \S+) (?P<hostname>\S+) (?P<app>\S+): (?P<message>.*)$'
      - labels:
          app:
          hostname:

  # Recopilar logs de Nginx
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          host: __HOSTNAME__
          __path__: /var/log/nginx/*.log
    pipeline_stages:
      - regex:
          expression: '^(?P<remote_addr>\S+) - (?P<remote_user>\S+) \[(?P<time_local>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<body_bytes_sent>\d+)'
      - labels:
          method:
          status:
      - metrics:
          http_nginx_requests_total:
            type: Counter
            description: Total de peticiones HTTP de Nginx
            source: status
            config:
              action: inc

  # Recopilar logs de aplicación personalizada
  - job_name: mi_aplicacion
    static_configs:
      - targets:
          - localhost
        labels:
          job: mi_aplicacion
          entorno: produccion
          __path__: /var/log/mi_aplicacion/*.log
    pipeline_stages:
      # Parsear logs JSON
      - json:
          expressions:
            level: level
            message: message
            request_id: request_id
            user_id: user_id
      - labels:
          level:
          request_id:
      - timestamp:
          source: timestamp
          format: RFC3339
EOF

# Crear el servicio systemd para Promtail
cat > /etc/systemd/system/promtail.service << 'EOF'
[Unit]
Description=Promtail log agent
After=network.target

[Service]
Type=simple
User=promtail
Group=promtail
ExecStart=/usr/local/bin/promtail --config.file=/etc/promtail/promtail.yaml
StandardOutput=journal
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable promtail && systemctl start promtail

# Verificar que Promtail está enviando logs
curl -s http://localhost:9080/metrics | grep promtail_sent

Extracción de Labels con Pipelines

# Configuración avanzada de pipeline stages para logs de aplicación
cat >> /etc/promtail/promtail.yaml << 'EOF'

  # Ejemplo avanzado: log de PostgreSQL
  - job_name: postgresql
    static_configs:
      - targets:
          - localhost
        labels:
          job: postgresql
          __path__: /var/log/postgresql/postgresql-*.log
    pipeline_stages:
      # Manejar logs multilínea de PostgreSQL
      - multiline:
          firstline: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
          max_wait_time: 3s
      
      # Extraer campos del log de PostgreSQL
      - regex:
          expression: '^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d+) \S+ \[(?P<pid>\d+)\] (?P<user>\S+)@(?P<database>\S+) (?P<level>\w+):  (?P<message>.*)'
      
      # Solo indexar el nivel y la base de datos como labels
      - labels:
          level:
          database:
      
      # Filtrar logs de DEBUG para no saturar Loki
      - drop:
          expression: 'level="DEBUG"'
      
      # Agregar timestamp extraído del log
      - timestamp:
          source: timestamp
          format: '2006-01-02 15:04:05.000'
EOF

systemctl restart promtail

Backends de Almacenamiento de Loki

# Configuración de Loki con almacenamiento en S3 (para producción)
cat > /etc/loki/loki-s3.yaml << 'EOF'
auth_enabled: false

server:
  http_listen_port: 3100

common:
  path_prefix: /var/lib/loki
  replication_factor: 1
  ring:
    kvstore:
      store: consul
      consul:
        host: consul:8500

ingester:
  chunk_idle_period: 1h
  chunk_target_size: 1048576
  max_chunk_age: 1h
  wal:
    enabled: true
    dir: /var/lib/loki/wal

schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: s3
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  aws:
    s3: s3://us-east-1/mi-bucket-loki-logs
    s3forcepathstyle: false
    region: us-east-1
    access_key_id: AKIAIOSFODNN7EXAMPLE
    secret_access_key: wJalrXUtnFEMI/K7MDENG
  boltdb_shipper:
    active_index_directory: /var/lib/loki/boltdb-shipper-active
    cache_location: /var/lib/loki/boltdb-shipper-cache
    cache_ttl: 24h
    shared_store: s3

compactor:
  working_directory: /var/lib/loki/compactor
  shared_store: s3
  retention_enabled: true

limits_config:
  retention_period: 720h  # 30 días
  ingestion_rate_mb: 32
EOF

Consultas con LogQL

# LogQL es el lenguaje de consultas de Loki, similar a PromQL

# Consultas básicas desde la línea de comandos con logcli
# Instalar logcli
wget "https://github.com/grafana/loki/releases/download/v2.9.4/logcli-linux-amd64.zip"
unzip logcli-linux-amd64.zip && mv logcli-linux-amd64 /usr/local/bin/logcli

export LOKI_ADDR=http://localhost:3100

# Ver los streams (combinaciones de labels) disponibles
logcli labels
logcli series '{job="nginx"}'

# Consultar logs de nginx del último 1 hora
logcli query '{job="nginx"}' --limit=100

# Filtrar por método HTTP y código de estado
logcli query '{job="nginx", status="500"}' --since=1h

# Buscar texto específico en los logs
logcli query '{job="nginx"} |= "error" |= "timeout"'

# Buscar usando regex
logcli query '{job="mi_aplicacion"} |~ "ERROR|CRITICAL" !~ "test|staging"'

# Métricas derivadas de logs (LogQL metrics queries)
# Tasa de errores en Nginx en los últimos 5 minutos:
logcli query 'rate({job="nginx", status=~"5.."}[5m])'

# Número total de peticiones HTTP por estado:
logcli query 'sum by(status) (count_over_time({job="nginx"}[1h]))'

# Latencia del percentil 99 (si el log incluye un campo de duración):
logcli query 'quantile_over_time(0.99, {job="nginx"} | json | unwrap duration [5m])'

Visualización en Grafana

# Agregar Loki como datasource en Grafana mediante la API
curl -s -X POST \
  -H "Content-Type: application/json" \
  -u admin:contraseña \
  "http://grafana.midominio.com:3000/api/datasources" \
  -d '{
    "name": "Loki",
    "type": "loki",
    "url": "http://loki:3100",
    "access": "proxy",
    "isDefault": false,
    "jsonData": {
      "maxLines": 1000,
      "derivedFields": [
        {
          "matcherRegex": "request_id=(\\w+)",
          "name": "TraceID",
          "url": "http://tempo:3200/trace/$${__value.raw}",
          "datasourceUid": "tempo"
        }
      ]
    }
  }'

# Crear un panel de dashboard en Grafana para visualizar logs
# Usar el plugin "Logs" de Grafana con la query:
# {job="nginx", status=~"4..|5.."}

Integración con Kubernetes

# Instalar Loki Stack en Kubernetes con Helm
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Instalar el stack completo: Loki + Promtail + Grafana
helm install loki-stack grafana/loki-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.enabled=true \
  --set prometheus.enabled=false \
  --set loki.persistence.enabled=true \
  --set loki.persistence.size=50Gi

# Verificar la instalación
kubectl get pods -n monitoring | grep loki

# Promtail en Kubernetes recoge automáticamente los logs de todos los pods
# Con labels del pod automáticamente como labels de Loki:
# namespace, pod, container, node, app

# Ver los logs de un deployment específico en Kubernetes con logcli
export LOKI_ADDR=http://$(kubectl get svc -n monitoring loki -o jsonpath='{.spec.clusterIP}'):3100
logcli query '{namespace="produccion", app="api-backend"} |= "error"' --since=1h

Solución de Problemas

Loki rechaza logs con "too many outstanding requests":

# Aumentar los límites de ingesta en loki.yaml
# limits_config:
#   ingestion_rate_mb: 64
#   ingestion_burst_size_mb: 128
#   max_streams_per_user: 50000

systemctl restart loki

Promtail no envía logs:

# Verificar el estado de Promtail
curl -s http://localhost:9080/ready
curl -s http://localhost:9080/metrics | grep promtail_sent_entries_total

# Ver los logs de Promtail
journalctl -u promtail -f

# Verificar que el archivo de posiciones está actualizado
cat /var/lib/promtail/positions.yaml

Consultas lentas en Loki:

# Verificar el uso de labels (no usar labels de alta cardinalidad)
# MALO: usar user_id, ip_address como labels
# BUENO: usar job, env, level, service como labels

# Ver las estadísticas de los chunks
curl -s http://localhost:3100/metrics | grep loki_store_chunk

Conclusión

El stack Promtail + Loki + Grafana ofrece una alternativa más económica y fácil de operar que el stack ELK para la mayoría de casos de uso de agregación de logs. La filosofía de Loki de indexar solo los metadatos y comprimir los logs reduce significativamente los costes de almacenamiento y computación, mientras que LogQL proporciona la flexibilidad necesaria para hacer consultas analíticas sobre los logs. Para equipos que ya usan Prometheus y Grafana, añadir Loki es el camino natural hacia una observabilidad completa de sus sistemas.