[EF-05] Alertes de la zone argent vers la zone or #168

Closed
opened 2026-09-07 11:41:59 +00:00 by marvin · 1 comment
Member

Exigence couverte

EF-05, ENF-07

Épreuve servie

EC05 · Data, ETL et BI

Charge estimée

1 j.h

Ce qu'on veut obtenir

Le #165 fait entrer les alertes de la source en zone argent. Ce ticket termine le chemin : silver.alertegold/table=alertepublic.alerte, la seule couche que l'API et Grafana lisent.

Sans lui, public.alerte reste vide, et l'écran Qualité comme le pavé « alertes ouvertes » de l'écran Parc affichent des fixtures le jour du jury. Les PR #155 et #156 en dépendent.

Dépend du #165, qui doit être fusionné avant.

Critères d'acceptation

  • gold/table=alerte/dt=AAAA-MM-JJ est écrite depuis la partition argent du même jour, une ligne par alerte.
  • public.alerte est chargée en upsert ; rejouer la journée ne crée pas de doublon, la clé (site_id, horodatage, type, source) y suffit.
  • Toutes les lignes portent source = 'api_simulation', jamais 'regle' : mélanger les deux interdirait de mesurer nos règles contre le repère de calibrage (migration 0012).
  • taux_de_charge est calculé depuis la capacité que la ligne argent porte déjà, sans jointure au référentiel.
  • Une valeur de type ou de sévérité hors énumération fait échouer le job avant l'écriture, comme le fait gold.agregation.

Comment on le vérifie

Tests      tests/unit/gold/test_alertes_or.py
Commande   pytest tests/unit/gold/test_alertes_or.py -v
Preuve     la sortie du test, et le décompte des lignes chargées en base pour une journée réelle

Le nom du fichier évite d'emblée la collision de modules qui a coûté un renommage au #165 : test_alertes.py est pris par le collecteur, test_alertes_silver.py par la zone argent, et pytest refuse deux modules homonymes tant que les répertoires de test n'ont pas d'__init__.py.

Hors périmètre

Les alertes produites par nos propres règles (source = 'regle'), qui relèvent du #39. La mise en cron, portée par la #159. L'affichage, porté par le #24. Le découpage messagetitre / description qu'attend AlerteOut, qui est une décision d'affichage.

### Exigence couverte EF-05, ENF-07 ### Épreuve servie EC05 · Data, ETL et BI ### Charge estimée 1 j.h ### Ce qu'on veut obtenir Le #165 fait entrer les alertes de la source en zone argent. Ce ticket termine le chemin : `silver.alerte` → `gold/table=alerte` → `public.alerte`, la seule couche que l'API et Grafana lisent. Sans lui, `public.alerte` reste vide, et l'écran Qualité comme le pavé « alertes ouvertes » de l'écran Parc affichent des fixtures le jour du jury. Les PR #155 et #156 en dépendent. Dépend du #165, qui doit être fusionné avant. ### Critères d'acceptation - [ ] `gold/table=alerte/dt=AAAA-MM-JJ` est écrite depuis la partition argent du même jour, une ligne par alerte. - [ ] `public.alerte` est chargée en upsert ; rejouer la journée ne crée pas de doublon, la clé `(site_id, horodatage, type, source)` y suffit. - [ ] Toutes les lignes portent `source = 'api_simulation'`, jamais `'regle'` : mélanger les deux interdirait de mesurer nos règles contre le repère de calibrage (migration 0012). - [ ] `taux_de_charge` est calculé depuis la capacité que la ligne argent porte déjà, sans jointure au référentiel. - [ ] Une valeur de type ou de sévérité hors énumération fait échouer le job avant l'écriture, comme le fait `gold.agregation`. ### Comment on le vérifie ``` Tests tests/unit/gold/test_alertes_or.py Commande pytest tests/unit/gold/test_alertes_or.py -v Preuve la sortie du test, et le décompte des lignes chargées en base pour une journée réelle ``` Le nom du fichier évite d'emblée la collision de modules qui a coûté un renommage au #165 : `test_alertes.py` est pris par le collecteur, `test_alertes_silver.py` par la zone argent, et pytest refuse deux modules homonymes tant que les répertoires de test n'ont pas d'`__init__.py`. ### Hors périmètre Les alertes produites par nos propres règles (`source = 'regle'`), qui relèvent du #39. La mise en cron, portée par la #159. L'affichage, porté par le #24. Le découpage `message` → `titre` / `description` qu'attend `AlerteOut`, qui est une décision d'affichage.
marvin self-assigned this 2026-09-07 11:41:59 +00:00
Author
Member

