Grafana Alloy: Configuración como Colector OpenTelemetry

Grafana Alloy es la evolución del Grafana Agent, un colector de telemetría de código abierto que unifica la recolección de métricas, logs, trazas y perfiles en un único agente configurable mediante el lenguaje de flujo River. Compatible con OpenTelemetry, Prometheus, Loki y Pyroscope, Alloy actúa como pieza central del stack de observabilidad de Grafana, permitiendo recolectar, procesar y enrutar señales de telemetría hacia múltiples destinos simultáneamente. Esta guía cubre la instalación, los componentes principales de su pipeline y la integración con el stack LGTM (Loki, Grafana, Tempo, Mimir).

Requisitos Previos

  • Ubuntu 20.04/22.04, Debian 11+ o CentOS 8+/Rocky Linux 8+
  • 256 MB de RAM mínimo (varía según el volumen de datos)
  • Acceso a las fuentes de telemetría (puertos de métricas, archivos de log, etc.)
  • Destinos configurados: Loki, Mimir, Tempo, Grafana Cloud o cualquier receptor OTLP

Instalación de Grafana Alloy

Instalación en Ubuntu/Debian

# Añadir el repositorio de Grafana
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor \
  | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null

echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" \
  | sudo tee /etc/apt/sources.list.d/grafana.list

sudo apt update && sudo apt install -y alloy

# Habilitar e iniciar el servicio
sudo systemctl enable alloy --now
sudo systemctl status alloy

Instalación en CentOS/Rocky Linux

# Añadir repositorio de Grafana
sudo tee /etc/yum.repos.d/grafana.repo > /dev/null <<EOF
[grafana]
name=grafana
baseurl=https://rpm.grafana.com
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://rpm.grafana.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
EOF

sudo dnf install -y alloy
sudo systemctl enable alloy --now

Instalación con Docker

docker run -d \
  --name alloy \
  -v /etc/alloy:/etc/alloy:ro \
  -v /var/log:/var/log:ro \
  -p 12345:12345 \
  -p 4317:4317 \
  -p 4318:4318 \
  grafana/alloy:latest run \
  --server.http.listen-addr=0.0.0.0:12345 \
  /etc/alloy/config.alloy

Lenguaje de Configuración River

Alloy usa el lenguaje River (.alloy) para definir pipelines de telemetría mediante componentes conectados:

// Estructura básica de una configuración River

// Los componentes se definen con: tipo.nombre { ... }
// Los componentes se conectan referenciando sus outputs: componente.nombre.output

prometheus.scrape "mi_scraper" {
  targets = [{"__address__" = "localhost:9090"}]
  forward_to = [prometheus.remote_write.mimir.receiver]
}

prometheus.remote_write "mimir" {
  endpoint {
    url = "http://mimir:8080/api/v1/push"
  }
}

El archivo de configuración principal se encuentra en /etc/alloy/config.alloy.

Recolección de Métricas (Prometheus)

// /etc/alloy/config.alloy - Sección de métricas

// Descubrimiento automático de servicios via Docker
discovery.docker "contenedores" {
  host = "unix:///var/run/docker.sock"
}

// Filtrar solo contenedores con la etiqueta de métricas habilitada
discovery.relabel "contenedores_con_metricas" {
  targets = discovery.docker.contenedores.targets

  rule {
    source_labels = ["__meta_docker_container_label_metrics_enabled"]
    regex         = "true"
    action        = "keep"
  }

  // Extraer el puerto de métricas de la etiqueta del contenedor
  rule {
    source_labels = ["__meta_docker_container_label_metrics_port"]
    target_label  = "__address__"
    replacement   = "${1}"
  }
}

// Hacer scraping de los targets descubiertos
prometheus.scrape "docker_metricas" {
  targets    = discovery.relabel.contenedores_con_metricas.output
  forward_to = [prometheus.remote_write.mimir.receiver]

  scrape_interval = "15s"
  scrape_timeout  = "10s"
}

// Scraping estático de métricas del sistema
prometheus.scrape "node_exporter" {
  targets = [{"__address__" = "localhost:9100", "job" = "node"}]
  forward_to = [prometheus.remote_write.mimir.receiver]
}

