❯ monitoring · Running

Observabilité v2 — logs, alertes & annotations en une journée Observability v2 — Logs, Alerts & Annotations in One Day

Un pic de charge inexpliqué a révélé quatre angles morts dans mon monitoring. Un jour plus tard, le cluster avait des logs centralisés (VictoriaLogs + Vector), un vrai alerting (vmalert → Alertmanager → Telegram) et des annotations automatiques sur chaque graphe Grafana — plus une leçon sur les probes qui flappent sans jamais alerter. An unexplained load spike exposed four blind spots in my monitoring. One day later the cluster had centralized logs (VictoriaLogs + Vector), real alerting (vmalert → Alertmanager → Telegram) and auto-annotations on every Grafana graph — plus a hard lesson about why flapping probes never alert.

· 7 min de lecture 7 min read ·
k3s-clustermonitoringvictoriametricsvictorialogsvectorvmalertalertmanagertelegramgrafanak8s
🇬🇧 Read this article in English

Le 11 juin, en analysant les métriques du cluster sur 6 heures, je tombe sur un pic de load à 7.18 sur worker-1 (×16 sa moyenne) à 17h20. Aucun pod créé dans la fenêtre. Rien dans les logs de k3s-agent. Aucun moyen de savoir quel workload avait mangé le CPU — parce que vmagent ne scrapait pas cAdvisor, donc aucun historique par pod.

Ce pic inattribuable a servi de révélateur. Mon monitoring v1 (VictoriaMetrics + Grafana) savait dire « le nœud souffre », mais pas « voilà le coupable ». L’audit a listé quatre angles morts :

  1. Pas de métriques par pod/container — cAdvisor non scrapé, impossible d’attribuer un pic a posteriori
  2. Pas de logs centralisés — corrélation = SSH manuel sur chaque nœud, un par un
  3. Pas d’alerting — la readiness probe d’un backend échouait en silence depuis des jours
  4. Pas d’annotations — les graphes Grafana ne montraient ni les events K8s ni les déploiements ArgoCD

Le lendemain soir, les quatre étaient comblés. Voici comment, et surtout ce que j’ai appris en route.

① cAdvisor — le quick win (30 minutes)

cAdvisor est déjà dans le kubelet, sur chaque nœud, port 10250. Il suffit de le scraper :

# monitoring/vm/vmagent-values.yaml — job ajouté
- job_name: cadvisor
  scheme: https
  tls_config: { insecure_skip_verify: true }
  bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
  kubernetes_sd_configs: [{ role: node }]
  relabel_configs:
  - source_labels: [__address__]
    regex: '(.*):10250'
    target_label: __metrics_path__
    replacement: /metrics/cadvisor

Le ClusterRole existant de vmagent avait déjà nodes/metrics — un helm upgrade plus tard : 6/6 cibles up, 426 séries container_cpu_usage_seconds_total. Le dashboard K8s (15760) s’enrichit tout seul. Le prochain pic aura un nom et un namespace.

② Logs centralisés — VictoriaLogs + Vector, pas Loki

Loki était sur ma roadmap depuis des mois. Je ne l’ai jamais déployé, et avec le recul c’était une bonne procrastination : VictoriaLogs est plus léger, ARM64-natif, et cohérent avec ma stack VictoriaMetrics existante (même éditeur, même philosophie single-binary).

L’architecture tient en deux pièces :

  • VictoriaLogs single-node (v1.50.0) — PVC Longhorn 10 Gi, rétention 7 jours
  • Vector en DaemonSet (0.56.0) sur les 6 nœuds — lit /var/log/pods, enrichit par namespace/pod, pousse via l’API Elasticsearch bulk
# victorialogs/vector.yaml — le cœur du pipeline
[sources.k8s]
type = "kubernetes_logs"

[sinks.vlogs]
type = "elasticsearch"
endpoints = ["http://victorialogs:9428/insert/elasticsearch/"]
api_version = "v8"
[sinks.vlogs.query]
_msg_field = "message"
_time_field = "timestamp"
_stream_fields = "kubernetes.pod_namespace,kubernetes.pod_name"

Vérification après déploiement : ~360 000 lignes ingérées en 2 minutes, requêtables dans Grafana via le plugin datasource victoriametrics-logs-datasource. Le gain concret : métriques et logs dans le même time range Grafana. Un pic sur un graphe → un clic → les logs de la même fenêtre. Fini le journalctl en SSH sur six machines.

Un détail vécu : les premiers pods Vector ont crash-loopé pendant le rollout (5-6 restarts le temps que VictoriaLogs et son PVC soient prêts). C’est exactement le genre d’événement que la suite de cet article rend visible.

③ Alerting — vmalert → Alertmanager → Telegram

