jguillaumesio
devopsarchitecture

Le débogage compte plus que les fonctionnalités

Mon blog renvoyait 404 sur chaque page à 2 h du matin. Zéro visibilité sur la cause. Voici l'infrastructure de débogage que j'aurais aimé mettre en place dès le départ.

Vue d'ensemble de l'infrastructure de débogage

Mon blog renvoyait 404 sur chaque page à 2 h du matin.

Pas d’erreur. Pas d’alerte. Aucune entrée de log. Juste des pages blanches pour chaque visiteur jusqu’à ce que je vérifie le site à la main 6 heures plus tard.

La cause : un volume hostPath de PVC a silencieusement perdu tous les fichiers HTML après un redéploiement. Seuls les assets _astro et les images restaient. Le conteneur Caddy tournait sans problème. Les health checks passaient. Rien n’était “en panne”.

J’avais zéro visibilité sur la production. Et j’ai plus appris sur le débogage avec cet incident qu’avec les 6 mois précédents de travail sur les fonctionnalités.

La défaillance silencieuse est la pire. Votre site est “en ligne” mais sert du contenu cassé. Votre API renvoie 200 mais de mauvaises données. Votre déploiement a réussi mais le nouveau code n’a jamais vraiment chargé. Ce sont les défaillances qui brûlent des heures parce que vous ne savez même pas où regarder.

L’infrastructure de débogage compte plus que les fonctionnalités. Chaque heure passée sur l’observabilité économise 10 heures de débogage à 2 h du matin. Voici ce que j’utilise vraiment.

Ce que j’avais (rien)

Avant l’incident, mon “monitoring” de production, c’était curl.

curl -s -o /dev/null -w "%{http_code}" http://my-site/
200

Ça vous dit que la couche HTTP fonctionne. Ça ne vous dit pas si les pages chargent correctement, si les assets existent, si le bon contenu est servi, ou si une corruption silencieuse a mangé vos fichiers HTML.

Je n’avais pas :

  • De logs structurés (j’utilisais console.log)
  • D’identifiants de requête pour tracer un parcours utilisateur
  • D’agrégation de logs (kubectl logs, bonne chance)
  • De health checks au-delà de “le processus tourne-t-il”
  • De surveillance de disponibilité
  • De suivi d’erreurs
  • D’alerte d’aucune sorte

Le site était une boîte noire. Soit il marchait, soit non, et je ne savais lequel qu’en regardant à la main.

Les défaillances silencieuses que j’ai vraiment vues

Chacune est arrivée sur un vrai petit projet. Chacune a pris des heures à diagnostiquer parce que le monitoring se résumait à “le site renvoie 200, donc tout va bien”.

La perte de contenu silencieuse (mon blog)

Un déploiement k3s a déclenché un redémarrage progressif. Le nouveau pod a démarré. Les health checks passaient. Chaque page renvoyait 200.

Mais le volume hostPath du PVC était vide. Le dossier dist n’avait aucun fichier HTML, seulement des chunks _astro et des images. Caddy servait 200 OK avec un corps vide pour chaque route.

Aucune erreur dans les logs. Aucun health check en échec. Aucune alerte. Je l’ai découvert 6 heures plus tard.

Le correctif : un health check basé sur le contenu qui vérifie le vrai contenu de la page, pas juste le HTTP 200. Plus de détails ci-dessous.

L’angle mort du démarrage à froid

Une fonction serverless avait un démarrage à froid de 4 secondes. Quand il survenait, le timeout de l’API Gateway (configuré à 3 secondes) tuait la requête avant même que la fonction ne démarre.

CloudWatch affichait “200 OK” pour les invocations réussies. Les timeouts étaient loggués comme “Task timed out” mais enfouis dans un groupe de logs avec des milliers d’autres lignes. Le tableau de bord affichait un taux d’erreur de 0 % parce que les timeouts ne sont pas des erreurs dans le dashboard par défaut.

