Découverte automatique de métriques Prometheus avec les labels Docker

Vincent Bernat

Akvorado, un collecteur de flux réseau, s’appuie sur Traefik, un proxy HTTP, pour exposer les services HTTP d’un environnement Docker Compose. Des labels Docker attachés à chaque service définissent les règles de routage. Traefik les prend en compte automatiquement au démarrage d’un conteneur. Plutôt que de maintenir un fichier de configuration statique pour collecter les métriques Prometheus, la même approche s’applique avec Grafana Alloy, simplifiant sa configuration.

Traefik & Docker#

Traefik écoute les événements sur la socket Docker. Chaque service annonce sa configuration via des labels. Par exemple, voici le service Loki dans Akvorado :

services:
  loki:
    # …
    expose:
      - 3100/tcp
    labels:
      - traefik.enable=true
      - traefik.http.routers.loki.rule=PathPrefix(`/loki`)

Dès que le conteneur est opérationnel, Traefik crée un routeur qui redirige les requêtes correspondant à /loki vers le premier port exposé. Placer la configuration Traefik au sein même de la définition du service est attirant. Comment obtenir la même chose pour les métriques Prometheus ?

Découverte de métriques avec Alloy#

Grafana Alloy, un collecteur de métriques capable de collecter des métriques Prometheus, inclut un composant discovery.docker. Tout comme Traefik, il se connecte à la socket Docker1. Avec quelques règles de réétiquetage, on peut lui apprendre à utiliser les labels Docker pour localiser et collecter les métriques.

On définit trois labels sur chaque service :

  • metrics.enable active la collecte de métriques,
  • metrics.port indique le port exposant les métriques Prometheus,
  • metrics.path indique le chemin vers les métriques.

Si le service expose plus d’un port, metrics.port est obligatoire, sinon il prend par défaut la valeur de l’unique port exposé. La valeur par défaut de metrics.path est /metrics. Le service Loki devient :

services:
  loki:
    # …
    expose:
      - 3100/tcp
    labels:
      - traefik.enable=true
      - traefik.http.routers.loki.rule=PathPrefix(`/loki`)
      - metrics.enable=true
      - metrics.path=/loki/metrics

La configuration d’Alloy se divise en quatre parties :

  1. découvrir les conteneurs via la socket Docker,
  2. filtrer et réétiqueter les cibles à l’aide des labels Docker,
  3. collecter les métriques,
  4. transmettre les métriques à Prometheus.

Découverte des conteneurs Docker#

Le premier bloc découvre les conteneurs en cours d’exécution :

discovery.docker "docker" {
  host             = "unix:///var/run/docker.sock"
  refresh_interval = "30s"
  filter {
    name   = "label"
    values = ["com.docker.compose.project=akvorado"]
  }
}

Alloy se connecte à la socket Docker et liste les conteneurs toutes les 30 secondes2. Le bloc filter restreint la découverte aux conteneurs du projet akvorado, évitant toute interférence avec d’autres conteneurs sur le même hôte. Pour chaque conteneur découvert, Alloy produit une cible avec des labels tels que __meta_docker_container_label_metrics_port pour le label Docker metrics.port.

Réétiquetage des cibles#

L’étape de réétiquetage filtre et transforme les cibles brutes issues de la découverte Docker en cibles exploitables. La première étape ne conserve que les cibles dont metrics.enable vaut true :

discovery.relabel "prometheus" {
  targets = discovery.docker.docker.targets

  // Keep only targets with metrics.enable=true
  rule {
    source_labels = ["__meta_docker_container_label_metrics_enable"]
    regex         = `true`
    action        = "keep"
  }

  // …
}

La deuxième étape remplace le port découvert lorsque metrics.port est défini :

// When metrics.port is set, override __address__.
rule {
  source_labels = ["__address__", "__meta_docker_container_label_metrics_port"]
  regex         = `(.+):\d+;(.+)`
  target_label  = "__address__"
  replacement   = "$1:$2"
}

