Aller au contenu
Outils medium

PromQL : Le langage de requête Prometheus

20 min de lecture

PromQL (Prometheus Query Language) est le langage de requête de Prometheus. Ce guide vous accompagne des bases aux requêtes avancées, avec des exemples concrets que vous pouvez tester immédiatement.

Accédez à l'interface Prometheus (http://localhost:9090) et utilisez l'onglet Graph pour tester vos requêtes.

Interface PromQL de Prometheus

Tapez le nom d'une métrique pour voir toutes ses séries :

up

Résultat : toutes les séries de la métrique up, une par target scrapée.

up{instance="localhost:9090", job="prometheus"} 1
up{instance="node1:9100", job="node"} 1
up{instance="node2:9100", job="node"} 0

Les sélecteurs de labels filtrent les séries à l'intérieur des accolades. Sans sélecteur, vous récupérez toutes les séries portant ce nom de métrique, ce qui devient vite illisible sur un cluster de plusieurs dizaines de cibles.

up{job="node"}

Quatre opérateurs de matching existent, dont deux fondés sur les expressions régulières. Les regex de PromQL sont ancrées automatiquement : job=~"node" ne correspond qu'à la valeur exacte node, pas à node-exporter. C'est le piège le plus courant chez les débutants.

OpérateurDescriptionExemple
=Égalité exactejob="prometheus"
!=Différent dejob!="test"
=~Regex matchjob=~"node.*"
!~Regex not matchjob!~"test.*"

Exemples pratiques :

# Toutes les requêtes HTTP sauf les 200
http_requests_total{status!="200"}
# Méthodes GET ou POST
http_requests_total{method=~"GET|POST"}
# Namespaces commençant par "prod"
container_memory_usage_bytes{namespace=~"prod.*"}

PromQL manipule quatre types de données, et la plupart des messages d'erreur de l'interface viennent d'une confusion entre eux. Les deux premiers concentrent l'essentiel de l'usage quotidien : l'instant vector représente l'état courant, le range vector l'historique récent. Chaque fonction attend un type précis en entrée et en produit un autre en sortie, ce qui détermine quelles expressions peuvent s'imbriquer.

TypeDescriptionExemple
Instant vectorValeurs actuelles (un point par série)up
Range vectorValeurs sur une périodeup[5m]
ScalarNombre flottant3.14
StringChaîne (rarement utilisé)"hello"

C'est le type le plus courant. Chaque série a une seule valeur (la dernière).

node_memory_MemAvailable_bytes

Sélectionne toutes les valeurs sur une période. Nécessaire pour les fonctions comme rate().

http_requests_total[5m]

Périodes disponibles : s (secondes), m (minutes), h (heures), d (jours), w (semaines), y (années).

Les opérateurs permettent de calculer, de comparer et de combiner des séries entre elles. Deux points les distinguent d'un langage de programmation classique : ils travaillent sur des ensembles de séries et non sur des nombres isolés, et une opération entre deux métriques n'a lieu que si leurs labels correspondent. C'est cette règle de correspondance qui explique la plupart des résultats vides obtenus en combinant deux métriques.

Les opérateurs classiques s'appliquent à chaque série individuellement. Quand un des deux membres est un scalaire, le calcul porte sur toutes les séries et le nom de la métrique disparaît du résultat, puisque la valeur ne représente plus la métrique d'origine.

# Mémoire utilisée en pourcentage
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)
/ node_memory_MemTotal_bytes * 100
# Conversion bytes → gigabytes
node_filesystem_size_bytes / 1024 / 1024 / 1024
OpérateurDescription
+Addition
-Soustraction
*Multiplication
/Division
%Modulo
^Puissance

Par défaut, une comparaison filtre : elle conserve les séries qui satisfont la condition et supprime les autres, en gardant leur valeur d'origine. C'est ce comportement qui rend les règles d'alerte lisibles, une alerte se déclenchant dès que le résultat n'est pas vide. Le modificateur bool change la donne : il conserve toutes les séries et remplace leur valeur par 0 ou 1.

# Instances avec CPU > 80%
instance:cpu_usage:percent > 80
# Garder uniquement les séries au-dessus du seuil
http_request_duration_seconds > 0.5
OpérateurDescription
==Égal
!=Différent
>Supérieur
<Inférieur
>=Supérieur ou égal
<=Inférieur ou égal

