Taller fundamental 1 — PromQL: una métrica, una etiqueta, una función

Práctica guiada15 min
Duración
20 min
Módulo
1/7
Requisitos previos
el labo lleva corriendo al menos dos minutos (etat muestra 8/8 cibles up), la pestaña Query de Prometheus abierta en http://localhost:9090
Vas a construir
diez consultas PromQL escritas a mano, de la más corta (api_info) a la primera verdadera agregación (sum by (code) (rate(…[1m]))), cambiando una sola cosa a la vez
Entregable
la salida del paso 10 tal como se muestra en la pestaña Table, dos líneas, con una frase que diga lo que mide cada una

Cómo leer esta página. Diez pasos, una consulta a la vez. Para cada uno: la consulta a escribir, la respuesta exacta del labo (pestaña Table), y lo que hay que mirar en ella. Escribe tú mismo cada consulta (sin copiar y pegar): es al escribir las llaves, las comillas y los corchetes que la gramática se asimila. Las cifras serán distintas en tu máquina; las formas (número de líneas, etiquetas, orden de magnitud) deben ser las mismas. Los bloques «Para entender mejor» son opcionales. Si el labo no está iniciado, vuelve a la práctica guiada: la sección En resumen da los comandos, kit incluido (https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr). Nada se crea ni se modifica en este taller: PromQL solo lee.

Objetivo

La práctica guiada te hizo escribir doce consultas ya redactadas. Viste los resultados, pero si te quitan la hoja, ¿sabes escribir sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) sin equivocarte de paréntesis? Aquí, partes de la consulta más corta posible, un nombre de métrica, y agregas un solo pedazo en cada paso: una etiqueta, un operador, una función, un rango de tiempo, un agrupamiento. Dos pasos son trampas deliberadas: vas a provocar los dos mensajes de error que todo principiante encuentra, para reconocerlos la próxima vez. Al final, sabes lo que es una métrica, una etiqueta y una función porque ensamblaste los tres pedazos tú mismo.

El vocabulario en una imagen

Prometheus es un cuaderno de lecturas. Cada 15 segundos, pasa delante de cada destino, lee su página /metrics y anota cada valor con la hora. Una métrica es el nombre de una columna del cuaderno (up, http_requetes_total). Una etiqueta es un rótulo pegado en la línea para decir de qué se habla (job="api", code="200"); un mismo nombre de métrica con rótulos distintos son series distintas. Una función es una operación sobre lo que se leyó: contar las líneas, calcular una pendiente, sumar.

PromQLBase SQL clásicaEn este taller
métricatablaup, api_info, http_requetes_total
seriefila de la tablaup{instance="api:8000", job="api", service="api"}
etiquetacolumnajob, instance, route, code
selector {job="api"}WHERE job = 'api'paso 3
=~WHERE job LIKE 'a%' (en expresión regular)paso 5
count(…), sum(…)COUNT(*), SUM(…)pasos 6 y 10
by (code)GROUP BY codepaso 10
rango [1m]«las filas del último minuto»paso 7
rate(…[1m])sin equivalente simple: una pendiente por segundopaso 8
vector instantáneoun valor por serie, ahoralo que devuelve up
vector de rangovarios valores fechados por serielo que devuelve up[1m]

Dónde escribir, y cómo leer una respuesta

Abre http://localhost:9090. Estás en la página Query. El campo de entrada acepta una consulta; Execute (o Enter) la envía. El resultado se muestra bajo el campo, en la pestaña Table. Quédate en Table durante todo el taller: es ahí donde ves las etiquetas escritas en claro. La pestaña Graph dibuja lo mismo en el tiempo; la pestaña Explain descompone la consulta.

Una línea de resultado siempre tiene la misma forma: el nombre de la métrica, luego entre llaves las etiquetas ordenadas alfabéticamente, luego el valor a la derecha:

text
up{instance="api:8000", job="api", service="api"}    1

Cuando la consulta hizo desaparecer el nombre (una función, una agregación), las llaves quedan, a veces vacías: {} 8. Debajo de las pestañas, Result series: N te dice cuántas líneas tienes. Un resultado vacío se escribe Empty query result; una consulta mal escrita muestra un cuadro rojo Error executing query seguido del mensaje.

