Retoma el tablero del automóvil de la lección 01. El odómetro solo aumenta: es un contador (counter), como http_requetes_total. El indicador de combustible sube al llenar el tanque y baja al rodar: es un medidor (gauge), como requetes_en_cours. El cuaderno de mantenimiento que anota «12 trayectos de menos de 10 km, 30 de menos de 50 km, 3 de más de 100 km» clasifica cada trayecto en un rango: es un histograma, como http_duree_requete_seconds. Y la caja negra, que escribe una línea por evento con la hora exacta, es el log JSON de la API. Prometheus no recibe nada: es él quien viene a leer los indicadores cada 15 segundos, como un inspector que pasa a tomar la lectura de los contadores de cada vehículo de la flota.
Una métrica Prometheus siempre tiene un nombre, eventualmente etiquetas, y un valor numérico. El tipo dice cómo leer ese valor en el tiempo. Prometheus conoce cuatro: el contador, el medidor, el histograma y el resumen. Los tres primeros los usa la API de catálogo; el cuarto no, y la razón es instructiva. Todas las líneas siguientes están copiadas de http://localhost:8000/metrics en la máquina del curso; los valores serán distintos en la tuya.
Contador (counter). Un contador solo aumenta, o vuelve a cero si el servicio se reinicia. Cuenta eventos acumulados. La API expone http_requetes_total, con tres etiquetas: el código de respuesta, el método HTTP y la ruta:
# HELP http_requetes_total Nombre de requêtes HTTP reçues, par méthode, route normalisée et code de réponse.
# TYPE http_requetes_total counter
http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0
http_requetes_total{code="201",methode="POST",route="/inscriptions"} 855.0
http_requetes_total{code="404",methode="GET",route="/cours/{id}"} 210.0
http_requetes_total{code="500",methode="GET",route="/cours"} 32.0Lo que nunca haces con un contador: leer su valor bruto para decir «hay 3247 solicitudes en este momento». Un contador nunca baja; lo que te interesa es su velocidad de aumento, calculada con la función rate() (módulo 2). Documentación: Prometheus — Metric types, Counter.
Medidor (gauge). Un medidor sube y baja libremente: un valor en un instante dado. La API expone requetes_en_cours, la cantidad de solicitudes en curso de procesamiento en el instante preciso de la lectura:
# HELP requetes_en_cours Nombre de requêtes HTTP en cours de traitement à cet instant.
# TYPE requetes_en_cours gauge
requetes_en_cours 1.0Hay una solicitud en curso en el instante de la lectura; el instante siguiente, ese número puede ser 0 o 4. La API también expone medidores de configuración, cuyo valor es más una información que una medida: api_info{version="1.0.0"} 1.0 (la versión está en la etiqueta, el valor siempre vale 1) y api_panne_taux_erreurs 0.01 (la parte de solicitudes que la API hace fallar a propósito: 1 % en funcionamiento normal). Documentación: Prometheus — Metric types, Gauge.
Histograma (histogram). Un histograma mide la distribución de un valor, como la duración de una solicitud HTTP: ¿cuántas solicitudes tomaron menos de 5 ms, menos de 10 ms, menos de 25 ms…? La API expone http_duree_requete_seconds. A diferencia de un contador o un medidor, un histograma publica varias líneas para una sola métrica: un contador por rango (bucket), más un conteo total y una suma. Aquí están las doce líneas de la ruta /cours:
# HELP http_duree_requete_seconds Durée de traitement des requêtes HTTP, en secondes, par route normalisée.
# TYPE http_duree_requete_seconds histogram
http_duree_requete_seconds_bucket{le="0.005",route="/cours"} 32.0
http_duree_requete_seconds_bucket{le="0.01",route="/cours"} 74.0
http_duree_requete_seconds_bucket{le="0.025",route="/cours"} 1566.0
http_duree_requete_seconds_bucket{le="0.05",route="/cours"} 3248.0
http_duree_requete_seconds_bucket{le="0.1",route="/cours"} 3276.0
http_duree_requete_seconds_bucket{le="0.25",route="/cours"} 3278.0
http_duree_requete_seconds_bucket{le="0.5",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="1.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="2.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="+Inf",route="/cours"} 3279.0
http_duree_requete_seconds_count{route="/cours"} 3279.0
http_duree_requete_seconds_sum{route="/cours"} 85.42293146001248Cada _bucket{le="…"} (le por less or equal) cuenta las solicitudes más rápidas o iguales a ese umbral, de forma acumulativa: las 32 solicitudes de menos de 5 ms también se cuentan en las 74 de menos de 10 ms, y en las 3279 del cubo +Inf (todas). _count es el número total de observaciones (3279, igual al cubo +Inf), _sum la suma de todas las duraciones (85,4 segundos en total, es decir, 26 ms en promedio por solicitud). Esto es lo que permite reconstruir un cuantil a posteriori, con histogram_quantile() (módulo 2). Lectura directa: 3248 solicitudes de 3279 tomaron menos de 50 ms, es decir, el 99 %. Documentación: Prometheus — Metric types, Histogram.
Resumen (summary), y por qué el labo no lo usa. Un resumen también mide una distribución, pero calcula sus cuantiles directamente en el programa observado, antes de publicarlos: una línea {quantile="0.5"}, una línea {quantile="0.9"}, más _sum y _count. El problema: un cuantil calculado del lado del cliente no puede recombinarse con el de otra instancia. Si el labo hiciera correr tres copias de la API, no podrías promediar los tres quantile="0.9" para obtener el verdadero percentil 90 del conjunto. Un histograma publica contadores brutos por rango: Prometheus puede sumarlos entre instancias antes de calcular el cuantil. Por eso no hay ninguna línea summary en el /metrics de la API del labo. Documentación: Prometheus — Histograms and summaries.
| Tipo | Lo que mide | En el labo | Pregunta a la que responde |
|---|---|---|---|
| Contador | Un acumulado que solo aumenta | http_requetes_total, inscriptions_total, cours_consultes_total | «¿Cuántas solicitudes por segundo?» (con rate) |
| Medidor | Un valor instantáneo que sube y baja | requetes_en_cours, api_disque_libre_octets, api_info | «¿Cuántas en este momento?» |
| Histograma | Una distribución, por rangos acumulativos | http_duree_requete_seconds | «¿El 95 % de las solicitudes toma menos de cuánto?» |
| Resumen | Una distribución, cuantiles calculados del lado del cliente | ninguno | La misma, pero sin poder agregar entre instancias |
Cada combinación única de nombre de métrica y etiquetas forma una serie temporal: una sucesión de pares (marca de tiempo, valor) que Prometheus almacena y consulta. http_requetes_total{code="200",methode="GET",route="/cours"} es una serie; http_requetes_total{code="201",methode="POST",route="/inscriptions"} es otra. En la máquina del curso, http_requetes_total cuenta 14 series (14 combinaciones de código, método y ruta aparecidas desde el arranque). Las etiquetas permiten filtrar y agrupar sin cambiar el nombre: «los errores de servidor, solo en /inscriptions» se lee http_requetes_total{route="/inscriptions",code=~"5.."}. Documentación: Prometheus — Data model.
La cardinalidad de una métrica es el número de series distintas que produce. http_requetes_total{code,methode,route}, con cinco códigos, dos métodos y siete rutas en plantilla, da algunas decenas de series como máximo. Mira el log del paso 2 de la lección 01: la solicitud llamó a /cours/C0038, pero la métrica lleva route="/cours/{id}", la plantilla de la ruta tal como FastAPI la declaró. Si la etiqueta contuviera el identificador real, cada curso consultado crearía una serie: 64 cursos × 5 códigos × 2 métodos. En un catálogo real de decenas de miles de cursos, la métrica explotaría y Prometheus se volvería lento. La regla: una etiqueta debe tener un número acotado y razonable de valores; un identificador único, una dirección IP o una marca de tiempo no tienen nada que hacer en una etiqueta. Una ruta que no existe se cuenta bajo route="inconnue", nunca bajo su camino real, por la misma razón.
El labo contiene a propósito una excepción instructiva: inscriptions_total{cours_id="C0028"} y cours_consultes_total{cours_id="…"} tienen una etiqueta por curso. Con 64 cursos, sigue acotado (64 series cada una). Con un millón de cursos, sería un error grave. De hecho, el labo tiene una alerta PrometheusTropDeSeries que suena por encima de 100 000 series. Documentación: Prometheus — Instrumentation, cardinality.
Prometheus funciona en pull (va a buscar): consulta por sí mismo, cada 15 segundos en el labo (scrape_interval: 15s en prometheus/prometheus.yml), la URL /metrics de cada servicio que vigila. Es lo contrario de un sistema en push, donde el servicio observado envía sus métricas hacia un colector. El pull tiene una ventaja directa: si un servicio deja de responder, Prometheus lo sabe de inmediato, la métrica up pasa a 0, sin depender del servicio caído para que señale por sí mismo su ausencia. Es exactamente lo que la práctica de este módulo te hace provocar con casser api.
| Término | Qué es | En el labo |
|---|---|---|
| scrape | Una lectura de /metrics por Prometheus | Cada 15 s, en cada uno de los 8 destinos |
| target (destino) | Una URL /metrics que Prometheus lee | http://api:8000/metrics, http://node-exporter:9100/metrics… |
| exporter | Un programa que expone en formato Prometheus las métricas de un sistema que no lo habla de forma nativa | node-exporter (la máquina anfitriona), cadvisor (los contenedores) |
| job | Un grupo de destinos que hacen el mismo trabajo | job="api", job="prometheus", job="loki"… 8 jobs |
| instance | Un destino individual dentro de un job | instance="api:8000" |
up | La métrica que Prometheus fabrica por sí mismo en cada scrape: 1 si el destino respondió, 0 si no | up{job="api"} vale 1 en funcionamiento normal |
Los nombres api:8000, node-exporter:9100 son los de la red interna de Docker Compose: es Prometheus, en su contenedor, hablándole a la API en el suyo. Desde tu máquina, la misma página es http://localhost:8000/metrics. Documentación: Prometheus — Configuration, scrape_config y Prometheus — Jobs and instances.
Un log estructurado se escribe en un formato que la máquina descompone sin adivinar (JSON, la mayoría de las veces), en lugar de una frase libre. La API escribe en su salida estándar una línea JSON por solicitud procesada. Aquí hay una línea real, leída con .\labo.ps1 journal api en la máquina del curso:
{"horodatage": "2026-09-15T19:33:25.839+00:00", "niveau": "INFO", "id_requete": "46bb533b33e9", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 13.9, "message": "GET /cours/C0038 -> 200"}| Campo | Ejemplo | Qué es |
|---|---|---|
horodatage | 2026-09-15T19:33:25.839+00:00 | El instante exacto del evento, en formato ISO 8601, en UTC (+00:00) |
niveau | INFO | La gravedad: INFO (2xx), WARNING (404, 422), ERROR (500). Tres valores en el labo |
id_requete | 46bb533b33e9 | Un identificador único de 12 caracteres generado para esta solicitud, devuelto al cliente en el encabezado HTTP x-id-requete |
methode | GET | El método HTTP |
route | /cours/{id} | La plantilla de la ruta, la misma que en la métrica |
code | 200 | El código de respuesta HTTP |
duree_ms | 13.9 | La duración del procesamiento, en milisegundos |
message | GET /cours/C0038 -> 200 | La frase legible: es aquí, y solo aquí, donde aparece el identificador real del curso |
Lo que hay que ver: route conserva la plantilla /cours/{id} (como la métrica), pero message contiene C0038, el identificador real. Un log puede permitirse ese detalle: Loki no indexa el texto del mensaje, indexa solo cuatro etiquetas (service, conteneur, niveau, code) extraídas por Alloy. El módulo 5 te muestra cómo Alloy lee el JSON y fabrica esas etiquetas. Documentación: Grafana Loki — Labels.
Los demás servicios del labo no escriben todos JSON. El webhook, al arrancar, escribe texto libre: INFO: Uvicorn running on http://0.0.0.0:8090 (Press CTRL+C to quit). El servicio charge escribe JSON, pero con otros campos: cada 30 segundos, un resumen {"niveau": "INFO", "message": "résumé des 30 dernières secondes", "requetes": {"200": 214, "total": 262, "201": 26, "500": 4, "404": 16, "422": 2}}. Un log estructurado no es un formato universal: es una decisión tomada servicio por servicio.
Una traza sigue una solicitud única a través de varios servicios: cada etapa (un span) registra su nombre, su duración y su relación padre-hijo con las demás. Responde a «la solicitud tardó 800 ms, ¿en qué servicio se gastó ese tiempo?». El labo no instala ninguna herramienta de trazado: la API de catálogo y el generador de carga son los dos únicos servicios aplicativos, unidos por una simple llamada HTTP, lo que limita el interés pedagógico de una traza distribuida aquí. El id_requete del log ya es el primer pedazo de una traza: es lo que OpenTelemetry llamaría un trace id. El módulo 7 dice algo más al respecto. Documentación: OpenTelemetry — Traces.
| Servicio | Rol | Puerto | URL desde tu máquina |
|---|---|---|---|
prometheus | Lee los destinos, almacena las series, evalúa las reglas | 9090 | http://localhost:9090 |
alertmanager | Recibe las alertas de Prometheus, las agrupa y las enruta | 9093 | http://localhost:9093 |
grafana | Explora y visualiza Prometheus, Loki y Alertmanager | 3000 (GRAFANA_PORT) | http://localhost:3000 (admin / aiopsatlas2026) |
loki | Almacena los logs, indexados por etiquetas | 3100 | http://localhost:3100/ready |
alloy | Descubre los contenedores y transporta sus logs hacia Loki | 12345 | http://localhost:12345 |
node-exporter | Métricas de la máquina anfitriona | 9100 | http://localhost:9100/metrics |
cadvisor | Métricas de cada contenedor | 8080 | http://localhost:8080 |
api | El servicio observado | 8000 | http://localhost:8000/cours · http://localhost:8000/metrics |
webhook | Recibe y muestra las alertas | 8090 | http://localhost:8090 |
charge | Genera tráfico hacia la API | ninguno | ninguna: no tiene interfaz, solo un log |
Nueve puertos, diez servicios: charge no escucha en ninguno. Ocho destinos Prometheus, diez servicios: charge y webhook no exponen /metrics.
Este paso a paso supone el labo iniciado (lección 04). Si lees esta lección antes, guárdalo para después: cada paso se hace en el navegador o en una terminal, en solo lectura.
Abre http://localhost:8000/metrics en el navegador. Es la página bruta que Prometheus lee cada 15 segundos: texto, una línea por serie, precedida de sus líneas # HELP y # TYPE. En la máquina del curso, tiene 271 líneas. Compara con http://localhost:9100/metrics (node-exporter: 1578 líneas) y http://localhost:8080/metrics (cAdvisor: 3444 líneas, para diez contenedores).
Lo que hay que ver: las líneas que empiezan por python_ y process_ en la parte superior de la página de la API no las escribe el labo. La biblioteca prometheus_client las agrega por sí sola (memoria del proceso, recolector de basura de Python). Las métricas del curso empiezan en http_requetes_total.
Busca los cuatro # TYPE de la API. En la página, busca (Ctrl+F) # TYPE http_: encuentras counter para http_requetes_total e histogram para http_duree_requete_seconds. Busca # TYPE requetes_en_cours: gauge. Busca summary: ningún resultado.
Lo que hay que ver: los tres tipos usados, y la ausencia deliberada del cuarto.
Cuenta las series de http_requetes_total en Prometheus. Abre http://localhost:9090, escribe http_requetes_total en el campo de consulta y ejecuta. En la máquina del curso, la tabla muestra 14 líneas, entre ellas:
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 32Lo que hay que ver: Prometheus agregó tres etiquetas a las de la página /metrics: job="api" e instance="api:8000" (que identifican el destino) y service="api" (agregada por la configuración del job en prometheus.yml). El valor 3241 es un poco menor que el 3247 leído en el paso 1: Prometheus muestra el último scrape, que data de hace 0 a 15 segundos.
Lee una línea de log de la API en la terminal. Desde la carpeta lab3:
.\labo.ps1 journal apiEn la máquina del curso, las últimas líneas se parecen a:
labo-api | {"horodatage": "2026-09-15T19:33:25.916+00:00", "niveau": "INFO", "id_requete": "5ccaee4d1a2a", "methode": "GET", "route": "/sante", "code": 200, "duree_ms": 1.0, "message": "GET /sante -> 200"}
labo-api | {"horodatage": "2026-09-15T19:33:26.217+00:00", "niveau": "INFO", "id_requete": "e64206564a68", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 8.2, "message": "GET /cours/C0043 -> 200"}Lo que hay que ver: GET /sante cada pocos segundos es Docker verificando la salud del contenedor (el healthcheck); el resto es el servicio charge. El prefijo labo-api | lo agrega Docker Compose, no forma parte del JSON.
Verifica las etiquetas que Loki conoce. Abre http://localhost:3100/loki/api/v1/labels:
{"status":"success","data":["code","conteneur","niveau","service"]}Luego http://localhost:3100/loki/api/v1/label/niveau/values:
{"status":"success","data":["ERROR","INFO","WARNING"]}Lo que hay que ver: cuatro etiquetas, no ocho. Loki no indexa ni id_requete, ni duree_ms, ni message: esos campos quedan en el texto de la línea, donde LogQL puede extraerlos a demanda con | json (módulo 5). Los tres valores de niveau confirman la tabla de más arriba.
Los mensajes siguientes fueron provocados de verdad en Prometheus, en la máquina del curso. Volverán en el módulo 2; conviene reconocerlos desde ahora.
Olvidar las comillas alrededor de un valor de etiqueta: up{job=api} devuelve
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected stringUn valor de etiqueta es siempre una cadena entre comillas: up{job="api"}. Incluso para un número: http_requetes_total{code=500} devuelve parse error: unexpected character inside braces: '5'; hay que escribir code="500".
Llamar a rate() sin ventana de tiempo: rate(http_requetes_total) devuelve
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vectorrate() necesita un intervalo entre corchetes: rate(http_requetes_total[1m]). Sin corchetes, le das el último valor (un instant vector) cuando necesita una sucesión de valores (un range vector).
Llamar a rate() sobre un medidor: rate(requetes_en_cours[1m]) no provoca error, pero Prometheus muestra una advertencia:
PromQL info: metric might not be a counter, name does not end in _total/_sum/_count/_bucket: "requetes_en_cours" (1:6)El resultado (0.017… en la máquina del curso) no tiene ningún sentido: la velocidad de aumento de un valor que sube y baja no es una información. rate() está reservado a los contadores.
Equivocarse en el nombre de una métrica: http_request_total (en singular, sin la e francesa) devuelve un resultado vacío, sin mensaje de error. Prometheus no conoce esa métrica, no la corrige. El nombre exacto es http_requetes_total. Del mismo modo, http_requetes_total{route="/inexistant"} devuelve un resultado vacío: la ruta desconocida se cuenta bajo route="inconnue", no bajo su camino real.
Los cuatro tipos de métricas Prometheus son el contador (http_requetes_total, que solo aumenta en un sentido y se lee con rate()), el medidor (requetes_en_cours, que sube y baja), el histograma (http_duree_requete_seconds, doce líneas por ruta: diez _bucket acumulativos, _count, _sum) y el resumen (ausente del labo, porque sus cuantiles no se agregan entre instancias). Una serie temporal es una combinación única de nombre y etiquetas; http_requetes_total tiene 14 en la máquina del curso. La cardinalidad explica por qué la métrica lleva route="/cours/{id}" mientras que el log contiene C0038 en su mensaje. Prometheus funciona en pull: lee (scrape) ocho destinos (targets) cada 15 segundos, agrupados por job, identificados por instance, y fabrica por sí mismo la métrica up. Un log estructurado del labo es una línea JSON de ocho campos, entre ellos id_requete, devuelto también en el encabezado x-id-requete; Loki solo indexa cuatro etiquetas de él: service, conteneur, niveau, code. Diez servicios, nueve puertos (charge no tiene ninguno), ocho destinos Prometheus (charge y webhook no exponen /metrics).
/metrics que abriste en el paso 1 (las líneas # HELP, # TYPE, el escapado de las etiquetas)._total para un contador, _seconds para una duración, _bytes para un tamaño; la API del labo respeta estas convenciones, con una excepción: los nombres están en francés.histogram_quantile.api/app.py para exponer las métricas; el módulo 3 te hace agregar tu propia métrica con ella.up, filtrar por etiqueta, calcular un rate(), leer un histograma con histogram_quantile().