and, or et unless raisonnent sur la présence des séries, pas sur leurs valeurs, et ne s'appliquent qu'entre deux instant vectors. Deux séries se correspondent quand tous leurs labels sont identiques ; unless sert typiquement à exclure d'une alerte les cibles déjà en maintenance.

# Séries présentes dans les deux (intersection)
metric_a and metric_b
# Séries présentes dans l'un ou l'autre (union)
metric_a or metric_b
# Séries de A absentes de B
metric_a unless metric_b

Une poignée de fonctions couvre la quasi-totalité des tableaux de bord et des alertes. Elles se répartissent en deux familles : celles qui transforment un range vector en instant vector, comme rate(), et les agrégateurs qui réduisent plusieurs séries à une seule, comme sum(). La règle à retenir est que toute donnée de type counter passe forcément par la première famille avant d'être affichée.

La fonction la plus importante. Calcule le taux par seconde d'un counter.

# Requêtes par seconde (sur 5 minutes)
rate(http_requests_total[5m])

Comme rate() mais utilise uniquement les 2 derniers points. Plus réactif mais plus bruité.

# Taux instantané (derniers 5 min de données)
irate(http_requests_total[5m])

Nombre total d'augmentations sur la période.

# Nombre de requêtes sur 1 heure
increase(http_requests_total[1h])

Ces agrégateurs réduisent plusieurs séries à une seule valeur par instant. Sans clause by ou without, tous les labels disparaissent du résultat : vous obtenez un unique chiffre pour l'ensemble du parc, ce qui est rarement l'intention sur un graphique de supervision.

# Total des requêtes (toutes instances)
sum(rate(http_requests_total[5m]))
# Moyenne de mémoire par node
avg(node_memory_MemAvailable_bytes)
# Maximum de CPU
max(instance:cpu_usage:percent)

Utilisez by ou without pour contrôler l'agrégation :

# Somme par job
sum by(job) (rate(http_requests_total[5m]))
# Somme en ignorant le label instance
sum without(instance) (rate(http_requests_total[5m]))

Calcule les percentiles depuis un histogram.

# P95 de latence par job
histogram_quantile(0.95,
sum by(job, le) (rate(http_request_duration_seconds_bucket[5m]))
)
# P99 global
histogram_quantile(0.99,
sum by(le) (rate(http_request_duration_seconds_bucket[5m]))
)

Ces fonctions s'appliquent point par point à un instant vector et laissent les labels intacts. Elles servent surtout à rendre un graphique lisible : arrondir une valeur avant affichage, ramener une échelle très étalée à un logarithme, ou supprimer le signe d'une variation.

FonctionDescriptionExemple
abs()Valeur absolueabs(delta(temp[1h]))
ceil()Arrondi supérieurceil(value)
floor()Arrondi inférieurfloor(value)
round()Arrondiround(value, 0.1)
ln()Logarithme naturelln(value)
log2()Log base 2log2(value)
log10()Log base 10log10(value)
sqrt()Racine carréesqrt(value)

Elles donnent accès à l'horloge du serveur Prometheus, exprimée en UTC : hour() renvoie donc l'heure UTC, pas l'heure locale de vos équipes. C'est le détail qui décale d'une ou deux heures les alertes conditionnées aux horaires de bureau.

FonctionDescription
time()Timestamp Unix actuel
timestamp()Timestamp de chaque sample
day_of_week()Jour de la semaine (0-6)
hour()Heure (0-23)
# Alerte uniquement en heures ouvrées
up == 0 and hour() >= 9 and hour() <= 18

Toutes ces fonctions prennent un range vector en entrée, donc une métrique suivie d'une période entre crochets, et renvoient un instant vector affichable. Les variantes _over_time conviennent aux gauges dont on veut lisser les oscillations, tandis que resets() et delta() s'adressent respectivement aux counters et aux gauges.