Paso 1 — Leer una métrica

promql
api_info

Lo que pide la consulta: el último valor de la métrica api_info, para todas sus series.

text
api_info{instance="api:8000", job="api", service="api", version="1.0.0"}    1

Qué mirar: una sola línea, Result series: 1. El valor es 1 y nunca cambiará: api_info es una métrica de información, todo lo que tiene que decir está en su etiqueta version="1.0.0". Tres etiquetas más que la API no escribió: instance, job y service fueron agregadas por Prometheus en el momento de la lectura. En la página http://localhost:8000/metrics, la misma línea se escribe api_info{version="1.0.0"} 1.0.

Para entender mejor
  • ¿Por qué empezar por api_info y no por up? Porque tiene una serie. Ves la forma completa de una línea de resultado (nombre, etiquetas, valor) sin distraerte con siete líneas más.
  • Un nombre de métrica contiene letras, cifras, _ y :. Sin guion, sin espacio, sin punto. api-info se leería como api menos info.
  • El valor es siempre un número de punto flotante. 1 aquí; Prometheus no almacena texto, por eso la versión está en una etiqueta.

Paso 2 — Leer una métrica con varias series

promql
up

Lo que pide la consulta: el último valor de up, para todas sus series.

text
up{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"}    1

Qué mirar: Result series: 8, el mismo nombre en cada línea, y lo que cambia de una línea a otra: los valores de las etiquetas job e instance. Eso es una serie: un nombre más un juego de etiquetas. Ocho juegos distintos, ocho series. Observa que solo la tercera línea lleva service="api": esa etiqueta se agregó a mano en prometheus.yml, solo para el job api. up no existe en ninguna página /metrics: Prometheus la fabrica por sí mismo, 1 si la lectura tuvo éxito, 0 si no.

Paso 3 — Elegir una serie con una etiqueta

promql
up{job="api"}

Lo que pide la consulta: las series de up cuya etiqueta job vale exactamente api.

text
up{instance="api:8000", job="api", service="api"}    1

Qué mirar: una sola línea, la tercera del paso 2. Las llaves después del nombre son un filtro: se llaman un selector. job es el nombre de la etiqueta, "api" su valor, entre comillas dobles, = la igualdad exacta. Es el WHERE job = 'api' de SQL, palabra por palabra. Puedes poner varias condiciones separadas por comas; todas deben ser verdaderas.

Paso 4 — La trampa: sin comillas, luego con las mayúsculas incorrectas

Dos consultas erróneas, a propósito. Primero, olvida las comillas:

promql
up{job=api}
text
Error executing query
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected string

Qué mirar: parse error, Prometheus ni siquiera buscó: la consulta está mal escrita. 1:8 es la posición (línea 1, carácter 8, justo después de up{job=). expected string: esperaba una cadena entre comillas. Un valor de etiqueta es siempre una cadena, incluso cuando parece un número: {code=500} da la misma familia de error, {code="500"} es la forma correcta.

Después, pon las comillas pero cambia las mayúsculas:

promql
up{job="API"}
text
Empty query result

Qué mirar: ningún error, ninguna línea. Es la trampa más traicionera: la consulta es correcta, simplemente pide una serie que no existe. Los valores de etiquetas distinguen mayúsculas y ortografía ("api " con un espacio tampoco funciona). Cuando obtengas Empty query result sin razón, vuelve a escribir el paso 2 y relee los valores exactos.

Para entender mejor: leer un mensaje de error PromQL

Un mensaje de error de Prometheus tiene tres partes: parse error (la consulta está mal formada) o bad_data (la consulta está bien formada pero es imposible de ejecutar), una posición línea:columna, y una frase que dice lo que esperaba. Ve siempre a la posición indicada: el error está ahí o justo antes. Los tres mensajes de este taller cubren la gran mayoría de los casos: expected string (comillas), expected "(" (paréntesis alrededor de by), expected type range vector (corchetes, paso 9).

Paso 5 — Elegir varias series con un patrón

promql
up{job=~"a.*"}

Lo que pide la consulta: las series de up cuya etiqueta job coincide con la expresión regular a.*: una a seguida de cualquier cosa.

text
up{instance="alloy:12345", job="alloy"}    1
up{instance="api:8000", job="api", service="api"}    1
up{instance="alertmanager:9093", job="alertmanager"}    1

Qué mirar: tres líneas, los tres jobs que empiezan por a. Una sola novedad respecto al paso 3: =~ en lugar de =. La expresión regular debe coincidir con el valor entero: "a" solo no devolvería nada, hace falta "a.*". Los cuatro operadores de selección: = (igual), != (distinto: up{job!="api"} devuelve los otros siete), =~ (coincide), !~ (no coincide). Es =~ lo que la práctica guiada usaba en {code=~"5.."} para atrapar todos los 5xx.

Paso 6 — Aplicar una función

promql
count(up)

Lo que pide la consulta: el número de series que devuelve up.

text
{}    8

Qué mirar: una sola línea, y el nombre desapareció: {} vacío, luego 8. Es la primera función del taller, y el resultado ya no es up, es un número calculado a partir de up. count es una agregación: toma varias series y hace una. Sus primas: sum (la suma de los valores: sum(up) da también 8 mientras todo esté en 1, y 7 en cuanto un destino cae), min, max, avg. El tablero de control del kit y el comando etat cuentan los destinos exactamente así.

Paso 7 — Pedir un rango de tiempo

promql
http_requetes_total{route="/cours", code="200"}[1m]

Lo que pide la consulta: todos los valores de esta serie tomados durante el último minuto, no solo el último.

text
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}
    2439 @1789505608.199
    2499 @1789505623.199
    2550 @1789505638.196
    2609 @1789505653.197

Qué mirar: una serie, pero cuatro valores, cada uno seguido de @ y una fecha en segundos. Quince segundos de diferencia entre dos: es el scrape_interval. El contador sube de 2439 a 2609: 170 solicitudes 200 en /cours en 45 segundos. Una sola novedad: los corchetes [1m] después del selector. Transforman un vector instantáneo (un valor por serie) en vector de rango (una lista de valores fechados por serie). Haz clic en la pestaña Graph: rechaza esta consulta (Error executing query luego invalid expression type "range vector" for range query, must be Scalar or instant Vector). No se dibuja un rango bruto, se le da a una función. Es el paso 8. Vuelve a Table.

Para entender mejor: ¿por qué cuatro valores y no cinco?

Un minuto contiene cuatro intervalos de 15 segundos, así que cuatro o cinco lecturas según el instante en que lances la consulta respecto al ciclo de scrape. Si vuelves a escribir la consulta varias veces, verás a veces cinco líneas. Las fechas @1789505608.199 son segundos desde el 1 de enero de 1970 (la hora Unix); la pestaña Graph las convierte en horas legibles.

Paso 8 — Aplicar una función al rango

promql
rate(http_requetes_total{route="/cours", code="200"}[1m])

Lo que pide la consulta: la velocidad a la que este contador aumentó, en unidades por segundo, calculada sobre el rango del último minuto.

text
{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}    3.7779456864749545

Qué mirar: de nuevo un solo valor, y el nombre http_requetes_total desapareció de las llaves: ya no es un contador, es una velocidad. 3,78 solicitudes por segundo. Verifica con el paso 7: 170 solicitudes en 45 segundos dan 3,78. Una sola novedad: la función rate(), que toma un vector de rango y devuelve un vector instantáneo. Es la función más importante de PromQL: un contador bruto nunca se lee, su pendiente sí.

Paso 9 — La trampa: rate sin rango

promql
rate(http_requetes_total{route="/cours", code="200"})
text
Error executing query
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vector

Qué mirar: expected type range vector … got instant vector. Le diste a rate un vector instantáneo (un valor), quería un rango (varios valores fechados): sin dos puntos, no hay pendiente. El gesto correcto es el paso 8, con [1m]. Leerás este mensaje a menudo; siempre significa «falta […]».

Una variante que no da error pero no devuelve nada:

promql
rate(http_requetes_total{route="/cours", code="200"}[10s])
text
Empty query result

Qué mirar: un rango de 10 segundos contiene como mucho una lectura (están espaciadas 15 s), y rate necesita al menos dos. Regla práctica: el rango debe valer al menos dos veces el scrape_interval, así que [30s] mínimo aquí; [1m] o [5m] en la vida real.

Paso 10 — Agrupar

promql
sum by (code) (rate(http_requetes_total{route="/cours"}[1m]))

Lo que pide la consulta: la velocidad de todas las series de /cours (todos los códigos), sumada conservando solo la etiqueta code.

text
{code="200"}    3.7779456864749545
{code="500"}    0

Qué mirar: dos líneas, y solo queda una etiqueta en las llaves: code. Todas las demás (instance, job, methode, route, service) se fundieron en la suma. Una sola novedad: sum by (code) (…), la agregación del paso 6 con una cláusula by. Los paréntesis alrededor de code son obligatorios (sum by code (…) da parse error: … expected "("). La línea {code="500"} 0 merece una mirada: vale cero porque ningún 500 cayó en /cours durante el último minuto (la API produce alrededor de uno cada doce segundos, todas las rutas juntas). Cero no es ausente: la serie existe, solo tiene una pendiente nula. Si lanzas .\labo.ps1 casser erreurs (o ./labo.sh casser erreurs) y vuelves a escribir esta consulta un minuto más tarde, la segunda línea sube; reparer la hace bajar.

Este resultado es tu entregable: las dos líneas, y una frase para cada una («/cours sirve 3,78 respuestas 200 por segundo»; «ninguna respuesta 500 en /cours en el último minuto»).

Verificación final

Retoma las diez consultas de memoria, en orden, y marca:

  • api_info devuelve 1 serie, valor 1, con version="1.0.0" en las etiquetas.
  • up devuelve 8 series, todas en 1 (si no, un destino cayó: etat te dirá cuál).
  • up{job="api"} devuelve 1 serie.
  • up{job=api} devuelve parse error … expected string; up{job="API"} devuelve Empty query result.
  • up{job=~"a.*"} devuelve 3 series: alloy, api, alertmanager.
  • count(up) devuelve {} 8.
  • http_requetes_total{route="/cours", code="200"}[1m] devuelve 1 serie con 4 o 5 valores fechados @….
  • rate(…[1m]) devuelve 1 valor, entre 3 y 4 solicitudes por segundo en el labo del curso.
  • rate(…) sin corchetes devuelve expected type range vector … got instant vector.
  • sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) devuelve 2 series, {code="200"} y {code="500"}.