État : la chaîne est complète de bronze à public.alerte, la demande #169 est ouverte

Branche marvin/168-alertes-zone-or, trois commits, empilée sur le #165 — à recibler sur develop quand la #167 sera fusionnée.

Les critères, un par un

  • gold/table=alerte/dt=… est écrite depuis la partition argent du même jour, une ligne par alerte. etl.gold.alertes projette, il ne recalcule rien. Vérifié sur bronze réel : 2 984 alertes du 2026-09-06 traversent les trois zones sans perte ni doublon.

  • public.alerte est chargée en upsert ; rejouer la journée ne crée pas de doublon. Sur (site_id, horodatage, type, source), la clé unique de la 0012 — et non sur alerte_id, qui est generated always as identity : la base la produit, un insert qui la fournirait serait refusé. Les alertes partent dans la même transaction que la mesure et la qualité.

  • Toutes les lignes portent source = 'api_simulation'. Écrit en dur par la projection, jamais lu d'une colonne argent qui n'existe pas.

  • taux_de_charge est calculé depuis la capacité que la ligne argent porte déjà, sans jointure au référentiel. NULL et non zéro quand la capacité manque ou vaut zéro : un taux indéfini n'est pas un taux nul, et une division par zéro ferait échouer la journée entière pour une ligne de référentiel incomplète.

  • Une valeur hors énumération fait échouer le job avant l'écriture. Doublon délibéré avec le contrôle de la zone argent, comme agregation._controler_enumerations le fait pour la mesure : une partition argent peut venir d'une version antérieure du job ou d'un rattrapage à la main.

Les preuves

$ pytest tests/unit -q
665 passed

$ pytest tests/unit/gold tests/unit/db --cov=services/etl/etl/gold
services/etl/etl/gold/alertes.py       28    0   100%
services/etl/etl/gold/chargement.py    97    6    94%

$ ruff check services packages && ruff format --check services packages
All checks passed!

$ mypy --config-file etl/pyproject.toml etl        # strict
Success: no issues found in 23 source files

Chaîne des trois zones sur bronze réel, en local, sans rien écrire dans MinIO ni en base :

2 984 alertes bronze -> 2 984 lignes argent -> 2 984 lignes or
source      'api_simulation'   (uniforme)
etat        'ouverte'          (uniforme)
libelle     2984/2984 non nuls
cle_bronze  2984/2984 non nuls
types       anomaly, outage, sensor, spike, threshold

Ce que l'écriture a révélé, et qui dépasse le ticket

1. La table cible ne pouvait pas porter ce que l'écran demande. public.alerte a neuf colonnes, AlerteOut en attend dix, et quatre n'avaient aucune correspondance : titre, description, exigence, etat. Dans l'autre sens, la zone argent porte alert_id, message et bronze_key qui n'avaient nulle part où aller — ce dernier étant notable, puisque public.mesure porte sa clé bronze pour l'ENF-07 depuis la 0010.

D'où la migration 0017 : libelle, etat, cle_bronze, plus un index partiel sur les seules alertes ouvertes. titre, description et exigence restent composés par l'API : ce sont des mises en forme, et la 0012 pose déjà la règle à propos du taux de charge. resolue_a attend ce qui résoudra les alertes, comme au #164.

Ce n'est pas une surprise isolée : le #164 est le même ticket pour les recommandations. La zone or a été posée avant que les écrans ne soient contractualisés, et les deux tables d'événements ont le même trou.

2. COPY … PARTITION_BY (dt) avec zéro ligne n'écrit aucun objet. Sans valeur de dt à partitionner, DuckDB ne crée pas de répertoire : une journée qui passerait de N alertes à zéro garderait sa partition or précédente. Théorique — une alerte ne disparaît de bronze qu'avec la rétention de 180 jours — mais réel, et la zone argent ne l'a pas : elle écrit à un chemin nommé. Écrit dans le module et gardé par un cas.

3. Un piège à désamorcer plus tard. etat est dans le do update de l'upsert, sans effet tant que la zone or ne porte que « ouverte ». Le jour où quelque chose résoudra les alertes, cette colonne devra en sortir — sinon un rejeu rouvrirait toutes les alertes fermées de la journée. Un cas de test porte la remarque à l'endroit où elle se lira.

Ce qui reste, hors de ce lot

Rien ne tourne encore en cron : c'est la #159, ouverte et fusionnable. Tant qu'elle n'est pas passée, le seau gold reste vide et public.alerte aussi — vérifié à l'instant, 0 objet.