FonctionDescription
avg_over_time()Moyenne sur la période
min_over_time()Minimum sur la période
max_over_time()Maximum sur la période
sum_over_time()Somme sur la période
count_over_time()Nombre de points
stddev_over_time()Écart-type
changes()Nombre de changements
resets()Nombre de resets (counter)
delta()Différence premier-dernier
deriv()Dérivée (pente)
predict_linear()Prédiction linéaire
# Moyenne de CPU sur 1 heure
avg_over_time(instance:cpu_usage:percent[1h])
# Prédiction : dans 4h, espace disque
predict_linear(node_filesystem_avail_bytes[6h], 4*3600)

Les requêtes qui suivent sont celles que vous recopierez le plus souvent dans vos tableaux de bord. Elles reposent toutes sur les métriques exposées par node_exporter et par les instrumentations HTTP standard, avec des noms conformes aux conventions Prometheus. Adaptez les noms de labels à votre environnement avant de les mettre en alerte : handler, status ou namespace varient d'une bibliothèque d'instrumentation à l'autre.

Le CPU ne s'exporte pas directement en pourcentage : node_exporter publie un compteur de secondes passées dans chaque mode, dont le mode idle. On calcule donc l'inactivité, puis on la soustrait de 100.

# Pourcentage CPU utilisé par instance
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

Explication :

  1. node_cpu_seconds_total{mode="idle"} : temps CPU en idle
  2. rate(...[5m]) : taux par seconde sur 5 min
  3. avg by(instance) : moyenne des cores par machine
  4. * 100 : conversion en pourcentage
  5. 100 - ... : usage = 100% - idle

Utilisez MemAvailable et non MemFree : le noyau considère comme disponible la mémoire occupée par le cache disque, qu'il rendra à la demande. Un calcul basé sur MemFree déclenche des alertes permanentes sur des serveurs parfaitement sains.

# Pourcentage mémoire utilisée
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

Ce ratio d'erreurs est la brique de base d'un objectif de niveau de service. Les deux sum() sont indispensables : sans eux, la division se ferait série par série et échouerait, faute de labels identiques des deux côtés.

# Taux d'erreurs 5xx sur le total
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) * 100

Le P95 indique la durée sous laquelle se situent 95 % des requêtes. Le label le doit rester dans le by, sinon la fonction perd les bornes des buckets et ne peut plus interpoler ; handler permet ensuite de repérer l'endpoint responsable.

# P95 par endpoint
histogram_quantile(0.95,
sum by(handler, le) (rate(http_request_duration_seconds_bucket[5m]))
)

Alerter sur un seuil fixe de remplissage arrive souvent trop tard. En extrapolant la pente de consommation observée sur les six dernières heures, vous obtenez un délai avant saturation, bien plus actionnable. La fenêtre doit rester assez large pour absorber les variations : sur une heure, la moindre rotation de logs fausse la projection.

# Prédiction : disque plein dans combien de temps (secondes)
(node_filesystem_avail_bytes / node_filesystem_size_bytes)
/ deriv(node_filesystem_avail_bytes[6h]) * -1

topk() recalcule son classement à chaque point de temps, ce qui fait apparaître et disparaître des séries au fil du graphique. Le filtre container!="" écarte les séries agrégées par cgroup que cAdvisor publie en parallèle des conteneurs, et qui feraient doublon dans le classement.

# Top 5 pods par mémoire
topk(5, container_memory_usage_bytes{container!=""})

Le modificateur offset décale la fenêtre d'évaluation vers le passé, ce qui permet de comparer le trafic actuel à celui de la semaine précédente. Cette comparaison ne vaut que si votre durée de rétention couvre le décalage demandé, sinon la requête renvoie un résultat vide.

# Différence avec il y a 1 semaine
rate(http_requests_total[5m])
- rate(http_requests_total[5m] offset 1w)

Quand vous combinez deux vecteurs, PromQL fait du matching par labels : il cherche, pour chaque série de gauche, la série de droite qui porte exactement les mêmes labels. Aucune correspondance ne signifie aucun résultat, sans message d'erreur, et c'est la cause numéro un des graphiques désespérément vides. Les modificateurs présentés ici servent à relâcher cette règle quand les deux métriques ne partagent pas le même jeu de labels.

Chaque série d'un côté matche exactement une série de l'autre.

# Même labels des deux côtés
metric_a / metric_b

Spécifie quels labels utiliser pour le matching :

