Logs structurés, métriques RED et la gouvernance par ADRs
structlog : adopter le JSON sans réécrire une ligne
Le projet a ~230 logging.getLogger(__name__) hérités. Plutôt que tout migrer, structlog est monté en façade du logging stdlib (via ProcessorFormatter) : les anciens loggers héritent gratuitement du rendu JSON, du timestamp UTC, du contexte — et d'un processor redact_secrets qui masque authorization/password/token/api_key dans TOUT log. Deux rendus : JSON en prod (schéma aligné sur les autres services, pour Loki), couleur en dev. La leçon de conduite du changement : l'adoption incrémentale bat la réécriture — on change la sortie, pas les 230 appels.
Le middleware de logs : pur ASGI, et pourquoi c'est important
Le LoggingMiddleware pose le contexte (method, uri, trace_id), logge request.start/request.end (+ status, durée), et purge le contexte en finally. Détail d'expert : il est écrit en pur ASGI, pas avec BaseHTTPMiddleware — ce dernier exécute la suite dans une autre task et perd les ContextVars (bug Starlette connu). Quand ton outil de contexte repose sur les ContextVars, la plomberie qui les transporte n'est pas un détail.
Le trace_id est le fil rouge : récupéré de la transaction Sentry (ou généré), il part dans chaque ligne de log ET dans le header réponse X-Trace-Id. Un client signale une erreur avec son trace_id → une requête Loki → toute l'histoire. La corrélation support↔logs ne s'improvise pas après coup.
Métriques RED : la RAM, un port privé, la cardinalité bornée
Les métriques (actées dans un ADR) suivent le canon RED — Rate, Errors, Duration : http_requests_total + http_request_duration_seconds (histogramme). Trois choix de design :
- en RAM, zéro I/O : des compteurs incrémentés en mémoire, scrapés par Prometheus — rien n'est écrit sur le hot path ;
- port dédié (9090, jamais routé par l'ingress) :
/metricsn'existe pas pour l'extérieur — pas d'auth à gérer, pas de surface exposée ; - cardinalité bornée : le label route est le template (
/api/requests/{id}, pas le path brut — sinon un million de séries), et les status regroupés par classe (2xx/5xx).
La hiérarchie de confiance du projet : Prometheus pour la tendance et l'alerte, PostgreSQL pour la vérité exacte, les logs pour la preuve détaillée — trois outils, trois questions.
Le compteur qui ment : échantillon plafonné et taux non pondéré
Une métrique peut être fausse sans jamais lever d'erreur — et c'est la pire espèce, parce qu'on la lit avec confiance. Deux pièges rencontrés, tous deux sur un tableau de bord qui « marchait ».
1. Le compteur borné par ce qu'on garde. Un job traite des milliers d'items mais ne persiste que les 50 dernières erreurs (pour ne pas gonfler la table). Le dashboard lit count(errors) sur cette table et affiche fièrement « 50 erreurs ». Le vrai chiffre était peut-être 4 000. Le compteur ne mesure pas ce qui s'est passé, il mesure ce qu'on a décidé de stocker. Dès qu'une métrique dérive d'un échantillon tronqué (top-N, ring buffer, table capée), elle ment par construction — il faut un compteur d'occurrences séparé, incrémenté à chaque événement et jamais tronqué, à côté de l'échantillon détaillé qu'on garde pour le debug.
2. Le taux non pondéré. « Taux de succès : 92 % » — calculé en moyennant le succès par run. Sauf qu'un run traite 1 item et un autre en traite 5 000, et les deux pèsent pareil dans la moyenne. Un run raté à 1 item fait chuter le taux autant qu'un run raté à 5 000. Le chiffre honnête se pondère par l'unité qui compte : items_réussis / items_totaux, pas moyenne(taux_par_run). La question réflexe devant tout taux : le dénominateur, c'est quoi — des runs, des items, des requêtes ? Un taux sans dénominateur explicite est un chiffre décoratif.
La règle transférable : une métrique doit déclarer son échantillon et son dénominateur. Sépare « combien j'ai gardé » de « combien il s'est passé », et pondère les taux par l'unité métier, jamais par le lot. Sinon tu construis un tableau de bord qui rassure pendant que le système saigne — le pire des deux mondes, car personne ne va chercher un problème que le dashboard déclare résolu.
Les ADRs : l'architecture qui se souvient
Le projet compte 20+ Architecture Decision Records (format : Contexte → Décision → Alternatives considérées avec « pourquoi écartée » → Conséquences, positives ET dette). Tu en as croisé tout au long du cours : RLS, payload rejouable, propagation ContextVar, fail-closed, métriques…
Pourquoi c'est une pratique à voler : un ADR capture le pourquoi au moment T — les alternatives pesées, les contraintes d'alors. Six mois plus tard, quand quelqu'un demande « pourquoi pas Celery ? », la réponse est un lien, pas une archéologie de Slack. Les règles de code du projet (« jamais de WHERE tenant_id », « fail loud ») tirent leur autorité de là : une décision écrite et argumentée se respecte ; une habitude orale se dégrade. Écris l'ADR quand la décision est chaude — c'est 30 minutes qui en économisent des heures, et c'est le meilleur outil d'onboarding qui existe.
Comment structlog s'applique-t-il aux 230 loggers stdlib existants sans réécriture ?