Atelier fondamental 1 — PromQL : une métrique, un label, une fonction

Pratique guidée16 min
Durée
20 min
Module
1/7
Prérequis
le labo tourne depuis au moins deux minutes (etat affiche 8/8 cibles up), l'onglet Query de Prometheus ouvert sur http://localhost:9090
Tu vas construire
dix requêtes PromQL tapées à la main, de la plus courte (api_info) à la première vraie agrégation (sum by (code) (rate(…[1m]))), en ne changeant qu'une chose à la fois
Livrable
la sortie de l'étape 10 telle qu'affichée dans l'onglet Table, deux lignes, avec une phrase disant ce que chacune mesure

Comment lire cette page. Dix étapes, une requête à la fois. Pour chacune : la requête à taper, la réponse exacte du labo (onglet Table), et ce qu'il faut regarder dedans. Tape toi-même chaque requête (pas de copier-coller) : c'est en écrivant les accolades, les guillemets et les crochets que la grammaire rentre. Les chiffres seront différents chez toi ; les formes (nombre de lignes, labels, ordre de grandeur) doivent être les mêmes. Les blocs « Pour bien comprendre » sont facultatifs. Si le labo n'est pas démarré, retourne à la pratique guidée : la section En bref donne les commandes, kit compris (https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr). Rien n'est créé ni modifié dans cet atelier : PromQL ne fait que lire.

Objectif

La pratique guidée t'a fait taper douze requêtes déjà écrites. Tu as vu les résultats, mais si on te retire la feuille, sais-tu écrire sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) sans te tromper de parenthèse ? Ici, tu repars de la requête la plus courte possible, un nom de métrique, et tu ajoutes un seul morceau à chaque étape : un label, un opérateur, une fonction, une plage de temps, un regroupement. Deux étapes sont des pièges volontaires : tu vas provoquer les deux messages d'erreur que tout débutant rencontre, pour les reconnaître la prochaine fois. À la fin, tu sais ce qu'est une métrique, un label et une fonction parce que tu as assemblé les trois morceaux toi-même.

Le vocabulaire en une image

Prometheus est un carnet de relevés. Toutes les 15 secondes, il passe devant chaque cible, lit sa page /metrics et note chaque valeur avec l'heure. Une métrique est le nom d'une colonne du carnet (up, http_requetes_total). Un label est une étiquette collée sur la ligne pour dire de quoi on parle (job="api", code="200") ; un même nom de métrique avec des étiquettes différentes, ce sont des séries différentes. Une fonction est une opération sur ce qu'on a lu : compter les lignes, calculer une pente, additionner.

PromQLBase SQL classiqueDans cet atelier
métriquetableup, api_info, http_requetes_total
sérieligne de la tableup{instance="api:8000", job="api", service="api"}
labelcolonnejob, instance, route, code
sélecteur {job="api"}WHERE job = 'api'étape 3
=~WHERE job LIKE 'a%' (en expression régulière)étape 5
count(…), sum(…)COUNT(*), SUM(…)étapes 6 et 10
by (code)GROUP BY codeétape 10
plage [1m]« les lignes de la dernière minute »étape 7
rate(…[1m])pas d'équivalent simple : une pente par secondeétape 8
vecteur instantanéune valeur par série, maintenantce que rend up
vecteur de plageplusieurs valeurs datées par sériece que rend up[1m]

Où taper, et comment lire une réponse

Ouvre http://localhost:9090. Tu es sur la page Query. Le champ de saisie accepte une requête ; Execute (ou Entrée) l'envoie. Le résultat s'affiche sous le champ, dans l'onglet Table. Reste sur Table pendant tout l'atelier : c'est là que tu vois les labels écrits en clair. L'onglet Graph dessine la même chose dans le temps ; l'onglet Explain décompose la requête.

Une ligne de résultat a toujours la même forme : le nom de la métrique, puis entre accolades les labels triés par ordre alphabétique, puis la valeur à droite :

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

