supervision : ce que l'ETL transforme, pas seulement qu'il tourne (#244) #245

Merged
lenaic merged 2 commits from gabriel/244-supervision-etl into develop 2026-09-09 12:53:58 +00:00
Member

Ferme le #244.

Le trou

Depuis le #228, les trois passes de l'ETL publient leur état : code de retour, durée, dernière réussite. Elles ne publient pas ce qu'elles ont fait. Une passe argent qui rend 0 sans rien écrire ressemble, en supervision, trait pour trait à une passe qui a traité 5 040 lignes. Et quand la chaîne décroche, chaine-donnee montre la zone or vieillir sans dire lequel des trois maillons a lâché : le diagnostic se faisait en tail sur silver.log.

Grafana ne peut pas aller chercher ces volumes lui-même — l'argent et l'or vivent dans DuckDB et MinIO, que Grafana ne lit jamais (§ 7 des exigences collectives). Ils sont donc publiés par les passes, par le mécanisme textfile déjà en place.

Ce que ça ajoute

Trois métriques étiquetées par zone du médaillon (argent, or, base) :

Métrique Ce qu'elle dit
ev_etl_lignes_ecrites{zone} volume écrit par la dernière passe réussie
ev_etl_jour_traite_timestamp_seconds{zone} la journée de donnée traitée, minuit UTC
ev_etl_dernier_refus_timestamp_seconds{zone} date du dernier refus de journée

La deuxième attrape le mode de panne le plus sournois de la chaîne : load-postgres peut réussir tous les quarts d'heure en rechargeant indéfiniment la même journée d'hier, parce que la zone argent ne lui en propose plus de nouvelle. Code 0, dernière réussite fraîche, alerte muette, et la base gèle en silence.

Plus le tableau de bord etl.json, dix panneaux, dont « Où la donnée s'est arrêtée » : si l'argent est à jour et que l'or et la base traînent d'une journée, le maillon cassé est l'or, et c'est gold.log qu'on ouvre — pas les trois.

Le retard par zone ne demande aucune métrique nouvelle : ev_ops_tache_derniere_reussite_timestamp_seconds du #228 le porte déjà pour les trois tâches. Ce qui manquait, c'était de les mettre côte à côte.

Les deux points à regarder en revue

1. Le répertoire textfile garde UN SEUL écrivain. Le job Python dépose un résumé avec --resume, le lanceur le publie. node-exporter concatène tout le répertoire : une seule ligne mal formée y fait échouer la collecte entière, donc emporterait aussi les métriques des six autres tâches. D'où trois précautions cumulées — le résumé ne porte que des entiers (une journée y est un horodatage epoch, jamais une date ISO), le lanceur les refiltre sur [0-9]+, et l'étiquette zone vient du littéral du lanceur, jamais du fichier. Le banc fait passer un résumé hostile au mécanisme et vérifie qu'il ne publie rien.

2. Le refus de journée de la zone argent passe du code 1 au code 4. C'est le seul changement de comportement public de ce lot. Le 4 est celui que le contrat des codes de sortie réserve déjà à « journée refusée » et que la zone or rendait seule ; l'écart était connu, et docs/runbooks/etl.md devait en faire un avertissement (« Attention au refus de la zone argent : il vaut 1, pas 4 »), tandis que services/etl/README.md demandait explicitement de l'aligner « dans un lot qui touchera les deux ». Un refus se corrige en regardant la source, une panne en regardant le job : le code doit permettre de trancher sans ouvrir le journal, ce qu'il ne permettait pas tant qu'il se confondait avec le 1 générique d'une trace Python. Aucun test ne fixait l'ancien code ; les deux documents sont corrigés.

Vérifications jouées

  • pytest tests/unit/etl tests/unit/silver tests/unit/gold → 325 passent.
  • bash tests/ci/test-supervision.sh → tout passe, dont trois cas qui exécutent le mécanisme au lieu de le lire par grep : un résumé bien formé devient une métrique, un résumé aux valeurs non entières n'est pas publié, un répertoire absent ne demande rien au job.
  • Les trois lanceurs joués avec un faux Python : passe réussie (résumé publié puis nettoyé, code préservé), journée refusée (code 4, refus publié, volume laissé intact plutôt qu'écrasé par un zéro — un zéro se lirait comme une journée vide légitime), poste sans /var/log/enervision (code inchangé, rien d'écrit, aucune plainte). La branche flock, celle qui tourne en production, éprouvée avec un flock de substitution : macOS n'en a pas.
  • ruff check + format --check et mypy --strict sur etl : propres.

Preuve à joindre après déploiement

curl -s 127.0.0.1:9100/metrics | grep '^ev_etl_'

et une capture du tableau de bord etl pendant que la zone or est volontairement en retard d'un quart d'heure.

Ferme le #244. ## Le trou Depuis le #228, les trois passes de l'ETL publient leur **état** : code de retour, durée, dernière réussite. Elles ne publient pas **ce qu'elles ont fait**. Une passe argent qui rend 0 sans rien écrire ressemble, en supervision, trait pour trait à une passe qui a traité 5 040 lignes. Et quand la chaîne décroche, `chaine-donnee` montre la zone or vieillir sans dire lequel des trois maillons a lâché : le diagnostic se faisait en `tail` sur `silver.log`. Grafana ne peut pas aller chercher ces volumes lui-même — l'argent et l'or vivent dans DuckDB et MinIO, que Grafana ne lit jamais (§ 7 des exigences collectives). Ils sont donc publiés par les passes, par le mécanisme textfile déjà en place. ## Ce que ça ajoute Trois métriques étiquetées par **zone du médaillon** (`argent`, `or`, `base`) : | Métrique | Ce qu'elle dit | |---|---| | `ev_etl_lignes_ecrites{zone}` | volume écrit par la dernière passe réussie | | `ev_etl_jour_traite_timestamp_seconds{zone}` | la journée de **donnée** traitée, minuit UTC | | `ev_etl_dernier_refus_timestamp_seconds{zone}` | date du dernier **refus** de journée | La deuxième attrape le mode de panne le plus sournois de la chaîne : `load-postgres` peut réussir tous les quarts d'heure en rechargeant indéfiniment la même journée d'hier, parce que la zone argent ne lui en propose plus de nouvelle. Code 0, dernière réussite fraîche, alerte muette, et la base gèle en silence. Plus le tableau de bord `etl.json`, dix panneaux, dont « Où la donnée s'est arrêtée » : si l'argent est à jour et que l'or et la base traînent d'une journée, le maillon cassé est l'or, et c'est `gold.log` qu'on ouvre — pas les trois. **Le retard par zone ne demande aucune métrique nouvelle** : `ev_ops_tache_derniere_reussite_timestamp_seconds` du #228 le porte déjà pour les trois tâches. Ce qui manquait, c'était de les mettre côte à côte. ## Les deux points à regarder en revue **1. Le répertoire textfile garde UN SEUL écrivain.** Le job Python dépose un résumé avec `--resume`, le lanceur le publie. node-exporter concatène tout le répertoire : une seule ligne mal formée y fait échouer la collecte **entière**, donc emporterait aussi les métriques des six autres tâches. D'où trois précautions cumulées — le résumé ne porte que des entiers (une journée y est un horodatage epoch, jamais une date ISO), le lanceur les refiltre sur `[0-9]+`, et l'étiquette `zone` vient du littéral du lanceur, jamais du fichier. Le banc fait passer un résumé hostile au mécanisme et vérifie qu'il ne publie rien. **2. Le refus de journée de la zone argent passe du code 1 au code 4.** C'est le seul changement de comportement public de ce lot. Le 4 est celui que le contrat des codes de sortie réserve déjà à « journée refusée » et que la zone or rendait seule ; l'écart était connu, et `docs/runbooks/etl.md` devait en faire un avertissement (« Attention au refus de la zone argent : il vaut 1, pas 4 »), tandis que `services/etl/README.md` demandait explicitement de l'aligner « dans un lot qui touchera les deux ». Un refus se corrige en regardant la **source**, une panne en regardant le **job** : le code doit permettre de trancher sans ouvrir le journal, ce qu'il ne permettait pas tant qu'il se confondait avec le 1 générique d'une trace Python. Aucun test ne fixait l'ancien code ; les deux documents sont corrigés. ## Vérifications jouées - `pytest tests/unit/etl tests/unit/silver tests/unit/gold` → 325 passent. - `bash tests/ci/test-supervision.sh` → tout passe, dont trois cas qui **exécutent** le mécanisme au lieu de le lire par `grep` : un résumé bien formé devient une métrique, un résumé aux valeurs non entières n'est pas publié, un répertoire absent ne demande rien au job. - Les trois lanceurs joués avec un faux Python : passe réussie (résumé publié puis nettoyé, code préservé), journée refusée (code 4, refus publié, **volume laissé intact plutôt qu'écrasé par un zéro** — un zéro se lirait comme une journée vide légitime), poste sans `/var/log/enervision` (code inchangé, rien d'écrit, aucune plainte). La branche `flock`, celle qui tourne en production, éprouvée avec un `flock` de substitution : macOS n'en a pas. - `ruff check` + `format --check` et `mypy --strict` sur `etl` : propres. ## Preuve à joindre après déploiement ``` curl -s 127.0.0.1:9100/metrics | grep '^ev_etl_' ``` et une capture du tableau de bord `etl` pendant que la zone or est volontairement en retard d'un quart d'heure.
gabriel self-assigned this 2026-09-09 12:51:50 +00:00
supervision: ce que l'ETL transforme, pas seulement qu'il tourne (#244)
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 46s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 18s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m57s
4f0e4dd269
Depuis le #228, les trois passes de l'ETL publient leur etat : code de
retour, duree, derniere reussite. Elles ne publient pas ce qu'elles ont
fait. Une passe argent qui rend 0 sans rien ecrire ressemble, en
supervision, trait pour trait a une passe qui a traite 5 040 lignes, et
quand la chaine decroche le tableau de bord montre la zone or vieillir
sans dire lequel des trois maillons a lache. Le diagnostic se faisait en
tail sur silver.log.

Trois metriques de plus, etiquetees par zone du medaillon : le volume
ecrit, la journee de donnee traitee, la date du dernier refus. La
troisieme attrape le mode de panne le plus sournois de la chaine :
load-postgres peut reussir tous les quarts d'heure en rechargeant
indefiniment la meme journee d'hier, code 0 et derniere reussite fraiche,
pendant que la base gele.

Le job Python depose un resume avec --resume, le lanceur le publie. Le
repertoire textfile garde ainsi un seul ecrivain : node-exporter le
concatene, et une seule ligne mal formee y ferait echouer la collecte
entiere, donc emporterait aussi les metriques des six autres taches. Le
resume ne porte que des entiers, filtres une seconde fois par le lanceur,
et l'etiquette zone vient du lanceur, jamais du fichier.

Le refus de journee de la zone argent passe du code 1 au code 4, celui que
le contrat des codes de sortie reserve deja a ce cas et que la zone or
rendait seule. Un refus se corrige en regardant la source, une panne en
regardant le job : le code doit permettre de trancher sans ouvrir le
journal. L'ecart etait connu et documente comme un piege dans le manuel.

Sur un poste sans /var/log/enervision, rien n'est demande au job, rien
n'est publie, et les trois passes rendent le meme code qu'avant.
lenaic approved these changes 2026-09-09 12:53:47 +00:00
Merge branch 'develop' into gabriel/244-supervision-etl
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 45s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 23s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m9s
c708219ee8
lenaic merged commit 7361775134 into develop 2026-09-09 12:53:58 +00:00
lenaic deleted branch gabriel/244-supervision-etl 2026-09-09 12:53:58 +00:00
Sign in to join this conversation.
No reviewers
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision!245
No description provided.