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.
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.
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.
| PromQL | Base SQL classique | Dans cet atelier |
|---|---|---|
| métrique | table | up, api_info, http_requetes_total |
| série | ligne de la table | up{instance="api:8000", job="api", service="api"} |
| label | colonne | job, 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, maintenant | ce que rend up |
| vecteur de plage | plusieurs valeurs datées par série | ce que rend up[1m] |
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 :
up{instance="api:8000", job="api", service="api"} 1Quand 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.
api_infoCe que la requête demande : la dernière valeur de la métrique api_info, pour toutes ses séries.
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.
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._ et :. Pas de tiret, pas d'espace, pas de point. api-info serait lu comme api moins info.1 ici ; Prometheus ne stocke pas de texte, c'est pour ça que la version est dans un label.upCe que la requête demande : la dernière valeur de up, pour toutes ses séries.
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.
up{job="api"}Ce que la requête demande : les séries de up dont le label job vaut exactement api.
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.
Deux requêtes fausses, exprès. D'abord, oublie les guillemets :
up{job=api}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 :
up{job="API"}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.
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).
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.
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.
count(up)Ce que la requête demande : le nombre de séries que rend up.
{} 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.
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.
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.
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.
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.
{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.
rate sans plagerate(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 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 :
rate(http_requetes_total{route="/cours", code="200"}[10s])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.
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.
{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 »).
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).
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.