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.
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.
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.
| PromQL | Base SQL clásica | En este taller |
|---|---|---|
| métrica | tabla | up, api_info, http_requetes_total |
| serie | fila de la tabla | up{instance="api:8000", job="api", service="api"} |
| etiqueta | columna | job, 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 code | paso 10 |
rango [1m] | «las filas del último minuto» | paso 7 |
rate(…[1m]) | sin equivalente simple: una pendiente por segundo | paso 8 |
| vector instantáneo | un valor por serie, ahora | lo que devuelve up |
| vector de rango | varios valores fechados por serie | lo que devuelve up[1m] |
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:
up{instance="api:8000", job="api", service="api"} 1Cuando 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.
api_infoLo que pide la consulta: el último valor de la métrica api_info, para todas sus series.
api_info{instance="api:8000", job="api", service="api", version="1.0.0"} 1Qué 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.
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._ y :. Sin guion, sin espacio, sin punto. api-info se leería como api menos info.1 aquí; Prometheus no almacena texto, por eso la versión está en una etiqueta.upLo que pide la consulta: el último valor de up, para todas sus series.
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"} 1Qué 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.
up{job="api"}Lo que pide la consulta: las series de up cuya etiqueta job vale exactamente api.
up{instance="api:8000", job="api", service="api"} 1Qué 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.
Dos consultas erróneas, a propósito. Primero, olvida las comillas:
up{job=api}Error executing query
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected stringQué 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:
up{job="API"}Empty query resultQué 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.
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).
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.
up{instance="alloy:12345", job="alloy"} 1
up{instance="api:8000", job="api", service="api"} 1
up{instance="alertmanager:9093", job="alertmanager"} 1Qué 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.
count(up)Lo que pide la consulta: el número de series que devuelve up.
{} 8Qué 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í.
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.
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.197Qué 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.
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.
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.
{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3.7779456864749545Qué 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í.
rate sin rangorate(http_requetes_total{route="/cours", code="200"})Error executing query
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vectorQué 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:
rate(http_requetes_total{route="/cours", code="200"}[10s])Empty query resultQué 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.
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.
{code="200"} 3.7779456864749545
{code="500"} 0Qué 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»).
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).
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.