Le morceau principal. Deux déploiements et un SealedSecret :

  • vmalert (v1.140.0) évalue les règles toutes les 30 s contre vmsingle, et réécrit l’état ALERTS dans VictoriaMetrics (pratique : l’historique pending/firing devient requêtable)
  • Alertmanager (v0.32.2) groupe et route vers Telegram — le bot token vit dans un SealedSecret monté en bot_token_file, jamais en clair dans Git

Six règles pour commencer, volontairement simples :

- alert: NodeDown          # up{job="kubernetes-nodes"} == 0, for: 3m
- alert: NodeHighLoad      # node_load1 > 8, for: 10m
- alert: DiskAlmostFull    # >90%, for: 15m
- alert: NodeMemoryPressure # >92%, for: 10m
- alert: PodCrashLooping   # restarts > 3 en 15m, for: 5m
- alert: PodNotReady       # not-ready en phase Running, for: 15m

Premier faux positif dès le premier soir : PodNotReady se déclenchait pour les pods de Jobs terminés (Completed = ready false à vie — les snapshots Longhorn, les CronJobs de métriques…). Le fix, un join sur la phase :

- alert: PodNotReady
  expr: |
    kube_pod_status_ready{condition="true"} == 0
    and on(namespace, pod) kube_pod_status_phase{phase="Running"} == 1
  for: 15m

④ Annotations — les events K8s sur les graphes

Dernière pièce : un CronJob grafana-annotator (*/5 min) qui pousse vers l’API annotations de Grafana :

  • les events K8s Warning (tag k8s-warning) — probe failures, OOMKilled, FailedScheduling…
  • les syncs ArgoCD (tag argocd) — chaque déploiement devient une ligne verticale sur les graphes

C’est l’outil le moins spectaculaire et le plus rentable de la journée. Le lendemain matin, les dashboards racontaient une histoire : chaque pic de load avait sa ligne pointillée orange à côté, avec l’event qui l’expliquait.

La leçon du lendemain : pending n’est pas firing

Ironie immédiate : le jour suivant, les nouvelles annotations montraient des probe failures récurrentes sur un backend (timeouts /api/health sous charge) — et Telegram restait muet. Le pipeline était-il cassé ?

Non. Il fonctionnait exactement comme configuré :

L’enquête, chiffres à l’appui (l’état ALERTS réécrit dans VictoriaMetrics rend ça trivial) :

  • PodNotReady{pod="...backend..."} : 3 échantillons pending en 24 h, zéro firing — chaque épisode not-ready durait ~2 min, jamais les 15 min du for:
  • NodeHighLoad{instance="worker-2"} : 6× pending — le load dépassait 8 en pics, jamais 10 min en continu
  • Pendant ce temps, côté Alertmanager : alertmanager_notifications_total{integration="telegram"} = 8, zéro échec — les alertes qui avaient vraiment atteint firing (les crash-loops Vector, justement) étaient bien parties

Trois enseignements :

1. Le for: est une porte, pas un délai. La condition doit rester vraie sans interruption. Un workload qui flappe par épisodes courts vit en pending éternel — invisible si on ne regarde que les notifications.

2. Vérifier le pipeline de bout en bout, métriques à l’appui. alertmanager_notifications_failed_total par intégration dit en une requête si le problème est « rien n’a fire » ou « l’envoi échoue ». Dans mon cas : rien n’avait fire pour ce pod, et les 8 envois réels étaient partis vers le chat configuré — pas celui que je regardais. Vérifiez votre chat_id et ouvrez le bon chat.

3. Le flapping mérite sa propre règle. Ni PodNotReady ni NodeHighLoad ne peuvent l’attraper, par construction. La règle qui manquait :

- alert: PodReadyFlapping
  expr: changes(kube_pod_status_ready{condition="true"}[30m]) > 4
  for: 0m
  labels: { severity: warning }
  annotations:
    summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} flappe ({{ $value }} transitions ready en 30m)"

changes() compte les transitions — c’est la signature du flapping, insensible à la durée des épisodes.

Bilan

Quatre angles morts comblés en une journée, en GitOps (deux apps ArgoCD : victorialogs, alerting, plus le CronJob annotator), sur des Raspberry Pi qui encaissent sans broncher — VictoriaLogs et Vector ajoutent ~150 Mi de RAM au total.

La vraie victoire n’est pas la stack, c’est la boucle : l’instrumentation déployée un soir a rendu visible dès le lendemain un problème qui durait depuis des semaines — et a fourni les métriques pour comprendre pourquoi il était structurellement inalertable. L’observabilité, c’est ça : pas des dashboards, des questions auxquelles on peut enfin répondre.

k3s-clustermonitoringvictoriametricsvictorialogsvectorvmalertalertmanagertelegramgrafanak8s