Comment lire cette page. Chaque section est repliée sous son titre : clique sur « Afficher … » pour l'ouvrir, et referme-la quand tu as fini pour garder la page lisible. Ordre de lecture : Objectif, puis En bref (les commandes à taper), puis Le jeu de données (à lire avant toute requête), puis les requêtes Prometheus (P1 à P12) et Grafana Explore (G1 à G8), classées de la plus simple (
up,{service="api"}) à la plus parlante, avec une explication après chacune. Le pas à pas détaillé, avec la sortie attendue de chaque commande et la panne à provoquer, est en annexe : annexe A pour Windows (PowerShell), annexe B pour Linux, macOS, WSL 2 et Git Bash. Ouvre une seule annexe, celle de ton système. L'annexe C, commune, regroupe les cas où ça coince. Toutes les sorties de cette page ont été capturées sur le labo du cours ; les valeurs qui dépendent du moment (compteurs, durées, horodatages) seront différentes chez toi, les formes seront identiques.
Tu rejoins l'équipe qui exploite le catalogue de cours d'une plateforme en ligne. Ta cheffe te tend le kit du labo : « Demain matin, je veux la pile d'observabilité qui tourne sur ton poste, l'API dedans, et la preuve que tu sais lire une panne sans m'appeler. » Tu vas donc démarrer les dix services, prouver que Prometheus lit bien ses huit cibles et que Loki reçoit les journaux de l'API, taper douze requêtes PromQL et huit requêtes LogQL pour apprendre à lire ce que le labo mesure, puis arrêter l'API exprès. Tu regarderas la panne se propager : etat la voit en deux secondes, Prometheus met la cible en DOWN, l'alerte APIInjoignable passe de pending à firing, Alertmanager l'envoie au webhook. Puis tu répares et tu regardes l'alerte s'éteindre. Reconnaître « ce service est arrêté » en dix secondes, et savoir où le chercher, c'est ce qui évite des heures de recherche au mauvais endroit.
Les neuf étapes de ce schéma sont détaillées, avec la sortie attendue de chaque commande, dans l'annexe A (Windows) ou l'annexe B (Linux, macOS) en bas de page.
Kit du labo : https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr
Tu clones le kit dans un dossier lab3, tu vérifies que Docker est prêt, tu démarres les dix services, tu ouvres les pages web, tu tapes les requêtes, puis tu casses et tu répares. À la fin, etat doit afficher Labo : 10/10 services, 8/8 cibles up, 0 alertes actives., l'API doit connaître 64 cours, et le webhook doit avoir reçu deux notifications pour APIInjoignable : une firing, une resolved. Commence par exécuter ce bloc.
Windows (PowerShell)
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
ls # explorer le contenu : docker-compose.yml, labo.ps1, labo.sh, api/, prometheus/, grafana/, modules/
.\labo.ps1 prerequis
.\labo.ps1 demarrer
.\labo.ps1 etat # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Attends deux minutes (le temps que Prometheus ait quelques mesures), puis ouvre les pages dans le navigateur :
Prometheus http://localhost:9090 (Status → Target health : 8 cibles UP ; onglet Graph pour P1 à P12)
Grafana http://localhost:3000 (utilisateur admin · mot de passe aiopsatlas2026 ; menu → Explore, source Loki pour G1 à G8)
Alertmanager http://localhost:9093 (vide au départ)
API catalogue http://localhost:8000/cours · http://localhost:8000/metrics
Webhook http://localhost:8090 (vide au départ)Les requêtes P1 à P12 sont aussi dans modules\01-le-labo\requetes.txt, et G1 à G8 dans modules\01-le-labo\requetes-logql.txt : ouvre-les dans un éditeur et copie-colle. Puis la panne :
.\labo.ps1 casser api # arrête le conteneur de l'API ; la charge continue de frapper dans le vide
.\labo.ps1 etat # attendu : 9/10 services, 7/8 cibles up ; regarde aussi Targets, Alerts, 9093 et 8090
.\labo.ps1 reparer # redémarre l'API
.\labo.ps1 etat # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Si PowerShell refuse .\labo.ps1 (« l'exécution de scripts est désactivée ») : Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, réponds O, relance. Si le port 3000 est déjà pris sur ta machine, $env:GRAFANA_PORT = '3001' avant demarrer, et remplace 3000 par 3001 dans les URL de Grafana.
Linux, macOS, WSL 2, Git Bash
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
ls # explorer le contenu : docker-compose.yml, labo.sh, labo.ps1, api/, prometheus/, grafana/, modules/
./labo.sh prerequis
./labo.sh demarrer
./labo.sh etat # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Attends deux minutes, puis ouvre les pages dans le navigateur :
Prometheus http://localhost:9090 (Status → Target health : 8 cibles UP ; onglet Graph pour P1 à P12)
Grafana http://localhost:3000 (utilisateur admin · mot de passe aiopsatlas2026 ; menu → Explore, source Loki pour G1 à G8)
Alertmanager http://localhost:9093 (vide au départ)
API catalogue http://localhost:8000/cours · http://localhost:8000/metrics
Webhook http://localhost:8090 (vide au départ)Les requêtes sont dans modules/01-le-labo/requetes.txt (P1 à P12) et modules/01-le-labo/requetes-logql.txt (G1 à G8). Puis la panne :
./labo.sh casser api # arrête le conteneur de l'API ; la charge continue de frapper dans le vide
./labo.sh etat # attendu : 9/10 services, 7/8 cibles up ; regarde aussi Targets, Alerts, 9093 et 8090
./labo.sh reparer # redémarre l'API
./labo.sh etat # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Si le port 3000 est pris : GRAFANA_PORT=3001 ./labo.sh demarrer, puis 3001 dans les URL de Grafana.
Avant de taper une seule requête, regarde ce que le labo observe. Tout tourne autour d'une API de catalogue de cours : un petit service web écrit en Python (FastAPI) qui sert 64 cours et enregistre des inscriptions. Un deuxième service, charge, joue le rôle des utilisateurs : il appelle l'API en continu, avec des requêtes réussies, quelques 404 volontaires et, une fois sur cent, une erreur 500 que l'API fabrique elle-même. Tout ce que tu vas lire dans Prometheus et dans Loki vient de ces deux services. Le reste du kit, en clair :
lab3/
├── api/
│ ├── app.py l'API catalogue (FastAPI) : 6 routes publiques, 3 routes /admin
│ └── donnees/cours.json 64 cours → servis par GET /cours et GET /cours/{id}
├── charge/charge.py le générateur de trafic : GET /cours, /cours/{id}, POST /inscriptions, GET /lent, des 404
├── prometheus/
│ ├── prometheus.yml 8 cibles lues toutes les 15 s (scrape_interval: 15s)
│ └── regles/alertes.yml 10 règles d'alerte ; APIInjoignable est la première
├── alertmanager/alertmanager.yml groupe les alertes et les envoie au webhook (group_wait: 10s)
├── alloy/config.alloy lit les journaux des conteneurs et les pousse dans Loki
├── grafana/provisioning/ 3 tableaux de bord et 3 sources de données, créés au démarrage
└── modules/01-le-labo/
├── requetes.txt P1 à P12, à coller dans Prometheus
└── requetes-logql.txt G1 à G8, à coller dans Grafana ExploreOuvre les données toi-même, ça prend dix secondes :
# Windows (PowerShell), depuis le dossier lab3
Get-Content api\donnees\cours.json -TotalCount 16
Invoke-RestMethod "http://localhost:8000/cours?limite=2"
Invoke-RestMethod http://localhost:8000/cours/C0001
(Invoke-WebRequest http://localhost:8000/metrics -UseBasicParsing).Content -split "`n" | Select-String "^http_requetes_total"# Linux, macOS, WSL 2, Git Bash, depuis le dossier lab3
head -n 16 api/donnees/cours.json
curl -s "http://localhost:8000/cours?limite=2"
curl -s http://localhost:8000/cours/C0001
curl -s http://localhost:8000/metrics | grep "^http_requetes_total"api/donnees/cours.json est un tableau JSON de 64 objets, un par cours, identifiants C0001 à C0064. Le premier, tel que l'API le renvoie sur GET /cours/C0001 :
{"id":"C0001","titre":"Introduction à Python","categorie":"programmation","niveau":"debutant","prix":89,"duree_heures":6,"professeur":"Karim Haddad","tags":["code","algorithmes"],"note":4.8,"inscrits":2319}| Champ | Exemple | Ce que c'est |
|---|---|---|
id | C0001 | Identifiant du cours, C suivi de quatre chiffres, de C0001 à C0064 |
titre | Introduction à Python | Titre affiché |
categorie | programmation | Une des neuf catégories : cloud, donnees, gestion, ia, outils, programmation, securite, systemes, web |
niveau | debutant | debutant, intermediaire ou avance |
prix | 89 | Prix en dollars, entier |
duree_heures | 6 | Durée totale, en heures |
professeur | Karim Haddad | Un des dix professeurs du catalogue |
tags | ["code","algorithmes"] | Liste de mots-clés |
note | 4.8 | Note moyenne sur 5 |
inscrits | 2319 | Nombre d'inscrits au chargement ; les inscriptions faites pendant le labo sont comptées à part, dans la métrique inscriptions_total |
Les routes de l'API et ce qu'elles répondent sur le labo du cours :
| Route | Réponse réelle | Ce qu'elle fait |
|---|---|---|
GET /sante | {"etat":"ok","version":"1.0.0","cours":64} | Le test de santé que Docker appelle ; etat lit version et cours ici |
GET /cours?limite=2 | {"total":64,"page":1,"limite":2,"cours":[…]} | La liste paginée ; filtres categorie et niveau (?categorie=cloud → "total":6) |
GET /cours/C0001 | le document ci-dessus | Une fiche ; l'API compte chaque consultation dans cours_consultes_total{cours_id="C0001"} |
GET /cours/C9999 | 404 {"detail":"cours C9999 introuvable"} | Un 404 propre : le service est sain, la ressource n'existe pas |
POST /inscriptions | 201 (ou 404 si le cours n'existe pas, 422 si le corps est invalide) | Appelée par charge ; incrémente inscriptions_total{cours_id="…"} |
GET /lent | {"attente_ms":305} | Une route volontairement lente (300 à 900 ms) pour nourrir l'histogramme de latence |
GET /admin/etat | {"taux_erreurs":0.01,"lenteur_ms":0,"inscriptions_enregistrees":855,…} | Les réglages de panne ; casser erreurs et casser lenteur les changent, reparer les remet |
GET /metrics | environ 270 lignes de texte | Ce que Prometheus lit toutes les 15 secondes |
Chaque réponse porte un en-tête x-id-requete (par exemple x-id-requete: c5c49525ea53) : c'est le même identifiant que le champ id_requete de la ligne de log écrite pour cette requête. Il te servira à la requête G7.
/metrics expose, une vraie ligne par typeLa page http://localhost:8000/metrics est du texte, une série par ligne, précédée de deux lignes de commentaire # HELP (à quoi sert la métrique) et # TYPE (son type). Sur le labo du cours, elle fait environ 270 lignes. Les quatre métriques que tu vas interroger, copiées de la page :
# 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="500",methode="GET",route="/cours"} 32.0
# 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
# 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
# HELP api_info Informations sur l'API (toujours 1) ; la version est dans le label.
# TYPE api_info gauge
api_info{version="1.0.0"} 1.0| Type | Métrique du labo | Comment la lire |
|---|---|---|
| counter (compteur) | http_requetes_total | Ne fait que monter : 3247 réponses 200 sur /cours depuis le démarrage de l'API. Seule sa vitesse a un sens (P5) |
| gauge (jauge) | requetes_en_cours | Monte et descend : 1 requête en train d'être traitée à l'instant de la lecture. Se lit telle quelle (P11) |
| histogram (histogramme) | http_duree_requete_seconds | Des paniers cumulatifs : 3248 requêtes sur /cours ont pris moins de 50 ms (le="0.05"), 3279 en tout (+Inf = _count). _sum / _count = durée moyenne (ici 26 ms). P10 en tire un p95 |
| info (une jauge à 1) | api_info | La valeur est toujours 1 ; l'information est dans le label version="1.0.0" |
Il n'y a pas de summary dans cette API : Prometheus le déconseille au profit de l'histogramme, qui s'agrège entre instances.
Une série est un nom de métrique plus un jeu de paires label="valeur". Deux origines :
| Label | Posé par | Valeurs sur le labo |
|---|---|---|
methode | l'API | GET, POST |
route | l'API | /sante, /cours, /cours/{id}, /inscriptions, /lent, /admin/etat, inconnue (toute URL qui n'existe pas) |
code | l'API | 200, 201, 404, 422, 500 |
le | l'API, sur l'histogramme seulement | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, +Inf |
cours_id | l'API, sur inscriptions_total et cours_consultes_total | C0001 à C0064 |
job | Prometheus, d'après prometheus.yml | prometheus, api, node-exporter, cadvisor, alertmanager, grafana, loki, alloy |
instance | Prometheus | l'adresse lue : api:8000, localhost:9090, grafana:3000… |
service | Prometheus, ajouté à la main dans prometheus.yml pour le job api | api |
Remarque la route /cours/{id} : l'API normalise l'URL avant de compter. /cours/C0001 et /cours/C0043 tombent dans la même série. Sans ça, il y aurait 64 séries par code au lieu d'une, et 64 fois plus de lignes dans /metrics.
L'API écrit une ligne de journal par requête traitée, en JSON. Alloy lit la sortie de chaque conteneur labo-* et la pousse dans Loki. Une vraie ligne, lue avec .\labo.ps1 journal api (ou ./labo.sh journal api) :
{"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, "duree_ms": 0.1, "message": "GET /cours/C0019 -> 500"}| Champ | Exemple | Ce que c'est |
|---|---|---|
horodatage | 2026-09-15T19:33:14.781+00:00 | Date et heure UTC, à la milliseconde |
niveau | ERROR | INFO (2xx), WARNING (4xx), ERROR (5xx) |
id_requete | dc1c2ff189a6 | L'identifiant renvoyé dans l'en-tête x-id-requete de la réponse |
methode, route, code | GET, /cours/{id}, 500 | Les mêmes valeurs que les labels de http_requetes_total : c'est le pont entre métriques et journaux |
duree_ms | 0.1 | Durée de traitement, en millisecondes |
message | GET /cours/C0019 -> 500 | La phrase lisible, avec l'URL réelle cette fois (C0019, pas {id}) |
Loki ne lit pas le JSON ligne par ligne au moment de la requête, sauf si tu le lui demandes (| json, requête G5). Ce qu'il indexe, ce sont des labels posés par Alloy à l'arrivée :
| Label Loki | Valeurs | Posé par |
|---|---|---|
service | api, charge, webhook, prometheus, alertmanager, grafana, loki, alloy, node-exporter, cadvisor | Alloy, d'après le nom du service Compose |
conteneur | labo-api, labo-charge… | Alloy, d'après le nom du conteneur |
niveau | INFO, WARNING, ERROR | Alloy, extrait du champ niveau du JSON |
code | 200, 201, 404, 422, 500 | Alloy, extrait du champ code du JSON |
detected_level | info, warn, error | Loki lui-même, qui devine le niveau ; tu peux l'ignorer |
Retiens deux choses que tu vas retrouver partout : la série http_requetes_total{code="500",route="/cours"}, à 32 sur /metrics au moment de la capture, que tu reverras à 32 dans P4 ; et l'identifiant dc1c2ff189a6, celui de la ligne d'erreur ci-dessus, que tu retrouveras dans G2 puis que tu iras chercher seul dans G7. Les métriques comptent, les journaux racontent ; les deux parlent de la même requête.
Les deux sections qui suivent contiennent vingt requêtes : douze pour Prometheus (P1 à P12), huit pour Loki à travers Grafana Explore (G1 à G8). Elles sont classées de la plus simple à la plus parlante, et chacune n'ajoute qu'une seule nouveauté par rapport à la précédente. Si une requête te paraît obscure, c'est presque toujours que la précédente n'est pas encore claire : reviens en arrière plutôt que de continuer.
Tape chaque requête toi-même, compare le résultat à celui de la page, lis l'explication, puis passe à la suivante. Les chiffres seront différents chez toi : les compteurs montent depuis le démarrage de ton API, pas de celle du cours. Les formes (le nombre de séries, les labels, l'ordre de grandeur) doivent être les mêmes. Prometheus et Loki utilisent la même idée de départ, un jeu de labels entre accolades, et c'est exprès : ce que tu apprends dans P2 sert dans G1.
Ouvre http://localhost:9090. Tu arrives sur la page Query (le menu du haut propose Query, Alerts, Status). Colle une requête dans le champ, appuie sur Entrée ou clique Execute. Le résultat s'affiche en dessous dans l'onglet Table (une ligne par série, la valeur à droite) ; l'onglet Graph dessine les mêmes séries dans le temps. Reste sur Table pour cette section, sauf indication contraire. Sous les onglets, une ligne du type Load time: 40ms Result series: 8 te dit combien de séries ont répondu.
L'image à garder en tête : Prometheus est un carnet de relevés. Toutes les 15 secondes, il passe devant chacune de ses huit cibles, lit leur page /metrics et note chaque valeur avec l'heure. Une requête PromQL, c'est une question posée à ce carnet.
upup{instance="localhost:9090", job="prometheus"} 1
up{instance="alloy:12345", job="alloy"} 1
up{instance="api:8000", job="api", service="api"} 1
up{instance="cadvisor:8080", job="cadvisor"} 1
up{instance="alertmanager:9093", job="alertmanager"} 1
up{instance="loki:3100", job="loki"} 1
up{instance="node-exporter:9100", job="node-exporter"} 1
up{instance="grafana:3000", job="grafana"} 1Result series: 8, toutes à 1.
Ce que la requête demande : « Donne-moi la dernière valeur de la métrique up pour toutes les cibles. »
Zéro paramètre : juste un nom de métrique. up n'est exposée par aucune cible ; c'est Prometheus qui la fabrique à chaque scrape : 1 si la page /metrics a répondu, 0 sinon. Huit séries parce que prometheus.yml déclare huit jobs. Chaque ligne se lit : le nom de la métrique, puis entre accolades les labels que Prometheus a posés (job et instance sur toutes, service en plus sur l'API), puis la valeur. Équivalent SQL : SELECT * FROM up. Ce que Prometheus fait que SQL ne fait pas : il a produit lui-même cette table en allant frapper à huit portes.
Le labo a dix conteneurs mais Prometheus n'en lit que huit : charge et webhook n'exposent pas de page /metrics dans cette version du kit, donc ils ne sont pas des cibles. etat compte les deux à part : 10/10 services (les conteneurs) et 8/8 cibles up (les scrapes). Si un jour up rend 7 séries au lieu de 8, ce n'est pas qu'une cible est tombée (elle serait à 0) : c'est qu'un job a disparu de la configuration. Le kit a une alerte pour ça, CibleAbsente.
up{job="api"}up{instance="api:8000", job="api", service="api"} 1Ce que la requête demande : « La valeur de up, seulement pour les séries dont le label job vaut api. »
Une seule nouveauté : le sélecteur {job="api"}. Les accolades filtrent sur les labels, comme un WHERE job = 'api'. Les guillemets sont obligatoires autour de la valeur : up{job=api} est refusé avec parse error: unexpected identifier "api" in label matching, expected string. C'est exactement cette expression, up{job="api"} == 0, que la règle APIInjoignable surveille ; tu la verras passer à 0 dans l'annexe.
http_requetes_totalhttp_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/sante", service="api"} 91
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"} 2714
http_requetes_total{code="404", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"} 209
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/lent", service="api"} 314
http_requetes_total{code="404", instance="api:8000", job="api", methode="GET", route="inconnue", service="api"} 312
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241
http_requetes_total{code="201", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"} 855
http_requetes_total{code="404", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"} 46
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 32
http_requetes_total{code="422", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"} 51
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"} 27
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/admin/etat", service="api"} 14
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/lent", service="api"} 1
http_requetes_total{code="500", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"} 7Result series: 14 sur le labo du cours (le nombre exact dépend des combinaisons que charge a déjà produites ; il monte jusqu'à 16 avec le temps).
Ce que la requête demande : « Toutes les séries du compteur http_requetes_total, avec leur valeur actuelle. »
Rien de neuf dans la syntaxe : un nom, comme P1. Ce qui est neuf, c'est ce que tu lis. Compare avec la page /metrics : la ligne http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0 est devenue http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241. Prometheus a ajouté trois labels (instance, job, service) et la valeur diffère de quelques unités : la page a été lue à un autre instant. Une série par combinaison (methode, route, code) : c'est ce qu'on appelle la cardinalité de la métrique, ici 14.
La différence essentielle entre
/metricset Prometheus. La page/metricsest l'état de l'API à l'instant où tu l'ouvres, sans historique. Prometheus garde toutes les lectures, une toutes les 15 secondes, et c'est ça qui permet P5 : calculer une vitesse suppose d'avoir au moins deux points.
http_requetes_total{code="500"}http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 32
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"} 27
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/lent", service="api"} 1
http_requetes_total{code="500", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"} 7Ce que la requête demande : « Les séries de http_requetes_total dont le label code vaut 500. »
Rien de neuf : c'est P2 appliqué à P3. Quatre séries, une par route touchée. Le fil rouge est là : route="/cours" à 32, la valeur lue sur /metrics. Le 500 est une chaîne, pas un nombre : http_requetes_total{code=500} est refusé (parse error: unexpected character inside braces: '5'). Ces 500 ne sont pas une panne : charge provoque volontairement une erreur sur cent (taux_erreurs: 0.01 dans /admin/etat) pour que les courbes d'erreur ne soient jamais vides.
Bilan de P1 à P4 : tu n'as encore rien calculé. Tu as lu des valeurs instantanées et appris à les filtrer par label.
rate(http_requetes_total[1m]){code="200", instance="api:8000", job="api", methode="GET", route="/sante", service="api"} 0.11112345816201799
{code="200", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"} 3.000333370374486
{code="404", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"} 0.2666962995888432
{code="200", instance="api:8000", job="api", methode="GET", route="/lent", service="api"} 0.42226914101566837
{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3.733748194243805
{code="201", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"} 0.8667629736637403
{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 0.0222246916324036
…Result series: 14, en requêtes par seconde.
Ce que la requête demande : « Pour chaque série du compteur, de combien a-t-elle augmenté par seconde, en moyenne, sur la dernière minute ? »
Une seule nouveauté, en deux morceaux inséparables : [1m] transforme la série en plage (toutes les valeurs de la dernière minute, au lieu de la dernière seule), et rate() calcule la pente de cette plage. Regarde le résultat : le nom de la métrique a disparu des accolades, parce que ce n'est plus http_requetes_total, c'est une vitesse dérivée. /cours reçoit 3,7 requêtes par seconde, /sante 0,11 (une toutes les 9 secondes : c'est le healthcheck de Docker). Équivalent SQL : il n'y en a pas de simple ; il faudrait deux lectures, une soustraction et une division par le temps écoulé. rate() fait ça pour chaque série, et corrige en plus les remises à zéro quand l'API redémarre.
Sans la plage, Prometheus refuse : rate(http_requetes_total) donne parse error: expected type range vector in call to function "rate", got instant vector. Tu liras ce message souvent ; il veut dire « il manque […] ».
3241 requêtes 200 sur /cours ne dit rien : depuis quand ? Si l'API tourne depuis une heure, c'est calme ; depuis une minute, c'est une attaque. Un compteur ne vaut que par sa pente. C'est pour ça que sur un tableau de bord, tu ne verras jamais http_requetes_total brut, mais toujours rate(http_requetes_total[…]). La fenêtre [1m] lisse sur une minute ; [5m] lisse plus (courbe plus calme, réaction plus lente). Le kit utilise [5m] dans ses alertes et [1m] ici pour que tu voies bouger quelque chose. Règle pratique : la fenêtre doit contenir au moins deux scrapes, donc ici au moins [30s] ; rate(http_requetes_total[10s]) rend un résultat vide.
sum by (route) (rate(http_requetes_total[1m])){route="/sante"} 0.11112345816201799
{route="/cours/{id}"} 3.3114790532281364
{route="/lent"} 0.42226914101566837
{route="inconnue"} 0.3778197577508612
{route="/cours"} 3.7559728858762087
{route="/inscriptions"} 0.9334370485609511
{route="/admin/etat"} 0.0444493832648072Result series: 7.
Ce que la requête demande : « Prends les vitesses de P5 et additionne-les en gardant seulement le label route. »
Une seule nouveauté : l'agrégation sum by (route) (…). Elle fait fondre tous les autres labels (code, methode, instance…) et additionne ce qui reste. /cours/{id} passe de trois séries (200, 404, 500) à une : 3,00 + 0,27 + 0,04 = 3,31. Équivalent SQL : SELECT route, SUM(vitesse) FROM … GROUP BY route. Les parenthèses autour de route sont obligatoires : sum by route (…) est refusé (parse error: unexpected identifier "route" in grouping opts, expected "(").
sum by (code) (rate(http_requetes_total[1m])){code="200"} 7.311923547060784
{code="404"} 0.6445160573397044
{code="201"} 0.8667629736637403
{code="500"} 0.0888987665296144
{code="422"} 0.0444493832648072Ce que la requête demande : « La même somme que P6, mais regroupée par code HTTP. »
Rien de neuf : P6 avec un autre label. C'est ce que fait le panneau « Réponses par code » du tableau de bord « API catalogue — signaux dorés » dans Grafana. Cinq codes, cinq lignes ; les 500 à 0,09 par seconde, soit un peu plus d'une erreur toutes les douze secondes.
sum(rate(http_requetes_total[1m])){} 8.956550727858652Ce que la requête demande : « Additionne toutes les vitesses, sans garder aucun label. »
Une seule nouveauté : sum(…) sans by. Résultat : une série unique, avec un jeu de labels vide ({}), la valeur 8,96 requêtes par seconde. C'est le premier des quatre signaux dorés, le trafic. Vérifie : la somme des sept lignes de P6 fait bien 8,96.
Bilan de P5 à P8 : tu sais transformer un compteur en vitesse, puis regrouper cette vitesse comme tu veux. Les trois quarts des tableaux de bord Prometheus ne font que ça.
sum(rate(http_requetes_total{code=~"5.."}[1m])) / sum(rate(http_requetes_total[1m])){} 0.009925558312655085Ce que la requête demande : « La vitesse des réponses dont le code commence par 5, divisée par la vitesse de toutes les réponses. »
Deux nouveautés, mais petites. D'abord =~ : un sélecteur par expression régulière, "5.." = un 5 suivi de deux caractères quelconques, donc tous les 5xx. Ensuite la division de deux résultats : Prometheus divise les séries qui ont les mêmes labels, et ici les deux côtés ont un jeu vide {}, donc ça s'apparie. Résultat : 0,0099, soit 1 % ; c'est la valeur réglée dans /admin/etat (taux_erreurs: 0.01). Deuxième signal doré, les erreurs. La règle d'alerte TauxErreursEleve du kit se déclenche quand cette même expression, calculée sur 5 minutes, dépasse 0,05.
histogram_quantile(0.95, sum by (le) (rate(http_duree_requete_seconds_bucket[5m]))){} 0.09797705555555555Ce que la requête demande : « À partir des paniers de l'histogramme de durée, toutes routes confondues, sous quelle valeur tombent 95 % des requêtes des 5 dernières minutes ? »
Une seule nouveauté : histogram_quantile(0.95, …). Elle veut en entrée les paniers _bucket sommés par le (c'est pour ça que le sum by (le) est obligatoire : sans lui, elle calcule un quantile par route et le résultat n'a plus le sens que tu attendais). Résultat : 0,098 seconde, donc 95 % des requêtes sont servies en moins de 98 ms. C'est le troisième signal doré, la latence, et la métrique que l'alerte LatenceP95Elevee surveille (seuil : 0,5 s). Regarde la répartition des paniers de /cours dans le jeu de données : 3248 requêtes sur 3279 sous 50 ms, mais /lent (300 à 900 ms) tire le p95 global vers le haut.
Chaque ligne _bucket{le="0.05"} compte les requêtes qui ont pris au plus 0,05 seconde (le = less or equal). Les paniers sont cumulatifs : le="0.1" contient aussi tout ce qui était dans le="0.05". Le dernier, le="+Inf", contient tout, et vaut toujours _count. histogram_quantile cherche le panier où la courbe cumulative franchit 95 % et interpole à l'intérieur. La précision dépend donc du choix des paniers : entre 0.05 et 0.1, Prometheus suppose une répartition uniforme. C'est pour ça que le résultat, 0.0979…, n'est pas une valeur mesurée mais une estimation.
requetes_en_coursrequetes_en_cours{instance="api:8000", job="api", service="api"} 0Ce que la requête demande : « La dernière valeur de la jauge requetes_en_cours. »
Rien de neuf dans la syntaxe, c'est P1. Ce qui est neuf, c'est le type : une jauge se lit telle quelle, sans rate(). Zéro ou un, selon l'instant : l'API traite chaque requête en quelques millisecondes, il est rare d'en attraper une en cours. C'est le quatrième signal doré, la saturation : si cette valeur montait à 50, l'API serait débordée. rate(requetes_en_cours[1m]) ne provoque pas d'erreur, mais rend un nombre qui ne veut rien dire ; Prometheus ne te protège pas de cette confusion.
ALERTSEmpty query resultCe que la requête demande : « Les alertes actuellement pending ou firing. »
Rien de neuf : un nom de métrique, comme P1. ALERTS est, comme up, fabriquée par Prometheus : une série par alerte active, avec les labels alertname et alertstate. Sur un labo sain, le résultat est vide : Empty query result. Ce n'est pas une erreur, c'est la meilleure réponse possible. Tu la retaperas pendant la panne de l'annexe et tu verras apparaître ALERTS{alertname="APIInjoignable", alertstate="pending", …} puis alertstate="firing".
Bilan de P9 à P12 : les quatre signaux dorés (trafic P8, erreurs P9, latence P10, saturation P11) tiennent en quatre requêtes, et les alertes sont une métrique comme les autres.
Le message à faire passer. Ce que SQL fait aussi : filtrer par colonne (
{job="api"}=WHERE), regrouper et sommer (sum by (route)=GROUP BY), diviser deux agrégats. Ce que seul Prometheus fait : il est allé chercher lui-même les données toutes les 15 secondes chez huit services, il transforme n'importe quel compteur en vitesse d'une fonction (rate), il estime un quantile depuis des paniers (histogram_quantile), et il expose ses propres alertes comme une métrique (ALERTS).
Ouvre http://localhost:3000 (utilisateur admin, mot de passe aiopsatlas2026). Dans le menu principal (icône en haut à gauche), clique Explore. En haut de la page, le sélecteur de source de données propose Prometheus, Loki et Alertmanager : choisis Loki. À droite du champ de requête, deux modes : Builder (des menus) et Code (tu tapes). Passe en Code, colle la requête, puis Run query (ou Maj+Entrée). Les journaux s'affichent en bas, la ligne la plus récente en premier. En haut à droite, le sélecteur de période est à Last 1 hour par défaut : garde-le. Ces étapes sont identiques sous Windows et sous Linux, c'est le navigateur qui travaille.
L'image à garder en tête : Loki est une armoire de journaux de bord, un classeur par combinaison de labels. Il ne lit pas le contenu des lignes pour les ranger, seulement l'étiquette du classeur. Une requête LogQL commence donc toujours par choisir un classeur, entre accolades, puis éventuellement par filtrer les lignes dedans.
{service="api"}2026-09-15 19:33:30.416 {"horodatage": "2026-09-15T19:33:30.416+00:00", "niveau": "INFO", "id_requete": "875939d9cdac", "methode": "GET", "route": "/cours", "code": 200, "duree_ms": 11.5, "message": "GET /cours -> 200"}
2026-09-15 19:33:30.186 {"horodatage": "2026-09-15T19:33:30.186+00:00", "niveau": "INFO", "id_requete": "6d6e21e6d17b", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 14.3, "message": "GET /cours/C0001 -> 200"}
2026-09-15 19:33:30.142 {"horodatage": "2026-09-15T19:33:30.142+00:00", "niveau": "INFO", "id_requete": "42a64fb435bf", "methode": "GET", "route": "/cours", "code": 200, "duree_ms": 32.3, "message": "GET /cours -> 200"}
…Ce que la requête demande : « Toutes les lignes de journal dont le label service vaut api. »
Zéro nouveauté par rapport à P2 : un sélecteur entre accolades. La différence, c'est le résultat : des lignes de texte, pas des nombres. Grafana affiche l'heure (convertie dans ton fuseau) puis la ligne brute ; clique sur une ligne pour dérouler ses labels : service="api", conteneur="labo-api", niveau="INFO", code="200", detected_level="info". Neuf lignes par seconde environ, autant que P8 l'annonçait : une requête, une ligne. Les accolades sont obligatoires : service="api" seul est refusé (parse error at line 1, col 1: syntax error: unexpected IDENTIFIER), et {service="api" sans fermeture aussi (syntax error: unexpected $end, expecting } or ,).
Un label est une chaîne exacte. {service="API"} en majuscules ne rend aucune ligne, sans erreur : le classeur n'existe pas. Pareil pour {app="api"} : le label s'appelle service dans ce labo, pas app. Quand une requête LogQL rend zéro ligne, vérifie d'abord le nom et la casse du label ; dans Explore, le mode Builder te liste les labels et leurs valeurs existants, c'est la façon la plus sûre de les découvrir.
{service="api", niveau="ERROR"}2026-09-15 19:33:28.931 {"horodatage": "2026-09-15T19:33:28.931+00:00", "niveau": "ERROR", "id_requete": "f5e09e6ed543", "methode": "POST", "route": "/inscriptions", "code": 500, "duree_ms": 0.0, "message": "POST /inscriptions -> 500"}
2026-09-15 19:33:16.381 {"horodatage": "2026-09-15T19:33:16.381+00:00", "niveau": "ERROR", "id_requete": "18f95992a8fa", "methode": "POST", "route": "/inscriptions", "code": 500, "duree_ms": 0.0, "message": "POST /inscriptions -> 500"}
2026-09-15 19:33:14.781 {"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, "duree_ms": 0.1, "message": "GET /cours/C0019 -> 500"}
…Ce que la requête demande : « Les lignes de l'API dont le label niveau vaut ERROR. »
Une seule nouveauté : deux labels dans le sélecteur, séparés par une virgule, c'est un ET. Le label niveau n'est pas dans la ligne au départ : c'est Alloy qui l'a extrait du champ JSON niveau avant d'envoyer à Loki, et c'est ce qui rend cette requête rapide. Le fil rouge est là, troisième ligne : id_requete: dc1c2ff189a6, la ligne du jeu de données. Beaucoup moins de lignes qu'en G1 : environ une toutes les douze secondes, comme P7 le disait pour les 500.
{service="api", code="500"}2026-09-15 19:33:28.931 {"horodatage": "2026-09-15T19:33:28.931+00:00", "niveau": "ERROR", "id_requete": "f5e09e6ed543", "methode": "POST", "route": "/inscriptions", "code": 500, …}
2026-09-15 19:33:16.381 {"horodatage": "2026-09-15T19:33:16.381+00:00", "niveau": "ERROR", "id_requete": "18f95992a8fa", "methode": "POST", "route": "/inscriptions", "code": 500, …}
2026-09-15 19:33:14.781 {"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, …}
…Ce que la requête demande : « Les lignes de l'API dont le label code vaut 500. »
Rien de neuf : G2 avec un autre label. Les mêmes lignes qu'en G2, parce que dans cette API tout 500 est un ERROR et réciproquement. C'est la version journal de P4 : là où Prometheus te dit « 32 erreurs sur /cours », Loki te montre lesquelles, avec l'URL réelle (/cours/C0019) et l'identifiant de la requête. Remarque que code est ici une chaîne ("500") parce que c'est un label ; dans le JSON de la ligne, c'est un nombre (500). G6 te montrera la différence.
{service="api"} |= "inscriptions"2026-09-15 19:33:29.738 {"horodatage": "2026-09-15T19:33:29.738+00:00", "niveau": "INFO", "id_requete": "5afb73494ebd", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 22.9, "message": "POST /inscriptions -> 201"}
2026-09-15 19:33:28.931 {"horodatage": "2026-09-15T19:33:28.931+00:00", "niveau": "ERROR", "id_requete": "f5e09e6ed543", "methode": "POST", "route": "/inscriptions", "code": 500, "duree_ms": 0.0, "message": "POST /inscriptions -> 500"}
2026-09-15 19:33:27.187 {"horodatage": "2026-09-15T19:33:27.187+00:00", "niveau": "INFO", "id_requete": "c4e29db13e12", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 26.1, "message": "POST /inscriptions -> 201"}
…Ce que la requête demande : « Les lignes de l'API qui contiennent le texte inscriptions. »
Une seule nouveauté : le filtre de ligne |= "…", qui garde les lignes contenant exactement ce texte. C'est grep. Contrairement à un label, Loki doit ici ouvrir chaque ligne du classeur {service="api"} pour regarder dedans : plus lent, mais tu peux chercher n'importe quoi. Les variantes : != (ne contient pas), |~ (expression régulière), !~. Résultat mélangé : des 201 et des 500, tout ce qui touche aux inscriptions.
Bilan de G1 à G4 : deux façons de filtrer, par label (rapide, avant d'ouvrir les lignes) et par texte (souple, après). La bonne requête commence toujours par le label le plus étroit possible.
{service="api"} | json2026-09-15 19:33:30.416 {"horodatage": "2026-09-15T19:33:30.416+00:00", "niveau": "INFO", "id_requete": "875939d9cdac", "methode": "GET", "route": "/cours", "code": 200, "duree_ms": 11.5, "message": "GET /cours -> 200"}
labels : code="200" conteneur="labo-api" duree_ms="11.5" horodatage="2026-09-15T19:33:30.416+00:00" id_requete="875939d9cdac"
message="GET /cours -> 200" methode="GET" niveau="INFO" route="/cours" service="api" …Ce que la requête demande : « Les lignes de l'API, et pour chacune, transforme les champs du JSON en labels. »
Une seule nouveauté : le parseur | json. Les lignes affichées sont les mêmes qu'en G1, mais déroule-en une : elle a maintenant beaucoup plus de labels (route, methode, duree_ms, id_requete, message…), un par champ du JSON. Ces labels-là sont calculés au moment de la requête, pas stockés : Loki n'indexe toujours que service, conteneur, niveau, code. Tu verras aussi code_extracted et niveau_extracted : quand un champ du JSON porte le même nom qu'un label déjà posé par Alloy, Loki suffixe la copie plutôt que d'écraser.
{service="api"} | json | duree_ms > 5002026-09-15 19:33:27.838 {"horodatage": "2026-09-15T19:33:27.838+00:00", "niveau": "INFO", "id_requete": "416f1ab0eb41", "methode": "GET", "route": "/lent", "code": 200, "duree_ms": 587.4, "message": "GET /lent -> 200"}
2026-09-15 19:33:16.335 {"horodatage": "2026-09-15T19:33:16.335+00:00", "niveau": "INFO", "id_requete": "9d1cb1767846", "methode": "GET", "route": "/lent", "code": 200, "duree_ms": 737.8, "message": "GET /lent -> 200"}
2026-09-15 19:33:15.375 {"horodatage": "2026-09-15T19:33:15.375+00:00", "niveau": "INFO", "id_requete": "cab8f698f89b", "methode": "GET", "route": "/lent", "code": 200, "duree_ms": 526.2, "message": "GET /lent -> 200"}
…Ce que la requête demande : « Les lignes de l'API dont le champ duree_ms, une fois le JSON ouvert, dépasse 500. »
Une seule nouveauté : le filtre de label | duree_ms > 500, qui compare un label extrait à un nombre. Ça n'est possible qu'après | json, sinon duree_ms n'existe pas. Résultat : uniquement des /lent, la route volontairement lente (300 à 900 ms). C'est la version journal de P10 : Prometheus dit « le p95 est à 98 ms » ; Loki montre les requêtes individuelles qui ont dépassé un seuil, avec leur identifiant. Équivalent SQL : WHERE duree_ms > 500, à ceci près que la colonne n'existait pas avant la requête.
{service="api"} |= "dc1c2ff189a6"2026-09-15 19:33:14.781 {"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, "duree_ms": 0.1, "message": "GET /cours/C0019 -> 500"}Une seule ligne.
Ce que la requête demande : « La ligne de l'API qui contient l'identifiant dc1c2ff189a6. »
Rien de neuf : c'est G4 avec un autre texte. Ce qui change, c'est l'usage : chez toi, dc1c2ff189a6 n'existe pas ; copie un id_requete vu dans ta propre sortie de G2 et colle-le à la place. C'est le geste que tu feras en production : un utilisateur te donne l'identifiant renvoyé par l'en-tête x-id-requete de sa réponse en erreur, et tu retrouves en une requête la ligne exacte, avec la route, le code et la durée. Une seule ligne : l'identifiant est unique par requête.
sum by (niveau) (count_over_time({service="api"}[1m]))Cette fois, Grafana affiche un graphique au lieu de lignes : trois courbes, {niveau="INFO"} autour de 470 à 500 lignes par minute, {niveau="WARNING"} autour de 40 à 50, {niveau="ERROR"} entre 2 et 10, sur le labo du cours. Passe la souris sur le graphique pour lire les valeurs.
Ce que la requête demande : « Compte les lignes de l'API par tranche d'une minute, puis additionne en gardant le label niveau. »
Une seule nouveauté, en un morceau que tu connais déjà : count_over_time(…[1m]) compte les lignes d'un sélecteur sur une plage, exactement comme rate(…[1m]) calcule une pente en P5. Autour, sum by (niveau) est le sum by (route) de P6, mot pour mot. LogQL a emprunté cette grammaire à PromQL exprès : ce que tu as appris d'un côté sert de l'autre. Compare avec P7 : Prometheus compte 0,09 réponse 500 par seconde, soit 5 par minute ; Loki compte 5 lignes ERROR par minute. Deux outils, deux chemins, le même chiffre.
La différence essentielle entre Prometheus et Loki. Prometheus stocke des nombres déjà comptés par l'API (
http_requetes_total), Loki stocke les lignes et peut les recompter à la demande (count_over_time). Le premier est léger et rapide, et répond « combien » ; le second est lourd mais garde le détail, et répond « lesquelles ». Le labo a les deux parce qu'aucun ne remplace l'autre.
Le message à faire passer. Ce que
grepfait aussi : chercher un texte dans des lignes (|=). Ce que seul Loki fait : ranger les lignes de dix conteneurs par labels et ne lire que le bon classeur, ouvrir le JSON à la demande pour filtrer sur un champ numérique (| json | duree_ms > 500), et transformer des journaux en courbe avec la grammaire de PromQL (count_over_time).
Trois requêtes qui combinent ce que tu as vu, sans notion nouvelle. Tape-les, puis explique en une phrase ce que chacune montre.
topk(5, increase(inscriptions_total[1h]))Les cinq cours qui ont reçu le plus d'inscriptions dans la dernière heure. increase est rate multiplié par la durée de la fenêtre ; topk(5, …) garde les cinq plus grandes séries. Sur le labo du cours, C0001 arrive en tête avec environ 177 inscriptions : charge favorise quelques cours « populaires ».
histogram_quantile(0.95, sum by (le, route) (rate(http_duree_requete_seconds_bucket[5m])))P10 avec un label de plus dans le by : un p95 par route. Tu verras /lent autour de 0,7 s et les autres routes sous 0,03 s. Regarde ce que ça change par rapport au p95 global de P10.
{service="api"} |= "inscriptions" | json | code = 201G4, G5 et G6 enchaînés : les inscriptions réussies seulement. Vérifie que le nombre de lignes par minute correspond à la ligne {code="201"} de P7, environ 0,87 par seconde, soit une cinquantaine par minute.
Toutes les commandes se tapent dans PowerShell, depuis le dossier lab3. Docker Desktop doit être lancé (icône verte). Si PowerShell refuse d'exécuter .\labo.ps1, tape une fois Set-ExecutionPolicy -Scope CurrentUser RemoteSigned et réponds O.
cd C:\Users\<toi>\Documents
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
lsTu dois voir docker-compose.yml, labo.ps1, labo.sh, README.md, et les dossiers alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. Si tu as déjà cloné le kit à la leçon 03, saute cette étape et fais juste cd lab3.
.\labo.ps1 prerequis
== Prérequis ==
✔ docker : Docker version 29.3.1, build c2be9cc
✔ le démon Docker répond
✔ docker compose : 5.1.1
✔ mémoire disponible pour Docker : 31 Go
✔ processeurs : 20
✔ port 9090 libre
✔ port 9093 libre
✔ port 3000 libre
✔ port 3100 libre
✔ port 12345 libre
✔ port 9100 libre
✔ port 8080 libre
✔ port 8000 libre
✔ port 8090 libre
Tout est prêt. Lancez : .\labo.ps1 demarrerPoint de contrôle : la dernière ligne est Tout est prêt.. Les versions, la mémoire et le nombre de processeurs sont ceux de la machine du cours. Si un port est marqué ✘ … déjà occupé, la leçon 03 explique quoi faire ; pour le 3000, $env:GRAFANA_PORT = '3001' suffit.
.\labo.ps1 demarrerLa première fois, le téléchargement des six images publiques prend une à cinq minutes selon ta connexion. Fin de la sortie attendue :
== Attente que chaque service soit prêt ==
prometheus prêt (0 s)
alertmanager prêt (0 s)
loki prêt (0 s)
alloy .. prêt (6 s)
node-exporter prêt (0 s)
cadvisor prêt (0 s)
api prêt (0 s)
webhook prêt (0 s)
charge prêt (0 s)
grafana ... prêt (9 s)
Le labo est prêt.
Grafana http://localhost:3000 (utilisateur admin · mot de passe aiopsatlas2026)
Prometheus http://localhost:9090 (Status → Target health, puis onglet Graph)
Alertmanager http://localhost:9093
API catalogue http://localhost:8000/cours · http://localhost:8000/metrics
Webhook http://localhost:8090 (les alertes reçues)
Loki http://localhost:3100/ready · Alloy http://localhost:12345
node-exporter http://localhost:9100/metrics · cAdvisor http://localhost:8080
Étape suivante : .\labo.ps1 etat (laissez tourner 2 minutes pour avoir des courbes)Point de contrôle : dix prêt, puis Le labo est prêt.. Sur la machine du cours, images déjà en cache, la commande a pris 44 secondes. La leçon 04 commente cette sortie ligne par ligne.
etat.\labo.ps1 etat
== Conteneurs ==
NAME SERVICE STATUS
labo-alertmanager alertmanager Up About a minute (healthy)
labo-alloy alloy Up 48 seconds (healthy)
labo-api api Up About a minute (healthy)
labo-cadvisor cadvisor Up About a minute (healthy)
labo-charge charge Up About a minute (healthy)
labo-grafana grafana Up 48 seconds (healthy)
labo-loki loki Up About a minute (healthy)
labo-node-exporter node-exporter Up About a minute (healthy)
labo-prometheus prometheus Up About a minute (healthy)
labo-webhook webhook Up About a minute (healthy)
== Supervision ==
✔ Prometheus répond — cibles up : 8/8
séries en mémoire : 9038
alertes : 0 active(s), 0 en attente (pending)
✔ Alertmanager répond (http://localhost:9093)
✔ Grafana répond (http://localhost:3000)
✔ Loki répond (http://localhost:3100)
✔ API catalogue répond — version 1.0.0, 64 cours
✔ Webhook répond — 0 alerte(s) reçue(s) (http://localhost:8090)
Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Point de contrôle : dix (healthy), 8/8, 64 cours, 0 alerte(s) reçue(s), et la dernière ligne Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Le nombre de séries en mémoire monte pendant les premières minutes (9038 juste après le démarrage, 12 000 à 15 000 après une heure sur la machine du cours). Si alloy ou grafana sont encore (health: starting), attends trente secondes et relance.
Ouvre http://localhost:9090, menu Status, puis Target health. Huit blocs, un par job, chacun avec 1 / 1 up et une ligne :
api
1 / 1 up
Endpoint Labels Last scrape State
http://api:8000/metrics instance="api:8000" job="api" service="api" 6.014s ago UPPoint de contrôle : huit UP, aucun DOWN. La colonne Last scrape ne dépasse jamais 15 secondes : c'est le scrape_interval de prometheus.yml. En ligne de commande, la même information :
(Invoke-RestMethod http://localhost:9090/api/v1/targets).data.activeTargets | Select-Object @{n='job';e={$_.labels.job}}, health, scrapeUrl | Sort-Object jobjob health scrapeUrl
--- ------ ---------
alertmanager up http://alertmanager:9093/metrics
alloy up http://alloy:12345/metrics
api up http://api:8000/metrics
cadvisor up http://cadvisor:8080/metrics
grafana up http://grafana:3000/metrics
loki up http://loki:3100/metrics
node-exporter up http://node-exporter:9100/metrics
prometheus up http://localhost:9090/metricsAttends que demarrer ait au moins deux minutes derrière lui, puis suis les sections Prometheus, dans l'onglet Graph et Grafana, dans Explore plus haut. Les requêtes sont prêtes à copier :
Get-Content modules\01-le-labo\requetes.txt
Get-Content modules\01-le-labo\requetes-logql.txtPoint de contrôle : P1 rend 8 séries à 1, P12 rend Empty query result, G1 rend des lignes JSON, G8 rend trois courbes.
Avant de casser, note l'heure (Get-Date -Format HH:mm:ss). Puis :
.\labo.ps1 casser api
== Panne : arrêt de l'API ==
Container labo-api Stopping
Container labo-api Stopped
✔ API arrêtée. La charge continue de frapper dans le vide.
À observer : .\labo.ps1 etat · http://localhost:9090/targets (api → down)
http://localhost:9090/alerts (APIInjoignable : pending puis firing après 30 s)
http://localhost:9093 et http://localhost:8090 (l'alerte arrive ~10 s après firing)
Pour tout remettre en ordre : .\labo.ps1 reparerLe script a fait un docker compose stop api : le conteneur est arrêté proprement, ses données et son image sont intactes. Maintenant, observe la panne par cinq chemins, dans l'ordre. Tu as environ 70 secondes avant que l'alerte arrive au webhook : lance etat tout de suite.
Chemin 1, etat :
.\labo.ps1 etat
== Conteneurs ==
NAME SERVICE STATUS
labo-alertmanager alertmanager Up 22 minutes (healthy)
labo-alloy alloy Up 22 minutes (healthy)
labo-api api Exited (0) About a minute ago
labo-cadvisor cadvisor Up 22 minutes (healthy)
labo-charge charge Up 22 minutes (healthy)
labo-grafana grafana Up 22 minutes (healthy)
labo-loki loki Up 22 minutes (healthy)
labo-node-exporter node-exporter Up 22 minutes (healthy)
labo-prometheus prometheus Up 22 minutes (healthy)
labo-webhook webhook Up 22 minutes (healthy)
== Supervision ==
✔ Prometheus répond — cibles up : 7/8
✘ cible api (http://api:8000/metrics) : down — Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host
séries en mémoire : 15116
alertes : 2 active(s), 0 en attente (pending)
✘ APIInjoignable [critique] — L'API catalogue ne répond plus
✘ TauxErreursEleve [critique] — Plus de 5 % des requêtes de l'API échouent
✔ Alertmanager répond (http://localhost:9093)
✔ Grafana répond (http://localhost:3000)
✔ Loki répond (http://localhost:3100)
✘ API catalogue ne répond pas (http://localhost:8000)
✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)
Labo : 9/10 services, 7/8 cibles up, 2 alertes actives.Ce qu'il faut lire, de haut en bas : labo-api est Exited (0) (code 0 : arrêt volontaire, pas un plantage) ; Prometheus ne lit plus que 7/8 cibles et te dit pourquoi (lookup api … no such host : le nom api n'existe plus sur le réseau Docker puisque le conteneur est arrêté) ; l'alerte APIInjoignable est active ; l'API ne répond pas sur le port 8000 ; le webhook a reçu quelque chose. Cette sortie a été capturée sur la machine du cours une minute après casser api, alors qu'un casser erreurs venait d'être joué quelques minutes avant : c'est pour ça qu'une deuxième alerte, TauxErreursEleve, apparaît aussi. Sur ton labo, tu n'auras que APIInjoignable, 1 alertes actives et 1 alerte(s) reçue(s). Si tu lances etat dans les 30 premières secondes, l'alerte est encore en attente (pending) et le webhook est encore à 0 : relance une minute plus tard.
Chemin 2, les cibles. Recharge http://localhost:9090 → Status → Target health. Le bloc api est passé à 0 / 1 up, état DOWN, et la colonne Error porte le même message que etat : Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Les sept autres restent UP. Retape up dans Query : la ligne up{instance="api:8000", job="api", service="api"} est à 0, les sept autres à 1. Puis up == 0 : une seule ligne.
Chemin 3, les alertes dans Prometheus. Menu Alerts. La règle APIInjoignable change d'état en trois temps, chronométrés sur la machine du cours :
t+0 s : APIInjoignable inactive (Prometheus n'a pas encore rescrappé l'API)
t+40 s : APIInjoignable pending (up{job="api"} == 0 est vrai, le compte à rebours « for: 30s » tourne)
t+70 s : APIInjoignable firing (vrai depuis 30 s : Prometheus envoie à Alertmanager)
t+70 s : APIInjoignable reçue par le webhook (firing)Pourquoi 40 secondes avant pending : Prometheus lit l'API toutes les 15 secondes, donc il faut jusqu'à 15 secondes pour qu'un scrape échoue, puis il évalue les règles toutes les 15 secondes. Pourquoi 30 de plus avant firing : la règle dit for: 30s. Sur une autre capture, pending est arrivé à 31 s et firing à 61 s : l'ordre de grandeur est le même, le détail dépend de l'instant où tu as cassé par rapport au cycle de scrape. Retape ALERTS dans Query :
ALERTS{alertname="APIInjoignable", alertstate="pending", instance="api:8000", job="api", service="api", severite="critique"} 1puis, trente secondes plus tard, alertstate="firing". Note l'heure du passage en firing : c'est la première des trois lignes de ton livrable.
Chemin 4, Alertmanager. Ouvre http://localhost:9093. La page Alerts montre un groupe alertname="APIInjoignable" service="api" (c'est le group_by: [alertname, service] de alertmanager.yml) avec l'alerte, ses labels (instance="api:8000", job="api", labo="observabilite", severite="critique"), son résumé L'API catalogue ne répond plus et sa description. Le label labo="observabilite" n'était pas dans la règle : c'est l'external_labels de prometheus.yml, ajouté à tout ce qui sort de Prometheus. En ligne de commande :
(Invoke-RestMethod http://localhost:9093/api/v2/alerts) | Select-Object @{n='alerte';e={$_.labels.alertname}}, @{n='etat';e={$_.status.state}}, startsAtalerte etat startsAt
------ ---- --------
APIInjoignable active 2026-09-15T19:41:11.496ZChemin 5, le webhook. Ouvre http://localhost:8090. La page « Alertes reçues d'Alertmanager » n'est plus vide : une ligne APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, et l'en-tête compte 1 alerte(s) en mémoire · 1 notification(s) reçue(s). Le format brut, http://localhost:8090/alertes.json, montre ce qu'Alertmanager a envoyé :
{"recu_a":"2026-09-15T19:41:26+00:00","etat":"firing","nom":"APIInjoignable","severite":"critique","service":"api","resume":"L'API catalogue ne répond plus","description":"Prometheus n'arrive plus à lire http://api:8000/metrics depuis 30 secondes (cible api:8000).","debut":"2026-09-15T19:41:11.496Z","fin":"0001-01-01T00:00:00Z","labels":{"alertname":"APIInjoignable","instance":"api:8000","job":"api","labo":"observabilite","service":"api","severite":"critique"}}Lis debut (19:41:11, l'heure du firing dans Prometheus) et recu_a (19:41:26) : quinze secondes d'écart, dont les 10 secondes de group_wait d'Alertmanager. fin à l'an 0001 veut dire « pas encore terminée ». Note recu_a : deuxième ligne de ton livrable.
Ce que voit la charge, pendant ce temps :
.\labo.ps1 journal chargelabo-charge | {"horodatage": "2026-09-15T19:40:36.600+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge | {"horodatage": "2026-09-15T19:40:40.925+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge | {"horodatage": "2026-09-15T19:40:45.393+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}Et ce que tu vois si tu appelles l'API toi-même :
Invoke-RestMethod http://localhost:8000/sante -TimeoutSec 5Invoke-RestMethod : Le délai de l'opération a expiré.(Sans -TimeoutSec, PowerShell attend plus longtemps avant de renoncer ; curl.exe -sS http://localhost:8000/sante répond plus vite : curl: (7) Failed to connect to localhost:8000 after 2237 ms: Could not connect to server.)
La différence essentielle entre un 404 et pas de réponse. À l'étape A.8,
/cours/C9999répondra404: l'API tourne et te dit poliment que ce cours n'existe pas ; ça se compte danshttp_requetes_total{code="404"}, ça s'écrit dans un logWARNING, etupreste à1. Ici,Le délai de l'opération a expiré: personne ne répond, il n'y a ni code ni log côté API, et c'estupqui tombe à0. Deux situations, deux signaux, deux endroits où chercher.
.\labo.ps1 reparer
== Réparation ==
✔ API redémarrée
api .. prêt (6 s)
✔ taux d'erreurs remis à 0.01, lenteur à 0 ms
Les alertes passent en « resolved » dans les minutes qui suivent (voir http://localhost:8090).Le script a fait docker compose start api, attendu que /sante réponde, puis appelé /admin/reparer (utile pour les deux autres scénarios de panne). Vérifie que l'API travaille :
.\labo.ps1 journal apilabo-api | {"horodatage": "2026-09-15T19:41:48.720+00:00", "niveau": "INFO", "id_requete": "6a4228a46a56", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 15.6, "message": "POST /inscriptions -> 201"}
labo-api | {"horodatage": "2026-09-15T19:41:48.914+00:00", "niveau": "WARNING", "id_requete": "b4ebe9fa32d8", "methode": "GET", "route": "inconnue", "code": 404, "duree_ms": 0.4, "message": "GET /inexistant -> 404"}
labo-api | {"horodatage": "2026-09-15T19:41:48.937+00:00", "niveau": "INFO", "id_requete": "732adf61ee17", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 25.7, "message": "GET /cours/C0043 -> 200"}Puis regarde l'alerte s'éteindre, dans le même ordre qu'elle s'est allumée. Chronométré sur la machine du cours après reparer :
t+15 s : APIInjoignable firing (Prometheus) · webhook : firing
t+40 s : APIInjoignable inactive (Prometheus) · webhook : firing
t+55 s : APIInjoignable inactive (Prometheus) · webhook : resolvedAu premier scrape réussi, up{job="api"} revient à 1 et la règle repasse inactive ; Alertmanager envoie alors une notification resolved au webhook. Recharge http://localhost:8090 : deux lignes maintenant pour APIInjoignable, une firing et une resolved, et dans /alertes.json la seconde a un champ fin rempli :
{"recu_a":"2026-09-15T19:42:26+00:00","etat":"resolved","nom":"APIInjoignable",…,"debut":"2026-09-15T19:41:11.496Z","fin":"2026-09-15T19:41:56.496Z",…}fin moins debut : la panne a duré 45 secondes aux yeux de Prometheus. Note recu_a de la ligne resolved : troisième ligne de ton livrable.
L'API tourne. Demande-lui un cours qui n'existe pas :
Invoke-RestMethod http://localhost:8000/cours/C9999Invoke-RestMethod : {"detail":"cours C9999 introuvable"}C'est une erreur HTTP 404 : l'API a répondu. Pour voir le code lui-même :
try { Invoke-WebRequest http://localhost:8000/cours/C9999 -UseBasicParsing } catch { $_.Exception.Response.StatusCode.value__ }404Retape P4 dans Prometheus en remplaçant 500 par 404 : la série route="/cours/{id}" a augmenté de 1. Retape G1 en ajoutant |= "C9999" : ta requête est là, niveau WARNING, avec son id_requete. Rien de tout ça n'existe pour la panne de A.6 : une API arrêtée ne compte rien et n'écrit rien.
.\labo.ps1 etat
== Conteneurs ==
NAME SERVICE STATUS
labo-alertmanager alertmanager Up 23 minutes (healthy)
labo-alloy alloy Up 23 minutes (healthy)
labo-api api Up 53 seconds (healthy)
labo-cadvisor cadvisor Up 23 minutes (healthy)
labo-charge charge Up 23 minutes (healthy)
labo-grafana grafana Up 23 minutes (healthy)
labo-loki loki Up 23 minutes (healthy)
labo-node-exporter node-exporter Up 23 minutes (healthy)
labo-prometheus prometheus Up 23 minutes (healthy)
labo-webhook webhook Up 23 minutes (healthy)
== Supervision ==
✔ Prometheus répond — cibles up : 8/8
séries en mémoire : 15395
alertes : 0 active(s), 0 en attente (pending)
✔ Alertmanager répond (http://localhost:9093)
✔ Grafana répond (http://localhost:3000)
✔ Loki répond (http://localhost:3100)
✔ API catalogue répond — version 1.0.0, 64 cours
✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)
Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Ce que tu dois avoir : labo-api de nouveau Up … (healthy) avec un temps plus court que les autres (il vient de redémarrer), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s) (la firing et la resolved), et la dernière ligne Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Sur la machine du cours, cette sortie portait encore 1 alertes actives et 3 alerte(s) reçue(s) à cause du TauxErreursEleve résiduel évoqué en A.6 ; chez toi, avec la seule panne api, les valeurs sont celles ci-dessus. Cette sortie et tes trois heures notées sont ton livrable. Tu peux laisser le labo tourner pour les ateliers 06 et 07, ou l'arrêter avec .\labo.ps1 arreter : les données sont conservées et demarrer reprend là où tu étais.
Toutes les commandes se tapent dans un terminal bash, depuis le dossier lab3. Sur macOS et Windows (WSL 2 ou Git Bash), Docker Desktop doit être lancé ; sur Linux natif, docker info doit répondre sans sudo (sinon sudo usermod -aG docker $USER, puis ouvre une nouvelle session). Le script bash a besoin de curl.
cd ~
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
lsTu dois voir docker-compose.yml, labo.sh, labo.ps1, README.md, et les dossiers alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. Sur Linux et macOS, rends le script exécutable une fois : chmod +x labo.sh (sans ça, ./labo.sh répond bash: ./labo.sh: Permission denied). Si tu as déjà cloné le kit à la leçon 03, saute cette étape et fais juste cd lab3.
./labo.sh prerequis
== Prérequis ==
✔ docker : Docker version 29.3.1, build c2be9cc
✔ le démon Docker répond
✔ docker compose : 5.1.1
✔ curl : présent
✔ mémoire disponible pour Docker : 31 Go
✔ processeurs : 20
✔ port 9090 libre
✔ port 9093 libre
✔ port 3000 libre
✔ port 3100 libre
✔ port 12345 libre
✔ port 9100 libre
✔ port 8080 libre
✔ port 8000 libre
✔ port 8090 libre
Tout est prêt. Lancez : ./labo.sh demarrerPoint de contrôle : la dernière ligne est Tout est prêt.. Le script bash vérifie une ligne de plus que PowerShell, curl : présent. Les versions et la mémoire sont celles de la machine du cours (Git Bash sous Windows). Si un port est occupé, la leçon 03 explique quoi faire ; pour le 3000, GRAFANA_PORT=3001 ./labo.sh demarrer.
./labo.sh demarrerFin de la sortie attendue, une fois les images téléchargées :
== Attente que chaque service soit prêt ==
prometheus prêt (0 s)
alertmanager prêt (0 s)
loki prêt (0 s)
alloy .. prêt (6 s)
node-exporter prêt (0 s)
cadvisor prêt (0 s)
api prêt (0 s)
webhook prêt (0 s)
charge prêt (0 s)
grafana ... prêt (9 s)
Le labo est prêt.
Grafana http://localhost:3000 (utilisateur admin · mot de passe aiopsatlas2026)
Prometheus http://localhost:9090 (Status → Target health, puis onglet Graph)
Alertmanager http://localhost:9093
API catalogue http://localhost:8000/cours · http://localhost:8000/metrics
Webhook http://localhost:8090 (les alertes reçues)
Loki http://localhost:3100/ready · Alloy http://localhost:12345
node-exporter http://localhost:9100/metrics · cAdvisor http://localhost:8080
Étape suivante : ./labo.sh etat (laissez tourner 2 minutes pour avoir des courbes)Point de contrôle : dix prêt, puis Le labo est prêt..
etat./labo.sh etat
== Conteneurs ==
NAME SERVICE STATUS
labo-alertmanager alertmanager Up 11 minutes (healthy)
labo-alloy alloy Up 10 minutes (healthy)
labo-api api Up 4 minutes (healthy)
labo-cadvisor cadvisor Up 11 minutes (healthy)
labo-charge charge Up 10 minutes (healthy)
labo-grafana grafana Up 10 minutes (healthy)
labo-loki loki Up 11 minutes (healthy)
labo-node-exporter node-exporter Up 11 minutes (healthy)
labo-prometheus prometheus Up 11 minutes (healthy)
labo-webhook webhook Up 11 minutes (healthy)
== Supervision ==
✔ Prometheus répond — cibles up : 8/8
séries en mémoire : 10602
alertes : 0 active(s), 0 en attente (pending)
✔ Alertmanager répond (http://localhost:9093)
✔ Grafana répond (http://localhost:3000)
✔ Loki répond (http://localhost:3100)
✔ API catalogue répond — version 1.0.0, 64 cours
✔ Webhook répond — 0 alerte(s) reçue(s) (http://localhost:8090)
Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Point de contrôle : dix (healthy), 8/8, 64 cours, et la dernière ligne Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Le bloc Conteneurs vient de docker compose ps ; tu peux le taper toi-même. Troisième chemin, en curl :
curl -s http://localhost:8000/sante
curl -s http://localhost:8090/sante{"etat":"ok","version":"1.0.0","cours":64}
{"etat":"ok","alertes_en_memoire":0,"notifications":0,"alertes":0}Dans le navigateur, http://localhost:9090 → Status → Target health : huit blocs 1 / 1 up, état UP, colonne Last scrape toujours sous 15 secondes. En curl, avec python3 pour lire le JSON (ou jq si tu l'as) :
curl -s http://localhost:9090/api/v1/targets | python3 -c 'import json,sys; [print(t["labels"]["job"].ljust(14), t["health"], t["scrapeUrl"]) for t in sorted(json.load(sys.stdin)["data"]["activeTargets"], key=lambda t: t["labels"]["job"])]'alertmanager up http://alertmanager:9093/metrics
alloy up http://alloy:12345/metrics
api up http://api:8000/metrics
cadvisor up http://cadvisor:8080/metrics
grafana up http://grafana:3000/metrics
loki up http://loki:3100/metrics
node-exporter up http://node-exporter:9100/metrics
prometheus up http://localhost:9090/metricsAttends deux minutes après demarrer, puis suis les sections Prometheus, dans l'onglet Graph et Grafana, dans Explore plus haut ; elles se font dans le navigateur, à l'identique sous tous les systèmes. Les requêtes sont prêtes à copier :
cat modules/01-le-labo/requetes.txt
cat modules/01-le-labo/requetes-logql.txtTroisième chemin, une requête PromQL en curl :
curl -s 'http://localhost:9090/api/v1/query?query=up' | python3 -m json.tool | head -n 20Tu reconnais dans le JSON les mêmes séries que dans l'onglet Table : "metric": {"__name__": "up", "instance": "localhost:9090", "job": "prometheus"} et "value": [1789500809.696, "1"].
Point de contrôle : P1 rend 8 séries à 1, P12 rend Empty query result, G1 rend des lignes JSON, G8 rend trois courbes.
Note l'heure (date +%T), puis :
./labo.sh casser api
== Panne : arrêt de l'API ==
Container labo-api Stopping
Container labo-api Stopped
✔ API arrêtée. La charge continue de frapper dans le vide.
À observer : ./labo.sh etat · http://localhost:9090/targets (api → down)
http://localhost:9090/alerts (APIInjoignable : pending puis firing après 30 s)
http://localhost:9093 et http://localhost:8090 (l'alerte arrive ~10 s après firing)
Pour tout remettre en ordre : ./labo.sh reparerLe script a fait docker compose stop api. Tu as environ 70 secondes avant que l'alerte arrive au webhook. Observe par cinq chemins.
Chemin 1, etat :
./labo.sh etat
== Conteneurs ==
NAME SERVICE STATUS
labo-alertmanager alertmanager Up 22 minutes (healthy)
labo-alloy alloy Up 22 minutes (healthy)
labo-api api Exited (0) About a minute ago
labo-cadvisor cadvisor Up 22 minutes (healthy)
labo-charge charge Up 22 minutes (healthy)
labo-grafana grafana Up 22 minutes (healthy)
labo-loki loki Up 22 minutes (healthy)
labo-node-exporter node-exporter Up 22 minutes (healthy)
labo-prometheus prometheus Up 22 minutes (healthy)
labo-webhook webhook Up 22 minutes (healthy)
== Supervision ==
✔ Prometheus répond — cibles up : 7/8
✘ cible api (http://api:8000/metrics) : down — Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host
séries en mémoire : 15116
alertes : 2 active(s), 0 en attente (pending)
✘ APIInjoignable [critique] — L'API catalogue ne répond plus
✘ TauxErreursEleve [critique] — Plus de 5 % des requêtes de l'API échouent
✔ Alertmanager répond (http://localhost:9093)
✔ Grafana répond (http://localhost:3000)
✔ Loki répond (http://localhost:3100)
✘ API catalogue ne répond pas (http://localhost:8000)
✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)
Labo : 9/10 services, 7/8 cibles up, 2 alertes actives.Ce qu'il faut lire : labo-api est Exited (0) (arrêt volontaire) ; Prometheus ne lit plus que 7/8 cibles et dit pourquoi (lookup api … no such host : le nom api a disparu du réseau Docker) ; APIInjoignable est active ; l'API ne répond pas ; le webhook a reçu l'alerte. Cette sortie a été capturée une minute après casser api, sur une machine où casser erreurs venait d'être joué : d'où la deuxième alerte TauxErreursEleve. Chez toi : 1 alertes actives, 1 alerte(s) reçue(s). Si tu lances etat dans les 30 premières secondes, l'alerte est encore pending et le webhook à 0 : relance une minute plus tard. Troisième chemin, en curl :
curl -s http://localhost:8000/santecurl: (7) Failed to connect to localhost:8000 after 2237 ms: Could not connect to serverChemin 2, les cibles. Recharge http://localhost:9090 → Status → Target health : le bloc api est à 0 / 1 up, état DOWN, colonne Error : Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Retape up dans Query : up{instance="api:8000", job="api", service="api"} est à 0. Puis up == 0 : une seule ligne.
Chemin 3, les alertes dans Prometheus. Menu Alerts. APIInjoignable change d'état en trois temps, chronométrés sur la machine du cours :
t+0 s : APIInjoignable inactive (Prometheus n'a pas encore rescrappé l'API)
t+40 s : APIInjoignable pending (up{job="api"} == 0 est vrai, le compte à rebours « for: 30s » tourne)
t+70 s : APIInjoignable firing (vrai depuis 30 s : Prometheus envoie à Alertmanager)
t+70 s : APIInjoignable reçue par le webhook (firing)Pourquoi 40 secondes avant pending : jusqu'à 15 secondes pour qu'un scrape échoue (scrape_interval: 15s), puis jusqu'à 15 secondes pour la prochaine évaluation des règles (evaluation_interval: 15s). Pourquoi 30 de plus : for: 30s dans alertes.yml. Retape ALERTS dans Query :
ALERTS{alertname="APIInjoignable", alertstate="pending", instance="api:8000", job="api", service="api", severite="critique"} 1puis alertstate="firing". Note l'heure du firing : première ligne de ton livrable. En curl, la même chose :
curl -s http://localhost:9090/api/v1/alerts | python3 -m json.toolChemin 4, Alertmanager. Ouvre http://localhost:9093 : la page Alerts montre un groupe alertname="APIInjoignable" service="api" (le group_by de alertmanager.yml) avec l'alerte, ses labels (instance="api:8000", job="api", labo="observabilite", severite="critique"), le résumé L'API catalogue ne répond plus. Le label labo="observabilite" vient des external_labels de prometheus.yml. En curl :
curl -s http://localhost:9093/api/v2/alerts | python3 -c 'import json,sys; [print(a["labels"]["alertname"], a["status"]["state"], a["startsAt"]) for a in json.load(sys.stdin)]'APIInjoignable active 2026-09-15T19:41:11.496ZChemin 5, le webhook. Ouvre http://localhost:8090 : une ligne APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, en-tête 1 alerte(s) en mémoire · 1 notification(s) reçue(s). Le format brut :
curl -s http://localhost:8090/alertes.json | python3 -m json.tool[
{
"recu_a": "2026-09-15T19:41:26+00:00",
"etat": "firing",
"nom": "APIInjoignable",
"severite": "critique",
"service": "api",
"resume": "L'API catalogue ne répond plus",
"description": "Prometheus n'arrive plus à lire http://api:8000/metrics depuis 30 secondes (cible api:8000).",
"debut": "2026-09-15T19:41:11.496Z",
"fin": "0001-01-01T00:00:00Z",
"labels": {
"alertname": "APIInjoignable",
"instance": "api:8000",
"job": "api",
"labo": "observabilite",
"service": "api",
"severite": "critique"
}
}
]debut (19:41:11) est l'heure du firing dans Prometheus ; recu_a (19:41:26) arrive quinze secondes plus tard, dont les 10 secondes de group_wait. fin à l'an 0001 : pas encore terminée. Note recu_a : deuxième ligne de ton livrable.
Ce que voit la charge :
./labo.sh journal chargelabo-charge | {"horodatage": "2026-09-15T19:40:36.600+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge | {"horodatage": "2026-09-15T19:40:40.925+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge | {"horodatage": "2026-09-15T19:40:45.393+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}La différence essentielle entre un 404 et pas de réponse. À l'étape B.8,
/cours/C9999répondra404: l'API tourne et te dit que ce cours n'existe pas ; ça se compte danshttp_requetes_total{code="404"}, ça s'écrit dans un logWARNING, etupreste à1. Ici,curl: (7) Failed to connect: personne ne répond, il n'y a ni code ni log côté API, et c'estupqui tombe à0.
./labo.sh reparer
== Réparation ==
✔ API redémarrée
api .. prêt (6 s)
✔ taux d'erreurs remis à 0.01, lenteur à 0 ms
Les alertes passent en « resolved » dans les minutes qui suivent (voir http://localhost:8090).Le script a fait docker compose start api, attendu /sante, puis appelé /admin/reparer. Vérifie que l'API travaille :
./labo.sh journal apilabo-api | {"horodatage": "2026-09-15T19:41:48.720+00:00", "niveau": "INFO", "id_requete": "6a4228a46a56", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 15.6, "message": "POST /inscriptions -> 201"}
labo-api | {"horodatage": "2026-09-15T19:41:48.914+00:00", "niveau": "WARNING", "id_requete": "b4ebe9fa32d8", "methode": "GET", "route": "inconnue", "code": 404, "duree_ms": 0.4, "message": "GET /inexistant -> 404"}
labo-api | {"horodatage": "2026-09-15T19:41:48.937+00:00", "niveau": "INFO", "id_requete": "732adf61ee17", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 25.7, "message": "GET /cours/C0043 -> 200"}Puis l'alerte s'éteint, chronométré sur la machine du cours après reparer :
t+15 s : APIInjoignable firing (Prometheus) · webhook : firing
t+40 s : APIInjoignable inactive (Prometheus) · webhook : firing
t+55 s : APIInjoignable inactive (Prometheus) · webhook : resolvedTroisième chemin, surveiller en boucle :
watch -n 5 'curl -s http://localhost:8090/alertes.json | python3 -c "import json,sys; [print(a[\"recu_a\"], a[\"nom\"], a[\"etat\"]) for a in json.load(sys.stdin)]"'2026-09-15T19:42:26+00:00 APIInjoignable resolved
2026-09-15T19:41:26+00:00 APIInjoignable firingLa ligne resolved porte un champ fin rempli (2026-09-15T19:41:56.496Z) : 45 secondes de panne aux yeux de Prometheus. Note son recu_a : troisième ligne de ton livrable. Ctrl+C pour sortir de watch.
curl -s -i http://localhost:8000/cours/C9999HTTP/1.1 404 Not Found
date: Tue, 15 Sep 2026 20:50:01 GMT
server: uvicorn
content-length: 36
content-type: application/json
x-id-requete: bbc7be667f1d
{"detail":"cours C9999 introuvable"}L'API a répondu : un code 404, un identifiant x-id-requete, un corps JSON. Retape P4 dans Prometheus avec 404 à la place de 500 : la série route="/cours/{id}" a augmenté de 1. Retape G1 en ajoutant |= "bbc7be667f1d" (ton propre identifiant, lu dans l'en-tête) : ta requête est là, niveau WARNING. Rien de tout ça n'existe pour la panne de B.6 : une API arrêtée ne compte rien et n'écrit rien.
./labo.sh etat
== Conteneurs ==
NAME SERVICE STATUS
labo-alertmanager alertmanager Up 23 minutes (healthy)
labo-alloy alloy Up 23 minutes (healthy)
labo-api api Up 53 seconds (healthy)
labo-cadvisor cadvisor Up 23 minutes (healthy)
labo-charge charge Up 23 minutes (healthy)
labo-grafana grafana Up 23 minutes (healthy)
labo-loki loki Up 23 minutes (healthy)
labo-node-exporter node-exporter Up 23 minutes (healthy)
labo-prometheus prometheus Up 23 minutes (healthy)
labo-webhook webhook Up 23 minutes (healthy)
== Supervision ==
✔ Prometheus répond — cibles up : 8/8
séries en mémoire : 15395
alertes : 0 active(s), 0 en attente (pending)
✔ Alertmanager répond (http://localhost:9093)
✔ Grafana répond (http://localhost:3000)
✔ Loki répond (http://localhost:3100)
✔ API catalogue répond — version 1.0.0, 64 cours
✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)
Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.Ce que tu dois avoir : labo-api de nouveau Up … (healthy), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s) (la firing et la resolved), et Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Sur la machine du cours, cette sortie portait encore 1 alertes actives et 3 alerte(s) reçue(s) à cause du TauxErreursEleve résiduel évoqué en B.6 ; chez toi, avec la seule panne api, les valeurs sont celles ci-dessus. Cette sortie et tes trois heures notées sont ton livrable. Tu peux laisser tourner pour les ateliers 06 et 07, ou ./labo.sh arreter : les données sont conservées.
etat dit 9/10 services alors que tu n'as rien cassé. Regarde quel conteneur n'est pas (healthy). Si c'est grafana ou alloy juste après demarrer avec (health: starting), attends trente secondes. Si c'est labo-api en Exited, quelqu'un (peut-être toi, à un essai précédent) a lancé casser api : reparer. Si un conteneur est Restarting, lis son journal : .\labo.ps1 journal <service> ou ./labo.sh journal <service>.
cibles up : 7/8 et APIInjoignable active, mais l'API répond sur http://localhost:8000. Prometheus lit l'API par le réseau Docker (http://api:8000/metrics), toi par le port publié (localhost:8000). Si l'API vient de redémarrer, Prometheus peut être en retard d'un scrape (15 s) et l'alerte reste firing jusqu'à la prochaine évaluation, puis quelques secondes de plus pour que le webhook reçoive le resolved. Attends une minute et relance etat.
Les alertes tardent : pending ne passe pas firing. APIInjoignable a for: 30s ; il faut donc jusqu'à 15 s (scrape) + 15 s (évaluation) + 30 s (for) = 60 à 70 s pour firing, puis 10 s de group_wait pour le webhook. Ce n'est pas lent, c'est réglé pour éviter les fausses alertes sur un raté isolé. Si au bout de deux minutes rien ne bouge, vérifie que labo-api est bien Exited (docker compose ps).
Le webhook reste à 0 alerte(s) reçue(s) alors qu'Alertmanager montre l'alerte. Ouvre http://localhost:9093 → Status : la section Config doit montrer receiver: webhook et url: http://webhook:8090/alertes. Puis journal webhook : tu dois voir une ligne POST /alertes à chaque notification. Si le conteneur labo-webhook n'est pas healthy, docker compose restart webhook.
Empty query result sur P3, P5 ou G1, juste après demarrer. Prometheus a besoin d'au moins un scrape (15 s) pour P3, de deux pour P5 (rate veut deux points dans [1m]), et Alloy met quelques secondes à envoyer la première ligne à Loki. Attends deux minutes après Le labo est prêt.. Si {service="api"} reste vide après cinq minutes, vérifie http://localhost:12345 (Alloy doit être ready et ses composants Healthy) et journal alloy.
P10 rend NaN. histogram_quantile rend NaN quand la fenêtre [5m] ne contient pas encore assez de points. Attends cinq minutes après le démarrage, ou remplace [5m] par [1m] pour voir une valeur plus tôt (moins stable).
G1 rend zéro ligne alors que l'API tourne. Vérifie d'abord la période (en haut à droite, Last 1 hour) et le nom du label (service, en minuscules). Puis http://localhost:12345 : Alloy doit répondre Alloy is ready. et journal alloy ne doit pas montrer d'erreur répétée. En dernier recours, docker compose restart alloy.
parse error dans Prometheus. Les trois plus fréquents, tous vus dans cette page : unexpected identifier "api" in label matching, expected string (guillemets oubliés : {job=api}) ; unexpected character inside braces: '5' ({code=500} au lieu de {code="500"}) ; expected type range vector in call to function "rate", got instant vector (fenêtre [1m] oubliée).
parse error dans Loki. syntax error: unexpected IDENTIFIER : tu as oublié les accolades (service="api" au lieu de {service="api"}). unexpected $end, expecting } or , : accolade fermante manquante. Zéro ligne sans erreur : nom ou casse du label ({service="API"}, {app="api"}).
Windows seulement — Invoke-RestMethod affiche des caractères bizarres (é) dans les titres de cours. C'est l'affichage de la console, pas l'API. [Console]::OutputEncoding = [Text.Encoding]::UTF8 avant la commande, ou lis dans le navigateur.
Windows seulement — .\labo.ps1 : « l'exécution de scripts est désactivée sur ce système ». Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, réponds O, relance.
Windows seulement — demarrer échoue avec port is already allocated sur 3000. Un autre programme écoute déjà (souvent une application Node). $env:GRAFANA_PORT = '3001' puis relance demarrer ; Grafana est alors sur http://localhost:3001 et etat l'affiche ainsi.
Linux natif seulement — permission denied while trying to connect to the Docker daemon socket. sudo usermod -aG docker $USER, ferme la session, rouvre-la, docker info doit répondre.
Linux natif et macOS — bash: ./labo.sh: Permission denied. Le fichier est enregistré sans le bit d'exécution dans le dépôt : chmod +x labo.sh une fois, ou lance bash labo.sh prerequis. Sous Git Bash (Windows), la question ne se pose pas.
Tu veux repartir de zéro. .\labo.ps1 reinitialiser ou ./labo.sh reinitialiser supprime les conteneurs et les volumes : Prometheus, Loki et Grafana repartent vides. Puis demarrer. À ne pas faire sur un labo partagé avec d'autres personnes.