Leçon 1 — Déployer la pile

Collecteur, Prometheus, Tempo, Loki et Grafana en Docker Compose

8 min de lecture

Collecteur, Prometheus, Tempo, Loki et Grafana en Docker Compose

Lundi, 9 h 40, chez Ondine Télécom (opérateur fictif, 350 conseillers au support). L'assistant IA du support répond en six secondes au lieu de deux, et la facture Gemini de la semaine précédente a pris 40 %. L'outil de supervision existant voit des réponses HTTP 200 et rien d'autre. Personne ne peut dire si le modèle ralentit, si le cache de réponses ne sert plus, ou si l'agent de diagnostic boucle sur un outil. Il manque trois signaux reliés entre eux : métriques, traces et journaux.

Ce module installe la pile qui les reçoit. Le code de l'application arrive au module 2.

Le mécanisme : un point d'entrée, trois stockages, un écran

flowchart LR
  APP["Service FastAPI<br/>(SDK OpenTelemetry)"] -->|"OTLP/HTTP :4318<br/>traces, métriques, journaux"| COL["Collecteur OTel<br/>memory_limiter, filtre, batch"]
  COL -->|"expose :8889"| PROM[("Prometheus<br/>métriques")]
  PROM -.->|"scrape toutes les 15 s"| COL
  COL -->|"OTLP gRPC"| TEMPO[("Tempo<br/>traces")]
  COL -->|"OTLP HTTP /otlp"| LOKI[("Loki<br/>journaux")]
  GRAF["Grafana<br/>provisionné en code"] --> PROM
  GRAF --> TEMPO
  GRAF --> LOKI

