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 :
- 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.
- 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é.
- 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
| Composant | Image | Rôle |
|---|---|---|
| Collecteur | otel/opentelemetry-collector-contrib:0.161.0 | réception OTLP, traitement, routage |
| Prometheus | prom/prometheus:v3.15.0 | séries temporelles, PromQL, exemplars |
| Tempo | grafana/tempo:2.10.8 | stockage des traces, TraceQL |
| Loki | grafana/loki:3.7.8 | journaux, ingestion OTLP native |
| Grafana | grafana/grafana:13.1.7 | tableaux 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
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/otlpL'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
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 droitsTous 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 upmake 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 upMé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ôme | Cause | Parade |
|---|---|---|
Le collecteur redémarre en boucle (OOMKilled) lors d'un pic | pas de memory_limiter, ou placé après batch | memory_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 service | l'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 denied | l'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 404 | exportateur pointé sur http://loki:3100 au lieu de /otlp | otlphttp avec le chemin /otlp (ingestion OTLP native de Loki 3) |
Les tableaux de bord changent de comportement après un pull | images en latest | versions é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 ?
L'export OTLP est asynchrone : quand le collecteur refuse des données, l'application ne voit aucune erreur. Les compteurs de refus du collecteur le montrent directement. Grafana ne fait qu'afficher ; une rétention trop courte supprimerait des données anciennes, pas un trou de 3 minutes au milieu.
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 ?
Le lab n'active d'authentification que sur Grafana. Prometheus, Tempo et Loki, joignables, donneraient accès aux traces et journaux sans aucun contrôle. Un tunnel ou un partage d'écran montre le tableau de bord sans rien ouvrir. « Temporaire » est la façon dont la plupart des ports restés ouverts ont commencé.
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.yamlUne 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_limiteretbatch. - 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.