Observer le service avec Prometheus et Grafana
La leçon précédente s'appuie sur une hypothèse : Prometheus collecte les bonnes séries, avec la bonne étiquette de version, assez souvent. Si l'une de ces conditions tombe, l'analyse canary ne voit rien et laisse tout passer. Et l'incident du ConfigMap partagé ne s'est vu ni dans Argo CD, ni dans Argo Rollouts : seulement dans les métriques. Cette leçon rend l'observabilité vérifiable : ce qui est collecté, les requêtes d'astreinte, le tableau de bord, et deux alertes testées contre un incident réel.
Le mécanisme
flowchart LR POD["Pod inference : /metrics (prometheus_client)"] -->|"toutes les 15 s"| PROM["Prometheus (kube-prometheus-stack)"] SM["ServiceMonitor (chart) : port http, /metrics, podTargetLabels"] -.->|"lu par l'opérateur"| OP["prometheus-operator"] OP -.->|"génère la configuration de collecte"| PROM PR["PrometheusRule (chart) : 2 alertes"] -.-> OP PROM --> GRAF["Grafana : tableau de bord (ConfigMap grafana_dashboard)"] PROM --> AR["Argo Rollouts : AnalysisRun"] PROM --> AM["Alertmanager (désactivé dans le lab)"]
L'application expose trois métriques (main.py) : inference_requests_total{status} (compteur), inference_request_seconds (histogramme), inference_inflight_requests (jauge). Le ServiceMonitor du chart demande à l'opérateur d'ajouter la cible ; deux réglages de kps-values.yaml le rendent possible :
prometheus:
prometheusSpec:
serviceMonitorSelectorNilUsesHelmValues: false # lire les ServiceMonitor de tous les namespaces
ruleSelectorNilUsesHelmValues: false # idem pour les PrometheusRuleSans eux, Prometheus ne lit que les objets portant l'étiquette de la release kps : le ServiceMonitor existe, kubectl get le montre, et aucune série n'arrive. C'est la panne la plus fréquente avec kube-prometheus-stack.
La mise en œuvre
Vérifier la collecte
make e2e # la dernière étape interroge Prometheus
make grafana-ui # http://localhost:3000, admin / lab-k8s-gitopsLes requêtes d'astreinte, à coller dans Grafana (Explore) ou dans l'API de Prometheus :
| Question | PromQL |
|---|---|
| Le service est-il collecté ? | up{namespace="assistant"} |
| Taux d'erreur vu par l'utilisateur | sum(rate(inference_requests_total{namespace="assistant",status=~"5.."}[5m])) / sum(rate(inference_requests_total{namespace="assistant"}[5m])) |
| Taux de succès par version | sum by (rollouts_pod_template_hash) (rate(inference_requests_total{status="200"}[1m])) / sum by (rollouts_pod_template_hash) (rate(inference_requests_total{status=~"200|5.."}[1m])) |
| Volume par version et code depuis 30 min | sum by (rollouts_pod_template_hash, status) (increase(inference_requests_total{namespace="assistant"}[30m])) |
| Pods saturés | max by (pod) (inference_inflight_requests) comparé à MAX_INFLIGHT (32) |
Le tableau de bord
Le tableau de bord est un ConfigMap (dashboard-assistant.yaml) que le sidecar de Grafana charge grâce à l'étiquette grafana_dashboard. Il vit dans Git comme le reste : un tableau de bord modifié à la main dans l'interface est perdu au prochain redémarrage de Grafana.
Les alertes
Le chart émet une PrometheusRule (prometheusrule.yaml) :
- alert: InferenceErreursElevees
expr: |
sum(rate(inference_requests_total{namespace="assistant",status=~"5.."}[5m]))
/ sum(rate(inference_requests_total{namespace="assistant"}[5m])) > 0.05
for: 2m
labels: { severity: page }
- alert: InferenceAucunPodCollecte
expr: absent(up{namespace="assistant",job="inference"} == 1)
for: 5mLa première mesure ce que subit l'utilisateur, tous pods confondus : c'est elle qui aurait réveillé l'astreinte pendant l'incident du ConfigMap partagé, que ni Argo CD ni Argo Rollouts ne signalaient. La seconde couvre le cas où il n'y a plus rien à mesurer : sans elle, un service à terre ne produit aucune série, donc aucune alerte d'erreur.
Une alerte qui n'a jamais sonné n'est pas testée. Évaluez son expression aux instants de l'incident, avec le paramètre time de l'API :
Q='sum(rate(inference_requests_total{namespace="assistant",status=~"5.."}[5m])) / sum(rate(inference_requests_total{namespace="assistant"}[5m]))'
kubectl --context kind-lab-k8s-gitops get --raw \
"/api/v1/namespaces/monitoring/services/kps-prometheus:9090/proxy/api/v1/query?time=2026-10-16T15:05:00Z&query=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' "$Q")"Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
| ServiceMonitor présent, aucune série | sélecteur de kube-prometheus-stack limité à sa release | serviceMonitorSelectorNilUsesHelmValues: false, ou étiquette release: kps sur le ServiceMonitor |
| p50 à 25 ms et p95 à 47,5 ms constants alors que la réponse prend moins d'une milliseconde | histogram_quantile interpole linéairement dans le premier bucket (0 à 50 ms) : il ne peut rien dire de plus fin | buckets adaptés à la latence réelle ; pour Gemini (secondes), les buckets 0,5 à 32 s du code sont corrects, pour le mode simulé ils ne le sont pas |
| Prometheus en manque de mémoire après l'ajout d'une étiquette | cardinalité : une étiquette par utilisateur ou par question multiplie les séries | jamais d'identifiant libre en étiquette ; ici 47 séries pour 3 pods, contrôlées par count({namespace="assistant"}) |
| Alerte silencieuse alors que le service est à terre | expression en ratio : plus de trafic, plus de série, plus d'alerte | alerte absent(up == 1) en complément |
| Le hash de version manque dans les séries | podTargetLabels absent, ou Deployment au lieu de Rollout | test de rendu test_servicemonitor_exports_version_label |
La règle de décision
- Dans le lab et sur un cluster autogéré : kube-prometheus-stack. Sur GKE : Google Cloud Managed Service for Prometheus, activé par défaut sur Autopilot ; le
ServiceMonitory devient unPodMonitoring, les règles un objetRules, et l'analyse canary interroge son frontend (valeurprometheusAddressde l'environnement gke). - Deux alertes qui réveillent (taux d'erreur utilisateur, absence de collecte) valent mieux que vingt qui s'ignorent. Le reste va au tableau de bord.
- À surveiller sur Prometheus lui-même : nombre de séries actives et durée de collecte.
Pratique
Vous ajoutez l'étiquette user_id au compteur inference_requests_total pour savoir qui utilise l'assistant (900 salariés). Quelle conséquence anticipez-vous ?
Chaque combinaison d'étiquettes est une série. 900 utilisateurs x 3 pods x 4 codes, c'est jusqu'à 10 800 séries au lieu de 12 pour ce compteur, et sur Managed Prometheus la facture est au nombre d'échantillons. Les requêtes existantes marchent encore (sum agrège), c'est ce qui rend l'erreur discrète. Pour l'usage par personne : les journaux, pas les métriques.
L'alerte InferenceErreursElevees n'a jamais sonné en six mois. Qu'en concluez-vous avant de la garder telle quelle ?
Une alerte jamais déclenchée est une alerte non testée : faute de frappe dans un nom de métrique, étiquette renommée, route Alertmanager absente. Dans ce lab, l'expression évaluée à 15:05 UTC donne 8,5 % pendant l'incident du ConfigMap partagé : elle aurait sonné. Baisser le seuil ou retirer le for produit des alertes sur des pics normaux.
Exercice
Ajoutez au tableau de bord un panneau « Part de 503 de délestage » qui montre, par pod, la part des requêtes refusées par MAX_INFLIGHT. Écrivez la requête, puis dites ce qu'une valeur durablement supérieure à 1 % signifie pour l'HPA.
Voir la solution
sum by (pod) (rate(inference_requests_total{namespace="assistant",status="503"}[5m]))
/ sum by (pod) (rate(inference_requests_total{namespace="assistant"}[5m]))Ajoutez un objet de panneau au JSON de monitoring/dashboard-assistant.yaml (copie du panneau 1 avec cette expression, unit: percentunit), puis kubectl apply -f monitoring/dashboard-assistant.yaml ; le sidecar recharge Grafana en quelques secondes.
Une part de 503 durable signifie que des pods atteignent leur limite de requêtes simultanées avant que l'HPA ne réagisse : le CPU n'est pas le bon signal pour un service qui attend un modèle distant. C'est l'argument de la métrique inference_inflight_requests comme cible de l'HPA (autoscaling.inflight dans le chart), expliquée dans le projet précédent.
Résultat obtenu
Exécution du 2026-10-16, après les démonstrations canary :
-- cibles collectées
up{job="inference", pod="inference-6cd4d8d86c-lpjdg", rollouts_pod_template_hash="6cd4d8d86c"} 1
up{job="inference", pod="inference-6cd4d8d86c-6chj6", rollouts_pod_template_hash="6cd4d8d86c"} 1
up{job="inference", pod="inference-6cd4d8d86c-bdbvl", rollouts_pod_template_hash="6cd4d8d86c"} 1
-- volume par version et code sur 30 min
8476bcfd4 200 224363 8476bcfd4 502 10437 <- hash STABLE : erreurs du ConfigMap partagé
7cb69f66b5 200 12011 7cb69f66b5 502 4944 <- canary 0.2.2, chart 0.2.0
c5f8b7776 200 12421 c5f8b7776 502 5451 <- canary 0.2.2, chart 0.3.0
6cd4d8d86c 200 140804 <- version stable actuelle
-- séries de l'application : 47
-- mémoire par pod (working set) : 44 à 53 Mio
-- règles chargées
InferenceErreursElevees inactive ok
InferenceAucunPodCollecte inactive ok
-- expression de InferenceErreursElevees aux instants de l'incident (UTC)
2026-10-16T15:00:00Z vide (aucun trafic)
2026-10-16T15:03:00Z 0.0542
2026-10-16T15:05:00Z 0.0853 <- au-dessus de 5 % plus de 2 minutes : l'alerte aurait sonné
2026-10-16T15:10:30Z 0.0512 (second essai, chart corrigé : pic bref)
$ make e2e
...
[OK] Prometheus collecte les métriques du service : 1 requêtes comptées
e2e : tout est vertLa ligne 8476bcfd4 502 10437 est la preuve chiffrée de l'incident de la leçon 3 : des erreurs attribuées au hash stable, ce qu'aucun outil de déploiement n'avait signalé.
Je sais faire
- Vérifier qu'un ServiceMonitor produit des séries, et corriger le sélecteur sinon
- Écrire les requêtes d'astreinte : taux d'erreur, succès par version, saturation
- Versionner un tableau de bord Grafana dans Git
- Écrire une alerte de taux d'erreur et une alerte d'absence, et tester la première sur un incident passé
- Reconnaître une latence fausse due aux buckets de l'histogramme