L'application ne connaît qu'une adresse : celle du collecteur OpenTelemetry, un processus qui reçoit les données au format OTLP (le protocole d'OpenTelemetry), les transforme et les répartit. Trois raisons de ne pas envoyer directement de l'application vers chaque stockage :

  1. Changer de destination sans toucher au code. La leçon 6 remplace Prometheus, Tempo et Loki par Google Cloud en changeant un seul fichier de configuration du collecteur.
  2. Appliquer une politique au même endroit. Le collecteur supprime tout contenu de prompt qui atteindrait un span, quelle que soit la bibliothèque qui l'a ajouté.
  3. Protéger l'application. L'export vers un collecteur local est rapide ; si Tempo est lent, c'est la file du collecteur qui grossit, pas la latence de l'assistant.

Prometheus fonctionne en tirage (pull) : il vient lire toutes les 15 secondes la page de métriques que le collecteur expose sur le port 8889. Tempo et Loki reçoivent en poussée (push) : le collecteur leur envoie traces et journaux en OTLP.

La mise en œuvre

1. Des versions épinglées

ComposantImageRôle
Collecteurotel/opentelemetry-collector-contrib:0.161.0réception OTLP, traitement, routage
Prometheusprom/prometheus:v3.15.0séries temporelles, PromQL, exemplars
Tempografana/tempo:2.10.8stockage des traces, TraceQL
Lokigrafana/loki:3.7.8journaux, ingestion OTLP native
Grafanagrafana/grafana:13.1.7tableaux de bord, exploration, alertes

Aucune étiquette latest : une mise à jour de Grafana ou du collecteur a déjà changé un format de configuration entre deux docker compose pull. On monte de version volontairement, en relançant make e2e.

2. Le collecteur

files/otel/collector.yaml (extrait)
processors:
  # Premier de chaque pipeline : refuse les données avant que le collecteur ne soit tué par manque de mémoire.
  memory_limiter:
    check_interval: 1s
    limit_mib: 400
    spike_limit_mib: 100
  batch:
    timeout: 5s
    send_batch_size: 1024
  attributes/contenu:
    actions:
      - { key: gen_ai.input.messages, action: delete }
      - { key: gen_ai.output.messages, action: delete }
      - { key: gen_ai.system_instructions, action: delete }

exporters:
  prometheus:
    endpoint: 0.0.0.0:8889
    resource_to_telemetry_conversion: { enabled: true }   # service_name, deployment_environment_name en labels
    enable_open_metrics: true                               # nécessaire aux exemplars
  otlp/tempo:
    endpoint: tempo:4317
    tls: { insecure: true }
  otlphttp/loki:
    endpoint: http://loki:3100/otlp

L'ordre des processeurs compte : memory_limiter d'abord, pour refuser les données avant qu'elles n'occupent de la mémoire ; batch en dernier, pour regrouper ce qui reste. resource_to_telemetry_conversion recopie les attributs de ressource (service.name, service.instance.id, deployment.environment.name) en labels Prometheus : sans lui, impossible de filtrer par service dans une requête PromQL.

Le collecteur expose aussi ses propres métriques sur le port 8888. Prometheus les lit (job="otel-collector-self") : le tableau de bord du module 3 affiche les données refusées par memory_limiter.

3. La composition

files/docker-compose.yml (extrait)
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.161.0
    command: ["--config=/etc/otelcol/collector.yaml"]
    volumes:
      - ./otel/collector.yaml:/etc/otelcol/collector.yaml:ro
    ports:
      - 127.0.0.1:4317:4317   # OTLP gRPC (application sur l'hôte)
      - 127.0.0.1:4318:4318   # OTLP HTTP
  tempo:
    image: grafana/tempo:2.10.8
    user: "0"   # écrit dans le volume nommé sans réglage de droits

Tous les ports sont liés à 127.0.0.1 : aucun de ces services n'a d'authentification activée dans le lab, ils ne doivent pas être joignables depuis le réseau du bureau. Grafana est publié sur 3300 et Prometheus sur 9095, pour ne pas entrer en conflit avec un Grafana ou un Prometheus déjà présent sur le poste.

4. Démarrer

cd files
make setup    # .venv, dépendances, .env avec un mot de passe Grafana aléatoire
make up

make setup remplace GRAFANA_ADMIN_PASSWORD=changeme par openssl rand -hex 16. Le Makefile ne fait ni include .env ni export : une variable passée en ligne de commande (FAUX_MODELE=1 make e2e) l'emporte toujours sur .env, que docker compose et Python lisent eux-mêmes.

Résultat obtenu

Le 2026-10-16, sur un poste Linux avec Docker 28.2 :

$ make up
 Container lab-otel-grafana-1  Started
 Container lab-otel-otel-collector-1  Started
Attente de Grafana...................... : prêt. http://localhost:3300 (admin / GRAFANA_ADMIN_PASSWORD de .env)

$ docker compose -p lab-otel ps --format 'table {{.Service}}\t{{.Ports}}'
SERVICE          PORTS
grafana          127.0.0.1:3300->3000/tcp
loki             127.0.0.1:3100->3100/tcp
otel-collector   127.0.0.1:4317-4318->4317-4318/tcp, 55679/tcp
prometheus       127.0.0.1:9095->9090/tcp
tempo            127.0.0.1:3200->3200/tcp

$ curl -s localhost:9095/api/v1/targets | jq -r '.data.activeTargets[] | "\(.labels.job) \(.health)"'
otel-collector-app up
otel-collector-self up

Mémoire mesurée (docker stats) après une charge de test : Grafana 638 Mio, Loki 91 Mio, collecteur 70 Mio, Tempo 64 Mio, Prometheus 60 Mio. Moins de 1 Go pour l'ensemble : la pile tient sur un portable.

Ce qui casse en production

SymptômeCauseParade
Le collecteur redémarre en boucle (OOMKilled) lors d'un picpas de memory_limiter, ou placé après batchmemory_limiter en tête de chaque pipeline ; surveiller otelcol_receiver_refused_*
Une série Prometheus continue d'exister 5 minutes après l'arrêt du servicel'exportateur prometheus du collecteur garde chaque série jusqu'à metric_expiration (5 min par défaut)normal pour un exportateur en tirage ; ne pas conclure qu'un service tourne parce que sa série existe, regarder up ou le trafic
Tempo démarre puis s'arrête sur permission deniedl'image tourne sans droits sur le volume nomméuser: "0" dans le lab ; en production, volume préparé avec le bon propriétaire
Loki rejette les journaux en 404exportateur pointé sur http://loki:3100 au lieu de /otlpotlphttp avec le chemin /otlp (ingestion OTLP native de Loki 3)
Les tableaux de bord changent de comportement après un pullimages en latestversions épinglées, montée de version testée par make e2e

La règle de décision

Mettez un collecteur entre l'application et les stockages dès qu'il y a plus d'une destination, une règle de filtrage, ou une perspective de changer de fournisseur ; pour un script jetable, l'export direct suffit. Choisissez cette pile ouverte (Prometheus, Tempo, Loki, Grafana) quand l'équipe l'opère déjà ou doit rester hors cloud ; sinon, l'export vers un service géré de la leçon 6 évite d'opérer trois bases de données. Surveillez dès le premier jour les métriques du collecteur lui-même : un collecteur qui jette des données ne prévient personne.

Pratique

Après un pic de trafic, les graphes de l'assistant présentent un trou de 3 minutes alors que l'application n'a enregistré aucune erreur. Que regardez-vous en premier ?

Une collègue propose d'exposer le port 3300 de Grafana sur 0.0.0.0 pour montrer le tableau de bord en réunion. Que décidez-vous ?

Exercice. Ajoutez au collecteur une règle qui supprime l'attribut assistant.client_id des spans avant leur envoi à Tempo, puis vérifiez que la configuration reste valide sans démarrer la pile.

Voir la solution

Une ligne de plus dans le processeur attributes/contenu de otel/collector.yaml :

      - { key: assistant.client_id, action: delete }

Validation par le binaire officiel, sans rien démarrer :

docker run --rm -v $PWD/otel:/cfg otel/opentelemetry-collector-contrib:0.161.0 \
  validate --config=/cfg/collector.yaml

Une clé mal orthographiée (acton: delete) fait échouer la commande avec has invalid keys: acton. Avant de supprimer l'attribut, demandez-vous qui en a besoin : sans lui, le support ne peut plus retrouver les traces d'un client qui se plaint. Une alternative est l'action hash, qui garde la possibilité de filtrer sans exposer l'identifiant en clair.

  • Je sais expliquer pourquoi l'application parle à un collecteur et non directement aux stockages.
  • Je sais ordonner les processeurs memory_limiter et batch.
  • Je sais démarrer la pile et vérifier que Prometheus lit bien le collecteur.
  • Je sais quels ports ne doivent jamais quitter 127.0.0.1.
Suivant