
OĂč en est lâenquĂȘte ? Un mardi soir, un virement de 15 000 ⏠sâest volatilisĂ© sur une plateforme dâinvestissement : validĂ© Ă 22h13, jamais arrivĂ© chez le prestataire, figĂ© en base en statut processing. Lâarticle 2 a montrĂ© pourquoi aucune trace de sa mort nâexistait et comment journaliser pour quâelle existe la prochaine fois. Depuis, lâenquĂȘte a avancĂ© dâun cran : le job sâest exĂ©cutĂ© Ă 22h39, vingt-six minutes aprĂšs son enfilement. Deux questions restent ouvertes. Pourquoi ce retard ? Et que faisaient ces GET /documents en volume anormal Ă la mĂȘme heure ?
Cette sĂ©rie suit un mĂȘme incident, en fil rouge, au fil des techniques abordĂ©es. Les encadrĂ©s « âȘ Ce que vous auriez vu » montrent les lignes de log ou les Ă©crans que le bon outillage aurait produits cette nuit-lĂ , et qui nâont jamais existĂ© faute de prĂ©paration. Vous pouvez les sauter : le reste se lit comme un tutoriel autonome.
Sans outillage, lâenquĂȘte se fait avec ce qui traĂźne : la base, les access logs de nginx, le log Rails brut, Redis.
La latence, dâabord. La table transfers porte un created_at (validation du virement) et un updated_at (dernier changement dâĂ©tat, en pratique le passage en sent). Pour notre virement, le delta ne dit rien : restĂ© en processing, il nâa jamais changĂ© dâĂ©tat. Pour les autres virements de la soirĂ©e, qui ont fini par partir, il apprend quelque chose : entre 22h et 23h, tous ont mis vingt Ă quarante minutes Ă passer en sent, contre quelques secondes la veille. Tous les jobs de la soirĂ©e Ă©taient en retard dans la file dâattente, le nĂŽtre comme les autres.
Le volume, ensuite. Les access logs de nginx contiennent chaque requĂȘte avec son chemin et son horodatage. Pour obtenir le nombre de tĂ©lĂ©chargements de documents par jour sur douze jours, il faut rassembler les fichiers de rotation, filtrer sur le chemin, extraire la date et compter :
awk '$7 ~ /^\/documents\// { split($4, t, ":"); d = substr(t[1], 2); c[d]++ }
END { for (d in c) print d, c[d] }' access.log* | sortLa commande renvoie des dates et des nombres, quâon colle dans un tableur pour obtenir une courbe. Comptez quelques heures pour rĂ©cupĂ©rer les fichiers sur chaque serveur, et acceptez de perdre les lignes des conteneurs remplacĂ©s entre-temps. Et personne ne pourra reproduire ce graphique demain avec une question lĂ©gĂšrement diffĂ©rente.
Note : le champ $7 suppose le format combined par dĂ©faut de nginx. Avec un format personnalisĂ©, le numĂ©ro de colonne change et la commande casse en silence : zĂ©ro ligne, pas dâerreur.
Les rĂ©essais, enfin. Le job a Ă©chouĂ© Ă sa premiĂšre exĂ©cution. En relisant la politique de rĂ©essai de lâarticle 2 dans le code, on dĂ©duit les tentatives suivantes Ă 22h59, 23h19 et 23h39, et la mort du job Ă 23h59. Aucune ligne ne les atteste. Quant au dead set de Sidekiq, la rĂ©serve des jobs morts dont lâarticle 2 a dĂ©crit le plafond et lâĂ©viction des plus anciens, il ne contient plus rien dâutile : abaissĂ© Ă 1 000 jobs par la configuration hĂ©ritĂ©e, il a Ă©tĂ© rempli dans la nuit par des milliers de jobs morts, et les paiements, morts parmi les premiers, ont Ă©tĂ© poussĂ©s dehors.
Note : le plafond par dĂ©faut est de 10 000 jobs (dead_max_jobs), conservĂ©s six mois (dead_timeout_in_seconds). Un job mort pĂšse de quelques centaines dâoctets Ă quelques kilo-octets selon ses arguments ; mesurez ce que le dead set occupe rĂ©ellement dans Redis avant de le rĂ©duire.
Quatre limites se dĂ©gagent. On ne peut ni compter ni agrĂ©ger sans Ă©crire un script par question. Les logs sont Ă©parpillĂ©s entre serveurs et conteneurs Ă©phĂ©mĂšres. La rotation efface plus vite que la durĂ©e de conservation retenue Ă lâarticle 2. Et les journaux vivent sur la machine quâils sont censĂ©s surveiller.
Note (DORA) : ce dernier point a aussi une lecture rĂ©glementaire. Lâarticle 12 du rĂšglement dĂ©lĂ©guĂ© (UE) 2024/1774 impose aux entitĂ©s financiĂšres des mesures protĂ©geant les systĂšmes de journalisation et les logs contre la manipulation, la suppression et lâaccĂšs non autorisĂ©, au repos comme en transit. Lâexigence pĂšse sur le client rĂ©gulĂ© et redescend vers son prestataire par contrat. Des logs qui ne vivent que sur le serveur applicatif tombent avec lui, et un attaquant qui obtient la machine obtient aussi les traces de son passage, avec les moyens de les effacer.
Lâapplication Ă©crit sur sa sortie standard, un tuyau achemine ce flux vers un service extĂ©rieur, et ce service indexe les lignes pour quâon puisse les interroger.
Un drain est un tuyau qui recopie en continu la sortie de votre application vers un service extĂ©rieur. Un agent est un petit programme installĂ© sur le serveur qui fait ce mĂȘme travail en lisant les fichiers ou le flux des conteneurs. Lâensemble forme un pipeline de logs.