Quand la requête a fait fondre le nom (une fonction, une agrégation), les accolades restent, parfois vides : {} 8. Sous les onglets, Result series: N te dit combien de lignes tu as. Un résultat vide s'écrit Empty query result ; une requête mal écrite affiche une boîte rouge Error executing query suivie du message.

Étape 1 — Lire une métrique

promql
api_info

Ce que la requête demande : la dernière valeur de la métrique api_info, pour toutes ses séries.

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

À regarder : une seule ligne, Result series: 1. La valeur est 1 et ne changera jamais : api_info est une métrique d'information, tout ce qu'elle a à dire est dans son label version="1.0.0". Trois autres labels que l'API n'a pas écrits : instance, job et service ont été ajoutés par Prometheus au moment de la lecture. Sur la page http://localhost:8000/metrics, la même ligne s'écrit api_info{version="1.0.0"} 1.0.

Pour bien comprendre
  • Pourquoi commencer par api_info et pas par up ? Parce qu'elle a une série. Tu vois la forme complète d'une ligne de résultat (nom, labels, valeur) sans être distrait par sept autres lignes.
  • Un nom de métrique contient des lettres, des chiffres, _ et :. Pas de tiret, pas d'espace, pas de point. api-info serait lu comme api moins info.
  • La valeur est toujours un nombre flottant. 1 ici ; Prometheus ne stocke pas de texte, c'est pour ça que la version est dans un label.

Étape 2 — Lire une métrique à plusieurs séries

promql
up

Ce que la requête demande : la dernière valeur de up, pour toutes ses séries.

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

À regarder : Result series: 8, le même nom sur chaque ligne, et ce qui change d'une ligne à l'autre : les valeurs des labels job et instance. C'est ça, une série : un nom plus un jeu de labels. Huit jeux différents, huit séries. Remarque que seule la troisième ligne porte service="api" : ce label a été ajouté à la main dans prometheus.yml, pour le job api seulement. up n'existe sur aucune page /metrics : Prometheus la fabrique lui-même, 1 si la lecture a réussi, 0 sinon.

Étape 3 — Choisir une série avec un label

promql
up{job="api"}

Ce que la requête demande : les séries de up dont le label job vaut exactement api.

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

À regarder : une seule ligne, la troisième de l'étape 2. Les accolades après le nom sont un filtre : on les appelle un sélecteur. job est le nom du label, "api" sa valeur, entre guillemets doubles, = l'égalité exacte. C'est le WHERE job = 'api' de SQL, au mot près. Tu peux mettre plusieurs conditions séparées par des virgules ; toutes doivent être vraies.

Étape 4 — Le piège : sans guillemets, puis avec la mauvaise casse

Deux requêtes fausses, exprès. D'abord, oublie les guillemets :

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

À regarder : parse error, Prometheus n'a même pas cherché : la requête est mal écrite. 1:8 est la position (ligne 1, caractère 8, juste après up{job=). expected string : il attendait une chaîne entre guillemets. Une valeur de label est toujours une chaîne, même quand elle ressemble à un nombre : {code=500} donne la même famille d'erreur, {code="500"} est la bonne forme.

Ensuite, mets les guillemets mais change la casse :

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

À regarder : aucune erreur, aucune ligne. C'est le piège le plus vicieux : la requête est correcte, elle demande simplement une série qui n'existe pas. Les valeurs de labels sont sensibles à la casse et à l'orthographe ("api " avec un espace ne marche pas non plus). Quand tu obtiens Empty query result sans raison, retape l'étape 2 et relis les valeurs exactes.

Pour bien comprendre : lire un message d'erreur PromQL

Un message d'erreur de Prometheus a trois parties : parse error (la requête est mal formée) ou bad_data (la requête est bien formée mais impossible à exécuter), une position ligne:colonne, et une phrase qui dit ce qu'il attendait. Va toujours à la position indiquée : l'erreur est là ou juste avant. Les trois messages de cet atelier couvrent la grande majorité des cas : expected string (guillemets), expected "(" (parenthèses autour de by), expected type range vector (crochets, étape 9).

Étape 5 — Choisir plusieurs séries avec un motif

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