Ensuite, on gère les conteneurs en mode réseau host. Quand __meta_docker_network_name vaut host, l’adresse est réécrite en host.docker.internal au lieu de localhost3 :

// When host networking, override __address__ to host.docker.internal.
rule {
  source_labels = ["__meta_docker_container_label_metrics_port", "__meta_docker_network_name"]
  regex         = `(.+);host`
  target_label  = "__address__"
  replacement   = "host.docker.internal:$1"
}

L’étape suivante dérive le label job à partir du nom du service, en supprimant tout suffixe numéroté. Le label instance est l’adresse sans le port :

rule {
  source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
  regex         = `(.+)(?:-\d+)?`
  target_label  = "job"
}
rule {
  source_labels = ["__address__"]
  regex         = `(.+):\d+`
  target_label  = "instance"
}

Si un conteneur définit metrics.path, Alloy l’utilise comme chemin. Sinon, la valeur par défaut est /metrics :

rule {
  source_labels = ["__meta_docker_container_label_metrics_path"]
  regex         = `(.+)`
  target_label  = "__metrics_path__"
}
rule {
  source_labels = ["__metrics_path__"]
  regex         = ""
  target_label  = "__metrics_path__"
  replacement   = "/metrics"
}

Collecte et transmission#

Une fois les cibles correctement réétiquetées, la collecte et la transmission sont sommaires :

prometheus.scrape "docker" {
  targets         = discovery.relabel.prometheus.output
  forward_to      = [prometheus.remote_write.default.receiver]
  scrape_interval = "30s"
}

prometheus.remote_write "default" {
  endpoint {
    url = "http://prometheus:9090/api/v1/write"
  }
}

prometheus.scrape récupère périodiquement les métriques des cibles découvertes. prometheus.remote_write les transmet à Prometheus.

Exporteurs intégrés#

Certains services n’exposent pas de point d’accès Prometheus. Redis et Kafka en sont des exemples courants. Alloy embarque des exporteurs Prometheus capables d’interroger ces services et d’exposer les métriques à leur place.

prometheus.exporter.redis "docker" {
  redis_addr = "redis:6379"
}
discovery.relabel "redis" {
  targets = prometheus.exporter.redis.docker.targets
  rule {
    target_label = "job"
    replacement  = "redis"
  }
}
prometheus.scrape "redis" {
  targets         = discovery.relabel.redis.output
  forward_to      = [prometheus.remote_write.default.receiver]
  scrape_interval = "30s"
}

Le même schéma s’applique à Kafka :

prometheus.exporter.kafka "docker" {
  kafka_uris = ["kafka:9092"]
}
discovery.relabel "kafka" {
  targets = prometheus.exporter.kafka.docker.targets
  rule {
    target_label = "job"
    replacement  = "kafka"
  }
}
prometheus.scrape "kafka" {
  targets         = discovery.relabel.kafka.output
  forward_to      = [prometheus.remote_write.default.receiver]
  scrape_interval = "30s"
}

Chaque exporteur est un composant distinct avec sa propre configuration de réétiquetage et de collecte. Le label job est défini explicitement.


Avec cette configuration, ajouter des métriques à un nouveau service disposant d’un point d’accès Prometheus se résume à quelques labels dans docker-compose.yml, tout comme l’ajout d’une route Traefik. Alloy s’en charge automatiquement. 🩺


  1. Traefik et Alloy nécessitent tous deux l’accès à la socket Docker, ce qui confère un accès root à la machine hôte. Un Docker socket proxy atténue ce risque en n’exposant que les points d’accès de l’API en lecture seule nécessaires à la découverte. ↩︎

  2. Contrairement à Traefik, qui surveille les événements, Grafana Alloy interroge la liste des conteneurs à intervalles réguliers — un comportement hérité de Prometheus. ↩︎

  3. Le service Alloy nécessite extra_hosts: ["host.docker.internal:host-gateway"] dans sa définition. ↩︎