Articles liés

La newsletter française des développeurs Ruby on Rails. Retrouve du contenu similaire gratuitement tous les mois dans ta boßte mail !
S'inscrire
Partager :
Blog
>

đŸȘ⚒ Outiller son observabilitĂ© Rails : le tableau de bord qui n'a jamais existĂ©

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.

Reconstituer Ă  la main : ce que le fichier veut bien donner

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* | sort

La 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.

Centraliser : sortir les logs de la machine

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.

fig4-pipeline-logs.png

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.

Interroger : compter, grouper, superposer

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.

fig1-soiree-file-et-latence (1).png

âȘ 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
  # ...
end

Note : 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.

figure5-file-partagee (1).png

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.

Les erreurs méritent leur propre outil

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.

Alerter sans ĂȘtre noyĂ©

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.

Ce que l’outillage ne fait 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.

Un seul compte

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.

fig2-douze-jours-par-compte (3).png

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

fig3-chronologie (1).png

En deux mots

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

‍