Les incidents de disque plein n’annoncent pas souvent leur arrivée poliment. Ils se manifestent par « impossible de créer le fichier », « base de données en lecture seule » ou « nœud non prêt », puis vous découvrez que le coupable est un conteneur qui a traité stdout comme un journal intime.
La journalisation Docker est simple par conception : écrivez sur stdout/stderr, et le runtime s’en occupe. Le runtime écrit aussi fidèlement chaque octet—qu’il soit utile, redondant, ou une même erreur imprimée dix mille fois par minute. Si vous voulez préserver vos disques (et votre santé mentale en astreinte), les plus grands gains se trouvent à la source : dans l’application, à la ligne précise où le log est émis.
Pourquoi « corriger dans Docker » ne suffit pas
Oui, vous devriez configurer la rotation des logs de Docker. C’est la base. Mais c’est aussi du contrôle des dégâts. Si votre application journalise comme un commissaire-priseur paniqué, la rotation transforme un gros problème en plusieurs petits problèmes qui continuent de remplir le disque, de consumer le CPU, de saturer les E/S, d’étouffer votre signal dans le bruit, et d’alourdir les coûts d’ingestion des systèmes de centralisation.
Les plateformes de conteneurs encouragent un péché particulier : « imprimez tout sur stdout ». C’est le chemin de moindre résistance et le chemin du maximum de regrets. Un runtime de conteneur ne connaît pas votre intention. Il ne peut pas distinguer « échec du paiement utilisateur » de « debug : itération de boucle 892341 ». Il écrit juste des octets.
Limiter à la source signifie : générer moins d’événements de logs, et faire en sorte que ceux qui sont générés soient plus compressibles, plus recherchables et plus exploitables. C’est là que les ingénieurs applicatifs et les SRE se rencontrent dans le couloir et s’accordent : moins de logs, mieux vaut des logs de qualité.
Voici la vérité opérationnelle : le volume de logs est une caractéristique de performance. Traitez-la comme la latence. Mesurez-la. Budgétez-la. Les régressions doivent faire échouer les builds.
Une citation à mettre sur chaque revue de PR de journalisation :
Idée paraphrasée — Werner Vogels : vous le construisez, vous l’exploitez ; la responsabilité inclut ce que votre logiciel fait en production, y compris son bruit.
Faits et contexte expliquant le désordre actuel des logs
Ce ne sont pas des anecdotes. Ce sont les raisons pour lesquelles vos disques se retrouvent pleins de bêtises parfaitement conservées.
- Unix considérait d’abord les logs comme des fichiers, ensuite comme des flux. Syslog et les fichiers texte sont arrivés avant le « tout sur stdout ». La journalisation conteneurisée a inversé le transport par défaut vers les flux.
- Le driver de logging par défaut originel de Docker (
json-file) écrit un objet JSON par ligne. C’est à la fois lisible pour l’humain, ingérable par machine, et dangereux parce que facile à faire grossir sans limites. - Les « 12-factor » ont popularisé la journalisation sur stdout. Pratique pour la portabilité. Mais cela n’a pas apporté de discipline intégrée pour contrôler le volume ; cette responsabilité vous incombe.
- Les fournisseurs d’agrégation de logs facturent à l’ingestion. Votre directeur financier peut maintenant être impacté par un simple
logger.debugdans une boucle chaude. - La culture microservices initiale a normalisé « tout logger ; rechercher après ». Ça fonctionnait quand le trafic était faible et les systèmes peu nombreux. À grande échelle, c’est comme sauvegarder chaque frappe au clavier « au cas où ».
- La journalisation structurée a fait son retour parce que grep n’était plus suffisant. Les logs JSON sont excellents—jusqu’à ce que vous émettiez 20 champs par requête et tripliez vos octets.
- Les conteneurs ont rendu ambiguë la persistance des logs. Dans les VM, vous faisiez de la rotation de fichiers. Dans les conteneurs, vous n’avez souvent pas un système de fichiers inscriptible digne de confiance, donc les gens écrivent sur stdout en espérant le meilleur.
- Les étiquettes et champs à haute cardinalité sont devenus une taxe silencieuse. Les IDs de traces sont utiles ; ajouter des entrées utilisateur uniques comme champ sur chaque ligne, c’est construire un lac de données par accident.
Mode d’intervention rapide pour le diagnostic
Si vous êtes d’astreinte et que le nœud hurle, vous n’avez pas le temps de philosopher. Vous devez trouver rapidement le goulot d’étranglement et décider s’il s’agit d’un problème de journalisation, du runtime ou du stockage.
Première étape : confirmer le symptôme et le périmètre (disque vs E/S vs CPU)
- Pression disque :
dfmontre le système de fichiers proche de 100%. - Pression E/S : temps d’attente élevés, IOPS d’écriture importants, réponses applicatives lentes.
- Pression CPU : la sérialisation des logs et le formatage JSON peuvent consommer du CPU, surtout avec des traces de pile et des objets volumineux.
Deuxième étape : identifier le plus gros émetteur (conteneur, processus ou agent hôte)
- Cherchez les plus gros fichiers de logs Docker et les conteneurs associés.
- Vérifiez si un expéditeur de logs (Fluent Bit, Filebeat, etc.) amplifie le problème avec des boucles de retry/backpressure.
- Confirmez si l’application répète le même message ; si oui, appliquez un rate-limit ou une déduplication côté application.
Troisième étape : décider de la mitigation la plus rapide et sûre
- Atténuation d’urgence : arrêter/redémarrer le pire coupable, appliquer la rotation si elle manque, réduire le niveau de logs via un flag de config, ou échantillonner temporairement les logs.
- Correctif post-incident : changer les patterns de journalisation pour que l’incident ne puisse pas se reproduire à partir d’un seul chemin de code.
Blague #1 : Si votre disque se remplit de logs, félicitations — vous avez inventé une base de données très coûteuse et très lente sans index.
Tâches pratiques : commandes, sorties, décisions
Voici les contrôles que vous effectuez sur un hôte réel à 02:13. Chaque tâche inclut la commande, ce que signifie la sortie et la décision à en tirer.
Tâche 1 : Confirmer la pression disque et quel système de fichiers est affecté
cr0x@server:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 220G 214G 2.9G 99% /
tmpfs 32G 0 32G 0% /dev/shm
Sens : le système racine est essentiellement plein. Les conteneurs et leurs logs résident souvent sous /var/lib/docker sur /.
Décision : Ne lancez pas de « scripts de nettoyage » à l’aveugle. Identifiez d’abord ce qui consomme l’espace ; évitez de supprimer l’état d’exécution à moins d’accepter une indisponibilité.
Tâche 2 : Trouver les plus grands répertoires sous le stockage Docker
cr0x@server:~$ sudo du -xhd1 /var/lib/docker | sort -h
1.2G /var/lib/docker/containers
8.4G /var/lib/docker/image
12G /var/lib/docker/overlay2
22G /var/lib/docker
Sens : containers est suffisamment volumineux pour suspecter une croissance des logs. overlay2 peut aussi être large à cause des layers inscriptibles.
Décision : Approfondir /var/lib/docker/containers pour trouver les gros fichiers de logs et les mapper aux conteneurs.
Tâche 3 : Localiser les plus gros fichiers de logs de conteneurs
cr0x@server:~$ sudo find /var/lib/docker/containers -name "*-json.log" -printf "%s %p\n" | sort -n | tail -5
2147483648 /var/lib/docker/containers/2c1c3e.../2c1c3e...-json.log
3221225472 /var/lib/docker/containers/7a8b9c.../7a8b9c...-json.log
4294967296 /var/lib/docker/containers/aa0bb1.../aa0bb1...-json.log
Sens : vous avez des fichiers JSON de plusieurs gigaoctets. Ce n’est pas « quelques logs de debug », c’est une prise d’eau de logs.
Décision : Identifiez les conteneurs derrière ces IDs et inspectez ce qu’ils émettent.
Tâche 4 : Mapper un ID de conteneur à un nom et une image
cr0x@server:~$ docker ps --no-trunc --format "table {{.ID}}\t{{.Names}}\t{{.Image}}" | grep aa0bb1
aa0bb1d3f0e9c1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4 payments-api registry.local/payments-api:3.14.2
Sens : le conteneur payments-api génère le fichier de logs massif.
Décision : Inspecter les logs récents et chercher des motifs de répétition (même ligne, même trace de pile, même chemin de requête).
Tâche 5 : Prélever un échantillon des logs récents sans vider le fichier entier
cr0x@server:~$ docker logs --tail 50 payments-api
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
Sens : erreurs identiques répétées. Probablement une boucle de retry qui logge chaque tentative.
Décision : Mitiger maintenant en réduisant le niveau de logs ou en limitant le débit de ce message spécifique. Puis corriger le code : logger une fois par fenêtre d’échec, pas une fois par retry.
Tâche 6 : Vérifier le driver de logging du conteneur et ses options
cr0x@server:~$ docker inspect -f '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' payments-api
json-file {"max-file":"1","max-size":"0"}
Sens : max-size vaut 0 (effectivement illimité) tandis que max-file est sans effet.
Décision : Corriger la configuration du runtime (compose/systemd/daemon.json) mais ne vous arrêtez pas là ; l’application émet toujours trop.
Tâche 7 : Vérifier les valeurs par défaut du daemon
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Sens : si ceci est présent, les nouveaux conteneurs devraient tourner. Les conteneurs existants ont peut-être été créés avant ces valeurs par défaut, ou ont des overrides par conteneur.
Décision : Standardiser les chemins de création. S’assurer que les stacks compose ou les specs d’orchestrateur ne contournent pas ces limites.
Tâche 8 : Identifier les écritures E/S élevées causées par la journalisation
cr0x@server:~$ iostat -xz 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
12.31 0.00 6.44 38.27 0.00 42.98
Device r/s rkB/s rrqm/s %rrqm r_await w/s wkB/s w_await aqu-sz %util
nvme0n1 2.1 86.3 0.0 0.0 3.21 912.4 18432.0 42.10 39.2 98.7
Sens : utilisation d’écriture extrêmement élevée et w_await élevé. L’écriture des logs sur disque peut dominer le temps du périphérique.
Décision : Réduire le volume de logs maintenant. Si vous continuez à écrire à ce rythme, le disque devient le goulot pour tout le reste.
Tâche 9 : Confirmer quels processus écrivent le plus
cr0x@server:~$ sudo pidstat -d 1 3
Linux 6.5.0 (server) 01/03/2026 _x86_64_ (16 CPU)
01:12:11 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
01:12:12 0 2471 0.00 18240.00 0.00 dockerd
01:12:12 0 19382 0.00 3100.00 0.00 fluent-bit
Sens : le démon Docker écrit d’énormes volumes (logs des conteneurs). L’expéditeur (shipper) écrit/traite aussi beaucoup.
Décision : Traiter d’abord le conteneur source ; puis tuner le buffering/retry du shipper pour éviter les boucles de rétroaction.
Tâche 10 : Vérifier si l’application logge des traces de pile répétées
cr0x@server:~$ docker logs --tail 200 payments-api | grep -c "Traceback\|Exception\|stack"
147
Sens : les traces de pile fréquentes coûtent en octets et en CPU. Souvent la même exception qui se répète.
Décision : Logger une seule trace par erreur unique et par fenêtre temporelle ; émettre des compteurs/métriques pour le reste.
Tâche 11 : Identifier le taux d’événements de logs (lignes par seconde) depuis le fichier brut
cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' payments-api); sudo sh -c "tail -n 20000 /var/lib/docker/containers/$cid/${cid}-json.log | wc -l"
20000
Sens : voilà 20k lignes dans le segment tail récent. Si ce tail couvre « quelques secondes », vous inondez. Si c’est « quelques minutes », c’est quand même trop bavard.
Décision : Fixer un budget : par exemple état stable < 50 lignes/sec par instance ; les pics autorisés seulement avec échantillonnage et plafonds.
Tâche 12 : Mesurer la vitesse de croissance du fichier de logs
cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' payments-api); sudo sh -c "stat -c '%s %y' /var/lib/docker/containers/$cid/${cid}-json.log; sleep 5; stat -c '%s %y' /var/lib/docker/containers/$cid/${cid}-json.log"
4294967296 2026-01-03 01:12:41.000000000 +0000
4311744512 2026-01-03 01:12:46.000000000 +0000
Sens : ~16 Mo en 5 secondes (~3.2 Mo/s). Ça remplira les disques rapidement et asphyxiera les E/S.
Décision : Mitigation immédiate : réduire le niveau, désactiver le composant bruyant, redémarrer avec un flag d’env sûr. Long terme : implémenter throttling/déduplication.
Tâche 13 : Vérifier si le conteneur redémarre en boucle et génère des logs de démarrage verbeux
cr0x@server:~$ docker inspect -f '{{.State.Status}} {{.RestartCount}}' payments-api
running 47
Sens : nombreux redémarrages. Chaque redémarrage peut réémettre de grandes bannières/dumps de config, multipliant le bruit.
Décision : Corriger la cause des plantages et supprimer les dumps verbeux de démarrage ; faire en sorte que les informations de démarrage « one-time » le soient vraiment.
Tâche 14 : Vérifier si les shippers sont en backpressure et réessaient (amplifiant le bruit)
cr0x@server:~$ docker logs --tail 50 fluent-bit
[warn] [output:es:es.0] HTTP status=429 URI=/_bulk
[warn] [engine] failed to flush chunk '1-173587...' retry in 8 seconds
Sens : le système aval est en throttling. Vos logs ne remplissent pas seulement le disque ; ils provoquent aussi une tempête de retries et du buffering mémoire/disque.
Décision : Réduire le volume à la source, puis tuner le buffering du shipper et envisager de supprimer les logs peu utiles sous pression.
Tâche 15 : Inspecter la fréquence d’un message suspect (lignes répétées principales)
cr0x@server:~$ docker logs --tail 5000 payments-api | jq -r '.msg' | sort | uniq -c | sort -nr | head
4821 db timeout
112 cache miss
45 payment authorized
Sens : un message domine. C’est une cible parfaite pour la déduplication et le rate limiting.
Décision : Remplacer le log par événement par : (a) un résumé périodique, (b) un compteur métrique, (c) un exemplaire échantillonné avec contexte.
Modèles de journalisation applicative qui réduisent réellement le volume
C’est le cœur : des patterns de codage et des contrats opérationnels qui empêchent le spam de logs. Vous pouvez les implémenter dans n’importe quel langage ; les principes ne dépendent pas du framework.
1) Arrêtez de logger à chaque tentative de retry ; loggez par fenêtre de résultat
Les retries sont normaux. Logger chaque retry ne l’est pas. Si une dépendance est en panne, une boucle de retry peut créer un amplificateur parfait : les échecs provoquent des retries, les retries provoquent des logs, les logs provoquent de la pression E/S, la pression E/S provoque plus de timeouts, les timeouts provoquent plus d’échecs.
Faites ceci : loggez la première erreur avec le contexte ; ensuite limitez le débit des logs suivants ; puis émettez un résumé toutes les N secondes : « db timeout persiste ; 4 821 erreurs similaires supprimées ».
Un bon modèle :
- Un « exemplaire » d’erreur avec trace de pile et métadonnées de requête (mais voir les notes sur la confidentialité ci-dessous).
- Un compteur métrique pour chaque événement d’échec.
- Un résumé périodique par dépendance, par instance.
2) Choisissez un budget de logs en état stable et appliquez-le
La plupart des équipes débattent des formats de logs. Mieux vaut débattre du budget de débit. Par exemple :
- Par instance de service en état stable : < 1 KB/s moyen de débit de logs.
- Pic autorisé : jusqu’à 50 KB/s pendant 60 secondes en cas d’incident.
- Au-delà du pic : échantillonner à 1 %, conserver des exemplaires d’erreur, supprimer debug/info.
Cela donne aux SRE un seuil clair de type SLO et aux équipes applicatives un objectif testable. Ajoutez un contrôle CI qui exécute un test de charge synthétique et échoue si les logs dépassent le budget.
3) Par défaut, préférez les logs structurés, mais ne sur-structurez pas
Les logs JSON sont la norme dans le monde des conteneurs. Ils sont aussi faciles à abuser. Chaque champ supplémentaire coûte en octets. Certains champs coûtent aussi en frais d’indexation.
Conserver : timestamp, level, message, nom du service, ID d’instance, request ID/trace ID, latence, code de statut, nom de dépendance, classe d’erreur.
Éviter : corps de requête complet, tableaux sans limite, chaînes SQL brutes, et labels à haute cardinalité copiés sur chaque ligne.
4) Ne loggez pas dans des boucles serrées sans throttle
Les boucles apparaissent partout : polling, consommation de files, scan de répertoires, retries de locks, vérifications de santé de connexion. Si vous loggez dans une boucle, vous avez créé un incident futur. Pas une possibilité ; un rendez-vous planifié.
Règle : toute instruction de log susceptible de s’exécuter plus d’une fois par seconde en état stable doit être protégée par un limiteur de débit, une porte basée sur un changement d’état, ou les deux.
5) Loggez les changements d’état, pas les confirmations d’état
« Toujours connecté » toutes les 5 secondes est un gaspillage. « Connexion rétablie après 42 secondes, 500 échecs supprimés » est utile. Les humains ont besoin de transitions. Les machines ont besoin de compteurs.
Implémentez une machine d’état simple pour la santé des dépendances (UP → DEGRADED → DOWN) et n’émettez des logs que sur les transitions et les résumés périodiques.
6) Utilisez des « clés de déduplication » pour les erreurs répétées
Les erreurs répétées partagent souvent une signature : même type d’exception, même dépendance, même endpoint. Calculez une clé de déduplication comme :
dedupe_key = hash(error_class + dependency + path + error_code)
Puis gardez une petite map en mémoire par processus : timestamp du dernier aperçu, compteur de suppressions, et un exemplaire. Émettre :
- Première occurrence : log normal.
- Dans la fenêtre : incrémenter le compteur de suppressions, éventuellement émettre du debug échantillonné.
- Fin de fenêtre : log de résumé avec nombre supprimé et ID d’un exemplaire.
7) Échantillonnez les logs d’information ; ne jamais échantillonner les métriques
L’échantillonnage est un scalpel. Utilisez-le pour les logs à fort volume et faible valeur : logs d’accès par requête, « cache miss », « job démarré ». Gardez les erreurs majoritairement non échantillonnées, mais vous pouvez échantillonner les erreurs identiques répétées après le premier exemplaire.
Les métriques servent à compter. Un compteur est peu coûteux et précis. Ne remplacez pas les métriques par des logs ; c’est comme remplacer un thermomètre par une danse interprétative.
8) Faites du « mode debug » un coupe-circuit, pas un simple niveau
Les logs debug en production doivent être temporaires, ciblés et réversibles sans redéploiement. L’approche la plus sûre :
- Les logs debug existent, mais sont désactivés par défaut.
- Activer le debug pour un request ID spécifique, un user ID (haché), ou un tenant pour une durée limitée.
- Désactivation automatique après un TTL.
Cela évite l’erreur classique : « on a activé le debug pour enquêter, on a oublié, et on l’a payé pendant une semaine ».
9) Cessez de logger les erreurs « attendues » en niveau ERROR
Si un client annule une requête, ce n’est pas une erreur ; c’est un mardi. Si un utilisateur saisit un mauvais mot de passe, ce n’est pas une erreur serveur ; c’est la réalité produit. Si vous loggez ça en error, vous apprenez à l’astreinte à ignorer ERROR. C’est ainsi qu’on manque une vraie panne.
Pattern :
- Utiliser
infoouwarnpour les échecs pilotés par le client. - Utiliser
errorpour les défaillances côté serveur nécessitant de l’attention. - Utiliser
fatalrarement, et seulement quand le processus va quitter.
10) Supprimez les payloads ; loggez des pointeurs
Logger des corps de requête/réponse complets est un consommateur de disque et un piège de confidentialité. À la place :
- Logger la taille du payload (octets).
- Logger un hash de contenu (pour corréler les répétitions sans stocker le contenu).
- Logger un ID d’objet qui peut être récupéré depuis un stockage sécurisé si besoin.
11) Rendez les traces de pile optionnelles et bornées
Les traces de pile peuvent être précieuses. Elles peuvent aussi faire 200 lignes de bruit répétées 10 000 fois. Limitez-les :
- Inclure les traces pour la première occurrence d’une clé de déduplication par fenêtre.
- Tronquer la profondeur de pile quand possible.
- Préférer type d’exception + message + frames supérieurs pour les répétitions.
12) Utilisez un logger « une seule fois » pour la configuration de démarrage
Les logs de démarrage impriment souvent la config complète, l’environnement, les flags et les dépendances. C’est acceptable une fois. C’est le chaos quand le processus redémarre en boucle et l’imprime 50 fois.
Pattern : logger un résumé compact de démarrage et un hash de config. Stocker la config détaillée ailleurs (ou l’exposer via un endpoint protégé), pas dans les logs.
13) Traitez la journalisation comme une dépendance avec backpressure
La plupart des bibliothèques de logging font comme si les écritures étaient gratuites. Elles ne le sont pas. Quand la sortie bloque (disque lent, pipe stdout bloqué, pression du driver), votre application peut se bloquer.
Faites ceci :
- Préférer la journalisation asynchrone avec des queues bornées.
- Quand la queue est pleine, abandonner d’abord les logs de faible priorité.
- Exposer des métriques : logs abandonnés, profondeur de queue, temps de logging.
14) Rendez les logs plus faciles à compresser
Si vous ne pouvez pas réduire suffisamment le volume, faites au moins en sorte qu’il se compresse bien. La répétition se compresse. L’aléatoire non. Une bonne journalisation :
- Utilise des templates de message stables :
"db timeout"et non"db timeout after 123ms on host a1b2"embarqué dans la chaîne message. - Place les données variables dans des champs, pas dans le message.
- Évite d’imprimer des UUID aléatoires sur chaque ligne sauf si nécessaire pour la corrélation.
15) Ajoutez un « fusible de logs » pour les urgences
Parfois, il vous faut un coupe-circuit : « si les logs dépassent X lignes/sec pendant Y secondes, augmentez automatiquement l’échantillonnage et supprimez les INFO/WARN répétitifs ». Ce n’est pas joli, mais c’est mieux qu’une panne disque.
Implémentez-le avec un compteur local et une fenêtre mouvante. Lorsqu’il se déclenche, émettez un log clair : « fusible de logs engagé ; échantillonnage 1% ; N lignes supprimées ».
Blague #2 : La journalisation, c’est comme le café — de petites quantités améliorent les performances, mais trop transforme votre système en un fou qui parle sans arrêt.
Trois mini-histoires d’entreprise (anonymisées, plausibles, et techniquement exactes)
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Ils supposaient que Docker faisait la rotation des logs par défaut. L’équipe était passée d’une configuration VM où logrotate était omniprésent, et a traité le runtime de conteneur comme un remplaçant moderne avec des valeurs par défaut raisonnables.
Pendant un test d’intégration avec un partenaire, un service a commencé à échouer à l’authentification. Le service avait une politique de retry assez normale : backoff exponentiel avec jitter. Mais le développeur avait ajouté un log error dans la boucle de retry pour « rendre visible » le problème. Ce fut visible. Et implacable.
Le premier signe de problème n’était pas une alerte d’utilisation disque. C’était un nœud de base de données se plaignant de requêtes lentes. L’hôte exécutant le conteneur bavard avait son disque racine proche du plein, et la latence E/S avait déraillé. Le driver de logging écrivait des lignes JSON comme un métronome.
L’astreinte a fait ce que font les humains : redémarrer le service. Cela a réduit temporairement le volume de logs parce que cela a acheté quelques secondes avant que les retries ne rampent à nouveau. Ils ont redémarré encore. Même résultat. Pendant ce temps, le shipper de logs réessayait l’ingestion parce que l’aval était throttlé, ce qui ajoutait une autre couche de churn d’écriture.
Le correctif fut ridiculement simple : ajouter la rotation au niveau Docker et modifier l’app pour ne logger que la première erreur par fenêtre, puis résumer. La leçon fut plus nette : « les valeurs par défaut supposées » ne sont pas une stratégie de fiabilité.
Mini-histoire 2 : L’optimisation qui a mal tourné
Une autre organisation voulait une « observabilité parfaite ». Ils ont ajouté de la journalisation structurée partout, ce qui est bien. Puis ils ont décidé que chaque ligne de log devait inclure le contexte complet de la requête pour faciliter le debug : headers, params, et un morceau du body.
Ça fonctionnait magnifiquement en staging. En production, c’est devenu un brasier de coûts d’ingestion. Pire, c’est devenu un problème de performance : la sérialisation JSON de gros objets pour chaque requête a mangé du CPU, et le runtime du conteneur écrivait des lignes plus grosses. La latence a augmenté, ce qui a créé plus de timeouts, plus d’erreurs, et donc des traces de pile plus volumineuses. Une boucle de rétroaction classique.
Le symptôme d’astreinte ressemblait à un problème de capacité : « il nous faut des nœuds plus gros ». Mais le vrai goulot était auto-infligé : pression I/O et CPU causée par la journalisation. Lorsqu’ils ont réduit le logging des payloads et sont passés à des « pointeurs de log » (request ID, hash du payload, taille), le système s’est stabilisé sans changer la taille des instances.
L’optimisation visait à « réduire le temps de debug en tout loggant ». Le retour de bâton fut « augmenter le taux d’incidents et les coûts en loggant tout ». La fin heureuse : ils ont gardé des logs structurés — juste pas les parties qui appartenaient à un store de traces sécurisé.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Un service proche des finances traitait des jobs batch périodiques. Rien de sexy. L’équipe avait une pratique presque à l’ancienne : chaque service avait un budget de logs écrit et un test mesurant le débit de logs sous charge. Si un changement augmentait les logs au-delà du budget, le build échouait à moins que l’ingénieur ne le justifie.
Un vendredi, une dépendance a commencé à renvoyer des 500 intermittents. Le service a réessayé, mais les patterns de logs étaient déjà limités et dédupliqués. Ils ont émis un exemplaire d’erreur avec un trace ID, puis un résumé toutes les 30 secondes : « erreurs dépendance persistantes ; N supprimées ». Les compteurs métriques ont grimpé, les alertes se sont déclenchées, mais les disques sont restés calmes.
Tandis que d’autres équipes luttaient contre la pression disque et étaient noyées sous des traces de pile répétées, ce service restait lisible. L’astreinte pouvait voir ce qui avait changé (comportement de la dépendance), le quantifier (métriques), et le corréler (trace IDs). L’incident fut toujours pénible, mais il n’a pas muté en panne au niveau du nœud.
Après coup, personne n’a écrit un grand post interne sur « l’héroïsme ». C’était ennuyeux. C’est le but. Les pratiques de fiabilité ennuyeuses vieillissent bien.
Erreurs courantes : symptôme → cause racine → correctif
1) Symptom : Les logs Docker croissent sans limite
Cause racine : driver json-file sans max-size/max-file, ou des conteneurs créés avant la mise en place des valeurs par défaut.
Correctif : Définir des valeurs par défaut au niveau du daemon et appliquer une configuration par service. Recréer les conteneurs pour prendre en compte les limites. Corriger aussi l’application pour qu’elle n’émette pas de déchets.
2) Symptom : Disque plein après une panne de dépendance
Cause racine : boucle de retry qui logge chaque tentative (souvent avec traces de pile).
Correctif : logger la première erreur + résumé ; compter les retries en métriques ; limiter le débit des logs par clé de déduplication ; ajouter des coupe-circuits.
3) Symptom : L’astreinte ignore ERROR parce que c’est toujours bruyant
Cause racine : événements clients attendus loggés en error (annulations, 4xx, échecs de validation).
Correctif : corriger la cartographie des niveaux et les règles d’alerte ; réserver ERROR aux fautes serveurs actionnables.
4) Symptom : CPU élevé sans augmentation claire de la charge métier
Cause racine : formatage de logs coûteux (interpolation de chaîne, sérialisation JSON d’objets volumineux) sur des chemins chauds.
Correctif : journalisation paresseuse (ne formater que si activé), éviter de sérialiser des objets entiers, précompiler des templates de message, échantillonner les logs de faible valeur.
5) Symptom : Le shipper de logs montre des retries, une croissance mémoire ou des chunks droppés
Cause racine : throttling en aval couplé à un volume en amont élevé ; le buffering du shipper amplifie l’utilisation disque.
Correctif : réduire le volume d’app ; configurer le backpressure et les politiques de drop du shipper ; prioriser les exemplaires d’erreurs et les résumés.
6) Symptom : « On ne trouve pas les lignes pertinentes » pendant les incidents
Cause racine : champs de contexte manquants (request ID, version du service, nom de dépendance) et trop de bruit répétitif.
Correctif : ajouter les champs de contexte essentiels ; dédupliquer les logs répétitifs ; logger les transitions d’état ; garder des messages cohérents.
7) Symptom : Des données sensibles apparaissent dans les logs
Cause racine : logging de payloads request/response, dumps d’en-têtes, ou messages d’exception contenant des secrets.
Correctif : redaction à la source, arrêter de logguer les payloads, ajouter des allowlists pour les champs, auditer automatiquement les logs, traiter les logs comme des données de production.
8) Symptom : La « solution » a été d’augmenter la taille du disque, mais le problème revient
Cause racine : pansement de capacité ; aucun changement des patterns d’émission.
Correctif : implémenter des budgets de logs, appliquer des limites de débit, et ajouter des tests de régression pour le volume de logs.
Listes de contrôle / plan étape par étape
Étape par étape : arrêter l’hémorragie pendant un incident actif
- Confirmer l’utilisation disque :
df -h. Si le root > 95%, considérer comme urgent. - Trouver les fichiers de logs principaux :
find /var/lib/docker/containers -name "*-json.log"triés par taille. - Mapper fichier → conteneur :
docker ps --no-truncetdocker inspect. - Identifier la répétition : prélever des logs récents ; vérifier les messages les plus répétés.
- Mitiger rapidement : réduire temporairement le niveau de logs, activer l’échantillonnage, ou désactiver le composant bruyant. Si besoin, redémarrer le conteneur avec des paramètres plus sûrs.
- Restaurer de l’espace : une fois l’émission arrêtée, supprimer ou tronquer uniquement le fichier de log le plus problématique si vous acceptez de perdre ces logs. Préférer la rotation et des redémarrages contrôlés plutôt que la suppression manuelle.
- Confirmer la récupération des E/S :
iostatet les latences des services devraient se normaliser.
Étape par étape : prévenir la récidive (après l’incident)
- Définir les valeurs par défaut Docker :
max-sizeetmax-filedans/etc/docker/daemon.json. - Auditer les overrides par service : fichiers compose, unités systemd, specs orchestrateur.
- Instrumenter le volume de logs : suivre lignes/sec et octets/sec par instance de service.
- Implémenter dédup + limites de débit : par signature d’erreur, par dépendance.
- Remplacer le spam par des résumés : rollups périodiques, plus exemplaires.
- Déplacer le contexte volumineux vers les traces : garder les logs légers ; utiliser des request IDs pour pivoter.
- Ajouter un garde-fou CI : test de charge et échec sur régressions de budget de logs.
- Faire une revue de confidentialité : rediger, allowlist, et vérifier que les secrets ne peuvent pas fuir.
Checklist opérationnelle : à quoi ressemble le « bien »
- Les logs ERROR sont rares, actionnables, et non dominés par une seule ligne répétée.
- Les logs Info sont échantillonnés ou limités sur les chemins chauds (requêtes, consommateurs de files).
- Chaque service a un budget de logs et un taux stable connu.
- Chaque erreur répétitive a une clé de déduplication, une fenêtre de suppression et une ligne de résumé.
- Les logs contiennent le contexte nécessaire pour corréler (trace/request ID, version), pas les données qu’il ne faut pas stocker.
- Quand l’ingestion est throttlée, le système se dégrade proprement (suppression d’abord des logs de faible valeur).
FAQ
1) Dois-je simplement changer le driver de logging Docker pour régler ça ?
Non. Changer de driver peut aider pour la rotation, l’acheminement ou les caractéristiques de performance, mais cela ne résout pas une application qui émet des déchets. Corrigez l’émission d’abord ; puis choisissez le driver selon les besoins opérationnels.
2) Journaliser sur stdout est-ce toujours la bonne approche dans les conteneurs ?
C’est l’approche standard, pas automatiquement la bonne. Stdout convient si vous le traitez comme un canal contraint avec budgets, échantillonnage et limites de débit. Si vous avez besoin de logs locaux durables, utilisez un volume et gérez la rotation—mais ce doit être une décision délibérée, pas un accident.
3) Quel niveau de logs pour la production ?
Typiquement info ou warn, avec des bascules debug ciblées. Si vous avez besoin de debug en permanence pour opérer, il vous manque probablement des métriques, des traces ou du contexte structuré.
4) Comment convaincre les équipes d’arrêter de logger les corps de requête ?
Dites-leur la vérité : c’est un risque de fiabilité et de sécurité. Proposez une alternative : logger des request IDs, tailles de payload, hashes, et stocker les payloads détaillés dans un système sécurisé et contrôlé si vraiment nécessaire.
5) Quelle est la méthode la plus simple de rate-limiting côté application ?
Une fenêtre temporelle par message (ou par clé de déduplication) : logger la première occurrence, puis supprimer pendant N secondes en comptant les suppressions, puis émettre un résumé.
6) L’échantillonnage ne rendra-t-il pas le debug plus difficile ?
L’échantillonnage rend le debug possible quand l’alternative est d’être noyé. Conservez des exemplaires (première occurrence, signatures uniques) et des compteurs métriques pour l’exhaustivité. Vous ne pouvez pas déboguer ce que vous ne pouvez pas lire.
7) Comment détecter le spam de logs avant qu’il n’affecte un nœud ?
Alertez sur le taux de croissance des logs (octets/sec) et sur les changements soudains des messages répétés principaux. Si vous n’alertez que sur « disque > 90% », vous serez prévenu trop tard.
8) Pourquoi les traces de pile répétées font-elles autant de dégâts ?
Ce sont des blocs volumineux, lentes à formater et souvent identiques. Elles gaspillent CPU et disque, et ruinent le signal de recherche. Gardez un exemplaire par fenêtre ; comptez le reste.
9) Puis-je supprimer en toute sécurité un gros fichier *-json.log pour récupérer de l’espace ?
Parfois, mais c’est un outil tranchant. Supprimer un fichier encore ouvert par un processus peut ne pas libérer l’espace tant que le handle est fermé. Préférez la rotation, le redémarrage de conteneur ou la troncature contrôlée pendant un incident—puis corrigez l’émission sous-jacente.
10) Comment conserver l’utilité des logs en réduisant le volume ?
Rendez les logs événementiels : transitions d’état, résumés, exemplaires. Poussez les détails à fort volume dans les métriques (comptes) et les traces (contexte riche par requête). Les logs doivent expliquer les incidents, pas les recréer.
Prochaines étapes à faire cette semaine
Si vous ne faites qu’une seule chose, faites celle-ci : supprimez la journalisation dans les boucles de retry et remplacez-la par des exemplaires dédupliqués plus des résumés périodiques. Ce seul pattern prévient toute une classe d’incidents de disque plein et de thrash I/O.
Puis :
- Définir les valeurs par défaut de rotation des logs Docker et vérifier que chaque conteneur les hérite réellement.
- Définir un budget de logs par service et mesurer lignes/sec et octets/sec sous charge.
- Implémenter des limites de débit et des clés de déduplication pour les erreurs répétées et les logs des chemins chauds.
- Arrêter de logger les payloads ; logger des pointeurs et des hashes à la place.
- Ajouter un « fusible de logs » pour qu’un mauvais déploiement ne puisse pas faire tomber un nœud en parlant trop.
Vous ne gagnez pas en fiabilité en écrivant plus de logs. Vous la gagnez en rendant les logs que vous conservez dignes des octets qu’ils occupent.