// Exportar métricas a Mimir (o Prometheus compatible)
prometheus.remote_write "mimir" {
  endpoint {
    url = "http://mimir:8080/api/v1/push"

    // Para autenticación básica
    basic_auth {
      username = "usuario"
      password = env("MIMIR_PASSWORD")
    }
  }

  external_labels = {
    cluster = "produccion",
    region  = "eu-west-1",
  }
}

Recolección de Logs (Loki)

// Leer archivos de log con el componente loki.source.file
local.file_match "logs_nginx" {
  path_targets = [{"__path__" = "/var/log/nginx/access.log", "job" = "nginx"}]
  sync_period  = "5s"
}

loki.source.file "nginx_access" {
  targets    = local.file_match.logs_nginx.targets
  forward_to = [loki.process.nginx.receiver]
}

// Procesar y parsear logs de Nginx
loki.process "nginx" {
  forward_to = [loki.write.loki_server.receiver]

  // Parsear el log con una expresión regular
  stage.regex {
    expression = `(?P<ip>\S+) \S+ \S+ \[(?P<timestamp>[^\]]+)\] "(?P<metodo>\S+) (?P<ruta>\S+) \S+" (?P<status>\d+) (?P<bytes>\d+)`
  }

  // Extraer el status como label para filtrar en Loki
  stage.labels {
    values = {
      status = "",
      metodo = "",
    }
  }

  // Añadir labels adicionales
  stage.static_labels {
    values = {
      entorno = "produccion",
      servidor = env("HOSTNAME"),
    }
  }
}

// Leer logs de contenedores Docker
loki.source.docker "contenedores" {
  host = "unix:///var/run/docker.sock"
  targets = discovery.docker.contenedores.targets
  forward_to = [loki.write.loki_server.receiver]

  relabel_rules = loki.relabel.docker_labels.rules
}

// Renombrar labels de Docker para Loki
loki.relabel "docker_labels" {
  rule {
    source_labels = ["__meta_docker_container_name"]
    regex         = "/(.*)"
    target_label  = "container"
  }
}

// Enviar logs a Loki
loki.write "loki_server" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }

  external_labels = {
    cluster = "produccion",
  }
}

Recolección de Trazas (OpenTelemetry)

// Recibir trazas, métricas y logs via OTLP
otelcol.receiver.otlp "receptor_otlp" {
  // Recibir via gRPC
  grpc {
    endpoint = "0.0.0.0:4317"
  }
  // Recibir via HTTP
  http {
    endpoint = "0.0.0.0:4318"
  }

  output {
    metrics = [otelcol.processor.batch.procesador.input]
    logs    = [otelcol.processor.batch.procesador.input]
    traces  = [otelcol.processor.batch.procesador.input]
  }
}

// Agrupar señales en lotes para envío eficiente
otelcol.processor.batch "procesador" {
  timeout          = "5s"
  send_batch_size  = 10000

  output {
    metrics = [otelcol.exporter.prometheus.exportador.input]
    logs    = [otelcol.exporter.loki.exportador.input]
    traces  = [otelcol.exporter.otlp.tempo.input]
  }
}

// Exportar métricas OTLP a Prometheus/Mimir
otelcol.exporter.prometheus "exportador" {
  forward_to = [prometheus.remote_write.mimir.receiver]
}

// Exportar logs OTLP a Loki
otelcol.exporter.loki "exportador" {
  forward_to = [loki.write.loki_server.receiver]
}

// Exportar trazas a Tempo (Grafana)
otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "tempo:4317"
    tls {
      insecure = true
    }
  }
}

Procesamiento y Transformaciones

// Filtrar métricas: eliminar métricas de alta cardinalidad
prometheus.relabel "filtrar_metricas" {
  forward_to = [prometheus.remote_write.mimir.receiver]

  // Eliminar etiquetas de alta cardinalidad
  rule {
    action      = "labeldrop"
    regex       = "pod_uid|container_id"
  }

  // Eliminar métricas de debug que no necesitamos
  rule {
    source_labels = ["__name__"]
    regex         = "go_gc_.*|go_memstats_.*"
    action        = "drop"
  }
}

