Reprends le tableau de bord de la voiture de la leçon 01. Le compteur kilométrique ne fait qu'augmenter : c'est un compteur (counter), comme http_requetes_total. La jauge d'essence monte au plein et descend en roulant : c'est une jauge (gauge), comme requetes_en_cours. Le carnet d'entretien qui note « 12 trajets de moins de 10 km, 30 de moins de 50 km, 3 de plus de 100 km » range chaque trajet dans une tranche : c'est un histogramme, comme http_duree_requete_seconds. Et la boîte noire, qui écrit une ligne par événement avec l'heure exacte, c'est le log JSON de l'API. Prometheus ne reçoit rien : c'est lui qui vient lire les compteurs toutes les 15 secondes, comme un contrôleur qui passe relever les compteurs de chaque voiture du parc.
Une métrique Prometheus a toujours un nom, éventuellement des labels, et une valeur numérique. Le type dit comment lire cette valeur dans le temps. Prometheus en connaît quatre : le compteur, la jauge, l'histogramme et le résumé. Les trois premiers sont utilisés par l'API catalogue ; le quatrième ne l'est pas, et la raison est instructive. Toutes les lignes ci-dessous sont copiées de http://localhost:8000/metrics sur la machine du cours ; les valeurs seront différentes chez toi.
Compteur (counter). Un compteur ne fait qu'augmenter, ou repart à zéro si le service redémarre. Il compte des événements cumulés. L'API expose http_requetes_total, avec trois labels : le code de réponse, la méthode HTTP et la route :
# HELP http_requetes_total Nombre de requêtes HTTP reçues, par méthode, route normalisée et code de réponse.
# TYPE http_requetes_total counter
http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0
http_requetes_total{code="201",methode="POST",route="/inscriptions"} 855.0
http_requetes_total{code="404",methode="GET",route="/cours/{id}"} 210.0
http_requetes_total{code="500",methode="GET",route="/cours"} 32.0Ce que tu ne fais jamais avec un compteur : lire sa valeur brute pour dire « il y a 3247 requêtes en ce moment ». Un compteur ne redescend jamais ; ce qui t'intéresse, c'est sa vitesse d'augmentation, calculée avec la fonction rate() (module 2). Documentation : Prometheus — Metric types, Counter.
Jauge (gauge). Une jauge monte et descend librement : une valeur à un instant donné. L'API expose requetes_en_cours, le nombre de requêtes en cours de traitement à l'instant précis du relevé :
# HELP requetes_en_cours Nombre de requêtes HTTP en cours de traitement à cet instant.
# TYPE requetes_en_cours gauge
requetes_en_cours 1.0Une requête est en cours à l'instant du relevé ; l'instant d'après, ce nombre peut être 0 ou 4. L'API expose aussi des jauges de configuration, dont la valeur est une information plus qu'une mesure : api_info{version="1.0.0"} 1.0 (la version est dans le label, la valeur vaut toujours 1) et api_panne_taux_erreurs 0.01 (la part de requêtes que l'API fait volontairement échouer : 1 % en fonctionnement normal). Documentation : Prometheus — Metric types, Gauge.
Histogramme (histogram). Un histogramme mesure la distribution d'une valeur, comme la durée d'une requête HTTP : combien de requêtes ont pris moins de 5 ms, moins de 10 ms, moins de 25 ms… ? L'API expose http_duree_requete_seconds. Contrairement à un compteur ou une jauge, un histogramme publie plusieurs lignes pour une seule métrique : un compteur par tranche (bucket), plus un compte total et une somme. Voici les douze lignes de la route /cours :
# HELP http_duree_requete_seconds Durée de traitement des requêtes HTTP, en secondes, par route normalisée.
# TYPE http_duree_requete_seconds histogram
http_duree_requete_seconds_bucket{le="0.005",route="/cours"} 32.0
http_duree_requete_seconds_bucket{le="0.01",route="/cours"} 74.0
http_duree_requete_seconds_bucket{le="0.025",route="/cours"} 1566.0
http_duree_requete_seconds_bucket{le="0.05",route="/cours"} 3248.0
http_duree_requete_seconds_bucket{le="0.1",route="/cours"} 3276.0
http_duree_requete_seconds_bucket{le="0.25",route="/cours"} 3278.0
http_duree_requete_seconds_bucket{le="0.5",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="1.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="2.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="+Inf",route="/cours"} 3279.0
http_duree_requete_seconds_count{route="/cours"} 3279.0
http_duree_requete_seconds_sum{route="/cours"} 85.42293146001248Chaque _bucket{le="…"} (le pour less or equal) compte les requêtes plus rapides ou égales à ce seuil, de façon cumulative : les 32 requêtes de moins de 5 ms sont aussi comptées dans les 74 de moins de 10 ms, et dans les 3279 du seau +Inf (toutes). _count est le nombre total d'observations (3279, égal au seau +Inf), _sum la somme de toutes les durées (85,4 secondes au total, soit 26 ms en moyenne par requête). C'est ce qui permet de reconstruire un quantile après coup, avec histogram_quantile() (module 2). Lecture directe : 3248 requêtes sur 3279 ont pris moins de 50 ms, soit 99 %. Documentation : Prometheus — Metric types, Histogram.
Résumé (summary), et pourquoi le labo ne l'utilise pas. Un résumé mesure aussi une distribution, mais il calcule ses quantiles directement dans le programme observé, avant de les publier : une ligne {quantile="0.5"}, une ligne {quantile="0.9"}, plus _sum et _count. Le problème : un quantile calculé côté client ne peut pas être recombiné avec celui d'une autre instance. Si le labo faisait tourner trois copies de l'API, tu ne pourrais pas faire la moyenne des trois quantile="0.9" pour obtenir le vrai 90ᵉ centile de l'ensemble. Un histogramme publie des compteurs bruts par tranche : Prometheus peut les additionner entre instances avant de calculer le quantile. Il n'y a donc aucune ligne summary dans le /metrics de l'API du labo. Documentation : Prometheus — Histograms and summaries.
| Type | Ce qu'il mesure | Dans le labo | Question à laquelle il répond |
|---|---|---|---|
| Compteur | Un cumul qui ne fait qu'augmenter | http_requetes_total, inscriptions_total, cours_consultes_total | « Combien de requêtes par seconde ? » (avec rate) |
| Jauge | Une valeur instantanée qui monte et descend | requetes_en_cours, api_disque_libre_octets, api_info | « Combien en ce moment ? » |
| Histogramme | Une distribution, par tranches cumulatives | http_duree_requete_seconds | « 95 % des requêtes prennent moins de combien ? » |
| Résumé | Une distribution, quantiles calculés côté client | aucun | La même, mais sans pouvoir agréger entre instances |
Chaque combinaison unique de nom de métrique et de labels forme une série temporelle : une suite de couples (horodatage, valeur) que Prometheus stocke et interroge. http_requetes_total{code="200",methode="GET",route="/cours"} est une série ; http_requetes_total{code="201",methode="POST",route="/inscriptions"} en est une autre. Sur la machine du cours, http_requetes_total compte 14 séries (14 combinaisons de code, méthode et route apparues depuis le démarrage). Les labels permettent de filtrer et de regrouper sans changer le nom : « les erreurs serveur, seulement sur /inscriptions » se lit http_requetes_total{route="/inscriptions",code=~"5.."}. Documentation : Prometheus — Data model.
La cardinalité d'une métrique est le nombre de séries distinctes qu'elle produit. http_requetes_total{code,methode,route}, avec cinq codes, deux méthodes et sept routes en gabarit, donne quelques dizaines de séries au maximum. Regarde le log de l'étape 2 de la leçon 01 : la requête a appelé /cours/C0038, mais la métrique porte route="/cours/{id}", le gabarit de la route tel que FastAPI l'a déclaré. Si le label contenait l'identifiant réel, chaque cours consulté créerait une série : 64 cours × 5 codes × 2 méthodes. Sur un vrai catalogue de dizaines de milliers de cours, la métrique exploserait et Prometheus ralentirait. La règle : un label doit avoir un nombre borné et raisonnable de valeurs ; un identifiant unique, une adresse IP ou un horodatage n'ont rien à faire dans un label. Une route qui n'existe pas est comptée sous route="inconnue", jamais sous son chemin réel, pour la même raison.
Le labo contient volontairement une exception instructive : inscriptions_total{cours_id="C0028"} et cours_consultes_total{cours_id="…"} ont un label par cours. Avec 64 cours, ça reste borné (64 séries chacune). Avec un million de cours, ce serait une faute. Le labo a d'ailleurs une alerte PrometheusTropDeSeries qui sonne au-dessus de 100 000 séries. Documentation : Prometheus — Instrumentation, cardinality.
Prometheus fonctionne en pull (il va chercher) : il interroge lui-même, toutes les 15 secondes dans le labo (scrape_interval: 15s dans prometheus/prometheus.yml), l'URL /metrics de chaque service qu'il surveille. C'est l'inverse d'un système en push, où le service observé envoie ses métriques vers un collecteur. Le pull a un avantage direct : si un service ne répond plus, Prometheus le sait immédiatement, la métrique up passe à 0, sans dépendre du service en panne pour signaler lui-même son absence. C'est exactement ce que la pratique de ce module te fait provoquer avec casser api.
| Terme | Ce que c'est | Dans le labo |
|---|---|---|
| scrape | Une lecture de /metrics par Prometheus | Toutes les 15 s, sur chacune des 8 cibles |
| target (cible) | Une URL /metrics que Prometheus lit | http://api:8000/metrics, http://node-exporter:9100/metrics… |
| exporter | Un programme qui expose au format Prometheus les métriques d'un système qui ne le parle pas nativement | node-exporter (la machine hôte), cadvisor (les conteneurs) |
| job | Un groupe de cibles qui font le même travail | job="api", job="prometheus", job="loki"… 8 jobs |
| instance | Une cible individuelle dans un job | instance="api:8000" |
up | La métrique que Prometheus fabrique lui-même à chaque scrape : 1 si la cible a répondu, 0 sinon | up{job="api"} vaut 1 en fonctionnement normal |
Les noms api:8000, node-exporter:9100 sont ceux du réseau interne de Docker Compose : c'est Prometheus, dans son conteneur, qui parle à l'API dans le sien. Depuis ta machine, la même page est http://localhost:8000/metrics. Documentation : Prometheus — Configuration, scrape_config et Prometheus — Jobs and instances.
Un log structuré est écrit dans un format que la machine découpe sans deviner (JSON, le plus souvent), plutôt qu'une phrase libre. L'API écrit sur sa sortie standard une ligne JSON par requête traitée. Voici une ligne réelle, lue par .\labo.ps1 journal api sur la machine du cours :
{"horodatage": "2026-09-15T19:33:25.839+00:00", "niveau": "INFO", "id_requete": "46bb533b33e9", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 13.9, "message": "GET /cours/C0038 -> 200"}| Champ | Exemple | Ce que c'est |
|---|---|---|
horodatage | 2026-09-15T19:33:25.839+00:00 | L'instant exact de l'événement, au format ISO 8601, en UTC (+00:00) |
niveau | INFO | La gravité : INFO (2xx), WARNING (404, 422), ERROR (500). Trois valeurs dans le labo |
id_requete | 46bb533b33e9 | Un identifiant unique de 12 caractères généré pour cette requête, renvoyé au client dans l'en-tête HTTP x-id-requete |
methode | GET | La méthode HTTP |
route | /cours/{id} | Le gabarit de la route, le même que dans la métrique |
code | 200 | Le code de réponse HTTP |
duree_ms | 13.9 | La durée de traitement, en millisecondes |
message | GET /cours/C0038 -> 200 | La phrase lisible : c'est ici, et seulement ici, qu'apparaît l'identifiant réel du cours |
Ce qu'il faut voir : route garde le gabarit /cours/{id} (comme la métrique), mais message contient C0038, l'identifiant réel. Un log peut se permettre ce détail : Loki n'indexe pas le texte du message, il indexe seulement quatre labels (service, conteneur, niveau, code) extraits par Alloy. Le module 5 te montre comment Alloy lit le JSON et fabrique ces labels. Documentation : Grafana Loki — Labels.
Les autres services du labo n'écrivent pas tous du JSON. Le webhook, au démarrage, écrit du texte libre : INFO: Uvicorn running on http://0.0.0.0:8090 (Press CTRL+C to quit). Le service charge écrit du JSON, mais avec d'autres champs : toutes les 30 secondes, un résumé {"niveau": "INFO", "message": "résumé des 30 dernières secondes", "requetes": {"200": 214, "total": 262, "201": 26, "500": 4, "404": 16, "422": 2}}. Un log structuré n'est pas un format universel : c'est une décision prise service par service.
Une trace suit une requête unique à travers plusieurs services : chaque étape (un span) enregistre son nom, sa durée, et sa relation parent-enfant avec les autres. Elle répond à « la requête a mis 800 ms, dans quel service ce temps a-t-il été passé ? ». Le labo n'installe aucun outil de traçage : l'API catalogue et le générateur de charge sont les deux seuls services applicatifs, reliés par un simple appel HTTP, ce qui limite l'intérêt pédagogique d'une trace distribuée ici. Le id_requete du log est déjà le premier morceau d'une trace : c'est lui qu'OpenTelemetry appellerait un trace id. Le module 7 en dit un mot de plus. Documentation : OpenTelemetry — Traces.
| Service | Rôle | Port | URL depuis ta machine |
|---|---|---|---|
prometheus | Lit les cibles, stocke les séries, évalue les règles | 9090 | http://localhost:9090 |
alertmanager | Reçoit les alertes de Prometheus, les groupe et les route | 9093 | http://localhost:9093 |
grafana | Explore et visualise Prometheus, Loki et Alertmanager | 3000 (GRAFANA_PORT) | http://localhost:3000 (admin / aiopsatlas2026) |
loki | Stocke les logs, indexés par labels | 3100 | http://localhost:3100/ready |
alloy | Découvre les conteneurs et transporte leurs logs vers Loki | 12345 | http://localhost:12345 |
node-exporter | Métriques de la machine hôte | 9100 | http://localhost:9100/metrics |
cadvisor | Métriques de chaque conteneur | 8080 | http://localhost:8080 |
api | Le service observé | 8000 | http://localhost:8000/cours · http://localhost:8000/metrics |
webhook | Reçoit et affiche les alertes | 8090 | http://localhost:8090 |
charge | Génère du trafic vers l'API | aucun | aucune : il n'a pas d'interface, seulement un journal |
Neuf ports, dix services : charge n'écoute sur rien. Huit cibles Prometheus, dix services : charge et webhook n'exposent pas de /metrics.
Ce pas à pas suppose le labo démarré (leçon 04). Si tu lis cette leçon avant, garde-le pour plus tard : chaque étape se fait dans le navigateur ou dans un terminal, en lecture seule.
Ouvre http://localhost:8000/metrics dans le navigateur. C'est la page brute que Prometheus lit toutes les 15 secondes : du texte, une ligne par série, précédée de ses lignes # HELP et # TYPE. Sur la machine du cours, elle compte 271 lignes. Compare avec http://localhost:9100/metrics (node-exporter : 1578 lignes) et http://localhost:8080/metrics (cAdvisor : 3444 lignes, pour dix conteneurs).
Ce qu'il faut voir : les lignes qui commencent par python_ et process_ en haut de la page de l'API ne sont pas écrites par le labo. La bibliothèque prometheus_client les ajoute d'elle-même (mémoire du processus, ramasse-miettes Python). Les métriques du cours commencent à http_requetes_total.
Cherche les quatre # TYPE de l'API. Dans la page, cherche (Ctrl+F) # TYPE http_ : tu trouves counter pour http_requetes_total et histogram pour http_duree_requete_seconds. Cherche # TYPE requetes_en_cours : gauge. Cherche summary : aucun résultat.
Ce qu'il faut voir : les trois types utilisés, et l'absence volontaire du quatrième.
Compte les séries de http_requetes_total dans Prometheus. Ouvre http://localhost:9090, tape http_requetes_total dans le champ de requête et exécute. Sur la machine du cours, le tableau affiche 14 lignes, dont :
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 32Ce qu'il faut voir : Prometheus a ajouté trois labels à ceux de la page /metrics : job="api" et instance="api:8000" (qui identifient la cible) et service="api" (ajouté par la configuration du job dans prometheus.yml). La valeur 3241 est un peu plus faible que le 3247 lu à l'étape 1 : Prometheus montre le dernier scrape, qui date de 0 à 15 secondes.
Lis une ligne de log de l'API dans le terminal. Depuis le dossier lab3 :
.\labo.ps1 journal apiSur la machine du cours, les dernières lignes ressemblent à :
labo-api | {"horodatage": "2026-09-15T19:33:25.916+00:00", "niveau": "INFO", "id_requete": "5ccaee4d1a2a", "methode": "GET", "route": "/sante", "code": 200, "duree_ms": 1.0, "message": "GET /sante -> 200"}
labo-api | {"horodatage": "2026-09-15T19:33:26.217+00:00", "niveau": "INFO", "id_requete": "e64206564a68", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 8.2, "message": "GET /cours/C0043 -> 200"}Ce qu'il faut voir : GET /sante toutes les quelques secondes, c'est Docker qui vérifie la santé du conteneur (le healthcheck) ; le reste, c'est le service charge. Le préfixe labo-api | est ajouté par Docker Compose, il ne fait pas partie du JSON.
Vérifie les labels que Loki connaît. Ouvre http://localhost:3100/loki/api/v1/labels :
{"status":"success","data":["code","conteneur","niveau","service"]}Puis http://localhost:3100/loki/api/v1/label/niveau/values :
{"status":"success","data":["ERROR","INFO","WARNING"]}Ce qu'il faut voir : quatre labels, pas huit. Loki n'indexe ni id_requete, ni duree_ms, ni message : ces champs restent dans le texte de la ligne, où LogQL peut les extraire à la demande avec | json (module 5). Les trois valeurs de niveau confirment le tableau plus haut.
Les messages ci-dessous ont été provoqués pour de vrai dans Prometheus, sur la machine du cours. Ils reviendront au module 2 ; autant les reconnaître dès maintenant.
Oublier les guillemets autour d'une valeur de label : up{job=api} renvoie
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected stringUne valeur de label est toujours une chaîne entre guillemets : up{job="api"}. Même pour un nombre : http_requetes_total{code=500} renvoie parse error: unexpected character inside braces: '5' ; il faut écrire code="500".
Appeler rate() sans fenêtre de temps : rate(http_requetes_total) renvoie
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vectorrate() a besoin d'un intervalle entre crochets : rate(http_requetes_total[1m]). Sans crochets, tu lui donnes la dernière valeur (un instant vector) alors qu'il lui faut une suite de valeurs (un range vector).
Appeler rate() sur une jauge : rate(requetes_en_cours[1m]) ne provoque pas d'erreur, mais Prometheus affiche un avertissement :
PromQL info: metric might not be a counter, name does not end in _total/_sum/_count/_bucket: "requetes_en_cours" (1:6)Le résultat (0.017… sur la machine du cours) n'a aucun sens : la vitesse d'augmentation d'une valeur qui monte et descend n'est pas une information. rate() est réservé aux compteurs.
Se tromper dans le nom d'une métrique : http_request_total (au singulier, sans le e français) renvoie un résultat vide, sans message d'erreur. Prometheus ne connaît pas cette métrique, il ne la corrige pas. Le nom exact est http_requetes_total. De même, http_requetes_total{route="/inexistant"} renvoie un résultat vide : la route inconnue est comptée sous route="inconnue", pas sous son chemin réel.
Les quatre types de métriques Prometheus sont le compteur (http_requetes_total, qui n'augmente que dans un sens et se lit avec rate()), la jauge (requetes_en_cours, qui monte et descend), l'histogramme (http_duree_requete_seconds, douze lignes par route : dix _bucket cumulatifs, _count, _sum) et le résumé (absent du labo, parce que ses quantiles ne s'agrègent pas entre instances). Une série temporelle est une combinaison unique de nom et de labels ; http_requetes_total en a 14 sur la machine du cours. La cardinalité explique pourquoi la métrique porte route="/cours/{id}" alors que le log contient C0038 dans son message. Prometheus fonctionne en pull : il lit (scrape) huit cibles (targets) toutes les 15 secondes, regroupées par job, identifiées par instance, et fabrique lui-même la métrique up. Un log structuré du labo est une ligne JSON à huit champs, dont id_requete, renvoyé aussi dans l'en-tête x-id-requete ; Loki n'en indexe que quatre labels : service, conteneur, niveau, code. Dix services, neuf ports (charge n'en a pas), huit cibles Prometheus (charge et webhook n'exposent pas de /metrics).
/metrics que tu as ouverte à l'étape 1 (les lignes # HELP, # TYPE, l'échappement des labels)._total pour un compteur, _seconds pour une durée, _bytes pour une taille ; l'API du labo respecte ces conventions, à une exception près : les noms sont en français.histogram_quantile.api/app.py pour exposer les métriques ; le module 3 te fait ajouter ta propre métrique avec elle.up, filtrer par label, calculer un rate(), lire un histogramme avec histogram_quantile().