Les utilisateurs voyaient “Chargement…” puis un message d’erreur. On n’en a rien su pendant 2 semaines.

Le correctif : une requête de log qui compte les timeouts par heure, avec une alerte quand le taux dépasse 1 %. Ça prend 30 secondes à configurer dans Loki ou CloudWatch Insights.

Le problème DNS “ça marche sur ma machine”

Une API marchait en local mais échouait par intermittence en production. L’erreur était un timeout de résolution DNS qui arrivait peut-être 1 fois sur 100 requêtes.

En local, le DNS se résolvait depuis le cache. En production, le résolveur DNS du conteneur avait un cache court et un amont lent. La défaillance était assez intermittente pour que redémarrer le pod “règle” le problème (jusqu’à la prochaine résolution DNS à froid).

Sans logging au niveau de la requête montrant le nom d’hôte et le temps de résolution, ça ressemblait à un souci réseau aléatoire. Il a fallu 3 jours pour le trouver.

Le correctif : des logs structurés qui incluent le temps de résolution DNS pour les appels externes. L’anomalie était évidente une fois que les données existaient : 99 % des requêtes se résolvaient en 2 ms, 1 % prenaient 4 secondes ou plus et expiraient.

La migration de base silencieuse

Une migration a ajouté une nouvelle colonne avec une valeur par défaut. La migration a réussi. La colonne existait. Toutes les lignes existantes avaient la valeur par défaut.

Mais le code applicatif n’a pas été mis à jour pour lire la nouvelle colonne. L’API marchait toujours, renvoyait toujours 200, affichait toujours des données. Elle ignorait simplement le nouveau champ en silence pendant 2 semaines, jusqu’à ce que quelqu’un remarque que la migration était incomplète.

Aucune erreur. Aucune entrée de log. Aucune alerte. Le système “marchait” mais ne faisait pas ce qu’il devait.

Le correctif : un test de fumée post-déploiement qui vérifie que la nouvelle colonne est réellement lue et renvoyée dans les réponses de l’API. Lancez-le une fois après chaque déploiement.

La fuite mémoire qui redémarrait à heure fixe

Un processus Node.js avait une lente fuite mémoire. Tous les 7 jours, l’usage mémoire atteignait la limite du conteneur. Le processus redémarrait. Le pod affichait “Running” pendant tout ce temps.

Le redémarrage coïncidait par hasard avec le job de sauvegarde quotidien. Pendant 3 mois, les gens ont cru que le job de sauvegarde causait le redémarrage. La vraie cause était une closure capturant un tableau qui grossissait sans limite.

Pendant les tests en local, le processus ne tournait jamais assez longtemps pour atteindre la limite. En production, ça prenait 7 jours.

Le correctif : un endpoint de métriques qui suit l’usage du tas (heap) dans le temps. La tendance à la hausse était évidente après 3 jours de données. Sans ça, on n’aurait jamais relié le redémarrage hebdomadaire à la mémoire.

L’infrastructure de débogage que j’utilise vraiment maintenant

1. Logging structuré avec identifiants de requête

Chaque requête reçoit un identifiant unique. Chaque ligne de log l’inclut. Quand quelque chose casse, vous grepez un seul identifiant et vous voyez tout le cycle de vie de la requête.

app.use((req, res, next) => {
  req.requestId = crypto.randomUUID();
  console.log(JSON.stringify({
    level: 'info',
    requestId: req.requestId,
    method: req.method,
    path: req.path,
    timestamp: new Date().toISOString()
  }));
  next();
});

Quand vous appelez un autre service, passez le requestId. Quand ce service logue, il reprend le même identifiant. D’un coup vous pouvez tracer une requête à travers toute votre stack.

Pour les services qui n’ont que des logs d’accès (comme Caddy), j’ai ajouté un script qui cherche les anomalies : pics de 5xx, réponses lentes, assets manquants.

2. Des health checks qui vérifient vraiment le contenu

