fix(backend,ci): repare trois angles morts de la surveillance de derive
Airflow / Construction de l'image (push) Successful in 1m2s
Backend / Analyse statique de sécurité (push) Successful in 7s
Backend / Tests exigeant une base (push) Failing after 5m3s
Airflow / Lint et intégrité des DAGs (push) Successful in 9m41s
ML / Analyse statique de sécurité (push) Successful in 6s
Backend / Lint, typage et tests (push) Successful in 10m10s
Backend / Audit des dépendances (push) Successful in 9m36s
ML / ML - DB et chaîne ML - DB - API (push) Failing after 5m6s
SonarQube / test-ml (push) Failing after 6m6s
ML / Lint, typage et tests (push) Successful in 11m31s
SonarQube / build-front (push) Successful in 9m49s
SonarQube / build-back (push) Successful in 9m54s
SonarQube / test-front (push) Failing after 5m10s
SonarQube / test-back (push) Failing after 5m13s
SonarQube / SonarQube (push) Skipped
Airflow / Construction de l'image (push) Successful in 1m2s
Backend / Analyse statique de sécurité (push) Successful in 7s
Backend / Tests exigeant une base (push) Failing after 5m3s
Airflow / Lint et intégrité des DAGs (push) Successful in 9m41s
ML / Analyse statique de sécurité (push) Successful in 6s
Backend / Lint, typage et tests (push) Successful in 10m10s
Backend / Audit des dépendances (push) Successful in 9m36s
ML / ML - DB et chaîne ML - DB - API (push) Failing after 5m6s
SonarQube / test-ml (push) Failing after 6m6s
ML / Lint, typage et tests (push) Successful in 11m31s
SonarQube / build-front (push) Successful in 9m49s
SonarQube / build-back (push) Successful in 9m54s
SonarQube / test-front (push) Failing after 5m10s
SonarQube / test-back (push) Failing after 5m13s
SonarQube / SonarQube (push) Skipped
Revue de la branche : trois defauts empechaient la surveillance de tenir ce qu'elle annonce. - `evaluate()` gardait les microsecondes de `now()` dans `window_end`, la cle de `uq_drift_report_window`. Deux executions ne collidaient donc jamais et l'index ne dedoublonnait rien, contrairement a ce qu'affirmaient l'ADR 0011, 20-backend et le docstring du DAG. L'instant de reference est desormais tronque a l'heure. - Un site qui cessait d'etre score disparaissait du rapport : la liste des sites ne venait que de la fenetre recente. La panne que cette surveillance existe pour dire etait exactement celle qu'elle taisait. La fenetre de reference entre maintenant dans l'union, et le site recoit sa ligne `indetermine` a zero observation. - Sans fenetre de reference, `_plafond` rendait `None` et le verdict tombait sur `stable`, une affirmation que la donnee ne portait pas. C'est `indetermine` desormais. `ml.yml` ecoute `apps/backend/app/**` et non les seuls modeles : ce workflow est le seul a jouer `-m chaine`, or la chaine traverse les endpoints, les services et les schemas jusqu'a `GET /predictions`. Une PR touchant `predictions.py` ne declenchait pas le test qui l'assert. Hygiene de tests : le nettoyage des fixtures API connait `drift_report` (cle etrangere RESTRICT vers `site`), le test sans rapport rend ses overrides en teardown, `test_chaine_ml_api` compare les `created_at` strictement (un `>=` passait aussi quand l'API resservait la premiere ligne), et `test_data_integration` filtre sur le site seme au lieu de juger tout le contenu d'une fenetre dans une base partagee. Docs remises d'aplomb : sept revisions Alembic et non six, `derive.py` dans l'inventaire de etl/README, et le diagramme de 20-backend gagne DriftService, le depot drift et sa treizieme table.
This commit is contained in:
@@ -81,14 +81,22 @@ modèle change n'est pas une dérive, c'est une régression de réentraînement.
|
||||
| Écrire le résultat dans `alert` | `ck_alert_source` et `ck_alert_type` bornent les valeurs autorisées, `alert.site_id` est `NOT NULL` et n'accueillerait donc pas la ligne globale, et toute alerte est ensuite relue par le moteur de recommandations, qui devrait apprendre une règle qui ne le concerne pas (ADR 0006). |
|
||||
| Une jauge Prometheus | `monitoring/` ne contient que des `.gitkeep` et aucun collecteur ne lit `/metrics` : une jauge que personne ne scrute n'est pas une preuve. Le calcul est de surcroît un traitement par lot, pas le processus qui sert l'API : la jauge disparaîtrait avec lui. |
|
||||
| Ne rien persister, journaliser seulement | La question posée à un jury est « comment savez-vous que le modèle se dégrade ? ». La réponse est une série dans le temps, pas une ligne de journal perdue avec le conteneur. Sans ligne écrite, l'endpoint n'a rien à lire et le test d'intégration rien à vérifier. |
|
||||
| Une tâche de plus dans le DAG `alertes` | La fenêtre fait 168 h : la recalculer chaque heure écrirait vingt-quatre lignes identiques par jour. Surtout, un échec de dérive ferait rougir `alertes` et laisserait croire que la détection a échoué. |
|
||||
| Une tâche de plus dans le DAG `alertes` | La fenêtre fait 168 h : la recalculer chaque heure écrirait vingt-quatre lignes par jour pour un verdict qui ne bouge pas à cette cadence. Surtout, un échec de dérive ferait rougir `alertes` et laisserait croire que la détection a échoué. |
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Une migration ajoute `drift_report`. Son idempotence passe par un **index unique à
|
||||
`coalesce(site_id, '')`** et non par une `UniqueConstraint` : deux lignes globales ont toutes
|
||||
deux `site_id` à NULL, et NULL n'est égal à rien, pas même à lui-même. Même forme que
|
||||
`uq_reading_source`.
|
||||
`uq_reading_source`. Cet index n'a de sens que parce que `evaluate()` **tronque son instant de
|
||||
référence à l'heure** : avec les microsecondes de `now()`, deux exécutions ne porteraient jamais
|
||||
la même clé et l'index ne dédoublonnerait rien.
|
||||
- **Sans fenêtre de référence, le verdict est `indetermine`, pas `stable`.** Au premier
|
||||
lancement, et après tout trou d'ingestion de plus de 168 h, il n'y a rien à quoi comparer :
|
||||
annoncer `stable` serait affirmer ce que la donnée ne dit pas.
|
||||
- **Un site présent dans la fenêtre de référence et absent de la récente reçoit sa ligne**, à
|
||||
zéro observation. Un site qui cesse d'être scoré est exactement la panne que cette surveillance
|
||||
existe pour dire : le taire en ne produisant aucune ligne serait l'inverse du besoin.
|
||||
- `GET /api/v1/monitoring/drift` est réservé à partir du rôle `operateur` : c'est l'opérateur
|
||||
qui agit sur un pipeline dégradé, pas l'administrateur de comptes. La route est classée dans
|
||||
`tests/api/acces.py`, donc couverte gratuitement par la matrice de rôles rejouée avec de vrais
|
||||
|
||||
@@ -14,9 +14,9 @@ Les quatre couches existent désormais, portées par l'authentification.
|
||||
flowchart TB
|
||||
ep["endpoints<br/>health, auth, users, sites, alerts,<br/>recommendations, stats, readings, sensors,<br/>predictions, monitoring"]
|
||||
sc["schemas<br/>Pydantic"]
|
||||
sv["services<br/>AuthService, UserService,<br/>SiteService, AlertService, RecommendationService,<br/>StatsService, ReadingService, SensorService, PredictionService"]
|
||||
rp["repositories<br/>user, refresh_token,<br/>login_attempt, audit_log,<br/>site, alert, recommendation, reading, prediction"]
|
||||
md["models<br/>10 tables"]
|
||||
sv["services<br/>AuthService, UserService,<br/>SiteService, AlertService, RecommendationService,<br/>StatsService, ReadingService, SensorService,<br/>PredictionService, DriftService"]
|
||||
rp["repositories<br/>user, refresh_token,<br/>login_attempt, audit_log,<br/>site, alert, recommendation, reading,<br/>prediction, drift"]
|
||||
md["models<br/>13 tables"]
|
||||
db[("PostgreSQL")]
|
||||
|
||||
ep --> sc
|
||||
@@ -229,8 +229,9 @@ côté pilote, en `500` plutôt qu'un refus propre.
|
||||
`DriftService.evaluate()` joint `prediction` et `reading` sur `(site_id, target_at = timestamp)`
|
||||
et compare deux fenêtres vives de 168 h, la récente et celle qui la précède. Il rend une ligne par
|
||||
site plus une ligne globale, que `DriftRepository.enregistre()` écrit dans `drift_report` avec
|
||||
`ON CONFLICT DO NOTHING` sur `uq_drift_report_window` : rejouer la commande sur la même fenêtre
|
||||
n'ajoute rien.
|
||||
`ON CONFLICT DO NOTHING` sur `uq_drift_report_window`. L'instant de référence est tronqué à
|
||||
l'heure, ce qui est la condition pour que cet index serve : rejouer la commande dans la même
|
||||
heure n'ajoute rien.
|
||||
|
||||
| Métrique | Ce qu'elle dit |
|
||||
|---|---|
|
||||
@@ -242,7 +243,10 @@ n'ajoute rien.
|
||||
| `model_references` | Les modèles vus dans la fenêtre : une MAE qui saute à l'instant où le modèle change est une régression de réentraînement, pas une dérive |
|
||||
|
||||
Le verdict a trois valeurs, `stable`, `derive` et `indetermine` : sous un nombre minimal
|
||||
d'observations, le service dit qu'il ne sait pas plutôt que de rendre un chiffre trompeur. La
|
||||
d'observations, ou faute de fenêtre de référence à laquelle comparer, le service dit qu'il ne
|
||||
sait pas plutôt que de rendre un chiffre trompeur. Un site qui figure dans la fenêtre de
|
||||
référence mais plus dans la récente reçoit sa ligne à zéro observation : cesser d'être scoré est
|
||||
la panne que cette surveillance existe pour dire. La
|
||||
fenêtre est fermée à droite par un délai de grâce de 2 h, le temps que l'ingestion livre le
|
||||
réalisé de la dernière heure. `python -m app.monitoring.drift` l'exécute, le DAG `derive`
|
||||
l'ordonnance, et `GET /api/v1/monitoring/drift` sert le dernier rapport de chaque site. Les
|
||||
|
||||
@@ -258,7 +258,7 @@ régénère, ce qui invalide les sessions et les connexions chiffrées par Airfl
|
||||
|
||||
### Pourquoi le job d'intégration ML installe aussi le backend
|
||||
|
||||
Le schéma de la base n'a qu'une source, les six révisions Alembic de `apps/backend/alembic` : le
|
||||
Le schéma de la base n'a qu'une source, les sept révisions Alembic de `apps/backend/alembic` : le
|
||||
backend est propriétaire du schéma, `ml/` n'en est que consommateur. Reconstruire ce schéma à la
|
||||
main dans le job ML donnerait un job vert sur une base qui n'est pas la nôtre, exactement l'erreur
|
||||
qu'évite déjà le choix de l'image `timescaledb-ha` plutôt qu'un `postgres` nu. Le job installe
|
||||
@@ -266,12 +266,14 @@ donc les deux environnements uv, applique `alembic upgrade head`, puis joue `-m
|
||||
`ml/` et `-m chaine` côté backend.
|
||||
|
||||
Conséquence sur le déclenchement : les `paths` de `ml.yml` incluent `apps/backend/alembic/**` et
|
||||
`apps/backend/app/models/**`. Sans eux, une migration qui renomme une colonne de `reading` ne
|
||||
`apps/backend/app/**`. Sans eux, une migration qui renomme une colonne de `reading` ne
|
||||
déclencherait pas ce job, le SQL brut du pipeline dériverait du schéma, et **rien ne casserait
|
||||
avant la production**. Le prix est qu'une PR touchant seulement une migration lance aussi le lint
|
||||
et le typage de `ml/` : environ deux minutes de runner, en parallèle. Même arbitrage que le filtre
|
||||
d'`airflow.yml`, qui écoute déjà `ml/**` et `apps/backend/app/**` parce que son image réunit les
|
||||
deux.
|
||||
avant la production**. `app/**` en entier, et non les seuls modèles : ce job est le seul à jouer
|
||||
`-m chaine`, or la chaîne traverse les endpoints, les services et les schémas jusqu'à
|
||||
`GET /predictions`. Un filtre plus étroit laisserait le test muet sur la PR même qui le casse. Le
|
||||
prix est qu'une PR backend lance aussi le lint et le typage de `ml/` : environ deux minutes de
|
||||
runner, en parallèle. Même arbitrage que le filtre d'`airflow.yml`, qui écoute déjà `ml/**` et
|
||||
`apps/backend/app/**` parce que son image réunit les deux.
|
||||
|
||||
Le marqueur `chaine` est distinct d'`integration` pour une raison mécanique : le job `integration`
|
||||
de `backend.yml` n'installe pas `ml/.venv`, et sélectionnerait sinon un test qui lance les
|
||||
@@ -297,7 +299,7 @@ Les tests d'intégration demandent une base **migrée**, et `db/init` ne crée `
|
||||
vide :
|
||||
|
||||
```bash
|
||||
make db-up migrate-test # la base de test reçoit les six révisions Alembic
|
||||
make db-up migrate-test # la base de test reçoit les sept révisions Alembic
|
||||
make test-integration # backend, marqueur `integration`
|
||||
make ml-test-integration # pipeline ML, marqueur `integration`
|
||||
make test-chaine # vrais binaires ML puis relecture par l'API, marqueur `chaine`
|
||||
|
||||
Reference in New Issue
Block a user