CĂŽtĂ© Rails, il nây a presque rien Ă faire si le travail de lâarticle 2 est fait : les logs sont en JSON avec Lograge et portent request_id et user_id.
Une application générée avec Rails 7.2 ou plus récent écrit déjà sur la sortie standard en production. Une application plus ancienne écrit dans log/production.log, sauf si la variable RAILS_LOG_TO_STDOUT est posée ; pour la basculer, on reprend ce que fait le template actuel :
# config/environments/production.rb (template Rails 7.2 / 8.0)
config.logger = ActiveSupport::Logger.new($stdout)
.tap { |logger| logger.formatter = ::Logger::Formatter.new }
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }La disponibilitĂ©, premier volet annoncĂ© pour cet article, a en fait Ă©tĂ© traitĂ©e Ă lâarticle 1 avec le health check et la sonde externe ; je nây reviens pas.
Lograge Ă©crit dans Rails.logger, donc il suit ; Sidekiq Ă©crit dĂ©jĂ sur la sortie standard. Le reste se rĂšgle cĂŽtĂ© hĂ©bergement, oĂč les options se rĂ©partissent en trois familles. Les solutions intĂ©grĂ©es Ă lâhĂ©bergeur (le drain de Heroku vers un add-on, CloudWatch Logs chez AWS, Cloud Logging chez Google) ne demandent aucune installation et sont rarement les plus confortables Ă interroger. Les suites dâobservabilitĂ© (Datadog, New Relic, Grafana Cloud, Better Stack) rĂ©unissent recherche, courbes et alerting au mĂȘme endroit, facturĂ©es au volume ingĂ©rĂ©. Les piles auto-hĂ©bergĂ©es (Loki avec Grafana, OpenSearch, la pile Elastic) coĂ»tent du temps dâexploitation plutĂŽt que de lâargent par gigaoctet : bon arbitrage Ă partir dâun certain volume, mauvais en dessous.
Note (coĂ»t) : le volume est le premier poste de la facture et la premiĂšre cause dâabandon dâun outil de centralisation. Deux leviers : ne pas logguer lâinutile, ce qui renvoie Ă lâarticle 2 ; et diffĂ©rencier les durĂ©es de conservation selon la finalitĂ©, car les logs de debug nâont aucune raison de vivre aussi longtemps que les traces dâaccĂšs ou de paiement, que la LCB-FT peut porter Ă cinq ans (voir lâarbitrage de lâarticle 2) et pour lesquelles DORA demande de fixer et documenter une durĂ©e selon leur finalitĂ©, bien au-delĂ des six mois Ă un an que la CNIL recommande pour des logs ordinaires.
Note (confidentialitĂ©) : centraliser, câest transmettre vos logs Ă un sous-traitant au sens du RGPD, avec le contrat et la ligne au registre des traitements qui vont avec. Le filtrage de lâarticle 2 prend ici tout son sens, et le piĂšge signalĂ© alors, les noms de fichiers dans les chemins dâURL que filter_parameters ne couvre pas, devient une fuite de donnĂ©es personnelles vers un tiers.
Une fois les logs indexĂ©s, ce qui demandait une matinĂ©e dâawk prend quelques secondes. Tous les outils savent les faire, avec des syntaxes diffĂ©rentes ; je dĂ©cris donc les requĂȘtes en français.
Compter. Filtrer sur path commençant par /documents/, grouper par jour, compter. RĂ©sultat : douze jours Ă quelques dizaines de tĂ©lĂ©chargements, puis plusieurs milliers le mardi. Câest la courbe de lâawk, obtenue cette fois par une requĂȘte que nâimporte qui peut rejouer.
Grouper. MĂȘme filtre, grouper par user_id. Câest ici que le travail de lâarticle 2 paie : sans le custom_payload qui a ajoutĂ© user_id Ă chaque ligne, nginx nâoffrait que des adresses IP. Trois adresses dans 203.0.113.0/24 ne mĂšnent nulle part. Un user_id rattache chaque requĂȘte Ă un compte. RĂ©sultat, pour le mardi soir : un compte Ă©crase tous les autres.
Superposer. Deux courbes sur la mĂȘme Ă©chelle de temps : la profondeur de la file Sidekiq default (le nombre de jobs en attente, que Sidekiq::Queue.new("default").size expose et quâun agent peut relever rĂ©guliĂšrement) et la latence des paiements, dĂ©lai entre enfilement et exĂ©cution dâaprĂšs les lignes de cycle de vie de lâarticle 2. La profondeur complĂšte la latence vue Ă lâarticle 1 : une file profonde de jobs rapides nâest pas un problĂšme, un vieux job qui attend en est un, et les deux courbes ensemble disent lequel des deux on regarde.