Un health check qui vérifie seulement “le processus tourne” n’aurait attrapé aucune des défaillances ci-dessus.

Maintenant j’utilise des health checks basés sur le contenu :

#!/bin/bash
# Vérifier qu'une vraie page renvoie un vrai contenu
RESPONSE=$(curl -sf http://localhost:30080/blog/stripe-alternatives-eu/ 2>/dev/null)
if echo "$RESPONSE" | grep -q "I was paying Stripe"; then
  exit 0
fi
exit 1

Le check vérifie qu’une vraie page renvoie un vrai contenu. Si les fichiers HTML manquent, le health check échoue. Le pod est redémarré. Fini les pannes silencieuses de 6 heures.

Je lance ce check toutes les 60 secondes. Ce sont les 8 lignes de bash les plus précieuses que j’aie écrites.

Pour le problème de migration de base, le health check vérifie aussi :

# Vérifier que la migration a été appliquée ET que l'appli l'utilise
curl -sf http://localhost:30000/api/migrations/status | grep -q "column_v3:active"

3. Surveillance de disponibilité depuis l’extérieur du cluster

J’utilise un cron qui interroge le site depuis l’extérieur toutes les 5 minutes. S’il reçoit 3 réponses non-200 d’affilée (ou si le contenu ne correspond pas aux attentes), il envoie un message Discord.

Cron : toutes les 5 minutes
Action : GET https://my-site.com/blog/
Attendu : 200 avec un contenu correspondant à "Stripe alternatives"
En cas d'échec : POST vers un webhook Discord

Coût total : zéro. Temps de mise en place total : 10 minutes. Valeur totale : je sais dans les 15 minutes si quelque chose ne va pas, même si Kubernetes affiche tout en Running.

4. Suivi d’erreurs avec Sentry (offre gratuite)

Offre gratuite : 5 000 erreurs/mois. Pour les petits sites, c’est virtuellement illimité.

Sentry attrape :

  • Les erreurs JavaScript côté frontend
  • Les exceptions non gérées côté backend
  • Les soucis de performance (appels d’API lents, timeouts)

La mise en place a pris 3 lignes de configuration. Au lieu de “le site semble lent”, j’obtiens “le traitement de l’événement a pris 4,2 s sur /api/checkout dans Firefox 124”.

Pour le problème DNS, les erreurs de timeout seraient apparues dans Sentry avec la stack trace complète et le nom d’hôte.

5. Agrégation de logs avec Loki auto-hébergé

Conteneur Loki en instance unique. Les logs vont vers stdout, Promtail les récupère, Loki les stocke. Grafana pour les requêtes.

Pour une configuration mono-serveur, c’est sans doute exagéré. Mais voici à quoi ça me sert :

rate({job="web-article"} |= "error" [5m])
# Taux d'erreur sur 5 minutes

{job="web-article"} |= "abc-123-request-id"
# Tracer chaque ligne de log d'une requête

quantile(0.95, rate({job="web-article"} | json | response_time > 0 [5h]))
# 95e centile du temps de réponse sur 5 heures

Le coût de stockage est peut-être de 200 Mo/mois. La capacité de requête remplace le workflow kubectl logs --tail=5000 | grep ERROR | awk '{print $4}' qui transforme une investigation de 5 minutes en une de 30.

Pour les timeouts de démarrage à froid, la requête Loki serait :

{job="api"} |= "Task timed out" | json | response_time > 3000

Lancez-la une fois, voyez le pic, connaissez le correctif.

Si Loki auto-hébergé c’est trop, même envoyer les logs vers un fichier et utiliser lnav vaut mieux que kubectl logs.

6. Un endpoint de métriques avec vérifications d’intégrité du contenu

J’ai ajouté un endpoint /metrics qui renvoie :

uptime_seconds 34560
requests_total 892
errors_total 12
last_deploy_timestamp 1717000000
content_files_expected 11
content_files_actual 11
last_content_check ok

Ce n’est pas Prometheus. C’est un objet JSON que je peux curler. Mais quand la corruption du PVC est arrivée, content_files_actual aurait été à 0. Le health check l’attrape sans que j’aie à visiter le site.

Pour la fuite mémoire, j’ai ajouté :

heap_used_mb 342
heap_limit_mb 512
memory_trend increasing

Le champ memory_trend compare les 24 dernières heures. S’il est “increasing” 3 jours d’affilée, c’est une fuite. On redémarre et on enquête avec des snapshots de tas.

7. Tests de fumée post-déploiement

Après chaque déploiement, un test de fumée tourne dans les 30 secondes :

1. GET / → attendre 200 avec "jguillaumesio" dans le corps
2. GET /blog/stripe-alternatives-eu/ → attendre 200 avec "Stripe" dans le corps
3. GET /metrics → attendre content_files_actual == content_files_expected
4. GET /api/health → attendre connexion à la base = ok

Si un check échoue, le déploiement est annulé automatiquement. Tout le test prend 20 secondes.

Ça a attrapé le problème de migration de base (l’étape 4 aurait échoué parce que la nouvelle colonne n’était pas interrogée). Ça a attrapé la corruption du PVC (l’étape 3 échoue). Ça attraperait le problème de démarrage à froid si le test de fumée tourne pendant une période froide.

Le coût de tout ça

OutilTemps de mise en placeCoût mensuelCe que ça attrape
IDs de requête dans les logs1 heure0Traçage des soucis entre services
Health check basé sur le contenu30 min0Défaillances de contenu silencieuses
Cron de disponibilité + alerte Discord15 min0Indisponibilité sous 15 minutes
Offre gratuite Sentry30 min0Erreurs frontend + backend
Loki auto-hébergé2 heures~0 (même VPS)Schémas d’erreur, requêtes de logs
Endpoint de métriques1 heure0Intégrité du contenu, suivi des déploiements
Tests de fumée post-déploiement30 min0Migrations incomplètes, mauvais déploiements

Temps de mise en place total : ~6 heures. Coût mensuel total : 0.

Comparez ça aux 2 semaines et plus cumulées de temps de débogage issues des 5 défaillances silencieuses ci-dessus.

Ce que je n’utiliserais pas

  • Datadog / New Relic : cher pour un petit site. Sentry + Loki vous donnent 80 % de la valeur.
  • PagerDuty : un webhook Discord est votre pager à 3 h du matin. Pas besoin de payer 20 $/mois.
  • Stack Prometheus + Grafana complète : un endpoint de métriques et un cron suffisent jusqu’à ce que vous ayez du vrai trafic.
  • Stack ELK : Loki est plus simple. N’ajoutez pas Elasticsearch juste pour des logs.

En résumé

J’ai été des deux côtés. Avant : 5 défaillances silencieuses, 6 heures et plus chacune, découvertes des heures ou des jours après leur début. Après : les vérifications de contenu attrapent la corruption en 60 secondes, les alertes me préviennent en 15 minutes, et les requêtes de logs font passer le diagnostic de quelques heures à quelques minutes.

La session de débogage la plus productive est celle qui n’arrive jamais parce que vous avez attrapé le souci au moment du déploiement. La deuxième plus productive est celle où vous avez déjà les données dont vous avez besoin.

Ce même réflexe « l’attraper avant la production » s’applique au code événementiel : voir Tester Lambda + EventBridge en local pour reproduire ces défaillances sur votre machine plutôt que dans les logs de prod.

Mettez en place du logging avec des IDs de requête. Faites en sorte que vos health checks vérifient le contenu, pas juste l’existence du processus. Ajoutez une surveillance de disponibilité avec des alertes. Envoyez vos logs quelque part où on peut les chercher. Lancez des tests de fumée après chaque déploiement.

Faites-le avant d’en avoir besoin. Parce que vous en aurez besoin à 2 h du matin, et ce n’est pas le moment de mettre en place une agrégation de logs.