À 9 h, le dashboard des ventes affiche un total rassurant. Le chargement s’est terminé à 8 h 55. Pourtant, un magasin manque dans les données du dimanche. Le traitement a bien fonctionné sur ce qu’il a reçu ; il n’a pas prouvé qu’il avait reçu tout ce qui était attendu.
Pour contrôler la fraîcheur des données, il faut préciser jusqu’à quelle période et pour quel périmètre elles doivent être disponibles. La date du dernier chargement ne répond pas à cette question. Elle devient utile lorsqu’on la rapproche d’une échéance métier et d’une preuve de couverture.
Cet article propose une méthode de data observability, ou observabilité des données, appliquée à un dashboard quotidien. Un atelier Python local montre huit situations, dont un rejeu ancien qui paraît récent et un magasin sans vente dont le lot est pourtant complet. Il s’agit de données fictives exécutées localement, sans essai d’un outil BI ni d’une plateforme d’observabilité.

Trois dates qui ne répondent pas à la même question
Une vente possède une date métier : le moment où elle s’est produite. Elle possède aussi une date d’arrivée dans l’entrepôt. Enfin, le pipeline peut publier une borne de couverture : la période qu’il déclare avoir entièrement traitée.
| Signal |
Ce qu’il permet d’observer |
Ce qu’il ne garantit pas |
| Dernier événement métier |
La date la plus récente présente dans les lignes |
La présence des autres magasins ou des ventes plus anciennes |
| Dernière ingestion |
L’arrivée récente d’au moins une ligne |
La nouveauté de cette ligne, ni la complétude du lot |
| Borne de couverture par magasin |
La période déclarée complète par le producteur |
La qualité de cette déclaration, ni l’exactitude des montants |
| Actualisation du dashboard |
La génération récente d’un affichage ou d’un snapshot |
L’utilisation des bons lots de données |
Cette séparation entre temps de l’événement et temps de traitement existe aussi dans les systèmes de streaming. Le guide Apache Beam décrit notamment les données retardataires et les watermarks. Dans notre cas batch, la borne de couverture est un contrat explicite du producteur ; ce n’est pas une garantie automatiquement fournie par une horloge.
Une vente du samedi réinjectée le lundi donne une ingestion récente, tout en laissant les ventes du dimanche manquantes. Inversement, un magasin fermé le dimanche peut ne produire aucun événement récent sans être en panne. Il faut une preuve que l’extraction a été faite et que le résultat vide est attendu.
Partir de la décision commerciale, pas du seuil technique
Prenons une enseigne fictive avec trois magasins : Lille, Lyon et Nantes. Son équipe commerciale utilise à 9 h les ventes de la veille pour préparer les réapprovisionnements. Le contrat retenu pour l’exercice est le suivant :
- Les trois magasins sont attendus, y compris lorsque l’un déclare zéro vente.
- Le lot du dimanche couvre la période jusqu’au lundi à minuit, borne exclusive, en Europe/Paris.
- À partir de 8 h 30, un lot absent ou incomplet doit déclencher une alerte ; à 9 h, le dashboard doit contenir les trois lots.
- À 9 h, son snapshot doit avoir moins de 15 minutes.
Ces horaires sont des hypothèses pédagogiques. Pour un reporting hebdomadaire ou une caisse qui ferme à une autre heure, il faut les changer. Le lundi après un dimanche fermé et un jour férié demandent aussi un calendrier explicite. Un seuil « moins de 24 heures » ne sait pas décider seul quelle journée devait être livrée.
Le choix du fuseau compte. Le 2 novembre 2026 à minuit Paris correspond au 1er novembre à 23 h UTC. Stocker une chaîne sans fuseau puis la lire comme de l’UTC peut déplacer la période contrôlée. L’atelier utilise des instants avec décalage et le fuseau Europe/Paris ; il refuse un horodatage sans fuseau.
Pourquoi un maximum récent laisse passer un magasin absent
Supposons que Lille et Lyon soient complets. Une ligne de Lille est arrivée à 8 h 55. Nantes n’a pas livré son lot.
Une requête qui demande seulement max(ingested_at) renvoie 8 h 55. À 9 h, un seuil de 30 minutes passe. Un max(event_at) global peut également passer grâce à une vente tardive de Lyon. Aucun de ces résultats ne prouve que Nantes est couvert.
La première correction consiste à contrôler le périmètre attendu avant de calculer un âge. Une liste des magasins extraite uniquement des lignes reçues échoue aussi : le magasin manquant n’y apparaît jamais. Il faut une liste de référence indépendante, puis une jointure qui conserve les absents.
Le même défaut existe avec des pays, des comptes publicitaires ou des partitions horaires. La bonne granularité correspond à ce qui peut manquer sans interrompre le traitement global. Si un canal peut disparaître seul, un maximum calculé sur tous les canaux masque ce risque.
Ajouter une preuve de lot, puis vérifier sa propagation
Dans l’exercice, chaque magasin publie un manifeste contenant cinq éléments : identifiant de lot, magasin, période couverte, état complet et nombre de lignes attendu. Le contrôle rapproche ce manifeste des lignes réellement reçues.
Le nombre attendu doit venir d’une réconciliation avec la source. Compter les lignes dans la table cible et recopier ce nombre dans le manifeste ne permettrait pas de détecter une perte pendant le transfert. Pour un magasin sans vente, le producteur doit confirmer un lot complet de zéro ligne. L’absence de lot reste une absence de preuve.
Ce contrôle suppose que le manifeste est publié de manière cohérente avec les données. En production, il faut éviter qu’un état « complet » soit visible avant la fin du chargement : transaction commune lorsque c’est possible, ou publication d’une référence de lot validée après écriture. Les reprises doivent conserver des identifiants non ambigus. Deux lots concurrents pour la même période exigent une règle de sélection ; notre atelier les refuse plutôt que d’en choisir un arbitrairement.
Ensuite, il reste à suivre le lot jusqu’au dashboard. Une source complète n’implique pas qu’un modèle transformé ou un cache BI a été rafraîchi. Notre snapshot fictif déclare les identifiants des lots qu’il contient. C’est cette référence que le contrôle vérifie, en plus de l’heure de génération. Dans un outil réel, la preuve peut être une version de dataset, une partition matérialisée ou un journal de rafraîchissement relié au lot.
La décision devient lisible : au moment du contrôle, tous les magasins sont couverts, les comptes concordent, les bons lots atteignent le snapshot et celui-ci est suffisamment récent. ready décrit cette disponibilité actuelle. Il ne certifie pas que la livraison était déjà complète à 8 h 30. Pour mesurer le respect historique de cette heure, il faudrait conserver les heures de fin des lots ou les résultats successifs des contrôles.
Une alerte à 8 h 30 peut donc être suivie d’un retour à ready après une reprise à 8 h 55. Le retour au vert permet d’utiliser les données ; il n’efface pas l’incident de retard. Un nombre de lignes identique ne prouve toujours pas la justesse des montants ou l’absence de doublons. Ces vérifications relèvent d’autres contrôles de qualité.
Exécuter le contrôle et observer ses refus
Téléchargez le script Python et son mode d’emploi. Avec Python 3.9 ou plus récent et une base de fuseaux IANA installée :
python3 controle_fraicheur.py > resultat.json
L’instant est fixé au lundi 2 novembre à 9 h Paris pour rendre le résultat reproductible. Aucune clé, aucun package tiers et aucune connexion ne sont utilisés. Le script génère les données fictives, applique le contrôle puis vérifie les décisions attendues.
| Situation exécutée |
Décision à 9 h |
Ce que le contrôle apprend |
| Trois lots complets et présents dans le snapshot |
ready |
Données disponibles à 9 h ; ponctualité à 8 h 30 non mesurée |
| Manifeste de Nantes absent |
not_ready |
Le maximum global des autres magasins ne suffit pas |
| Données de Nantes rejouées récemment, couverture ancienne |
not_ready |
Une arrivée récente ne remplace pas la période attendue |
| Nantes confirme un lot complet de zéro vente |
ready |
Zéro ligne et absence de livraison sont deux états différents |
| Deux lignes annoncées, une reçue pour Nantes |
not_ready |
La livraison ne correspond pas au manifeste |
| Bons lots, snapshot vieux d’une heure |
not_ready |
La chaîne BI reste trop ancienne |
| Horodatage d’événement dans le futur |
not_ready |
Une horloge incohérente peut tromper un maximum |
| Snapshot récent qui omet le lot de Nantes |
not_ready |
Rafraîchir l’affichage ne prouve pas sa couverture |
La sortie JSON conserve les motifs et les deux maxima. Dans les scénarios « magasin absent » et « rejeu ancien », l’ingestion reste récente ; la décision échoue grâce à la preuve de lot. Le champ delivery_deadline_met reste null : aucune preuve de ponctualité historique n’est calculée. Deux contrôles supplémentaires vérifient l’état awaiting avant l’heure d’alerte sur une situation sans date future, et le refus des dates sans fuseau.
Pour adapter la méthode, commencez par remplacer les trois magasins et la borne fixe par votre périmètre attendu et votre calendrier métier. Branchez ensuite de vraies preuves de livraison. Ne déduisez pas la complétude de la seule réussite de ce script : ici, son producteur et son snapshot sont simulés.
Raccorder ce contrôle aux outils déjà présents
Si l’équipe utilise dbt, elle dispose de contrôles de fraîcheur avec seuils d’avertissement et d’erreur. La documentation actuelle distingue la commande dbt freshness de dbt v2, qui peut contrôler sources et modèles configurés, de dbt source freshness, conservée pour les sources. Il faut donc lire la version du projet avant de reprendre un exemple de configuration. Configuration freshness de dbt.
Le timestamp surveillé doit avoir un sens connu : arrivée d’une ligne, métadonnée du stockage ou expression dédiée. Même un contrôle correctement configuré reste limité par le signal qu’il interroge. S’il examine un maximum global, il ne reconstitue pas notre contrat par magasin.
La place du contrôle dans le job compte également. La documentation de la plateforme dbt précise que dbt build n’inclut pas automatiquement les contrôles de fraîcheur des sources. Elle distingue aussi la case de configuration qui exécute ce contrôle sans arrêter les étapes suivantes et une commande ajoutée comme étape dont l’échec interrompt la suite. Vérifiez le comportement applicable à votre environnement avant d’en faire un verrou de publication. Source freshness dans les jobs dbt.
Un ordonnanceur peut donc continuer de surveiller les exécutions, tandis qu’un contrôle de couverture décide si le dataset peut être consommé. Une plateforme d’observabilité plus large devient pertinente lorsque les dépendances, les périmètres et le diagnostic sont difficiles à maintenir. Son intérêt doit se mesurer sur les incidents détectés avant utilisation, les alertes actionnables et l’effort d’exploitation. L’atelier ne compare ni tarifs ni performances de ces plateformes.
Préparer la réaction avant d’activer l’alerte
Pour notre dashboard, un refus à 9 h conduit à conserver le dernier état validé en affichant sa période réelle, ou à suspendre la décision commerciale. Le choix dépend de l’usage. Une ancienne valeur clairement datée peut convenir à une tendance hebdomadaire ; elle peut être inacceptable pour un réapprovisionnement quotidien.
L’alerte doit nommer le magasin, la période attendue, la dernière preuve de couverture, l’état du snapshot et le responsable de la reprise. Après correction, rejouez le contrôle jusqu’au consommateur final, pas seulement jusqu’à la table source. Le guide de lineage aide à identifier ces consommateurs ; le guide de CI SQL traite les contrôles sur les transformations.
Commencez avec un seul indicateur utilisé à heure fixe. Faites tourner le contrôle en observation, relevez les faux refus et les absences réellement détectées, puis décidez avec le métier du comportement à adopter. Vous aurez alors un contrat testable : quelle période est disponible, pour quel périmètre, avec quelle preuve, avant quelle décision.