Los tres pilares y el vocabulario

18 min
Público
principiante, lección 01 leída
Duración
35 a 45 min
Módulo
1/7
Competencia objetivo
reconocer los cuatro tipos de métricas Prometheus en las líneas reales de /metrics de la API del labo, entender las etiquetas y la cardinalidad, distinguir pull y push, leer una línea de log JSON campo por campo, y ubicar los diez servicios del labo por su puerto y su URL

En una imagen

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.

Cómo funciona

Los cuatro tipos de métricas, en las líneas reales del labo

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:

text
# HELP http_requetes_total Nombre de requêtes HTTP reçues, par méthode, route normalisée et code de réponse.
# TYPE http_requetes_total counter
http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0
http_requetes_total{code="201",methode="POST",route="/inscriptions"} 855.0
http_requetes_total{code="404",methode="GET",route="/cours/{id}"} 210.0
http_requetes_total{code="500",methode="GET",route="/cours"} 32.0

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

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

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

text
# HELP http_duree_requete_seconds Durée de traitement des requêtes HTTP, en secondes, par route normalisée.
# TYPE http_duree_requete_seconds histogram
http_duree_requete_seconds_bucket{le="0.005",route="/cours"} 32.0
http_duree_requete_seconds_bucket{le="0.01",route="/cours"} 74.0
http_duree_requete_seconds_bucket{le="0.025",route="/cours"} 1566.0
http_duree_requete_seconds_bucket{le="0.05",route="/cours"} 3248.0
http_duree_requete_seconds_bucket{le="0.1",route="/cours"} 3276.0
http_duree_requete_seconds_bucket{le="0.25",route="/cours"} 3278.0
http_duree_requete_seconds_bucket{le="0.5",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="1.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="2.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="+Inf",route="/cours"} 3279.0
http_duree_requete_seconds_count{route="/cours"} 3279.0
http_duree_requete_seconds_sum{route="/cours"} 85.42293146001248

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

TipoLo que mideEn el laboPregunta a la que responde
ContadorUn acumulado que solo aumentahttp_requetes_total, inscriptions_total, cours_consultes_total«¿Cuántas solicitudes por segundo?» (con rate)
MedidorUn valor instantáneo que sube y bajarequetes_en_cours, api_disque_libre_octets, api_info«¿Cuántas en este momento?»
HistogramaUna distribución, por rangos acumulativoshttp_duree_requete_seconds«¿El 95 % de las solicitudes toma menos de cuánto?»
ResumenUna distribución, cuantiles calculados del lado del clienteningunoLa misma, pero sin poder agregar entre instancias

Serie temporal, etiquetas y cardinalidad

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.

Pull, scrape, target, exporter, job, instance

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érminoQué esEn el labo
scrapeUna lectura de /metrics por PrometheusCada 15 s, en cada uno de los 8 destinos
target (destino)Una URL /metrics que Prometheus leehttp://api:8000/metrics, http://node-exporter:9100/metrics
exporterUn programa que expone en formato Prometheus las métricas de un sistema que no lo habla de forma nativanode-exporter (la máquina anfitriona), cadvisor (los contenedores)
jobUn grupo de destinos que hacen el mismo trabajojob="api", job="prometheus", job="loki"… 8 jobs
instanceUn destino individual dentro de un jobinstance="api:8000"
upLa métrica que Prometheus fabrica por sí mismo en cada scrape: 1 si el destino respondió, 0 si noup{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.

Los logs estructurados: una línea JSON por evento

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:

json
{"horodatage": "2026-09-15T19:33:25.839+00:00", "niveau": "INFO", "id_requete": "46bb533b33e9", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 13.9, "message": "GET /cours/C0038 -> 200"}
CampoEjemploQué es
horodatage2026-09-15T19:33:25.839+00:00El instante exacto del evento, en formato ISO 8601, en UTC (+00:00)
niveauINFOLa gravedad: INFO (2xx), WARNING (404, 422), ERROR (500). Tres valores en el labo
id_requete46bb533b33e9Un identificador único de 12 caracteres generado para esta solicitud, devuelto al cliente en el encabezado HTTP x-id-requete
methodeGETEl método HTTP
route/cours/{id}La plantilla de la ruta, la misma que en la métrica
code200El código de respuesta HTTP
duree_ms13.9La duración del procesamiento, en milisegundos
messageGET /cours/C0038 -> 200La 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.

Las trazas: mención, sin herramienta en este labo

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.

Los diez servicios: rol, puerto, URL

ServicioRolPuertoURL desde tu máquina
prometheusLee los destinos, almacena las series, evalúa las reglas9090http://localhost:9090
alertmanagerRecibe las alertas de Prometheus, las agrupa y las enruta9093http://localhost:9093
grafanaExplora y visualiza Prometheus, Loki y Alertmanager3000 (GRAFANA_PORT)http://localhost:3000 (admin / aiopsatlas2026)
lokiAlmacena los logs, indexados por etiquetas3100http://localhost:3100/ready
alloyDescubre los contenedores y transporta sus logs hacia Loki12345http://localhost:12345
node-exporterMétricas de la máquina anfitriona9100http://localhost:9100/metrics
cadvisorMétricas de cada contenedor8080http://localhost:8080
apiEl servicio observado8000http://localhost:8000/cours · http://localhost:8000/metrics
webhookRecibe y muestra las alertas8090http://localhost:8090
chargeGenera tráfico hacia la APIningunoninguna: 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.

Paso a paso

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.

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

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

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

    text
    http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}   3241
    http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}   32

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

  4. Lee una línea de log de la API en la terminal. Desde la carpeta lab3:

    powershell
    .\labo.ps1 journal api

    En la máquina del curso, las últimas líneas se parecen a:

    text
    labo-api  | {"horodatage": "2026-09-15T19:33:25.916+00:00", "niveau": "INFO", "id_requete": "5ccaee4d1a2a", "methode": "GET", "route": "/sante", "code": 200, "duree_ms": 1.0, "message": "GET /sante -> 200"}
    labo-api  | {"horodatage": "2026-09-15T19:33:26.217+00:00", "niveau": "INFO", "id_requete": "e64206564a68", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 8.2, "message": "GET /cours/C0043 -> 200"}

    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.

  5. Verifica las etiquetas que Loki conoce. Abre http://localhost:3100/loki/api/v1/labels:

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

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

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

Si algo falla

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

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

    Un 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

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

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

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

Para recordar

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

Para ir más allá

  • Prometheus — Exposition formats: la gramática exacta de la página /metrics que abriste en el paso 1 (las líneas # HELP, # TYPE, el escapado de las etiquetas).
  • Prometheus — Metric and label naming: por qué _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.
  • Prometheus — Histograms and summaries: el artículo de referencia sobre la elección histograma / resumen, con los errores de aproximación de histogram_quantile.
  • prometheus_client (Python): la biblioteca usada por api/app.py para exponer las métricas; el módulo 3 te hace agregar tu propia métrica con ella.
  • Grafana Loki — Labels: por qué pocas etiquetas, y nunca un identificador único dentro: la misma regla de cardinalidad que para Prometheus.
  • El módulo 2 retoma cada término en la práctica: escribir up, filtrar por etiqueta, calcular un rate(), leer un histograma con histogram_quantile().