âȘ Ce que vous auriez vu. La figure 1. Rien de tout cela nâa Ă©tĂ© enregistrĂ© : la file nâĂ©tait pas mesurĂ©e, et personne nâa regardĂ© Redis avant le lendemain matin.
Une fois les deux courbes cĂŽte Ă cĂŽte, la mĂ©canique apparaĂźt. Chaque tĂ©lĂ©chargement dĂ©clenche deux jobs : un filigrane sur le PDF, qui appelle un service externe, et un scan antivirus. Ils tournent sur la file default. Les paiements aussi, puisque TransferJob dĂ©clare queue: :default. Le mardi soir, les tĂ©lĂ©chargements ont rempli la file plus vite que les workers (les processus qui exĂ©cutent les jobs) ne la vidaient, le service de filigrane a saturĂ©, ses appels ont Ă©chouĂ© puis ont Ă©tĂ© rĂ©essayĂ©s, et le virement a attendu derriĂšre des centaines de PDF. La question de fiabilitĂ© posĂ©e Ă lâarticle 1 est rĂ©glĂ©e.
Le filigrane sert Ă savoir qui a tĂ©lĂ©chargĂ© quoi si une piĂšce se retrouve dans la nature. Câest un dispositif de traçabilitĂ©, et câest lui qui a provoquĂ© la panne. Journaliser et tracer ont un coĂ»t, qui doit rester bornĂ©, et je retiens trois garde-fous : prĂ©fĂ©rer une Ă©criture synchrone et lĂ©gĂšre (une ligne de log, une insertion en base) Ă un job par Ă©vĂ©nement ; ne jamais mettre un appel rĂ©seau externe sur le chemin dâune trace ; isoler les files critiques. Le principe de lâarticle 1, le monitoring ne doit jamais pouvoir faire tomber ce quâil surveille, vaut aussi pour la traçabilitĂ©.
class TransferJob
include Sidekiq::Job
sidekiq_options queue: :payments, retry: 4
# ...
end
class WatermarkDocumentJob
include Sidekiq::Job
sidekiq_options queue: :documents
# ...
endNote : déclarer la file ne suffit pas, il faut que des workers la consomment (option -q au lancement de Sidekiq, ou config/sidekiq.yml). Sans worker pour la consommer, une file accumule ses jobs indéfiniment, et rien ne le signale.