### État : la chaîne est complète de bronze à `public.alerte`, la demande #169 est ouverte Branche `marvin/168-alertes-zone-or`, trois commits, **empilée sur le #165** — à recibler sur `develop` quand la #167 sera fusionnée. ### Les critères, un par un - [x] **`gold/table=alerte/dt=…` est écrite depuis la partition argent du même jour, une ligne par alerte.** `etl.gold.alertes` projette, il ne recalcule rien. Vérifié sur bronze réel : 2 984 alertes du 2026-09-06 traversent les trois zones sans perte ni doublon. - [x] **`public.alerte` est chargée en upsert ; rejouer la journée ne crée pas de doublon.** Sur `(site_id, horodatage, type, source)`, la clé unique de la 0012 — et non sur `alerte_id`, qui est `generated always as identity` : la base la produit, un insert qui la fournirait serait refusé. Les alertes partent dans la **même transaction** que la mesure et la qualité. - [x] **Toutes les lignes portent `source = 'api_simulation'`.** Écrit en dur par la projection, jamais lu d'une colonne argent qui n'existe pas. - [x] **`taux_de_charge` est calculé depuis la capacité que la ligne argent porte déjà**, sans jointure au référentiel. `NULL` et non zéro quand la capacité manque ou vaut zéro : un taux indéfini n'est pas un taux nul, et une division par zéro ferait échouer la journée entière pour une ligne de référentiel incomplète. - [x] **Une valeur hors énumération fait échouer le job avant l'écriture.** Doublon délibéré avec le contrôle de la zone argent, comme `agregation._controler_enumerations` le fait pour la mesure : une partition argent peut venir d'une version antérieure du job ou d'un rattrapage à la main. ### Les preuves ``` $ pytest tests/unit -q 665 passed $ pytest tests/unit/gold tests/unit/db --cov=services/etl/etl/gold services/etl/etl/gold/alertes.py 28 0 100% services/etl/etl/gold/chargement.py 97 6 94% $ ruff check services packages && ruff format --check services packages All checks passed! $ mypy --config-file etl/pyproject.toml etl # strict Success: no issues found in 23 source files ``` Chaîne des trois zones sur bronze réel, en local, sans rien écrire dans MinIO ni en base : ``` 2 984 alertes bronze -> 2 984 lignes argent -> 2 984 lignes or source 'api_simulation' (uniforme) etat 'ouverte' (uniforme) libelle 2984/2984 non nuls cle_bronze 2984/2984 non nuls types anomaly, outage, sensor, spike, threshold ``` ### Ce que l'écriture a révélé, et qui dépasse le ticket **1. La table cible ne pouvait pas porter ce que l'écran demande.** `public.alerte` a neuf colonnes, `AlerteOut` en attend dix, et quatre n'avaient aucune correspondance : `titre`, `description`, `exigence`, `etat`. Dans l'autre sens, la zone argent porte `alert_id`, `message` et `bronze_key` qui n'avaient nulle part où aller — ce dernier étant notable, puisque `public.mesure` porte sa clé bronze pour l'ENF-07 depuis la 0010. D'où la migration `0017` : `libelle`, `etat`, `cle_bronze`, plus un index partiel sur les seules alertes ouvertes. `titre`, `description` et `exigence` restent **composés par l'API** : ce sont des mises en forme, et la 0012 pose déjà la règle à propos du taux de charge. `resolue_a` attend ce qui résoudra les alertes, comme au #164. Ce n'est pas une surprise isolée : le **#164** est le même ticket pour les recommandations. La zone or a été posée avant que les écrans ne soient contractualisés, et les deux tables d'événements ont le même trou. **2. `COPY … PARTITION_BY (dt)` avec zéro ligne n'écrit aucun objet.** Sans valeur de `dt` à partitionner, DuckDB ne crée pas de répertoire : une journée qui passerait de N alertes à zéro garderait sa partition or précédente. Théorique — une alerte ne disparaît de bronze qu'avec la rétention de 180 jours — mais réel, et la zone argent ne l'a pas : elle écrit à un chemin nommé. Écrit dans le module et gardé par un cas. **3. Un piège à désamorcer plus tard.** `etat` est dans le `do update` de l'upsert, sans effet tant que la zone or ne porte que « ouverte ». Le jour où quelque chose résoudra les alertes, cette colonne devra en **sortir** — sinon un rejeu rouvrirait toutes les alertes fermées de la journée. Un cas de test porte la remarque à l'endroit où elle se lira. ### Ce qui reste, hors de ce lot Rien ne tourne encore en cron : c'est la **#159**, ouverte et fusionnable. Tant qu'elle n'est pas passée, le seau `gold` reste vide et `public.alerte` aussi — vérifié à l'instant, 0 objet.
Sign in to join this conversation.
No project
No assignees
1 participant
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#168
No description provided.