// Transformar atributos de trazas OTLP
otelcol.processor.attributes "enriquecer" {
  action {
    key    = "deployment.environment"
    value  = "produccion"
    action = "upsert"
  }

  action {
    key    = "service.region"
    value  = env("AWS_REGION")
    action = "insert"
  }

  output {
    traces = [otelcol.exporter.otlp.tempo.input]
  }
}

// Muestreo de trazas (para reducir volumen en producción)
otelcol.processor.tail_sampling "muestreo" {
  decision_wait    = "10s"
  num_traces       = 100000

  // Mantener siempre las trazas con errores
  policy {
    name = "errores"
    type = "status_code"
    status_code {
      status_codes = ["ERROR"]
    }
  }

  // Mantener el 10% de las trazas normales
  policy {
    name = "porcentaje_aleatorio"
    type = "probabilistic"
    probabilistic {
      sampling_percentage = 10
    }
  }

  output {
    traces = [otelcol.exporter.otlp.tempo.input]
  }
}

Integración con Grafana Cloud y LGTM

Para enviar datos al stack LGTM autohospedado (Loki + Grafana + Tempo + Mimir):

// Configuración completa para stack LGTM autohospedado

// Credenciales desde variables de entorno (más seguro que texto plano)
// Crear /etc/alloy/env:
// LOKI_URL=http://loki:3100
// MIMIR_URL=http://mimir:8080
// TEMPO_URL=tempo:4317

prometheus.remote_write "mimir" {
  endpoint {
    url = env("MIMIR_URL") + "/api/v1/push"
    
    queue_config {
      capacity             = 10000
      max_shards           = 200
      max_samples_per_send = 500
    }
  }
}

loki.write "loki" {
  endpoint {
    url = env("LOKI_URL") + "/loki/api/v1/push"
  }
}

otelcol.exporter.otlp "tempo" {
  client {
    endpoint = env("TEMPO_URL")
    tls {
      insecure = true
    }
  }
}
# Verificar que Alloy está leyendo la configuración correctamente
sudo systemctl status alloy

# Ver el estado de los componentes en la interfaz web de Alloy
# Alloy expone una UI en http://localhost:12345
curl http://localhost:12345/-/ready
curl http://localhost:12345/-/healthy

Solución de Problemas

Alloy no arranca:

# Ver logs del servicio
sudo journalctl -u alloy -f

# Validar la configuración antes de aplicarla
alloy fmt /etc/alloy/config.alloy
alloy run --dry-run /etc/alloy/config.alloy

Los componentes muestran errores en la UI:

# Acceder a la interfaz de depuración de Alloy
# http://tu-ip:12345 - muestra el grafo de componentes y su estado

# Ver el estado de los componentes via API
curl http://localhost:12345/api/v0/web/components | python3 -m json.tool

No llegan métricas a Mimir:

# Verificar que Alloy puede alcanzar el destino
alloy run --inspect /etc/alloy/config.alloy

# Revisar las métricas internas de Alloy
curl http://localhost:12345/metrics | grep "prometheus_remote_write"
# agent_prometheus_remote_write_sent_samples_total debería incrementarse

Logs que no llegan a Loki:

# Verificar que los archivos de log existen y son legibles
ls -la /var/log/nginx/access.log
sudo -u alloy cat /var/log/nginx/access.log | head -5

# Si Alloy corre como servicio, puede necesitar permisos adicionales
sudo usermod -aG adm alloy
sudo systemctl restart alloy

Consumo de memoria elevado:

// Limitar el buffer en los exportadores para reducir memoria
prometheus.remote_write "mimir" {
  endpoint {
    url = "http://mimir:8080/api/v1/push"
    
    queue_config {
      capacity  = 2500    // Reducir el buffer
      max_shards = 10     // Menos shards paralelos
    }
  }
}

Conclusión

Grafana Alloy simplifica enormemente la configuración del stack de observabilidad al consolidar la recolección de métricas, logs, trazas y perfiles en un único agente con una sintaxis de configuración expresiva. Su compatibilidad nativa con OpenTelemetry garantiza que es a prueba de futuro, mientras que la interfaz web integrada facilita el diagnóstico de problemas en los pipelines de telemetría. Para equipos que ya usan herramientas de Grafana, Alloy es la evolución natural del Prometheus Agent y el Promtail, reduciendo la complejidad operativa de mantener múltiples agentes en cada nodo.