Les trois piliers et le vocabulaire

18 min
Public
débutant, leçon 01 lue
Durée
35 à 45 min
Module
1/7
Compétence visée
reconnaître les quatre types de métriques Prometheus sur les vraies lignes de /metrics de l'API du labo, comprendre les labels et la cardinalité, distinguer pull et push, lire une ligne de log JSON champ par champ, et situer les dix services du labo par leur port et leur URL

En une image

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.

Comment ça marche

Les quatre types de métriques, sur les vraies lignes du labo

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 :

text
# 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.0

Ce 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é :

text
# HELP requetes_en_cours Nombre de requêtes HTTP en cours de traitement à cet instant.
# TYPE requetes_en_cours gauge
requetes_en_cours 1.0

Une 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 :

text
# 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.42293146001248

Chaque _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.

TypeCe qu'il mesureDans le laboQuestion à laquelle il répond
CompteurUn cumul qui ne fait qu'augmenterhttp_requetes_total, inscriptions_total, cours_consultes_total« Combien de requêtes par seconde ? » (avec rate)
JaugeUne valeur instantanée qui monte et descendrequetes_en_cours, api_disque_libre_octets, api_info« Combien en ce moment ? »
HistogrammeUne distribution, par tranches cumulativeshttp_duree_requete_seconds« 95 % des requêtes prennent moins de combien ? »
RésuméUne distribution, quantiles calculés côté clientaucunLa même, mais sans pouvoir agréger entre instances

Série temporelle, labels et cardinalité

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.

Pull, scrape, target, exporter, job, instance

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.

TermeCe que c'estDans le labo
scrapeUne lecture de /metrics par PrometheusToutes les 15 s, sur chacune des 8 cibles
target (cible)Une URL /metrics que Prometheus lithttp://api:8000/metrics, http://node-exporter:9100/metrics
exporterUn programme qui expose au format Prometheus les métriques d'un système qui ne le parle pas nativementnode-exporter (la machine hôte), cadvisor (les conteneurs)
jobUn groupe de cibles qui font le même travailjob="api", job="prometheus", job="loki"… 8 jobs
instanceUne cible individuelle dans un jobinstance="api:8000"
upLa métrique que Prometheus fabrique lui-même à chaque scrape : 1 si la cible a répondu, 0 sinonup{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.

Les logs structurés : une ligne JSON par événement

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 :

json
{"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"}
ChampExempleCe que c'est
horodatage2026-09-15T19:33:25.839+00:00L'instant exact de l'événement, au format ISO 8601, en UTC (+00:00)
niveauINFOLa gravité : INFO (2xx), WARNING (404, 422), ERROR (500). Trois valeurs dans le labo
id_requete46bb533b33e9Un 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
methodeGETLa méthode HTTP
route/cours/{id}Le gabarit de la route, le même que dans la métrique
code200Le code de réponse HTTP
duree_ms13.9La durée de traitement, en millisecondes
messageGET /cours/C0038 -> 200La 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.

Les traces : mention, pas d'outil dans ce labo

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.

Les dix services : rôle, port, URL

ServiceRôlePortURL depuis ta machine
prometheusLit les cibles, stocke les séries, évalue les règles9090http://localhost:9090
alertmanagerReçoit les alertes de Prometheus, les groupe et les route9093http://localhost:9093
grafanaExplore et visualise Prometheus, Loki et Alertmanager3000 (GRAFANA_PORT)http://localhost:3000 (admin / aiopsatlas2026)
lokiStocke les logs, indexés par labels3100http://localhost:3100/ready
alloyDécouvre les conteneurs et transporte leurs logs vers Loki12345http://localhost:12345
node-exporterMétriques de la machine hôte9100http://localhost:9100/metrics
cadvisorMétriques de chaque conteneur8080http://localhost:8080
apiLe service observé8000http://localhost:8000/cours · http://localhost:8000/metrics
webhookReçoit et affiche les alertes8090http://localhost:8090
chargeGénère du trafic vers l'APIaucunaucune : 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.

Pas à pas

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.

  1. 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.

  2. 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.

  3. 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 :

    text
    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"}   32

    Ce 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.

  4. Lis une ligne de log de l'API dans le terminal. Depuis le dossier lab3 :

    powershell
    .\labo.ps1 journal api

    Sur la machine du cours, les dernières lignes ressemblent à :

    text
    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.

  5. Vérifie les labels que Loki connaît. Ouvre http://localhost:3100/loki/api/v1/labels :

    json
    {"status":"success","data":["code","conteneur","niveau","service"]}

    Puis http://localhost:3100/loki/api/v1/label/niveau/values :

    json
    {"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.

Si ça coince

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

    text
    invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected string

    Une 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

    text
    invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vector

    rate() 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 :

    text
    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.

À retenir

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).

Pour aller plus loin

  • Prometheus — Exposition formats : la grammaire exacte de la page /metrics que tu as ouverte à l'étape 1 (les lignes # HELP, # TYPE, l'échappement des labels).
  • Prometheus — Metric and label naming : pourquoi _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.
  • Prometheus — Histograms and summaries : l'article de référence sur le choix histogramme / résumé, avec les erreurs d'approximation d'histogram_quantile.
  • prometheus_client (Python) : la bibliothèque utilisée par api/app.py pour exposer les métriques ; le module 3 te fait ajouter ta propre métrique avec elle.
  • Grafana Loki — Labels : pourquoi peu de labels, et jamais un identifiant unique dedans : la même règle de cardinalité que pour Prometheus.
  • Le module 2 reprend chaque terme en pratique : écrire up, filtrer par label, calculer un rate(), lire un histogramme avec histogram_quantile().