Nada que limpiar: no creaste nada. etat sigue mostrando 8/8 cibles up y el mismo número de series en memoria, con unas decenas de diferencia (Prometheus sigue recolectando).

Si algo falla

Mostrar los casos en que algo falla

Empty query result en api_info o http_requetes_total. La API todavía no fue leída, o está detenida. etat: si labo-api está Exited, reparer; si todo está healthy, espera 15 segundos (un scrape) y relanza.

up devuelve 7 series en lugar de 8, todas en 1. Un job desapareció de la configuración, no un destino caído (estaría en 0). Ve a Status → Target health y compara con los ocho jobs del paso 2. Si modificaste prometheus/prometheus.yml, restaura el archivo original (git checkout prometheus/prometheus.yml) y docker compose restart prometheus.

El paso 7 devuelve Empty query result. Los 4 valores del rango deben existir: justo después de demarrer, hay que esperar un minuto. Si la API acaba de reiniciarse (reparer), lo mismo.

El paso 8 devuelve un valor negativo o enorme. Imposible en principio: rate maneja los reinicios a cero del contador. Si ves eso, verifica que no hayas escrito rate sobre un medidor (requetes_en_cours): no provoca error pero no tiene ningún sentido.

La pestaña Graph sigue vacía. Para una consulta de rango (paso 7), es normal: Graph muestra invalid expression type "range vector". Para las demás, amplía el período (botón -/+ encima del gráfico): justo después del arranque, solo hay unos minutos de datos.

parse error que no reconoces. Ve a la posición línea:columna del mensaje. Cuenta tus paréntesis: sum by (code) (rate(x[1m])) tiene tres pares. Verifica cada comilla: van de a dos, rectas ("), nunca tipográficas (“ ”); un copiar y pegar desde un procesador de texto las reemplaza a veces.