Ce que la requête demande : les séries de up dont le label job correspond à l'expression régulière a.* : un a suivi de n'importe quoi.

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

À regarder : trois lignes, les trois jobs qui commencent par a. Une seule nouveauté par rapport à l'étape 3 : =~ à la place de =. L'expression régulière doit correspondre à la valeur entière : "a" seul ne rendrait rien, il faut "a.*". Les quatre opérateurs de sélection : = (égal), != (différent : up{job!="api"} rend les sept autres), =~ (correspond), !~ (ne correspond pas). C'est =~ que la pratique guidée utilisait dans {code=~"5.."} pour attraper tous les 5xx.

Étape 6 — Appliquer une fonction

promql
count(up)

Ce que la requête demande : le nombre de séries que rend up.

text
{}    8

À regarder : une seule ligne, et le nom a disparu : {} vide, puis 8. C'est la première fonction de l'atelier, et le résultat n'est plus up, c'est un nombre calculé à partir de up. count est une agrégation : elle prend plusieurs séries et en fait une. Ses cousines : sum (la somme des valeurs : sum(up) donne aussi 8 tant que tout est à 1, et 7 dès qu'une cible tombe), min, max, avg. Le tableau de bord du kit et la commande etat comptent les cibles exactement comme ça.

Étape 7 — Demander une plage de temps

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

Ce que la requête demande : toutes les valeurs de cette série relevées pendant la dernière minute, pas seulement la dernière.

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

À regarder : une série, mais quatre valeurs, chacune suivie de @ et d'une date en secondes. Quinze secondes d'écart entre deux : c'est le scrape_interval. Le compteur monte de 2439 à 2609 : 170 requêtes 200 sur /cours en 45 secondes. Une seule nouveauté : les crochets [1m] après le sélecteur. Ils transforment un vecteur instantané (une valeur par série) en vecteur de plage (une liste de valeurs datées par série). Clique sur l'onglet Graph : il refuse cette requête (Error executing query puis invalid expression type "range vector" for range query, must be Scalar or instant Vector). On ne dessine pas une plage brute, on la donne à une fonction. C'est l'étape 8. Reviens sur Table.

Pour bien comprendre : pourquoi quatre valeurs et pas cinq ?

Une minute contient quatre intervalles de 15 secondes, donc quatre ou cinq relevés selon l'instant où tu lances la requête par rapport au cycle de scrape. Si tu retapes la requête plusieurs fois, tu verras parfois cinq lignes. Les dates @1789505608.199 sont des secondes depuis le 1er janvier 1970 (l'heure Unix) ; l'onglet Graph les convertit en heures lisibles.

Étape 8 — Appliquer une fonction à la plage

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

Ce que la requête demande : la vitesse à laquelle ce compteur a augmenté, en unités par seconde, calculée sur la plage de la dernière minute.

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

À regarder : de nouveau une seule valeur, et le nom http_requetes_total a disparu des accolades : ce n'est plus un compteur, c'est une vitesse. 3,78 requêtes par seconde. Vérifie avec l'étape 7 : 170 requêtes en 45 secondes font 3,78. Une seule nouveauté : la fonction rate(), qui prend un vecteur de plage et rend un vecteur instantané. C'est la fonction la plus importante de PromQL : un compteur brut ne se lit jamais, sa pente si.

Étape 9 — Le piège : rate sans plage

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

À regarder : expected type range vector … got instant vector. Tu as donné à rate un vecteur instantané (une valeur), elle voulait une plage (plusieurs valeurs datées) : sans deux points, pas de pente. Le geste correct est l'étape 8, avec [1m]. Tu liras ce message souvent ; il veut toujours dire « il manque […] ».

Une variante qui ne fait pas d'erreur mais ne rend rien :

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

À regarder : une plage de 10 secondes ne contient au mieux qu'un relevé (ils sont espacés de 15 s), et rate a besoin d'au moins deux. Règle pratique : la plage doit valoir au moins deux fois le scrape_interval, donc [30s] minimum ici ; [1m] ou [5m] dans la vraie vie.

