Le défaut, et pourquoi une règle de plus ne le corrige pas
Du 2026-09-04 02:41 au 2026-09-07 07:37, Prometheus a scruté ses 23 cibles et jeté le résultat (WAL en lecture seule). Les 11 règles d'alerte n'ont rien dit : elles vivent dans ce Prometheus, elles évaluent des requêtes contre le TSDB qui ne recevait plus rien, et sans échantillon frais une règle ne se déclenche pas — elle se tait. Sur Telegram, ce silence est indiscernable du bon fonctionnement.
⚠⚠ Ajouter une règle « Prometheus va mal » serait le gate incapable d'échouer : muette exactement quand il faudrait qu'elle parle.
Ce que la PR pose
prometheus/veilleur/veilleur.py + un CronJob */5 (templates/veilleur.yaml, rendu tel quel par ArgoCD comme vault-telegram.yaml). Il interroge Prometheus depuis dehors et pousse lui-même sur Telegram, sans passer par Alertmanager. Jeton et salon viennent du Secret alertmanager-telegramdéjà synchronisé depuis Vault — aucun second chemin de secret, aucun jeton en clair.
Le témoin toujours allumé Veilleur (expr: vector(1)) et sa route vers un récepteur vide : il ne sonne jamais, il est là pour être constaté dans /api/v2/alerts par le guetteur. ⚠ Il ne lit aucune série : il aurait continué de tirer pendant les 3 j 5 h. Il prouve la chaîne d'alerte, jamais le TSDB.
VeilleurMuet, l'auto-plainte : le guetteur pousse un battement au Pushgateway ; la règle crie quand il vieillit de plus de 20 min.
⚠⚠ Le piège, écrit dans le code là où on voudra simplifier
Pendant la panne : /-/healthy → 200, /api/v1/targets → 23 cibles presque toutes up, le pod → Running 2/2, 0 redémarrage, Longhorn → healthy… et count(up) → aucune série.
Un veilleur branché sur /-/healthyn'aurait rien vu. Le vecteur vide EST l'alarme — et il l'est structurellement : --query.lookback-delta vaut 5 min, donc au-delà de 5 min de retard up ne rend plus rien ; la branche « âge > seuil » ne peut mesurer qu'entre 0 et 5 min, jamais une panne de trois jours.
La propriété gardée
Si Prometheus cesse d'enregistrer, une notification arrive en ≤ 8 min 10 s.
terme
valeur
d'où
seuil de fraîcheur
180 s
3 × scrape_interval: 1m
période du CronJob
300 s
*/5, pire cas : on vient de manquer un tour
tolérance au roulement
180 s
2 × 90 s — un redéploiement ne réveille personne
exécution
~7 s
mesuré
au pire
487 s
contre 4 620 min de silence réel
Mesuré deux fois, cas (c) armé dans le CronJob déployé, sur la planification réelle : du top au « Telegram a accepté », 187 s (08:20:00 → 08:23:07, puis 08:30:02 → 08:33:09). Les 300 s d'attente du tour suivant ne sont pas mesurées — elles dépendent de la phase à laquelle la panne commence, et sont bornées par le schedule.
Il rejoue les réponses HTTP mesurées pendant l'incident. ⚠ Il ne rejoue pas la couche ext4/Longhorn : il rejoue la surface que la panne présentait.
sabotage
code
ce qui rougit
ce qui reste vert
(aucun)
0
—
8/8
S-A : on ignore le vecteur vide dans observer()
1
cas (c), auto-plainte
le silence nominal
S-B : age > -1 — il crie tout le temps
1
le silence nominal, la tolérance
les 6 alarmes
S-C : route Veilleur → telegram au lieu de neant
1
l'assertion jq de la CI
—
promtool : exp_alerts: [] sur le témoin
1
le test de règle
—
⚠ La première tentative de S-A n'a pas mordu comme prévu : elle rougissait sur une erreur de schéma YAML, pas sur l'assertion. Refaite pour être un vrai échec d'assertion.
Sabotages — en vrai, sur le cluster (vrais Jobs, vrai Telegram)
#
mode
comment
verdict
code
délai
L6
nominal
rien
SE TAIT, 0 message
0
18,7 s
L1
(a) Prometheus mort
scale deploy prometheus-server --replicas=0
crie « injoignable »
0
50 s
L4
(a) variante simple
URL vers un hôte inexistant
crie « injoignable »
0
8,4 s
L3
(b) données périmées
requête rendant 277 500 s
crie « retarde », « 3 j 5 h »
0
11,5 s
L2
(c) le défaut réel
sélecteur vide sur un Prometheus sain
crie « n'enregistre RIEN »
0
31,5 s
L5a
témoin présent
ALERTE_TEMOIN = une alerte vraiment active
SE TAIT
0
8,6 s
L5b
témoin absent
ALERTE_TEMOIN = un nom inexistant
crie « chaîne muette »
0
8,4 s
L7
auto-plainte
Telegram injoignable + cas (c)
Job Failed, aucun battement poussé
2
12,6 s
Le message de L2 portait la contradiction elle-même : « 23 cibles actives, 21 annoncées up » en face de zéro série.
⚠ Ce que L1 ne rejoue pas : descendre le déploiement à 0 rend Prometheus injoignable, pas menteur. Le défaut réel — répondre parfaitement en n'enregistrant rien — n'est pas reproductible par un scale ; il l'est par L2, contre un Prometheus en pleine santé.
⚠⚠ La gate était câblée à un job qui ne pouvait PAS passer
Le premier run (6722) a rendu la démonstration en direct : le job Application charts prometheus — le tout premier depuis que #32 a énuméré les charts — est mort en 7 s à helm_install_dependencies, curlcode 6, et le banc du veilleur en est sorti Skipped. Ni rouge, ni absent, rien ne dépasse : la forme même du gate qui n'existe pas.
Relevé posé, puis mesuré (run 6728) :
GITHUB_SERVER_URL=http://192.168.65.254:43000
--- gitea.arcodange.lab ---
PAS DE RÉSOLUTION (getent code 2)
curl: (6) Could not resolve host: gitea.arcodange.lab
--- prometheus-community.github.io ---
2606:50c0:8000::153 … http=404
Le runner ne résout pas le nom de la forge interne.Sept charts sur dix déclarent le registre Helm interne en dépendance — crowdsec, grafana, hashicorp-vault, pgbouncer, pgcat, prometheus, redis : depuis #32, leurs jobs mouraient tous à la même seconde, quel que soit leur contenu. Trou d'infrastructure rendu visible par #32, pas défaut d'un lot.
Deux corrections, toutes deux minimales :
le banc remonte juste après le checkout — il n'a besoin ni du réseau ni du registre interne, donc il rend un verdict quoi qu'il arrive ensuite ;
repli sur GITHUB_SERVER_URL pour le registre interne, essayé seulement après avoir constaté que le nom d'origine ne répond pas, avec réécriture des URLs de l'index seulement en repli (vérifié en local sur un poste qui résout : aucun repli, code 0, deux dépendances téléchargées). Lecture anonyme du registre vérifiée (HTTP 200) : aucun jeton n'entre là. ⚠ Piège évité : [[ … ]] && … sous set -eavorte le script quand la condition est fausse — c'est un if.
Run 6729 — Application charts prometheus : success en 53 s, toutes ses étapes vertes. Ran 8 tests … OK, repli journalisé (⚠ https://gitea.arcodange.lab/… injoignable — repli sur http://192.168.65.254:43000/…), helm template → 27 objets, dont le CronJob et le ConfigMap de veilleur.yaml. C'est la première fois que ce job est vert. Statut combiné de la PR : success, 11 contextes présents (2 verts, 9 Skipped — les charts non touchés).
Prometheus après les essais
Pod prêt, 0 redémarrage. Montage rw, 0 « input/output error », 0level=ERROR. count(up) = 23, count(count_over_time(up[30m])) = 23 (aucune série perdue par l'essai L1), fraîcheur du dernier point 1,6 s, kadans_jobs_jobs = 8 séries.
⚠ Effet de bord assumé et non réparé : le pod a été replanifié de pi3 vers pi1. Le commentaire du groupe homelab de ce même fichier affirme « Prometheus vit sur pi3 et Alertmanager sur pi2 : cette chaîne d'alerte survit donc à la perte de pi1 ». Ce n'est plus vrai. Un kubectl -n tools delete pod le rejoue, sans garantie d'atterrissage. Le veilleur, lui, est un CronJob : il n'est lié à aucun nœud.
Ce que ce lot ne couvre pas
La cause du passage en lecture seule reste inconnue (#36) : ce lot surveille le symptôme, pas le disque.
« Le veilleur en panne ET Prometheus en panne » : les deux moitiés de l'auto-plainte finissent sur Telegram, donc un Telegram cassé les emporte toutes les deux. Il faudrait un second canal, indépendant de ce homelab.
Le témoin Veilleur n'a pas été éprouvé EN VIVANT : sa règle n'est pas déployée (elle arrive avec cette PR), et l'installer à la main aurait voulu dire patcher deux ConfigMaps qu'ArgoCD possède, sur un Prometheus réparé deux heures plus tôt. Il est prouvé par promtool test rules (il tire avec un TSDB vide, là où NoeudInjoignable se tait — le silence de trois jours, reproduit) et par amtool config routes test (Veilleur → neant, NoeudInjoignable → telegram). La lecture du témoin, elle, est éprouvée en vivant contre le vrai Alertmanager (L5a/L5b).
prometheus/.helmignore est ajouté au passage : l'étape de publication fait tar -X <chart>/.helmignore, qui échoue sur un chart qui n'en a pas (vérifié : code 1 sans, 0 avec). grafana et redis sont dans le même cas — pas traités ici.
Remise en état
Tous les objets d'essai ont été retirés du cluster (8 Jobs, le Job planifié, le CronJob, son ConfigMap, le groupe Pushgateway, les fichiers écrits sur le volume pour promtool) — vérifié, il ne reste rien. Le veilleur n'est donc pas en service : c'est la fusion de cette PR qui le déploiera, par ArgoCD. Rien n'a été appliqué avec Tofu, aucun workflow Vault n'a été déclenché.
⚠ À la fusion, VeilleurMuet tirera une fois (absent(...)) tant que le premier tour du CronJob n'aura pas poussé son battement — au plus 5 min.
Closes #36
## Le défaut, et pourquoi une règle de plus ne le corrige pas
Du 2026-09-04 02:41 au 2026-09-07 07:37, Prometheus a scruté ses 23 cibles et **jeté** le résultat (WAL en lecture seule). Les 11 règles d'alerte n'ont rien dit : elles vivent **dans** ce Prometheus, elles évaluent des requêtes contre le TSDB qui ne recevait plus rien, et sans échantillon frais une règle ne se déclenche pas — **elle se tait**. Sur Telegram, ce silence est indiscernable du bon fonctionnement.
⚠⚠ Ajouter une règle « Prometheus va mal » serait le gate incapable d'échouer : muette exactement quand il faudrait qu'elle parle.
## Ce que la PR pose
1. **`prometheus/veilleur/veilleur.py` + un CronJob `*/5`** (`templates/veilleur.yaml`, rendu tel quel par ArgoCD comme `vault-telegram.yaml`). Il interroge Prometheus **depuis dehors** et **pousse lui-même sur Telegram, sans passer par Alertmanager**. Jeton et salon viennent du Secret `alertmanager-telegram` **déjà** synchronisé depuis Vault — aucun second chemin de secret, aucun jeton en clair.
2. **Le témoin toujours allumé `Veilleur`** (`expr: vector(1)`) et sa route vers un **récepteur vide** : il ne sonne jamais, il est là pour être *constaté* dans `/api/v2/alerts` par le guetteur. ⚠ Il ne lit aucune série : il aurait continué de tirer pendant les 3 j 5 h. Il prouve la chaîne d'alerte, **jamais** le TSDB.
3. **`VeilleurMuet`**, l'auto-plainte : le guetteur pousse un battement au Pushgateway ; la règle crie quand il vieillit de plus de 20 min.
### ⚠⚠ Le piège, écrit dans le code là où on voudra simplifier
Pendant la panne : `/-/healthy` → **200**, `/api/v1/targets` → **23 cibles presque toutes `up`**, le pod → **Running 2/2, 0 redémarrage**, Longhorn → **`healthy`**… et `count(up)` → **aucune série**.
Un veilleur branché sur `/-/healthy` **n'aurait rien vu**. **Le vecteur vide EST l'alarme** — et il l'est structurellement : `--query.lookback-delta` vaut 5 min, donc au-delà de 5 min de retard `up` ne rend plus rien ; la branche « âge > seuil » ne peut mesurer qu'entre 0 et 5 min, jamais une panne de trois jours.
## La propriété gardée
> **Si Prometheus cesse d'enregistrer, une notification arrive en ≤ 8 min 10 s.**
| terme | valeur | d'où |
|---|---|---|
| seuil de fraîcheur | 180 s | 3 × `scrape_interval: 1m` |
| période du CronJob | 300 s | `*/5`, pire cas : on vient de manquer un tour |
| tolérance au roulement | 180 s | 2 × 90 s — un redéploiement ne réveille personne |
| exécution | ~7 s | mesuré |
| **au pire** | **487 s** | contre **4 620 min** de silence réel |
**Mesuré deux fois**, cas (c) armé dans le CronJob déployé, sur la planification réelle : du top au « Telegram a accepté », **187 s** (08:20:00 → 08:23:07, puis 08:30:02 → 08:33:09). Les 300 s d'attente du tour suivant ne sont pas mesurées — elles dépendent de la phase à laquelle la panne commence, et sont bornées par le `schedule`.
## Sabotages — le banc unitaire (`python3 -m unittest discover -s prometheus/veilleur`)
Il rejoue les réponses HTTP **mesurées** pendant l'incident. ⚠ Il ne rejoue **pas** la couche ext4/Longhorn : il rejoue la **surface** que la panne présentait.
| sabotage | code | ce qui rougit | ce qui reste vert |
|---|---|---|---|
| (aucun) | **0** | — | 8/8 |
| S-A : on ignore le vecteur vide dans `observer()` | **1** | cas (c), auto-plainte | le silence nominal |
| S-B : `age > -1` — il crie tout le temps | **1** | le silence nominal, la tolérance | les 6 alarmes |
| S-C : route `Veilleur` → `telegram` au lieu de `neant` | **1** | l'assertion `jq` de la CI | — |
| `promtool` : `exp_alerts: []` sur le témoin | **1** | le test de règle | — |
⚠ La **première** tentative de S-A n'a pas mordu comme prévu : elle rougissait sur une erreur de schéma YAML, pas sur l'assertion. Refaite pour être un vrai échec d'assertion.
## Sabotages — en vrai, sur le cluster (vrais Jobs, vrai Telegram)
| # | mode | comment | verdict | code | délai |
|---|---|---|---|---|---|
| L6 | nominal | rien | **SE TAIT**, 0 message | 0 | 18,7 s |
| L1 | (a) Prometheus **mort** | `scale deploy prometheus-server --replicas=0` | crie « injoignable » | 0 | **50 s** |
| L4 | (a) variante simple | URL vers un hôte inexistant | crie « injoignable » | 0 | 8,4 s |
| L3 | (b) données périmées | requête rendant 277 500 s | crie « retarde », « 3 j 5 h » | 0 | 11,5 s |
| L2 | **(c) le défaut réel** | sélecteur vide sur un Prometheus **sain** | crie « n'enregistre RIEN » | 0 | 31,5 s |
| L5a | témoin présent | `ALERTE_TEMOIN` = une alerte vraiment active | **SE TAIT** | 0 | 8,6 s |
| L5b | témoin absent | `ALERTE_TEMOIN` = un nom inexistant | crie « chaîne muette » | 0 | 8,4 s |
| L7 | auto-plainte | Telegram injoignable + cas (c) | Job **Failed**, aucun battement poussé | **2** | 12,6 s |
Le message de L2 portait la contradiction elle-même : **« 23 cibles actives, 21 annoncées `up` »** en face de zéro série.
⚠ **Ce que L1 ne rejoue pas** : descendre le déploiement à 0 rend Prometheus *injoignable*, pas *menteur*. Le défaut réel — répondre parfaitement en n'enregistrant rien — n'est pas reproductible par un `scale` ; il l'est par L2, contre un Prometheus en pleine santé.
## ⚠⚠ La gate était câblée à un job qui ne pouvait PAS passer
Le premier run (6722) a rendu la démonstration en direct : le job **`Application charts prometheus` — le tout premier depuis que #32 a énuméré les charts —** est mort en **7 s** à `helm_install_dependencies`, `curl` **code 6**, et le banc du veilleur en est sorti **`Skipped`**. Ni rouge, ni absent, rien ne dépasse : la forme même du gate qui n'existe pas.
Relevé posé, puis mesuré (run 6728) :
```
GITHUB_SERVER_URL=http://192.168.65.254:43000
--- gitea.arcodange.lab ---
PAS DE RÉSOLUTION (getent code 2)
curl: (6) Could not resolve host: gitea.arcodange.lab
--- prometheus-community.github.io ---
2606:50c0:8000::153 … http=404
```
**Le runner ne résout pas le nom de la forge interne.** **Sept charts sur dix** déclarent le registre Helm interne en dépendance — `crowdsec`, `grafana`, `hashicorp-vault`, `pgbouncer`, `pgcat`, `prometheus`, `redis` : depuis #32, leurs jobs mouraient tous à la même seconde, quel que soit leur contenu. **Trou d'infrastructure rendu visible par #32, pas défaut d'un lot.**
Deux corrections, toutes deux minimales :
- **le banc remonte juste après le checkout** — il n'a besoin ni du réseau ni du registre interne, donc il rend un verdict quoi qu'il arrive ensuite ;
- **repli sur `GITHUB_SERVER_URL`** pour le registre interne, essayé **seulement après** avoir constaté que le nom d'origine ne répond pas, avec réécriture des URLs de l'index **seulement en repli** (vérifié en local sur un poste qui résout : aucun repli, code 0, deux dépendances téléchargées). Lecture anonyme du registre vérifiée (HTTP 200) : aucun jeton n'entre là. ⚠ Piège évité : `[[ … ]] && …` sous `set -e` **avorte** le script quand la condition est fausse — c'est un `if`.
**Run 6729 — `Application charts prometheus` : `success` en 53 s**, toutes ses étapes vertes. `Ran 8 tests … OK`, repli journalisé (`⚠ https://gitea.arcodange.lab/… injoignable — repli sur http://192.168.65.254:43000/…`), `helm template` → **27 objets**, dont le `CronJob` et le `ConfigMap` de `veilleur.yaml`. **C'est la première fois que ce job est vert.** Statut combiné de la PR : `success`, **11 contextes présents** (2 verts, 9 `Skipped` — les charts non touchés).
## Prometheus après les essais
Pod prêt, `0` redémarrage. Montage **`rw`**, **0** « input/output error », **0** `level=ERROR`. `count(up)` = **23**, `count(count_over_time(up[30m]))` = **23** (aucune série perdue par l'essai L1), fraîcheur du dernier point **1,6 s**, `kadans_jobs_jobs` = **8 séries**.
⚠ **Effet de bord assumé et non réparé** : le pod a été replanifié de **pi3 vers pi1**. Le commentaire du groupe `homelab` de ce même fichier affirme « Prometheus vit sur pi3 et Alertmanager sur pi2 : cette chaîne d'alerte survit donc à la perte de pi1 ». **Ce n'est plus vrai.** Un `kubectl -n tools delete pod` le rejoue, sans garantie d'atterrissage. Le veilleur, lui, est un CronJob : il n'est lié à aucun nœud.
## Ce que ce lot ne couvre pas
- **La cause** du passage en lecture seule reste inconnue (#36) : ce lot surveille le symptôme, pas le disque.
- **« Le veilleur en panne ET Prometheus en panne »** : les deux moitiés de l'auto-plainte finissent sur Telegram, donc un Telegram cassé les emporte toutes les deux. Il faudrait un second canal, indépendant de ce homelab.
- **Le témoin `Veilleur` n'a pas été éprouvé EN VIVANT** : sa règle n'est pas déployée (elle arrive avec cette PR), et l'installer à la main aurait voulu dire patcher deux ConfigMaps qu'ArgoCD possède, sur un Prometheus réparé deux heures plus tôt. Il est prouvé par `promtool test rules` (il tire **avec un TSDB vide**, là où `NoeudInjoignable` se tait — le silence de trois jours, reproduit) et par `amtool config routes test` (`Veilleur` → `neant`, `NoeudInjoignable` → `telegram`). La *lecture* du témoin, elle, est éprouvée en vivant contre le vrai Alertmanager (L5a/L5b).
- `prometheus/.helmignore` est ajouté au passage : l'étape de publication fait `tar -X <chart>/.helmignore`, qui échoue sur un chart qui n'en a pas (vérifié : code 1 sans, 0 avec). **`grafana` et `redis` sont dans le même cas — pas traités ici.**
## Remise en état
Tous les objets d'essai ont été retirés du cluster (8 Jobs, le Job planifié, le CronJob, son ConfigMap, le groupe Pushgateway, les fichiers écrits sur le volume pour `promtool`) — vérifié, il ne reste rien. **Le veilleur n'est donc pas en service : c'est la fusion de cette PR qui le déploiera, par ArgoCD.** Rien n'a été appliqué avec Tofu, aucun workflow Vault n'a été déclenché.
⚠ À la fusion, `VeilleurMuet` tirera une fois (`absent(...)`) tant que le premier tour du CronJob n'aura pas poussé son battement — au plus 5 min.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Du 2026-09-04 02:41 au 2026-09-07 07:37, le WAL de Prometheus était sur un
système de fichiers passé en lecture seule. Pendant 3 j 5 h il a continué de
scruter ses 23 cibles et de JETER le résultat. Les 11 règles d'alerte du dépôt
n'ont rien dit — elles vivent DANS ce Prometheus, elles évaluent des requêtes
contre le TSDB qui ne recevait plus rien, et sans échantillon frais une règle
ne se déclenche pas : elle SE TAIT. Sur Telegram, le silence d'une alerte est
indiscernable du bon fonctionnement.
⚠⚠ AJOUTER UNE RÈGLE « PROMETHEUS VA MAL » NE CORRIGE RIEN : elle serait muette
exactement quand il faudrait qu'elle parle. C'est le gate incapable d'échouer,
celui qui rassure. Ce lot pose donc un guetteur HORS de Prometheus.
Ce qui est livré
----------------
1. `prometheus/veilleur/veilleur.py` + un CronJob `*/5` (templates/veilleur.yaml,
rendu tel quel par ArgoCD comme vault-telegram.yaml). Il interroge Prometheus
depuis dehors et POUSSE LUI-MÊME sur Telegram, sans passer par Alertmanager.
Jeton et salon viennent du Secret `alertmanager-telegram` DÉJÀ synchronisé
depuis Vault — aucun second chemin de secret, aucun jeton en clair.
2. Le témoin toujours allumé `Veilleur` (`expr: vector(1)`) et sa route vers un
récepteur VIDE : il ne sonne jamais, il est là pour être CONSTATÉ dans
/api/v2/alerts par le guetteur. ⚠ Il ne lit aucune série : il aurait continué
de tirer pendant les 3 j 5 h. Il prouve la chaîne d'alerte, jamais le TSDB.
3. `VeilleurMuet`, l'auto-plainte : le guetteur pousse un battement au
Pushgateway ; la règle crie quand il vieillit de plus de 20 min.
⚠⚠ LE PIÈGE, ÉCRIT DANS LE CODE À L'ENDROIT OÙ ON VOUDRA SIMPLIFIER : pendant la
panne, /-/healthy rendait 200, /api/v1/targets annonçait 23 cibles presque
toutes `up`, le pod était Running 2/2 avec 0 redémarrage, Longhorn se disait
`healthy` — et `count(up)` ne rendait AUCUNE SÉRIE. Un veilleur branché sur
/-/healthy n'aurait rien vu ; un veilleur qui lit `resultat[0]` sans regarder
serait tombé dans la branche « rien à signaler ». LE VECTEUR VIDE EST L'ALARME.
Et il l'est structurellement : `--query.lookback-delta` vaut 5 min, donc au-delà
de 5 min de retard `up` ne rend PLUS RIEN — la branche « âge > seuil » ne peut
mesurer qu'entre 0 et 5 min, jamais une panne de trois jours.
La propriété gardée, et son arithmétique
----------------------------------------
« Si Prometheus cesse d'enregistrer, une notification arrive en ≤ 8 min 10 s. »
seuil de fraîcheur 180 s (3 × `scrape_interval: 1m`)
période du CronJob 300 s (*/5, pire cas : on vient de manquer un tour)
tolérance au roulement 180 s (2 × 90 s — un redéploiement ne réveille personne)
exécution ~7 s (mesuré)
------------------------------
au pire 487 s contre 4 620 minutes de silence réel.
Les deux premiers termes ne s'additionnent pas : le seuil est absorbé par le
vecteur vide dès la première observation.
MESURÉ, deux fois, sur la planification RÉELLE et avec le cas (c) armé dans le
CronJob déployé : du top de la planification au « Telegram a accepté »,
**187 s** (08:20:00 → 08:23:07, puis 08:30:02 → 08:33:09). Les 300 s d'attente
du tour suivant ne sont PAS mesurées — elles dépendent de la phase à laquelle la
panne commence ; elles sont bornées par le `schedule`.
SABOTAGES — le banc unitaire (`python3 -m unittest discover -s prometheus/veilleur`)
------------------------------------------------------------------------------------
Il rejoue les réponses HTTP MESURÉES pendant l'incident. ⚠ Ce qu'il ne rejoue
PAS : la couche ext4/Longhorn qui a causé la panne — il rejoue la SURFACE que
la panne présentait, c'est-à-dire tout ce qu'un guetteur extérieur peut voir.
| sabotage | code | ce qui rougit | ce qui reste vert |
|---------------------------------------------------|------|----------------------------------|-------------------|
| (aucun) | 0 | — | 8/8 |
| S-A : on ignore le vecteur vide dans `observer()` | 1 | cas (c), auto-plainte | le silence nominal |
| S-B : `age > -1` — il crie tout le temps | 1 | le silence nominal, la tolérance | les 6 alarmes |
| S-C : route `Veilleur` → telegram au lieu de néant| 1 | l'assertion jq de la CI | — |
| promtool `exp_alerts: []` sur le témoin | 1 | le test de règle | — |
⚠ La première tentative de S-A n'a PAS mordu comme prévu : elle a rougi sur une
erreur de schéma YAML, pas sur l'assertion. Refaite pour être un vrai échec
d'assertion. Un sabotage dont on ne vérifie pas qu'il mord au bon endroit rend
un « rouge » qui ne prouve rien.
⚠⚠ ET LA GATE EST CÂBLÉE : elle tourne dans `.gitea/workflows/helmcharts.yaml`,
dans le job qui construit déjà le chart prometheus, à chaque PR qui le touche.
Une gate que la CI ne déclenche jamais est la cinquième forme du gate qui
rassure.
SABOTAGES — en vrai, sur le cluster (vrais Jobs, vrai Telegram)
---------------------------------------------------------------
| # | mode | comment | verdict | code | délai |
|---|---------------------------|------------------------------------------------|----------------------------------|------|---------|
|L6 | nominal | rien | SE TAIT, 0 message | 0 | 18,7 s |
|L1 | (a) Prometheus MORT | `scale deploy prometheus-server --replicas=0` | crie « injoignable » | 0 | 50 s |
|L4 | (a) variante simple | URL vers un hôte inexistant | crie « injoignable » | 0 | 8,4 s |
|L3 | (b) données périmées | requête rendant 277 500 s | crie « retarde », « 3 j 5 h » | 0 | 11,5 s |
|L2 | (c) LE DÉFAUT RÉEL | sélecteur vide sur un Prometheus SAIN | crie « n'enregistre RIEN » | 0 | 31,5 s |
|L5a| témoin présent | `ALERTE_TEMOIN` = une alerte vraiment active | SE TAIT | 0 | 8,6 s |
|L5b| témoin absent | `ALERTE_TEMOIN` = un nom inexistant | crie « chaîne muette » | 0 | 8,4 s |
|L7 | auto-plainte | Telegram injoignable + cas (c) | Job **Failed**, AUCUN battement | 2 | 12,6 s |
Le message de L2 portait la contradiction elle-même : « 23 cibles actives,
21 annoncées `up` » en face de zéro série — le chiffre exact du matin.
⚠ CE QUE L1 NE REJOUE PAS : descendre le déploiement à 0 rend Prometheus
injoignable, pas MENTEUR. Le défaut réel — répondre parfaitement en
n'enregistrant rien — n'est PAS reproductible par un `scale` ; il l'est par L2,
contre un Prometheus en pleine santé.
Prometheus après les essais
---------------------------
Remonté à 08:13:50Z, prêt à 08:14:08Z (18 s). Montage `rw`, 0 « input/output
error » et 0 `level=ERROR` dans ses journaux, `count(up)` = 23 séries, fraîcheur
du dernier point 0,94 s, `kadans_jobs_jobs` = 8 séries.
⚠ EFFET DE BORD ASSUMÉ ET NON RÉPARÉ : le pod a été replanifié de pi3 vers pi1.
Le commentaire du groupe `homelab` de ce même fichier affirme « Prometheus vit
sur pi3 et Alertmanager sur pi2 : cette chaîne d'alerte survit donc à la perte
de pi1 ». Ce n'est plus vrai. Un `kubectl -n tools delete pod` le rejoue, sans
garantie d'atterrissage. Le veilleur, lui, est un CronJob : il n'est lié à aucun
nœud.
Ce que ce lot ne couvre PAS
---------------------------
- La CAUSE du passage en lecture seule reste inconnue (issue #36) : ce lot
surveille le symptôme, pas le disque.
- « Le veilleur en panne ET Prometheus en panne » : les deux moitiés de
l'auto-plainte finissent sur Telegram, donc un Telegram cassé les emporte
toutes les deux. Il faudrait un second canal, indépendant de ce homelab.
- `prometheus/.helmignore` est ajouté au passage : l'étape de publication du
chart fait `tar -X <chart>/.helmignore`, qui échouerait sur un chart qui n'en
a pas. `grafana` et `redis` sont dans le même cas — pas traités ici.
Refs: arcodange-org/tools#36
Remise en état
--------------
Tous les objets d'essai ont été retirés du cluster (8 Jobs, le Job planifié, le
CronJob, son ConfigMap, le groupe `veilleur-prometheus` du Pushgateway, et les
trois fichiers écrits sur le volume Prometheus pour `promtool`). Le veilleur
n'est donc PAS en service : c'est la fusion de cette PR qui le déploiera, par
ArgoCD. Rien n'a été appliqué avec Tofu, aucun workflow Vault n'a été déclenché.
Le tout premier run du job `Application charts prometheus` — le premier depuis
que #32 a énuméré les charts — est mort à l'étape `helm_install_dependencies`,
`curl` code **6** (hôte non résolu), en **7 s**, AVANT toute étape utile. Le
banc du veilleur en est sorti `Skipped` : ni rouge, ni absent, rien ne dépasse.
C'est la forme même du gate qui n'existe pas.
Deux changements, tous deux minimaux :
1. Le banc du veilleur remonte JUSTE APRÈS le checkout. Il n'a besoin ni du
réseau ni du registre Helm interne (stdlib Python, plus `yq`/`jq` déjà là),
donc il s'exécute et rend un verdict quoi qu'il arrive ensuite.
2. Un RELEVÉ (jamais une gate : `exit 0` en dur) nomme lequel des deux hôtes
tombe. **7 charts sur 10 dépendent du registre interne
`gitea.arcodange.lab`** — crowdsec, grafana, hashicorp-vault, pgbouncer,
pgcat, prometheus, redis. Si c'est bien lui que le runner ne résout pas,
alors sept jobs sur dix ne peuvent PAS passer, quel que soit leur contenu :
un trou d'infrastructure que #32 vient de rendre visible, pas un défaut de
ce lot. À retirer quand le trou est bouché.
Refs: arcodange-org/tools#36
MESURÉ (run 6728, étape « RELEVÉ ») :
GITHUB_SERVER_URL=http://192.168.65.254:43000
--- gitea.arcodange.lab ---
PAS DE RÉSOLUTION (getent code 2)
curl: (6) Could not resolve host: gitea.arcodange.lab
--- prometheus-community.github.io ---
2606:50c0:8000::153 … http=404
Le runner résout parfaitement les registres publics et PAS le nom interne.
SEPT charts sur dix déclarent le registre Helm interne en dépendance —
crowdsec, grafana, hashicorp-vault, pgbouncer, pgcat, prometheus, redis :
depuis que #32 a énuméré les charts, leurs jobs mouraient TOUS à la même
seconde, avant la moindre étape utile. C'est un trou d'infrastructure que #32
a rendu visible, pas un défaut d'un lot.
Le repli n'est essayé QU'APRÈS avoir constaté que le nom d'origine ne répond
pas, et la réécriture des URLs de l'index ne se fait QUE si l'on est
effectivement en repli : sur un runner qui sait résoudre, rien ne change —
vérifié en local, code 0, deux dépendances téléchargées, aucune ligne de repli.
Lecture anonyme du registre vérifiée (HTTP 200) : aucun jeton n'entre ici.
⚠ Piège évité au passage : `[[ … ]] && url=…` sous `set -e` AVORTE le script
quand la condition est fausse. C'est un `if`.
Refs: arcodange-org/tools#36
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #36
Le défaut, et pourquoi une règle de plus ne le corrige pas
Du 2026-09-04 02:41 au 2026-09-07 07:37, Prometheus a scruté ses 23 cibles et jeté le résultat (WAL en lecture seule). Les 11 règles d'alerte n'ont rien dit : elles vivent dans ce Prometheus, elles évaluent des requêtes contre le TSDB qui ne recevait plus rien, et sans échantillon frais une règle ne se déclenche pas — elle se tait. Sur Telegram, ce silence est indiscernable du bon fonctionnement.
⚠⚠ Ajouter une règle « Prometheus va mal » serait le gate incapable d'échouer : muette exactement quand il faudrait qu'elle parle.
Ce que la PR pose
prometheus/veilleur/veilleur.py+ un CronJob*/5(templates/veilleur.yaml, rendu tel quel par ArgoCD commevault-telegram.yaml). Il interroge Prometheus depuis dehors et pousse lui-même sur Telegram, sans passer par Alertmanager. Jeton et salon viennent du Secretalertmanager-telegramdéjà synchronisé depuis Vault — aucun second chemin de secret, aucun jeton en clair.Veilleur(expr: vector(1)) et sa route vers un récepteur vide : il ne sonne jamais, il est là pour être constaté dans/api/v2/alertspar le guetteur. ⚠ Il ne lit aucune série : il aurait continué de tirer pendant les 3 j 5 h. Il prouve la chaîne d'alerte, jamais le TSDB.VeilleurMuet, l'auto-plainte : le guetteur pousse un battement au Pushgateway ; la règle crie quand il vieillit de plus de 20 min.⚠⚠ Le piège, écrit dans le code là où on voudra simplifier
Pendant la panne :
/-/healthy→ 200,/api/v1/targets→ 23 cibles presque toutesup, le pod → Running 2/2, 0 redémarrage, Longhorn →healthy… etcount(up)→ aucune série.Un veilleur branché sur
/-/healthyn'aurait rien vu. Le vecteur vide EST l'alarme — et il l'est structurellement :--query.lookback-deltavaut 5 min, donc au-delà de 5 min de retardupne rend plus rien ; la branche « âge > seuil » ne peut mesurer qu'entre 0 et 5 min, jamais une panne de trois jours.La propriété gardée
scrape_interval: 1m*/5, pire cas : on vient de manquer un tourMesuré deux fois, cas (c) armé dans le CronJob déployé, sur la planification réelle : du top au « Telegram a accepté », 187 s (08:20:00 → 08:23:07, puis 08:30:02 → 08:33:09). Les 300 s d'attente du tour suivant ne sont pas mesurées — elles dépendent de la phase à laquelle la panne commence, et sont bornées par le
schedule.Sabotages — le banc unitaire (
python3 -m unittest discover -s prometheus/veilleur)Il rejoue les réponses HTTP mesurées pendant l'incident. ⚠ Il ne rejoue pas la couche ext4/Longhorn : il rejoue la surface que la panne présentait.
observer()age > -1— il crie tout le tempsVeilleur→telegramau lieu deneantjqde la CIpromtool:exp_alerts: []sur le témoin⚠ La première tentative de S-A n'a pas mordu comme prévu : elle rougissait sur une erreur de schéma YAML, pas sur l'assertion. Refaite pour être un vrai échec d'assertion.
Sabotages — en vrai, sur le cluster (vrais Jobs, vrai Telegram)
scale deploy prometheus-server --replicas=0ALERTE_TEMOIN= une alerte vraiment activeALERTE_TEMOIN= un nom inexistantLe message de L2 portait la contradiction elle-même : « 23 cibles actives, 21 annoncées
up» en face de zéro série.⚠ Ce que L1 ne rejoue pas : descendre le déploiement à 0 rend Prometheus injoignable, pas menteur. Le défaut réel — répondre parfaitement en n'enregistrant rien — n'est pas reproductible par un
scale; il l'est par L2, contre un Prometheus en pleine santé.⚠⚠ La gate était câblée à un job qui ne pouvait PAS passer
Le premier run (6722) a rendu la démonstration en direct : le job
Application charts prometheus— le tout premier depuis que #32 a énuméré les charts — est mort en 7 s àhelm_install_dependencies,curlcode 6, et le banc du veilleur en est sortiSkipped. Ni rouge, ni absent, rien ne dépasse : la forme même du gate qui n'existe pas.Relevé posé, puis mesuré (run 6728) :
Le runner ne résout pas le nom de la forge interne. Sept charts sur dix déclarent le registre Helm interne en dépendance —
crowdsec,grafana,hashicorp-vault,pgbouncer,pgcat,prometheus,redis: depuis #32, leurs jobs mouraient tous à la même seconde, quel que soit leur contenu. Trou d'infrastructure rendu visible par #32, pas défaut d'un lot.Deux corrections, toutes deux minimales :
GITHUB_SERVER_URLpour le registre interne, essayé seulement après avoir constaté que le nom d'origine ne répond pas, avec réécriture des URLs de l'index seulement en repli (vérifié en local sur un poste qui résout : aucun repli, code 0, deux dépendances téléchargées). Lecture anonyme du registre vérifiée (HTTP 200) : aucun jeton n'entre là. ⚠ Piège évité :[[ … ]] && …sousset -eavorte le script quand la condition est fausse — c'est unif.Run 6729 —
Application charts prometheus:successen 53 s, toutes ses étapes vertes.Ran 8 tests … OK, repli journalisé (⚠ https://gitea.arcodange.lab/… injoignable — repli sur http://192.168.65.254:43000/…),helm template→ 27 objets, dont leCronJobet leConfigMapdeveilleur.yaml. C'est la première fois que ce job est vert. Statut combiné de la PR :success, 11 contextes présents (2 verts, 9Skipped— les charts non touchés).Prometheus après les essais
Pod prêt,
0redémarrage. Montagerw, 0 « input/output error », 0level=ERROR.count(up)= 23,count(count_over_time(up[30m]))= 23 (aucune série perdue par l'essai L1), fraîcheur du dernier point 1,6 s,kadans_jobs_jobs= 8 séries.⚠ Effet de bord assumé et non réparé : le pod a été replanifié de pi3 vers pi1. Le commentaire du groupe
homelabde ce même fichier affirme « Prometheus vit sur pi3 et Alertmanager sur pi2 : cette chaîne d'alerte survit donc à la perte de pi1 ». Ce n'est plus vrai. Unkubectl -n tools delete podle rejoue, sans garantie d'atterrissage. Le veilleur, lui, est un CronJob : il n'est lié à aucun nœud.Ce que ce lot ne couvre pas
Veilleurn'a pas été éprouvé EN VIVANT : sa règle n'est pas déployée (elle arrive avec cette PR), et l'installer à la main aurait voulu dire patcher deux ConfigMaps qu'ArgoCD possède, sur un Prometheus réparé deux heures plus tôt. Il est prouvé parpromtool test rules(il tire avec un TSDB vide, là oùNoeudInjoignablese tait — le silence de trois jours, reproduit) et paramtool config routes test(Veilleur→neant,NoeudInjoignable→telegram). La lecture du témoin, elle, est éprouvée en vivant contre le vrai Alertmanager (L5a/L5b).prometheus/.helmignoreest ajouté au passage : l'étape de publication faittar -X <chart>/.helmignore, qui échoue sur un chart qui n'en a pas (vérifié : code 1 sans, 0 avec).grafanaetredissont dans le même cas — pas traités ici.Remise en état
Tous les objets d'essai ont été retirés du cluster (8 Jobs, le Job planifié, le CronJob, son ConfigMap, le groupe Pushgateway, les fichiers écrits sur le volume pour
promtool) — vérifié, il ne reste rien. Le veilleur n'est donc pas en service : c'est la fusion de cette PR qui le déploiera, par ArgoCD. Rien n'a été appliqué avec Tofu, aucun workflow Vault n'a été déclenché.⚠ À la fusion,
VeilleurMuettirera une fois (absent(...)) tant que le premier tour du CronJob n'aura pas poussé son battement — au plus 5 min.🤖 Generated with Claude Code
MESURÉ (run 6728, étape « RELEVÉ ») : GITHUB_SERVER_URL=http://192.168.65.254:43000 --- gitea.arcodange.lab --- PAS DE RÉSOLUTION (getent code 2) curl: (6) Could not resolve host: gitea.arcodange.lab --- prometheus-community.github.io --- 2606:50c0:8000::153 … http=404 Le runner résout parfaitement les registres publics et PAS le nom interne. SEPT charts sur dix déclarent le registre Helm interne en dépendance — crowdsec, grafana, hashicorp-vault, pgbouncer, pgcat, prometheus, redis : depuis que #32 a énuméré les charts, leurs jobs mouraient TOUS à la même seconde, avant la moindre étape utile. C'est un trou d'infrastructure que #32 a rendu visible, pas un défaut d'un lot. Le repli n'est essayé QU'APRÈS avoir constaté que le nom d'origine ne répond pas, et la réécriture des URLs de l'index ne se fait QUE si l'on est effectivement en repli : sur un runner qui sait résoudre, rien ne change — vérifié en local, code 0, deux dépendances téléchargées, aucune ligne de repli. Lecture anonyme du registre vérifiée (HTTP 200) : aucun jeton n'entre ici. ⚠ Piège évité au passage : `[[ … ]] && url=…` sous `set -e` AVORTE le script quand la condition est fausse. C'est un `if`. Refs: arcodange-org/tools#36