Cómo leer esta página. Cada sección está plegada bajo su título: haz clic en «Mostrar …» para abrirla, y ciérrala cuando termines para mantener la página legible. Orden de lectura: Objetivo, luego En resumen (los comandos a escribir), luego El conjunto de datos (para leer antes de cualquier consulta), luego las consultas Prometheus (P1 a P12) y Grafana Explore (G1 a G8), ordenadas de la más simple (
up,{service="api"}) a la más expresiva, con una explicación después de cada una. El paso a paso detallado, con la salida esperada de cada comando y la falla a provocar, está en anexo: anexo A para Windows (PowerShell), anexo B para Linux, macOS, WSL 2 y Git Bash. Abre un solo anexo, el de tu sistema. El anexo C, común, reúne los casos en que algo falla. Todas las salidas de esta página fueron capturadas en el labo del curso; los valores que dependen del momento (contadores, duraciones, marcas de tiempo) serán distintos en tu máquina, las formas serán idénticas.
Te incorporas al equipo que opera el catálogo de cursos de una plataforma en línea. Tu jefa te entrega el kit del labo: «Mañana por la mañana quiero la pila de observabilidad corriendo en tu computadora, la API adentro, y la prueba de que sabes leer una falla sin llamarme». Vas entonces a iniciar los diez servicios, demostrar que Prometheus lee correctamente sus ocho destinos y que Loki recibe los logs de la API, escribir doce consultas PromQL y ocho consultas LogQL para aprender a leer lo que el labo mide, y después detener la API a propósito. Mirarás la falla propagarse: etat la ve en dos segundos, Prometheus pone el destino en DOWN, la alerta APIInjoignable pasa de pending a firing, Alertmanager la envía al webhook. Luego reparas y miras la alerta apagarse. Reconocer «este servicio está detenido» en diez segundos, y saber dónde buscarlo, es lo que evita horas de búsqueda en el lugar equivocado.
Los nueve pasos de este esquema están detallados, con la salida esperada de cada comando, en el anexo A (Windows) o el anexo B (Linux, macOS) al final de la página.
Kit del labo: https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr
Clonas el kit en una carpeta lab3, verificas que Docker esté listo, inicias los diez servicios, abres las páginas web, escribes las consultas, y luego rompes y reparas. Al final, etat debe mostrar Labo : 10/10 services, 8/8 cibles up, 0 alertes actives., la API debe conocer 64 cours, y el webhook debe haber recibido dos notificaciones para APIInjoignable: una firing, una resolved. Comienza ejecutando este bloque.
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.Espera dos minutos (el tiempo para que Prometheus tenga algunas medidas), luego abre las páginas en el navegador:
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)Las consultas P1 a P12 también están en modules\01-le-labo\requetes.txt, y G1 a G8 en modules\01-le-labo\requetes-logql.txt: ábrelas en un editor y copia y pega. Luego la falla:
.\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 rechaza .\labo.ps1 («l'exécution de scripts est désactivée»): Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, responde O, relanza. Si el puerto 3000 ya está ocupado en tu máquina, $env:GRAFANA_PORT = '3001' antes de demarrer, y reemplaza 3000 por 3001 en las 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.Espera dos minutos, luego abre las páginas en el navegador:
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)Las consultas están en modules/01-le-labo/requetes.txt (P1 a P12) y modules/01-le-labo/requetes-logql.txt (G1 a G8). Luego la falla:
./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 el puerto 3000 está ocupado: GRAFANA_PORT=3001 ./labo.sh demarrer, luego 3001 en las URL de Grafana.
Antes de escribir una sola consulta, mira lo que el labo observa. Todo gira alrededor de una API de catálogo de cursos: un pequeño servicio web escrito en Python (FastAPI) que sirve 64 cursos y registra inscripciones. Un segundo servicio, charge, hace el papel de los usuarios: llama a la API de forma continua, con solicitudes exitosas, algunos 404 deliberados y, una vez de cada cien, un error 500 que la API fabrica por sí misma. Todo lo que vas a leer en Prometheus y en Loki viene de esos dos servicios. El resto del kit, en claro:
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 ExploreAbre los datos tú mismo, toma diez segundos:
# 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 es un arreglo JSON de 64 objetos, uno por curso, identificadores C0001 a C0064. El primero, tal como la API lo devuelve en 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}| Campo | Ejemplo | Qué es |
|---|---|---|
id | C0001 | Identificador del curso, C seguido de cuatro cifras, de C0001 a C0064 |
titre | Introduction à Python | Título mostrado |
categorie | programmation | Una de las nueve categorías: cloud, donnees, gestion, ia, outils, programmation, securite, systemes, web |
niveau | debutant | debutant, intermediaire o avance |
prix | 89 | Precio en dólares, entero |
duree_heures | 6 | Duración total, en horas |
professeur | Karim Haddad | Uno de los diez profesores del catálogo |
tags | ["code","algorithmes"] | Lista de palabras clave |
note | 4.8 | Calificación promedio sobre 5 |
inscrits | 2319 | Número de inscritos al cargar; las inscripciones hechas durante el labo se cuentan aparte, en la métrica inscriptions_total |
Las rutas de la API y lo que responden en el labo del curso:
| Ruta | Respuesta real | Lo que hace |
|---|---|---|
GET /sante | {"etat":"ok","version":"1.0.0","cours":64} | La prueba de salud que Docker llama; etat lee version y cours aquí |
GET /cours?limite=2 | {"total":64,"page":1,"limite":2,"cours":[…]} | La lista paginada; filtros categorie y niveau (?categorie=cloud → "total":6) |
GET /cours/C0001 | el documento anterior | Una ficha; la API cuenta cada consulta en cours_consultes_total{cours_id="C0001"} |
GET /cours/C9999 | 404 {"detail":"cours C9999 introuvable"} | Un 404 limpio: el servicio está sano, el recurso no existe |
POST /inscriptions | 201 (o 404 si el curso no existe, 422 si el cuerpo es inválido) | Llamada por charge; incrementa inscriptions_total{cours_id="…"} |
GET /lent | {"attente_ms":305} | Una ruta deliberadamente lenta (300 a 900 ms) para alimentar el histograma de latencia |
GET /admin/etat | {"taux_erreurs":0.01,"lenteur_ms":0,"inscriptions_enregistrees":855,…} | Los ajustes de falla; casser erreurs y casser lenteur los cambian, reparer los restablece |
GET /metrics | alrededor de 270 líneas de texto | Lo que Prometheus lee cada 15 segundos |
Cada respuesta lleva un encabezado x-id-requete (por ejemplo x-id-requete: c5c49525ea53): es el mismo identificador que el campo id_requete de la línea de log escrita para esa solicitud. Te servirá en la consulta G7.
/metrics expone, una línea real por tipoLa página http://localhost:8000/metrics es texto, una serie por línea, precedida de dos líneas de comentario # HELP (para qué sirve la métrica) y # TYPE (su tipo). En el labo del curso, tiene alrededor de 270 líneas. Las cuatro métricas que vas a consultar, copiadas de la página:
# 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| Tipo | Métrica del labo | Cómo leerla |
|---|---|---|
| counter (contador) | http_requetes_total | Solo sube: 3247 respuestas 200 en /cours desde el arranque de la API. Solo su velocidad tiene sentido (P5) |
| gauge (medidor) | requetes_en_cours | Sube y baja: 1 solicitud siendo procesada en el instante de la lectura. Se lee tal cual (P11) |
| histogram (histograma) | http_duree_requete_seconds | Cubos acumulativos: 3248 solicitudes en /cours tomaron menos de 50 ms (le="0.05"), 3279 en total (+Inf = _count). _sum / _count = duración promedio (aquí 26 ms). P10 saca de ahí un p95 |
| info (un medidor en 1) | api_info | El valor es siempre 1; la información está en la etiqueta version="1.0.0" |
No hay summary en esta API: Prometheus lo desaconseja en favor del histograma, que se agrega entre instancias.
Una serie es un nombre de métrica más un juego de pares label="valor". Dos orígenes:
| Etiqueta | Puesta por | Valores en el labo |
|---|---|---|
methode | la API | GET, POST |
route | la API | /sante, /cours, /cours/{id}, /inscriptions, /lent, /admin/etat, inconnue (toda URL que no existe) |
code | la API | 200, 201, 404, 422, 500 |
le | la API, solo en el histograma | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, +Inf |
cours_id | la API, en inscriptions_total y cours_consultes_total | C0001 a C0064 |
job | Prometheus, según prometheus.yml | prometheus, api, node-exporter, cadvisor, alertmanager, grafana, loki, alloy |
instance | Prometheus | la dirección leída: api:8000, localhost:9090, grafana:3000… |
service | Prometheus, agregada a mano en prometheus.yml para el job api | api |
Observa la ruta /cours/{id}: la API normaliza la URL antes de contar. /cours/C0001 y /cours/C0043 caen en la misma serie. Sin eso, habría 64 series por código en lugar de una, y 64 veces más líneas en /metrics.
La API escribe una línea de log por solicitud procesada, en JSON. Alloy lee la salida de cada contenedor labo-* y la empuja hacia Loki. Una línea real, leída con .\labo.ps1 journal api (o ./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"}| Campo | Ejemplo | Qué es |
|---|---|---|
horodatage | 2026-09-15T19:33:14.781+00:00 | Fecha y hora UTC, al milisegundo |
niveau | ERROR | INFO (2xx), WARNING (4xx), ERROR (5xx) |
id_requete | dc1c2ff189a6 | El identificador devuelto en el encabezado x-id-requete de la respuesta |
methode, route, code | GET, /cours/{id}, 500 | Los mismos valores que las etiquetas de http_requetes_total: es el puente entre métricas y logs |
duree_ms | 0.1 | Duración del procesamiento, en milisegundos |
message | GET /cours/C0019 -> 500 | La frase legible, con la URL real esta vez (C0019, no {id}) |
Loki no lee el JSON línea por línea en el momento de la consulta, salvo si se lo pides (| json, consulta G5). Lo que indexa son etiquetas puestas por Alloy a la llegada:
| Etiqueta Loki | Valores | Puesta por |
|---|---|---|
service | api, charge, webhook, prometheus, alertmanager, grafana, loki, alloy, node-exporter, cadvisor | Alloy, según el nombre del servicio Compose |
conteneur | labo-api, labo-charge… | Alloy, según el nombre del contenedor |
niveau | INFO, WARNING, ERROR | Alloy, extraída del campo niveau del JSON |
code | 200, 201, 404, 422, 500 | Alloy, extraída del campo code del JSON |
detected_level | info, warn, error | Loki mismo, que adivina el nivel; puedes ignorarla |
Recuerda dos cosas que volverás a encontrar en todas partes: la serie http_requetes_total{code="500",route="/cours"}, en 32 en /metrics en el momento de la captura, que volverás a ver en 32 en P4; y el identificador dc1c2ff189a6, el de la línea de error de arriba, que encontrarás en G2 y que luego irás a buscar solo en G7. Las métricas cuentan, los logs relatan; ambos hablan de la misma solicitud.
Las dos secciones que siguen contienen veinte consultas: doce para Prometheus (P1 a P12), ocho para Loki a través de Grafana Explore (G1 a G8). Están ordenadas de la más simple a la más expresiva, y cada una agrega una sola novedad respecto a la anterior. Si una consulta te parece oscura, casi siempre es porque la anterior todavía no está clara: vuelve atrás en lugar de continuar.
Escribe cada consulta tú mismo, compara el resultado con el de la página, lee la explicación, y luego pasa a la siguiente. Las cifras serán distintas en tu máquina: los contadores suben desde el arranque de tu API, no de la del curso. Las formas (el número de series, las etiquetas, el orden de magnitud) deben ser las mismas. Prometheus y Loki usan la misma idea de partida, un juego de etiquetas entre llaves, y es a propósito: lo que aprendes en P2 sirve en G1.
Abre http://localhost:9090. Llegas a la página Query (el menú superior propone Query, Alerts, Status). Pega una consulta en el campo, presiona Enter o haz clic en Execute. El resultado se muestra debajo en la pestaña Table (una línea por serie, el valor a la derecha); la pestaña Graph dibuja las mismas series en el tiempo. Quédate en Table para esta sección, salvo indicación contraria. Debajo de las pestañas, una línea del tipo Load time: 40ms Result series: 8 te dice cuántas series respondieron.
La imagen a tener en mente: Prometheus es un cuaderno de lecturas. Cada 15 segundos, pasa delante de cada uno de sus ocho destinos, lee su página /metrics y anota cada valor con la hora. Una consulta PromQL es una pregunta planteada a ese cuaderno.
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, todas en 1.
Lo que pide la consulta: «Dame el último valor de la métrica up para todos los destinos».
Cero parámetros: solo un nombre de métrica. up no la expone ningún destino; es Prometheus quien la fabrica en cada scrape: 1 si la página /metrics respondió, 0 si no. Ocho series porque prometheus.yml declara ocho jobs. Cada línea se lee: el nombre de la métrica, luego entre llaves las etiquetas que Prometheus puso (job e instance en todas, service además en la API), luego el valor. Equivalente SQL: SELECT * FROM up. Lo que Prometheus hace y SQL no: produjo él mismo esta tabla yendo a tocar ocho puertas.
El labo tiene diez contenedores pero Prometheus solo lee ocho: charge y webhook no exponen página /metrics en esta versión del kit, por lo que no son destinos. etat cuenta los dos por separado: 10/10 services (los contenedores) y 8/8 cibles up (los scrapes). Si un día up devuelve 7 series en lugar de 8, no es que un destino haya caído (estaría en 0): es que un job desapareció de la configuración. El kit tiene una alerta para eso, CibleAbsente.
up{job="api"}up{instance="api:8000", job="api", service="api"} 1Lo que pide la consulta: «El valor de up, solo para las series cuya etiqueta job vale api».
Una sola novedad: el selector {job="api"}. Las llaves filtran por las etiquetas, como un WHERE job = 'api'. Las comillas son obligatorias alrededor del valor: up{job=api} se rechaza con parse error: unexpected identifier "api" in label matching, expected string. Es exactamente esta expresión, up{job="api"} == 0, la que vigila la regla APIInjoignable; la verás pasar a 0 en el anexo.
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 en el labo del curso (el número exacto depende de las combinaciones que charge ya produjo; sube hasta 16 con el tiempo).
Lo que pide la consulta: «Todas las series del contador http_requetes_total, con su valor actual».
Nada nuevo en la sintaxis: un nombre, como P1. Lo nuevo es lo que lees. Compara con la página /metrics: la línea http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0 se convirtió en http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241. Prometheus agregó tres etiquetas (instance, job, service) y el valor difiere en algunas unidades: la página se leyó en otro instante. Una serie por combinación (methode, route, code): es lo que se llama la cardinalidad de la métrica, aquí 14.
La diferencia esencial entre
/metricsy Prometheus. La página/metricses el estado de la API en el instante en que la abres, sin historial. Prometheus guarda todas las lecturas, una cada 15 segundos, y eso es lo que permite P5: calcular una velocidad supone tener al menos dos puntos.
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"} 7Lo que pide la consulta: «Las series de http_requetes_total cuya etiqueta code vale 500».
Nada nuevo: es P2 aplicado a P3. Cuatro series, una por ruta afectada. El hilo conductor está ahí: route="/cours" en 32, el valor leído en /metrics. El 500 es una cadena, no un número: http_requetes_total{code=500} se rechaza (parse error: unexpected character inside braces: '5'). Esos 500 no son una falla: charge provoca a propósito un error de cada cien (taux_erreurs: 0.01 en /admin/etat) para que las curvas de error nunca estén vacías.
Balance de P1 a P4: todavía no calculaste nada. Leíste valores instantáneos y aprendiste a filtrarlos por etiqueta.
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 solicitudes por segundo.
Lo que pide la consulta: «Para cada serie del contador, ¿cuánto aumentó por segundo, en promedio, durante el último minuto?».
Una sola novedad, en dos pedazos inseparables: [1m] transforma la serie en rango (todos los valores del último minuto, en lugar del último solo), y rate() calcula la pendiente de ese rango. Mira el resultado: el nombre de la métrica desapareció de las llaves, porque ya no es http_requetes_total, es una velocidad derivada. /cours recibe 3,7 solicitudes por segundo, /sante 0,11 (una cada 9 segundos: es el healthcheck de Docker). Equivalente SQL: no hay uno simple; harían falta dos lecturas, una resta y una división por el tiempo transcurrido. rate() hace eso para cada serie, y además corrige los reinicios a cero cuando la API se reinicia.
Sin el rango, Prometheus se niega: rate(http_requetes_total) da parse error: expected type range vector in call to function "rate", got instant vector. Leerás este mensaje a menudo; significa «falta […]».
3241 solicitudes 200 en /cours no dice nada: ¿desde cuándo? Si la API lleva corriendo una hora, está tranquilo; si lleva un minuto, es un ataque. Un contador solo vale por su pendiente. Por eso en un tablero de control nunca verás http_requetes_total bruto, sino siempre rate(http_requetes_total[…]). La ventana [1m] suaviza sobre un minuto; [5m] suaviza más (curva más tranquila, reacción más lenta). El kit usa [5m] en sus alertas y [1m] aquí para que veas moverse algo. Regla práctica: la ventana debe contener al menos dos scrapes, así que aquí al menos [30s]; rate(http_requetes_total[10s]) devuelve un resultado vacío.
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.
Lo que pide la consulta: «Toma las velocidades de P5 y súmalas conservando solo la etiqueta route».
Una sola novedad: la agregación sum by (route) (…). Hace desaparecer todas las demás etiquetas (code, methode, instance…) y suma lo que queda. /cours/{id} pasa de tres series (200, 404, 500) a una: 3,00 + 0,27 + 0,04 = 3,31. Equivalente SQL: SELECT route, SUM(vitesse) FROM … GROUP BY route. Los paréntesis alrededor de route son obligatorios: sum by route (…) se rechaza (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.0444493832648072Lo que pide la consulta: «La misma suma que P6, pero agrupada por código HTTP».
Nada nuevo: P6 con otra etiqueta. Es lo que hace el panel «Réponses par code» (respuestas por código) del tablero de control «API catalogue — signaux dorés» en Grafana. Cinco códigos, cinco líneas; los 500 en 0,09 por segundo, es decir, un poco más de un error cada doce segundos.
sum(rate(http_requetes_total[1m])){} 8.956550727858652Lo que pide la consulta: «Suma todas las velocidades, sin conservar ninguna etiqueta».
Una sola novedad: sum(…) sin by. Resultado: una serie única, con un juego de etiquetas vacío ({}), el valor 8,96 solicitudes por segundo. Es la primera de las cuatro señales de oro, el tráfico. Verifica: la suma de las siete líneas de P6 da efectivamente 8,96.
Balance de P5 a P8: sabes transformar un contador en velocidad, y luego agrupar esa velocidad como quieras. Tres cuartas partes de los tableros de control Prometheus no hacen más que eso.
sum(rate(http_requetes_total{code=~"5.."}[1m])) / sum(rate(http_requetes_total[1m])){} 0.009925558312655085Lo que pide la consulta: «La velocidad de las respuestas cuyo código empieza por 5, dividida por la velocidad de todas las respuestas».
Dos novedades, pero pequeñas. Primero =~: un selector por expresión regular, "5.." = un 5 seguido de dos caracteres cualesquiera, por lo tanto todos los 5xx. Después la división de dos resultados: Prometheus divide las series que tienen las mismas etiquetas, y aquí los dos lados tienen un juego vacío {}, así que se emparejan. Resultado: 0,0099, es decir, 1 %; es el valor ajustado en /admin/etat (taux_erreurs: 0.01). Segunda señal de oro, los errores. La regla de alerta TauxErreursEleve del kit se dispara cuando esta misma expresión, calculada sobre 5 minutos, supera 0,05.
histogram_quantile(0.95, sum by (le) (rate(http_duree_requete_seconds_bucket[5m]))){} 0.09797705555555555Lo que pide la consulta: «A partir de los cubos del histograma de duración, todas las rutas juntas, ¿bajo qué valor caen el 95 % de las solicitudes de los últimos 5 minutos?».
Una sola novedad: histogram_quantile(0.95, …). Quiere como entrada los cubos _bucket sumados por le (por eso el sum by (le) es obligatorio: sin él, calcula un cuantil por ruta y el resultado ya no tiene el sentido que esperabas). Resultado: 0,098 segundos, así que el 95 % de las solicitudes se sirven en menos de 98 ms. Es la tercera señal de oro, la latencia, y la métrica que la alerta LatenceP95Elevee vigila (umbral: 0,5 s). Mira la distribución de los cubos de /cours en el conjunto de datos: 3248 solicitudes de 3279 bajo 50 ms, pero /lent (300 a 900 ms) tira el p95 global hacia arriba.
Cada línea _bucket{le="0.05"} cuenta las solicitudes que tomaron como máximo 0,05 segundos (le = less or equal). Los cubos son acumulativos: le="0.1" contiene también todo lo que estaba en le="0.05". El último, le="+Inf", contiene todo, y siempre vale _count. histogram_quantile busca el cubo donde la curva acumulativa cruza el 95 % e interpola dentro. La precisión depende entonces de la elección de los cubos: entre 0.05 y 0.1, Prometheus supone una distribución uniforme. Por eso el resultado, 0.0979…, no es un valor medido sino una estimación.
requetes_en_coursrequetes_en_cours{instance="api:8000", job="api", service="api"} 0Lo que pide la consulta: «El último valor del medidor requetes_en_cours».
Nada nuevo en la sintaxis, es P1. Lo nuevo es el tipo: un medidor se lee tal cual, sin rate(). Cero o uno, según el instante: la API procesa cada solicitud en unos milisegundos, es raro atrapar una en curso. Es la cuarta señal de oro, la saturación: si este valor subiera a 50, la API estaría desbordada. rate(requetes_en_cours[1m]) no provoca error, pero devuelve un número que no significa nada; Prometheus no te protege de esa confusión.
ALERTSEmpty query resultLo que pide la consulta: «Las alertas actualmente pending o firing».
Nada nuevo: un nombre de métrica, como P1. ALERTS es, como up, fabricada por Prometheus: una serie por alerta activa, con las etiquetas alertname y alertstate. En un labo sano, el resultado está vacío: Empty query result. No es un error, es la mejor respuesta posible. La volverás a escribir durante la falla del anexo y verás aparecer ALERTS{alertname="APIInjoignable", alertstate="pending", …} y luego alertstate="firing".
Balance de P9 a P12: las cuatro señales de oro (tráfico P8, errores P9, latencia P10, saturación P11) caben en cuatro consultas, y las alertas son una métrica como las demás.
El mensaje que debe quedar. Lo que SQL también hace: filtrar por columna (
{job="api"}=WHERE), agrupar y sumar (sum by (route)=GROUP BY), dividir dos agregados. Lo que solo Prometheus hace: fue a buscar él mismo los datos cada 15 segundos en ocho servicios, transforma cualquier contador en velocidad con una función (rate), estima un cuantil a partir de cubos (histogram_quantile), y expone sus propias alertas como una métrica (ALERTS).
Abre http://localhost:3000 (usuario admin, contraseña aiopsatlas2026). En el menú principal (ícono arriba a la izquierda), haz clic en Explore. En la parte superior de la página, el selector de fuente de datos propone Prometheus, Loki y Alertmanager: elige Loki. A la derecha del campo de consulta, dos modos: Builder (menús) y Code (escribes). Pasa a Code, pega la consulta, luego Run query (o Mayús+Enter). Los logs se muestran abajo, la línea más reciente primero. Arriba a la derecha, el selector de período está en Last 1 hour por defecto: déjalo. Estos pasos son idénticos en Windows y en Linux, es el navegador el que trabaja.
La imagen a tener en mente: Loki es un armario de bitácoras, una carpeta por combinación de etiquetas. No lee el contenido de las líneas para ordenarlas, solo la etiqueta de la carpeta. Una consulta LogQL comienza entonces siempre por elegir una carpeta, entre llaves, y luego eventualmente por filtrar las líneas dentro.
{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"}
…Lo que pide la consulta: «Todas las líneas de log cuya etiqueta service vale api».
Cero novedades respecto a P2: un selector entre llaves. La diferencia es el resultado: líneas de texto, no números. Grafana muestra la hora (convertida a tu zona horaria) y luego la línea bruta; haz clic en una línea para desplegar sus etiquetas: service="api", conteneur="labo-api", niveau="INFO", code="200", detected_level="info". Alrededor de nueve líneas por segundo, tantas como P8 anunciaba: una solicitud, una línea. Las llaves son obligatorias: service="api" solo se rechaza (parse error at line 1, col 1: syntax error: unexpected IDENTIFIER), y {service="api" sin cierre también (syntax error: unexpected $end, expecting } or ,).
Una etiqueta es una cadena exacta. {service="API"} en mayúsculas no devuelve ninguna línea, sin error: la carpeta no existe. Lo mismo para {app="api"}: la etiqueta se llama service en este labo, no app. Cuando una consulta LogQL devuelve cero líneas, verifica primero el nombre y las mayúsculas de la etiqueta; en Explore, el modo Builder te lista las etiquetas y sus valores existentes, es la forma más segura de descubrirlos.
{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"}
…Lo que pide la consulta: «Las líneas de la API cuya etiqueta niveau vale ERROR».
Una sola novedad: dos etiquetas en el selector, separadas por una coma, es un Y. La etiqueta niveau no está en la línea al principio: es Alloy quien la extrajo del campo JSON niveau antes de enviar a Loki, y eso es lo que hace rápida esta consulta. El hilo conductor está ahí, tercera línea: id_requete: dc1c2ff189a6, la línea del conjunto de datos. Muchas menos líneas que en G1: alrededor de una cada doce segundos, como P7 lo decía para los 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, …}
…Lo que pide la consulta: «Las líneas de la API cuya etiqueta code vale 500».
Nada nuevo: G2 con otra etiqueta. Las mismas líneas que en G2, porque en esta API todo 500 es un ERROR y viceversa. Es la versión log de P4: donde Prometheus te dice «32 errores en /cours», Loki te muestra cuáles, con la URL real (/cours/C0019) y el identificador de la solicitud. Observa que code es aquí una cadena ("500") porque es una etiqueta; en el JSON de la línea, es un número (500). G6 te mostrará la diferencia.
{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"}
…Lo que pide la consulta: «Las líneas de la API que contienen el texto inscriptions».
Una sola novedad: el filtro de línea |= "…", que conserva las líneas que contienen exactamente ese texto. Es grep. A diferencia de una etiqueta, Loki debe aquí abrir cada línea de la carpeta {service="api"} para mirar dentro: más lento, pero puedes buscar cualquier cosa. Las variantes: != (no contiene), |~ (expresión regular), !~. Resultado mezclado: 201 y 500, todo lo que toca a las inscripciones.
Balance de G1 a G4: dos formas de filtrar, por etiqueta (rápida, antes de abrir las líneas) y por texto (flexible, después). La consulta correcta siempre empieza por la etiqueta más estrecha posible.
{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" …Lo que pide la consulta: «Las líneas de la API, y para cada una, transforma los campos del JSON en etiquetas».
Una sola novedad: el parser | json. Las líneas mostradas son las mismas que en G1, pero despliega una: ahora tiene muchas más etiquetas (route, methode, duree_ms, id_requete, message…), una por campo del JSON. Esas etiquetas se calculan en el momento de la consulta, no se almacenan: Loki sigue indexando solo service, conteneur, niveau, code. Verás también code_extracted y niveau_extracted: cuando un campo del JSON lleva el mismo nombre que una etiqueta ya puesta por Alloy, Loki agrega un sufijo a la copia en lugar de sobrescribir.
{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"}
…Lo que pide la consulta: «Las líneas de la API cuyo campo duree_ms, una vez abierto el JSON, supera 500».
Una sola novedad: el filtro de etiqueta | duree_ms > 500, que compara una etiqueta extraída con un número. Solo es posible después de | json, si no duree_ms no existe. Resultado: únicamente /lent, la ruta deliberadamente lenta (300 a 900 ms). Es la versión log de P10: Prometheus dice «el p95 está en 98 ms»; Loki muestra las solicitudes individuales que superaron un umbral, con su identificador. Equivalente SQL: WHERE duree_ms > 500, salvo que la columna no existía antes de la consulta.
{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"}Una sola línea.
Lo que pide la consulta: «La línea de la API que contiene el identificador dc1c2ff189a6».
Nada nuevo: es G4 con otro texto. Lo que cambia es el uso: en tu máquina, dc1c2ff189a6 no existe; copia un id_requete visto en tu propia salida de G2 y pégalo en su lugar. Es el gesto que harás en producción: un usuario te da el identificador devuelto por el encabezado x-id-requete de su respuesta en error, y encuentras en una consulta la línea exacta, con la ruta, el código y la duración. Una sola línea: el identificador es único por solicitud.
sum by (niveau) (count_over_time({service="api"}[1m]))Esta vez, Grafana muestra un gráfico en lugar de líneas: tres curvas, {niveau="INFO"} alrededor de 470 a 500 líneas por minuto, {niveau="WARNING"} alrededor de 40 a 50, {niveau="ERROR"} entre 2 y 10, en el labo del curso. Pasa el ratón sobre el gráfico para leer los valores.
Lo que pide la consulta: «Cuenta las líneas de la API por tramo de un minuto, luego suma conservando la etiqueta niveau».
Una sola novedad, en un pedazo que ya conoces: count_over_time(…[1m]) cuenta las líneas de un selector sobre un rango, exactamente como rate(…[1m]) calcula una pendiente en P5. Alrededor, sum by (niveau) es el sum by (route) de P6, palabra por palabra. LogQL tomó prestada esta gramática de PromQL a propósito: lo que aprendiste de un lado sirve del otro. Compara con P7: Prometheus cuenta 0,09 respuestas 500 por segundo, es decir, 5 por minuto; Loki cuenta 5 líneas ERROR por minuto. Dos herramientas, dos caminos, la misma cifra.
La diferencia esencial entre Prometheus y Loki. Prometheus almacena números ya contados por la API (
http_requetes_total), Loki almacena las líneas y puede recontarlas a demanda (count_over_time). El primero es ligero y rápido, y responde «cuántos»; el segundo es pesado pero conserva el detalle, y responde «cuáles». El labo tiene los dos porque ninguno reemplaza al otro.
El mensaje que debe quedar. Lo que
greptambién hace: buscar un texto en líneas (|=). Lo que solo Loki hace: ordenar las líneas de diez contenedores por etiquetas y leer solo la carpeta correcta, abrir el JSON a demanda para filtrar por un campo numérico (| json | duree_ms > 500), y transformar logs en curva con la gramática de PromQL (count_over_time).
Tres consultas que combinan lo que viste, sin noción nueva. Escríbelas, y luego explica en una frase lo que muestra cada una.
topk(5, increase(inscriptions_total[1h]))Los cinco cursos que recibieron más inscripciones en la última hora. increase es rate multiplicado por la duración de la ventana; topk(5, …) conserva las cinco series más grandes. En el labo del curso, C0001 llega a la cabeza con alrededor de 177 inscripciones: charge favorece algunos cursos «populares».
histogram_quantile(0.95, sum by (le, route) (rate(http_duree_requete_seconds_bucket[5m])))P10 con una etiqueta más en el by: un p95 por ruta. Verás /lent alrededor de 0,7 s y las demás rutas bajo 0,03 s. Mira lo que eso cambia respecto al p95 global de P10.
{service="api"} |= "inscriptions" | json | code = 201G4, G5 y G6 encadenados: solo las inscripciones exitosas. Verifica que el número de líneas por minuto corresponda a la línea {code="201"} de P7, alrededor de 0,87 por segundo, es decir, unas cincuenta por minuto.
Todos los comandos se escriben en PowerShell, desde la carpeta lab3. Docker Desktop debe estar lanzado (ícono verde). Si PowerShell se niega a ejecutar .\labo.ps1, escribe una vez Set-ExecutionPolicy -Scope CurrentUser RemoteSigned y responde O.
cd C:\Users\<toi>\Documents
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
lsDebes ver docker-compose.yml, labo.ps1, labo.sh, README.md, y las carpetas alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. Si ya clonaste el kit en la lección 03, salta este paso y haz solo 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 demarrerPunto de control: la última línea es Tout est prêt.. Las versiones, la memoria y el número de procesadores son los de la máquina del curso. Si un puerto está marcado ✘ … déjà occupé, la lección 03 explica qué hacer; para el 3000, $env:GRAFANA_PORT = '3001' basta.
.\labo.ps1 demarrerLa primera vez, la descarga de las seis imágenes públicas toma de uno a cinco minutos según tu conexión. Final de la salida esperada:
== 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)Punto de control: diez prêt, luego Le labo est prêt.. En la máquina del curso, con las imágenes ya en caché, el comando tomó 44 segundos. La lección 04 comenta esta salida línea por línea.
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.Punto de control: diez (healthy), 8/8, 64 cours, 0 alerte(s) reçue(s), y la última línea Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. El número de series en memoria sube durante los primeros minutos (9038 justo después del arranque, 12 000 a 15 000 después de una hora en la máquina del curso). Si alloy o grafana todavía están (health: starting), espera treinta segundos y relanza.
Abre http://localhost:9090, menú Status, luego Target health. Ocho bloques, uno por job, cada uno con 1 / 1 up y una línea:
api
1 / 1 up
Endpoint Labels Last scrape State
http://api:8000/metrics instance="api:8000" job="api" service="api" 6.014s ago UPPunto de control: ocho UP, ningún DOWN. La columna Last scrape nunca supera los 15 segundos: es el scrape_interval de prometheus.yml. En línea de comandos, la misma información:
(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/metricsEspera a que demarrer tenga al menos dos minutos de recorrido, luego sigue las secciones Prometheus, en la pestaña Graph y Grafana, en Explore más arriba. Las consultas están listas para copiar:
Get-Content modules\01-le-labo\requetes.txt
Get-Content modules\01-le-labo\requetes-logql.txtPunto de control: P1 devuelve 8 series en 1, P12 devuelve Empty query result, G1 devuelve líneas JSON, G8 devuelve tres curvas.
Antes de romper, anota la hora (Get-Date -Format HH:mm:ss). Luego:
.\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 reparerEl script hizo un docker compose stop api: el contenedor está detenido limpiamente, sus datos y su imagen están intactos. Ahora, observa la falla por cinco caminos, en orden. Tienes alrededor de 70 segundos antes de que la alerta llegue al webhook: lanza etat de inmediato.
Camino 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.Lo que hay que leer, de arriba abajo: labo-api está Exited (0) (código 0: detención voluntaria, no un colapso); Prometheus ya no lee más que 7/8 destinos y te dice por qué (lookup api … no such host: el nombre api ya no existe en la red Docker puesto que el contenedor está detenido); la alerta APIInjoignable está activa; la API no responde en el puerto 8000; el webhook recibió algo. Esta salida se capturó en la máquina del curso un minuto después de casser api, cuando un casser erreurs acababa de ejecutarse unos minutos antes: por eso aparece también una segunda alerta, TauxErreursEleve. En tu labo, solo tendrás APIInjoignable, 1 alertes actives y 1 alerte(s) reçue(s). Si lanzas etat en los primeros 30 segundos, la alerta todavía está en attente (pending) y el webhook todavía está en 0: relanza un minuto más tarde.
Camino 2, los destinos. Recarga http://localhost:9090 → Status → Target health. El bloque api pasó a 0 / 1 up, estado DOWN, y la columna Error lleva el mismo mensaje que etat: Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Los otros siete siguen UP. Vuelve a escribir up en Query: la línea up{instance="api:8000", job="api", service="api"} está en 0, las otras siete en 1. Luego up == 0: una sola línea.
Camino 3, las alertas en Prometheus. Menú Alerts. La regla APIInjoignable cambia de estado en tres tiempos, cronometrados en la máquina del curso:
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)Por qué 40 segundos antes de pending: Prometheus lee la API cada 15 segundos, así que hacen falta hasta 15 segundos para que un scrape falle, y luego evalúa las reglas cada 15 segundos. Por qué 30 más antes de firing: la regla dice for: 30s. En otra captura, pending llegó a los 31 s y firing a los 61 s: el orden de magnitud es el mismo, el detalle depende del instante en que rompiste respecto al ciclo de scrape. Vuelve a escribir ALERTS en Query:
ALERTS{alertname="APIInjoignable", alertstate="pending", instance="api:8000", job="api", service="api", severite="critique"} 1luego, treinta segundos más tarde, alertstate="firing". Anota la hora del paso a firing: es la primera de las tres líneas de tu entregable.
Camino 4, Alertmanager. Abre http://localhost:9093. La página Alerts muestra un grupo alertname="APIInjoignable" service="api" (es el group_by: [alertname, service] de alertmanager.yml) con la alerta, sus etiquetas (instance="api:8000", job="api", labo="observabilite", severite="critique"), su resumen L'API catalogue ne répond plus y su descripción. La etiqueta labo="observabilite" no estaba en la regla: es el external_labels de prometheus.yml, agregado a todo lo que sale de Prometheus. En línea de comandos:
(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.496ZCamino 5, el webhook. Abre http://localhost:8090. La página «Alertes reçues d'Alertmanager» ya no está vacía: una línea APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, y el encabezado cuenta 1 alerte(s) en mémoire · 1 notification(s) reçue(s). El formato bruto, http://localhost:8090/alertes.json, muestra lo que Alertmanager envió:
{"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"}}Lee debut (19:41:11, la hora del firing en Prometheus) y recu_a (19:41:26): quince segundos de diferencia, incluidos los 10 segundos de group_wait de Alertmanager. fin en el año 0001 significa «todavía no terminada». Anota recu_a: segunda línea de tu entregable.
Lo que ve la carga, mientras tanto:
.\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"}Y lo que ves si llamas a la API tú mismo:
Invoke-RestMethod http://localhost:8000/sante -TimeoutSec 5Invoke-RestMethod : Le délai de l'opération a expiré.(Sin -TimeoutSec, PowerShell espera más tiempo antes de renunciar; curl.exe -sS http://localhost:8000/sante responde más rápido: curl: (7) Failed to connect to localhost:8000 after 2237 ms: Could not connect to server.)
La diferencia esencial entre un 404 y ninguna respuesta. En el paso A.8,
/cours/C9999responderá404: la API corre y te dice educadamente que ese curso no existe; eso se cuenta enhttp_requetes_total{code="404"}, se escribe en un logWARNING, yupsigue en1. Aquí,Le délai de l'opération a expiré: nadie responde, no hay ni código ni log del lado de la API, y esupel que cae a0. Dos situaciones, dos señales, dos lugares donde buscar.
.\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).El script hizo docker compose start api, esperó a que /sante respondiera, y luego llamó a /admin/reparer (útil para los otros dos escenarios de falla). Verifica que la API trabaja:
.\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"}Luego mira la alerta apagarse, en el mismo orden en que se encendió. Cronometrado en la máquina del curso después de reparer:
t+15 s : APIInjoignable firing (Prometheus) · webhook : firing
t+40 s : APIInjoignable inactive (Prometheus) · webhook : firing
t+55 s : APIInjoignable inactive (Prometheus) · webhook : resolvedEn el primer scrape exitoso, up{job="api"} vuelve a 1 y la regla vuelve a inactive; Alertmanager envía entonces una notificación resolved al webhook. Recarga http://localhost:8090: ahora dos líneas para APIInjoignable, una firing y una resolved, y en /alertes.json la segunda tiene un campo fin completado:
{"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 menos debut: la falla duró 45 segundos a los ojos de Prometheus. Anota recu_a de la línea resolved: tercera línea de tu entregable.
La API corre. Pídele un curso que no existe:
Invoke-RestMethod http://localhost:8000/cours/C9999Invoke-RestMethod : {"detail":"cours C9999 introuvable"}Es un error HTTP 404: la API respondió. Para ver el código en sí:
try { Invoke-WebRequest http://localhost:8000/cours/C9999 -UseBasicParsing } catch { $_.Exception.Response.StatusCode.value__ }404Vuelve a escribir P4 en Prometheus reemplazando 500 por 404: la serie route="/cours/{id}" aumentó en 1. Vuelve a escribir G1 agregando |= "C9999": tu solicitud está ahí, nivel WARNING, con su id_requete. Nada de eso existe para la falla de A.6: una API detenida no cuenta nada y no escribe nada.
.\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.Lo que debes tener: labo-api de nuevo Up … (healthy) con un tiempo más corto que los demás (acaba de reiniciarse), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s) (la firing y la resolved), y la última línea Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. En la máquina del curso, esta salida todavía llevaba 1 alertes actives y 3 alerte(s) reçue(s) a causa del TauxErreursEleve residual mencionado en A.6; en la tuya, con la única falla api, los valores son los de arriba. Esta salida y tus tres horas anotadas son tu entregable. Puedes dejar el labo corriendo para los talleres 06 y 07, o detenerlo con .\labo.ps1 arreter: los datos se conservan y demarrer retoma donde estabas.
Todos los comandos se escriben en una terminal bash, desde la carpeta lab3. En macOS y Windows (WSL 2 o Git Bash), Docker Desktop debe estar lanzado; en Linux nativo, docker info debe responder sin sudo (si no, sudo usermod -aG docker $USER, luego abre una nueva sesión). El script bash necesita curl.
cd ~
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
lsDebes ver docker-compose.yml, labo.sh, labo.ps1, README.md, y las carpetas alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. En Linux y macOS, vuelve ejecutable el script una vez: chmod +x labo.sh (sin eso, ./labo.sh responde bash: ./labo.sh: Permission denied). Si ya clonaste el kit en la lección 03, salta este paso y haz solo 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 demarrerPunto de control: la última línea es Tout est prêt.. El script bash verifica una línea más que PowerShell, curl : présent. Las versiones y la memoria son las de la máquina del curso (Git Bash en Windows). Si un puerto está ocupado, la lección 03 explica qué hacer; para el 3000, GRAFANA_PORT=3001 ./labo.sh demarrer.
./labo.sh demarrerFinal de la salida esperada, una vez descargadas las imágenes:
== 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)Punto de control: diez prêt, luego 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.Punto de control: diez (healthy), 8/8, 64 cours, y la última línea Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. El bloque Conteneurs viene de docker compose ps; puedes escribirlo tú mismo. Tercer camino, 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}En el navegador, http://localhost:9090 → Status → Target health: ocho bloques 1 / 1 up, estado UP, columna Last scrape siempre bajo 15 segundos. En curl, con python3 para leer el JSON (o jq si lo tienes):
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/metricsEspera dos minutos después de demarrer, luego sigue las secciones Prometheus, en la pestaña Graph y Grafana, en Explore más arriba; se hacen en el navegador, de forma idéntica en todos los sistemas. Las consultas están listas para copiar:
cat modules/01-le-labo/requetes.txt
cat modules/01-le-labo/requetes-logql.txtTercer camino, una consulta PromQL en curl:
curl -s 'http://localhost:9090/api/v1/query?query=up' | python3 -m json.tool | head -n 20Reconoces en el JSON las mismas series que en la pestaña Table: "metric": {"__name__": "up", "instance": "localhost:9090", "job": "prometheus"} y "value": [1789500809.696, "1"].
Punto de control: P1 devuelve 8 series en 1, P12 devuelve Empty query result, G1 devuelve líneas JSON, G8 devuelve tres curvas.
Anota la hora (date +%T), luego:
./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 reparerEl script hizo docker compose stop api. Tienes alrededor de 70 segundos antes de que la alerta llegue al webhook. Observa por cinco caminos.
Camino 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.Lo que hay que leer: labo-api está Exited (0) (detención voluntaria); Prometheus ya no lee más que 7/8 destinos y dice por qué (lookup api … no such host: el nombre api desapareció de la red Docker); APIInjoignable está activa; la API no responde; el webhook recibió la alerta. Esta salida se capturó un minuto después de casser api, en una máquina donde casser erreurs acababa de ejecutarse: de ahí la segunda alerta TauxErreursEleve. En tu máquina: 1 alertes actives, 1 alerte(s) reçue(s). Si lanzas etat en los primeros 30 segundos, la alerta todavía está pending y el webhook en 0: relanza un minuto más tarde. Tercer camino, en curl:
curl -s http://localhost:8000/santecurl: (7) Failed to connect to localhost:8000 after 2237 ms: Could not connect to serverCamino 2, los destinos. Recarga http://localhost:9090 → Status → Target health: el bloque api está en 0 / 1 up, estado DOWN, columna Error: Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Vuelve a escribir up en Query: up{instance="api:8000", job="api", service="api"} está en 0. Luego up == 0: una sola línea.
Camino 3, las alertas en Prometheus. Menú Alerts. APIInjoignable cambia de estado en tres tiempos, cronometrados en la máquina del curso:
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)Por qué 40 segundos antes de pending: hasta 15 segundos para que un scrape falle (scrape_interval: 15s), luego hasta 15 segundos para la próxima evaluación de las reglas (evaluation_interval: 15s). Por qué 30 más: for: 30s en alertes.yml. Vuelve a escribir ALERTS en Query:
ALERTS{alertname="APIInjoignable", alertstate="pending", instance="api:8000", job="api", service="api", severite="critique"} 1luego alertstate="firing". Anota la hora del firing: primera línea de tu entregable. En curl, lo mismo:
curl -s http://localhost:9090/api/v1/alerts | python3 -m json.toolCamino 4, Alertmanager. Abre http://localhost:9093: la página Alerts muestra un grupo alertname="APIInjoignable" service="api" (el group_by de alertmanager.yml) con la alerta, sus etiquetas (instance="api:8000", job="api", labo="observabilite", severite="critique"), el resumen L'API catalogue ne répond plus. La etiqueta labo="observabilite" viene de los 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.496ZCamino 5, el webhook. Abre http://localhost:8090: una línea APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, encabezado 1 alerte(s) en mémoire · 1 notification(s) reçue(s). El formato bruto:
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) es la hora del firing en Prometheus; recu_a (19:41:26) llega quince segundos más tarde, incluidos los 10 segundos de group_wait. fin en el año 0001: todavía no terminada. Anota recu_a: segunda línea de tu entregable.
Lo que ve la carga:
./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 diferencia esencial entre un 404 y ninguna respuesta. En el paso B.8,
/cours/C9999responderá404: la API corre y te dice que ese curso no existe; eso se cuenta enhttp_requetes_total{code="404"}, se escribe en un logWARNING, yupsigue en1. Aquí,curl: (7) Failed to connect: nadie responde, no hay ni código ni log del lado de la API, y esupel que cae a0.
./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).El script hizo docker compose start api, esperó /sante, y luego llamó a /admin/reparer. Verifica que la API trabaja:
./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"}Luego la alerta se apaga, cronometrado en la máquina del curso después de reparer:
t+15 s : APIInjoignable firing (Prometheus) · webhook : firing
t+40 s : APIInjoignable inactive (Prometheus) · webhook : firing
t+55 s : APIInjoignable inactive (Prometheus) · webhook : resolvedTercer camino, vigilar en bucle:
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 línea resolved lleva un campo fin completado (2026-09-15T19:41:56.496Z): 45 segundos de falla a los ojos de Prometheus. Anota su recu_a: tercera línea de tu entregable. Ctrl+C para salir 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"}La API respondió: un código 404, un identificador x-id-requete, un cuerpo JSON. Vuelve a escribir P4 en Prometheus con 404 en lugar de 500: la serie route="/cours/{id}" aumentó en 1. Vuelve a escribir G1 agregando |= "bbc7be667f1d" (tu propio identificador, leído en el encabezado): tu solicitud está ahí, nivel WARNING. Nada de eso existe para la falla de B.6: una API detenida no cuenta nada y no escribe nada.
./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.Lo que debes tener: labo-api de nuevo Up … (healthy), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s) (la firing y la resolved), y Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. En la máquina del curso, esta salida todavía llevaba 1 alertes actives y 3 alerte(s) reçue(s) a causa del TauxErreursEleve residual mencionado en B.6; en la tuya, con la única falla api, los valores son los de arriba. Esta salida y tus tres horas anotadas son tu entregable. Puedes dejarlo corriendo para los talleres 06 y 07, o ./labo.sh arreter: los datos se conservan.
etat dice 9/10 services aunque no rompiste nada. Mira qué contenedor no está (healthy). Si es grafana o alloy justo después de demarrer con (health: starting), espera treinta segundos. Si es labo-api en Exited, alguien (quizás tú, en un intento anterior) lanzó casser api: reparer. Si un contenedor está Restarting, lee su log: .\labo.ps1 journal <service> o ./labo.sh journal <service>.
cibles up : 7/8 y APIInjoignable activa, pero la API responde en http://localhost:8000. Prometheus lee la API por la red Docker (http://api:8000/metrics), tú por el puerto publicado (localhost:8000). Si la API acaba de reiniciarse, Prometheus puede estar atrasado un scrape (15 s) y la alerta sigue firing hasta la próxima evaluación, y luego unos segundos más para que el webhook reciba el resolved. Espera un minuto y relanza etat.
Las alertas tardan: pending no pasa a firing. APIInjoignable tiene for: 30s; hacen falta entonces hasta 15 s (scrape) + 15 s (evaluación) + 30 s (for) = 60 a 70 s para firing, y luego 10 s de group_wait para el webhook. No es lento, está ajustado para evitar falsas alertas ante un fallo aislado. Si al cabo de dos minutos nada se mueve, verifica que labo-api esté efectivamente Exited (docker compose ps).
El webhook sigue en 0 alerte(s) reçue(s) aunque Alertmanager muestra la alerta. Abre http://localhost:9093 → Status: la sección Config debe mostrar receiver: webhook y url: http://webhook:8090/alertes. Luego journal webhook: debes ver una línea POST /alertes en cada notificación. Si el contenedor labo-webhook no está healthy, docker compose restart webhook.
Empty query result en P3, P5 o G1, justo después de demarrer. Prometheus necesita al menos un scrape (15 s) para P3, dos para P5 (rate quiere dos puntos en [1m]), y Alloy tarda unos segundos en enviar la primera línea a Loki. Espera dos minutos después de Le labo est prêt.. Si {service="api"} sigue vacío después de cinco minutos, verifica http://localhost:12345 (Alloy debe estar ready y sus componentes Healthy) y journal alloy.
P10 devuelve NaN. histogram_quantile devuelve NaN cuando la ventana [5m] todavía no contiene suficientes puntos. Espera cinco minutos después del arranque, o reemplaza [5m] por [1m] para ver un valor antes (menos estable).
G1 devuelve cero líneas aunque la API corre. Verifica primero el período (arriba a la derecha, Last 1 hour) y el nombre de la etiqueta (service, en minúsculas). Luego http://localhost:12345: Alloy debe responder Alloy is ready. y journal alloy no debe mostrar un error repetido. Como último recurso, docker compose restart alloy.
parse error en Prometheus. Los tres más frecuentes, todos vistos en esta página: unexpected identifier "api" in label matching, expected string (comillas olvidadas: {job=api}); unexpected character inside braces: '5' ({code=500} en lugar de {code="500"}); expected type range vector in call to function "rate", got instant vector (ventana [1m] olvidada).
parse error en Loki. syntax error: unexpected IDENTIFIER: olvidaste las llaves (service="api" en lugar de {service="api"}). unexpected $end, expecting } or ,: llave de cierre faltante. Cero líneas sin error: nombre o mayúsculas de la etiqueta ({service="API"}, {app="api"}).
Solo Windows — Invoke-RestMethod muestra caracteres extraños (é) en los títulos de los cursos. Es la visualización de la consola, no la API. [Console]::OutputEncoding = [Text.Encoding]::UTF8 antes del comando, o lee en el navegador.
Solo Windows — .\labo.ps1: «l'exécution de scripts est désactivée sur ce système». Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, responde O, relanza.
Solo Windows — demarrer falla con port is already allocated en 3000. Otro programa ya escucha (a menudo una aplicación Node). $env:GRAFANA_PORT = '3001' y luego relanza demarrer; Grafana está entonces en http://localhost:3001 y etat lo muestra así.
Solo Linux nativo — permission denied while trying to connect to the Docker daemon socket. sudo usermod -aG docker $USER, cierra la sesión, vuelve a abrirla, docker info debe responder.
Linux nativo y macOS — bash: ./labo.sh: Permission denied. El archivo está guardado sin el bit de ejecución en el repositorio: chmod +x labo.sh una vez, o lanza bash labo.sh prerequis. En Git Bash (Windows), la cuestión no se plantea.
Quieres empezar de cero. .\labo.ps1 reinitialiser o ./labo.sh reinitialiser elimina los contenedores y los volúmenes: Prometheus, Loki y Grafana vuelven a empezar vacíos. Luego demarrer. No hacerlo en un labo compartido con otras personas.