Étape 10 — Regrouper

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

Ce que la requête demande : la vitesse de toutes les séries de /cours (tous codes), additionnée en ne gardant que le label code.

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

À regarder : deux lignes, et il ne reste qu'un label dans les accolades : code. Tous les autres (instance, job, methode, route, service) ont fondu dans la somme. Une seule nouveauté : sum by (code) (…), l'agrégation de l'étape 6 avec une clause by. Les parenthèses autour de code sont obligatoires (sum by code (…) donne parse error: … expected "("). La ligne {code="500"} 0 mérite un regard : elle vaut zéro parce qu'aucun 500 n'est tombé sur /cours pendant la dernière minute (l'API en produit environ un toutes les douze secondes, toutes routes confondues). Zéro n'est pas absent : la série existe, elle a juste une pente nulle. Si tu lances .\labo.ps1 casser erreurs (ou ./labo.sh casser erreurs) et que tu retapes cette requête une minute plus tard, la seconde ligne grimpe ; reparer la fait redescendre.

Ce résultat est ton livrable : les deux lignes, et une phrase pour chacune (« /cours sert 3,78 réponses 200 par seconde » ; « aucune réponse 500 sur /cours dans la dernière minute »).

Vérification finale

Reprends les dix requêtes de mémoire, dans l'ordre, et coche :

  • api_info rend 1 série, valeur 1, avec version="1.0.0" dans les labels.
  • up rend 8 séries, toutes à 1 (sinon, une cible est tombée : etat te dira laquelle).
  • up{job="api"} rend 1 série.
  • up{job=api} rend parse error … expected string ; up{job="API"} rend Empty query result.
  • up{job=~"a.*"} rend 3 séries : alloy, api, alertmanager.
  • count(up) rend {} 8.
  • http_requetes_total{route="/cours", code="200"}[1m] rend 1 série avec 4 ou 5 valeurs datées @….
  • rate(…[1m]) rend 1 valeur, entre 3 et 4 requêtes par seconde sur le labo du cours.
  • rate(…) sans crochets rend expected type range vector … got instant vector.
  • sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) rend 2 séries, {code="200"} et {code="500"}.

Rien à nettoyer : tu n'as rien créé. etat affiche toujours 8/8 cibles up et le même nombre de séries en mémoire, à quelques dizaines près (Prometheus continue de collecter).

Si ça coince

Afficher les cas où ça coince

Empty query result sur api_info ou http_requetes_total. L'API n'a pas encore été lue, ou elle est arrêtée. etat : si labo-api est Exited, reparer ; si tout est healthy, attends 15 secondes (un scrape) et relance.

up rend 7 séries au lieu de 8, toutes à 1. Un job a disparu de la configuration, pas une cible tombée (elle serait à 0). Va dans Status → Target health et compare avec les huit jobs de l'étape 2. Si tu as modifié prometheus/prometheus.yml, remets le fichier d'origine (git checkout prometheus/prometheus.yml) et docker compose restart prometheus.

L'étape 7 rend Empty query result. Les 4 valeurs de la plage doivent exister : juste après demarrer, il faut attendre une minute. Si l'API vient de redémarrer (reparer), même chose.

L'étape 8 rend une valeur négative ou énorme. Impossible en principe : rate gère les remises à zéro du compteur. Si tu vois ça, vérifie que tu n'as pas tapé rate sur une jauge (requetes_en_cours) : ça ne provoque pas d'erreur mais n'a aucun sens.

L'onglet Graph reste vide. Pour une requête de plage (étape 7), c'est normal : Graph affiche invalid expression type "range vector". Pour les autres, élargis la période (bouton -/+ au-dessus du graphique) : juste après le démarrage, il n'y a que quelques minutes de données.

parse error que tu ne reconnais pas. Va à la position ligne:colonne du message. Compte tes parenthèses : sum by (code) (rate(x[1m])) en a trois paires. Vérifie chaque guillemet : ils vont par deux, droits ("), jamais typographiques (“ ”) ; un copier-coller depuis un traitement de texte les remplace parfois.