Superposer deux courbes suppose que lâhorloge du serveur web, celle des workers et celle de la base racontent la mĂȘme minute. Avec deux minutes dâĂ©cart, la cause semble arriver aprĂšs lâeffet.
Note (DORA) : le mĂȘme article 12 exige la synchronisation des horloges de tous les systĂšmes sur une source de temps de rĂ©fĂ©rence documentĂ©e. Deux corollaires : logguer en UTC partout, et se mĂ©fier des horodatages posĂ©s par lâoutil de collecte Ă la rĂ©ception plutĂŽt que par lâapplication Ă lâĂ©mission. Un agent qui a pris du retard date toutes ses lignes en bloc, Ă la seconde oĂč il lâa rattrapĂ©.
Note : les logs ne rĂ©pondent pas Ă tout. Lâarticle 2 a retirĂ© les temps de rendu par partial en condensant chaque requĂȘte sur une ligne ; pour traquer une vue lente ou une requĂȘte SQL qui dĂ©rive, câest lâAPM (Application Performance Monitoring) qui prend le relais, en traçant chaque requĂȘte Ă©tape par Ă©tape. La plupart des suites citĂ©es plus haut en proposent un, et câest souvent le mĂȘme agent qui collecte logs, mĂ©triques et traces.
Centraliser des logs et suivre des erreurs ne rĂ©pondent pas Ă la mĂȘme question. Le premier dit ce qui sâest passĂ© entre 22h et 23h. Le second regroupe les occurrences dâune mĂȘme exception, garde la trace dâappel et le contexte, et dit si elle est nouvelle ou en augmentation, et depuis quel dĂ©ploiement. Les options se ressemblent (Sentry, Honeybadger, Rollbar, Bugsnag, AppSignal) ; les Ă©carts se jouent sur le prix par Ă©vĂ©nement, lâintĂ©gration Rails et Sidekiq, et la qualitĂ© du regroupement.
Ă la configuration, trois rĂ©glages comptent : associer chaque erreur Ă une version dĂ©ployĂ©e (le SHA de commit) ; y attacher le request_id de lâarticle 2, pour sauter de lâerreur aux logs de la mĂȘme requĂȘte ; et nây envoyer pas plus de donnĂ©es personnelles que dans les logs, car ces outils capturent les paramĂštres de requĂȘte par dĂ©faut et ne reprennent pas tous le filter_parameters de Rails ; vĂ©rifiez-le outil par outil.
Note : dans notre nuit, le service de filigrane saturĂ© a levĂ© des centaines de Net::ReadTimeout. Un outil de suivi dâerreurs les aurait regroupĂ©es en une seule entrĂ©e, avec un compteur qui grimpe dĂšs le dĂ©but de la soirĂ©e. Cette entrĂ©e est une alerte toute trouvĂ©e, sans avoir Ă lire une ligne de log.
Une alerte sert Ă faire agir quelquâun. Si la personne qui la reçoit nâa rien Ă faire, supprimez-la.
On rate son alerting de deux maniĂšres : aucune alerte, câest notre nuit ; ou trop dâalertes, et au bout de quelques semaines on les acquitte sans les lire.
Ce qui se rĂšgle : le seuil ; la durĂ©e de dĂ©passement avant dĂ©clenchement, pour absorber les Ă -coups ; et qui reçoit quoi. Le choix du canal selon la criticitĂ© a Ă©tĂ© posĂ© Ă lâarticle 1.
Notre incident en rĂ©clamait trois, qui nâexistaient pas. La profondeur de la file default au-dessus de son niveau habituel pendant plus de cinq minutes : Ă 22h, quelquâun aurait su quâune file dĂ©bordait, avant le premier paiement en retard. La mort dâun job de paiement, dĂ©clenchĂ©e par la ligne [payments] transfer job dead que le sidekiq_retries_exhausted de lâarticle 2 Ă©crit au niveau error : Ă minuit, quelquâun lâaurait su avant le client. Et lâabsence de logs : si lâapplication cesse dâĂ©mettre pendant dix minutes, ou si lâagent cesse de transmettre, quelquâun doit le savoir. Qui surveille le logueur ?
Note (DORA) : cette troisiĂšme alerte rĂ©pond Ă une exigence explicite de lâarticle 12, qui demande des mesures pour dĂ©tecter une dĂ©faillance du systĂšme de journalisation. On lâoublie souvent : les outils sont faits pour voir des pics, pas des silences.
Note (OPSEC) : les seuils donnĂ©s ici sont des ordres de grandeur. Les vĂŽtres dĂ©pendent de votre trafic et nâont rien Ă faire dans un article public ni dans un dĂ©pĂŽt trop largement accessible : ils indiquent aussi ce que vous ne surveillez pas.
Tout ce qui précÚde répond à deux questions : combien, et quand. Aucun de ces outils ne répond à pourquoi, ni à est-ce normal.
Un compte qui tĂ©lĂ©charge beaucoup de documents peut ĂȘtre un client zĂ©lĂ© qui prĂ©pare sa dĂ©claration fiscale, un cabinet dâaudit mandatĂ© par lâĂ©metteur, ou autre chose, et la courbe est la mĂȘme dans les trois cas. Distinguer ces cas demande de comparer un comportement Ă sa propre normale, et câest le sujet de lâarticle 4.
Revenons Ă la requĂȘte groupĂ©e par user_id, Ă©tendue aux douze jours (figure 2).
Elle montre une rĂ©gularitĂ© qui ne ressemble Ă aucun usage que je connaisse. Pendant douze jours, quelques dizaines de tĂ©lĂ©chargements par jour, Ă peu prĂšs au mĂȘme rythme, sans week-end. Puis, le mardi Ă 21h48, une rafale : des milliers de documents dans la soirĂ©e, depuis trois adresses. Et du premier jour Ă la derniĂšre minute, câest le mĂȘme compte qui tĂ©lĂ©charge.

La question qui ouvre lâarticle suivant : ce compte est-il utilisĂ© par son propriĂ©taire ?

On est passĂ© de la ligne au chiffre, puis du chiffre Ă la courbe. Chaque Ă©tape a demandĂ© un outil, et chaque outil a demandĂ© que les logs soient exploitables, donc le travail de lâarticle prĂ©cĂ©dent. Si vous partez de zĂ©ro, commencez par la centralisation : la recherche, les courbes et les alertes sâappuient toutes dessus.
On sait maintenant combien. Reste Ă savoir si câest normal : prochain Ă©pisode, comparer un comportement Ă sa propre normale.
â InĂšs, Responsable de la SĂ©curitĂ© des SystĂšmes dâInformation chez Capsens
Pour aller plus loin
production.rb de Rails 7.2 et version courantedead_max_jobs et dead_timeout_in_seconds : Error Handling, wiki Sidekiqcombined des access logs : module ngx_http_log_module, documentation nginxâ