# Matcher uniquement sur le label "job"
metric_a / on(job) metric_b
# Matcher en ignorant le label "instance"
metric_a / ignoring(instance) metric_b

Quand une série d'un côté doit servir à plusieurs séries de l'autre, PromQL refuse l'opération tant que vous n'avez pas indiqué quel côté porte la multiplicité. group_left désigne le membre de gauche comme celui qui contient plusieurs séries par correspondance, group_right fait l'inverse. L'usage classique consiste à enrichir des mesures avec les labels d'une métrique d'information comme kube_pod_info.

# Chaque série de B matchée à une série de A
metric_a / on(job) group_left metric_b

Une requête juste peut rester coûteuse ou trompeuse. Les trois points qui suivent traitent des sujets qui reviennent systématiquement en production : nommer les métriques pour qu'elles soient devinables, éviter les erreurs de typage qui produisent des courbes crédibles mais fausses, et contenir le coût des requêtes. La cardinalité, c'est-à-dire le nombre de séries distinctes, est le facteur qui pèse le plus sur la mémoire de Prometheus.

La convention de nommage n'est pas cosmétique : le suffixe d'unité et le suffixe de type indiquent au lecteur comment interroger la métrique sans consulter la documentation de l'exportateur. Prometheus attend des unités de base, donc des secondes et des octets, jamais des millisecondes ni des mégaoctets.

<namespace>_<name>_<unit>_<suffix>

Exemples :

  • http_requests_total (counter)
  • http_request_duration_seconds (histogram)
  • node_memory_MemAvailable_bytes (gauge)

Ces trois erreurs ne provoquent aucun message d'erreur : elles produisent un graphique qui s'affiche normalement, avec des valeurs fausses ou un serveur qui sature. Les onglets ci-dessous opposent la version fautive à sa correction.

Mauvais : rate() sur un gauge

rate(node_memory_MemAvailable_bytes[5m]) # FAUX

Correct : deriv() pour un gauge

deriv(node_memory_MemAvailable_bytes[5m])

Le coût d'une requête dépend du nombre de séries chargées et de la profondeur d'historique parcourue. Un tableau de bord rafraîchi toutes les quinze secondes rejoue ses requêtes en permanence : ce qui est acceptable en exploration manuelle devient un problème une fois affiché sur un écran mural. Les recording rules déplacent le calcul du moment de l'affichage vers le moment de l'ingestion.

  • Utilisez les recording rules pour les requêtes complexes fréquentes
  • Limitez la période : [5m] est souvent suffisant
  • Agrégez tôt : sum by(job) (rate(...)) plutôt que calculs sur toutes les séries
  • Évitez .* dans les regex : préférez des valeurs exactes

Ce récapitulatif regroupe les expressions à garder sous la main, classées par intention. Remplacez metric, counter et histogram_bucket par vos propres noms de métriques ; le reste de la syntaxe se recopie tel quel.

# === COUNTERS ===
rate(counter[5m]) # Taux par seconde
increase(counter[1h]) # Augmentation totale
irate(counter[5m]) # Taux instantané
# === AGRÉGATIONS ===
sum(metric) # Total
avg(metric) # Moyenne
max(metric) # Maximum
min(metric) # Minimum
count(metric) # Nombre de séries
sum by(label) (metric) # Grouper par label
# === PERCENTILES ===
histogram_quantile(0.95, sum by(le) (rate(histogram_bucket[5m])))
# === TEMPS ===
avg_over_time(metric[1h]) # Moyenne sur 1h
max_over_time(metric[24h]) # Max sur 24h
predict_linear(metric[6h], 3600) # Prédiction dans 1h
# === COMPARAISON ===
metric > 80 # Filtre
metric > bool 80 # Retourne 0/1
topk(5, metric) # Top 5
bottomk(3, metric) # Bottom 3
# === LABELS ===
label_replace(metric, "new", "$1", "old", "(.*)")
label_join(metric, "new", ",", "label1", "label2")
  • rate() pour les counters, deriv() pour les gauges
  • by() et without() contrôlent l'agrégation
  • histogram_quantile() pour les percentiles
  • Protégez les divisions contre le zéro
  • Testez dans l'UI avant de mettre en alerte

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn