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
>

🍪✈️ La boîte noire de votre app Rails : ce qu'on surveille sur nos plateformes Fintech

Il est 22h13, un mardi. Un utilisateur valide un virement sortant de 15 000 € sur votre plateforme d'investissement. Le lendemain matin, votre client vous appelle : les fonds ne sont jamais partis. Pas de crash, pas d'alerte, la page de confirmation s'est même affichée normalement. Que s'est-il passé hier à 22h13 dans votre application ?

Si votre seule réponse consiste à relire le code en espérant une illumination, cette série est pour vous.

Une application Rails en production est une boîte opaque. Elle traite des milliers de requêtes, exécute des jobs, appelle des services tiers et tout cela sans témoin. Les logs et les métriques sont votre boîte noire : celle d'un avion. Personne ne la consulte tant que le vol se passe bien mais le jour où quelque chose déraille, elle fait la différence entre comprendre et deviner.

L'observabilité, c'est la capacité à comprendre l'état interne d'un système à partir de ce qu'il émet : logs, métriques et traces. Un système observable n'est pas un système qui ne tombe jamais, c'est un système qui laisse des traces exploitables de tout ce qu'il fait.

Chez Capsens, nous développons des applications Fintech : des plateformes qui comptent parfois des dizaines, voire centaines de milliers d'utilisateurs, font transiter des fonds et opèrent sous des réglementations strictes. Dans ce contexte, l'observabilité est une obligation. Pouvoir répondre au régulateur sur l'indisponibilité de votre service de paiement ce trimestre, ou attester du contrôle exercé sur un prestataire, suppose des chiffres mesurés en continu. Et c'est aussi une affaire de sécurité : les mêmes signaux qui trahissent un bug révèlent une attaque en cours. Un pic d'erreurs d'authentification à 3h du matin raconte rarement une histoire innocente.

Cinq articles au programme de cette série : le premier dresse le panorama des indicateurs à suivre, les quatre suivants passent à la pratique (bien logguer, s'outiller, détecter les comportements anormaux et les gérer).

  1. Mesurer la fiabilité : les indicateurs qui comptent (cet article) : que mesurer, pourquoi, et avec quels seuils.
  2. Bien logguer dans Rails : quoi logguer, comment structurer ses erreurs, filtrer les données sensibles, Lograge.
  3. Outiller son observabilité : disponibilité, centralisation des logs, suivi des erreurs, notifications.
  4. Analyser les comportements anormaux : faire de ses logs un outil de détection.
  5. Le rate limiting : construire une politique de limitation à la mesure de vos parcours.

On commence donc par les indicateurs. Avant d'installer le moindre outil, il faut savoir ce qu'on veut voir. Ces mesures guideront aussi vos efforts de maintenance. Pour chaque indicateur, on verra pourquoi votre utilisateur, votre client ou votre régulateur s'en soucie, comment le mesurer concrètement dans Rails, et un seuil typique pour se situer.

La disponibilité : le premier indicateur à implémenter

Commençons par la question la plus simple qu'on puisse poser à une application : es-tu en vie ?

Si votre application offre un service payant, la réponse à cette question est probablement déjà une clause de contrat. Ouvrez les conditions de service de n'importe quel fournisseur numérique sérieux (AWS, Google Workspace, votre prestataire de signature électronique) : vous y trouverez un engagement de disponibilité chiffré, généralement réservé aux offres payantes, avec des pénalités à la clé. C'est une garantie que vous pouvez offrir à vos propres clients dans vos niveaux de service. À une condition : la mesurer.

Ces engagements sont aussi un levier commercial : une fois la mesure en place, rien n'empêche de décliner vos prestations en plusieurs niveaux, une disponibilité renforcée ou un monitoring dédié venant justifier une offre plus premium.

Un SLA (Service Level Agreement) est un engagement contractuel sur la qualité d'un service, dont le plus courant est le taux de disponibilité : le pourcentage du temps où le service répond normalement, mesuré sur un mois ou une année.

Côté Rails, la brique de base existe déjà : les applications générées depuis la version 7.1 exposent une route /up qui renvoie un code 200 si l'application a démarré sans exception.

# config/routes.rb
get "up" => "rails/health#show", as: :rails_health_check

C'est un bon début, mais ce check ne dit qu'une chose : le process Rails tourne. Votre application peut répondre 200 sur /up pendant que sa base de données agonise. Pour des engagements plus sérieux, on écrit un health check qui vérifie les dépendances vitales :

# app/controllers/health_controller.rb
class HealthController < ActionController::Base
  def show
    checks = {
      database: check { ActiveRecord::Base.connection.execute("SELECT 1") },
      cache: check { Rails.cache.write("health_check", Time.current.to_i) },
      queues: check { Sidekiq::Queue.new.latency < 5.minutes }
    }

    if checks.values.all?
      head :ok
    else
      Rails.logger.error("[health] #{checks.select { |_, v| !v }.keys.join(', ')} KO")
      head :service_unavailable
    end
  end

  private

  # Un service qui lève une exception est un service KO,
  # pas une erreur 500 sur le health check.
  def check
    yield.present?
  rescue StandardError
    false
  end
end


Note : on interroge la base avec un vrai
SELECT 1 plutôt qu'avec connection.active?. Ce dernier vérifie seulement l'état de la connexion extraite du pool, sans aller-retour vers la base : une connexion fermée côté serveur (timeout d'inactivité, failover) peut le faire répondre false alors que tout fonctionne, et provoquer des indisponibilités fantômes dans votre monitoring.

Deux règles d'or ensuite. D'abord, la sonde qui interroge ce endpoint ne doit idéalement pas vivre sur votre propre infrastructure : si votre serveur tombe, il emporte le monitoring avec lui, et vous voilà aveugle à un moment critique. Des services externes comme Pingdom ou UptimeRobot interrogent votre URL toutes les minutes depuis plusieurs régions du monde. Ensuite, branchez les notifications sur un canal que quelqu'un regarde vraiment : messagerie d'équipe, SMS, appel selon la criticité. Une alerte que personne ne lit ne sert à rien.

Note : remarquez que le health check ci-dessus renvoie un simple code HTTP, sans détail. Un endpoint public qui liste l'état de votre base, de votre cache et de vos queues est une mine d'or pour un attaquant en reconnaissance. Les détails vont dans les logs, pas dans la réponse.

Seuil typique : de 99 % à 99,9 % mensuel, selon le service, soit entre sept heures et 43 minutes d'indisponibilité tolérées par mois. Chaque 9 supplémentaire multiplie l'effort par un ordre de grandeur : sachez où placer le curseur avant de le promettre, et précisez si les maintenances planifiées comptent dans le calcul.

Le taux d'erreurs : toutes ne se valent pas

Une application disponible n'est pas forcément une application qui fonctionne. Elle peut répondre à chaque requête... par une erreur. D'où le deuxième indicateur : le taux de réponses en erreur. Ici, le premier chiffre du code HTTP change tout.

Les erreurs 500 sont vos erreurs : une exception non rattrapée, un bug, un service tiers qui ne répond plus. Elles sont toujours anormales et c'est sur elles que portent les engagements contractuels classiques, comme un taux d'erreurs serveur inférieur à 1 % sur l'année. Votre utilisateur s'en soucie parce qu'il vient de perdre son formulaire de souscription ; votre client, parce que chaque 500 sur un parcours de paiement est un revenu qui s'évapore.

Les erreurs 400, elles, sont ambiguës. Une 404 isolée ne veut rien dire, personne ne s'engage contractuellement dessus. Mais en volume, elles racontent des histoires : des 404 répétées sur un même chemin trahissent un lien cassé ou des assets mal chargés ; une rafale de 404 sur des chemins comme /wp-admin ou /.env signale un scanner de vulnérabilités qui teste votre surface ; des 422 en série sur une API révèlent un client dont l'intégration dysfonctionne. Rien qui justifie une alerte de nuit, mais beaucoup à apprendre en les consultant régulièrement.

Pour compter tout ça, pas besoin d'outil externe : Rails publie un événement à chaque requête traitée auquel on peut s'abonner.

# config/initializers/request_metrics.rb
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |event|
  payload = event.payload
  # une exception non rattrapée ne produit pas de statut : c'est une 500
  status = payload[:status] || 500

  REDIS.hincrby("http_status:#{Date.current}", "#{status / 100}xx", 1)
rescue StandardError => e
  Rails.logger.error("[metrics] #{e.class}: #{e.message}")
end

Note : le rescue final n'est pas optionnel. Les subscribers s'exécutent dans le thread de la requête, et une exception qui s'en échappe fait échouer la requête elle-même : sans ce garde-fou, un Redis indisponible transformerait votre collecte de métriques en panne générale. Un principe à retenir : le monitoring ne doit jamais pouvoir faire tomber ce qu'il surveille.

Quelques lignes, et vous savez chaque soir combien de 2xx, 4xx et 5xx votre application a servis. C'est rudimentaire, les outils de l'article 3 feront bien mieux, mais le principe est là.

Seuil typique : un taux de réponses 5xx inférieur à 0,1 %, 0,5 % ou 1 %, selon le niveau d'exigence du service. Au-delà de 1 %, les erreurs deviennent perceptibles pour les utilisateurs réguliers.

Les parcours critiques : là où le taux global ment

Un taux d'erreurs global de 0,05 %, c'est excellent. Sauf si ces 0,05 % sont tous concentrés sur le endpoint de paiement. Le taux global est une moyenne, et comme toute moyenne, elle noie les cas qui comptent : votre page d'accueil encaisse cent fois plus de trafic que votre tunnel de souscription, elle écrase donc le calcul.

La parade consiste à suivre les erreurs par contrôleur et à traiter à part vos parcours métier critiques : paiement, signature, souscription, tout ce dont l'échec coûte immédiatement de l'argent ou une obligation. Notre subscriber s'enrichit de quelques lignes :

if status >= 500
  route = "#{payload[:controller]}##{payload[:action]}"
  REDIS.hincrby("errors_by_route:#{Date.current}", route, 1)
end

Un tri sur ce hash chaque matin et vous savez où concentrer la maintenance : l'action qui accumule les erreurs est votre priorité, pas celle dont on s'est plaint le plus fort en réunion.

Sur le même principe, un health check métier peut aller plus loin que la simple vérification des dépendances : jouer périodiquement un parcours critique de bout en bout (créer un utilisateur de test, simuler une souscription en environnement contrôlé) pour garantir non pas « l'application répond » mais « l'application fait son travail ». C'est le niveau d'assurance qu'exigent les SLA les plus stricts.

Seuil typique : sur les parcours critiques, on raisonne moins en taux qu'en rapidité d'intervention. Une 500 sur un paiement justifie une alerte et un traitement prioritaire.

Les performances : la lenteur est une panne qui ne dit pas son nom

Entre « l'application répond » et « l'application répond en huit secondes », votre monitoring de disponibilité ne voit aucune différence. Votre utilisateur, si. Trois mesures suffisent pour couvrir l'essentiel.

Le temps de réponse serveur, d'abord. La moyenne donne la tendance mais elle cache les extrêmes ; vos utilisateurs vivent dans les extrêmes. D'où le p95 :

Le p95 (95e percentile) est la valeur sous laquelle se trouvent 95 % de vos temps de réponse. Un p95 à 800 ms signifie qu'une requête sur vingt prend plus de 800 ms. Le p95 vous montre ce que vivent vos utilisateurs les plus malchanceux, qui sont souvent vos plus gros clients puisque ce sont eux qui ont le plus de données à charger.

Suivre le temps de réponse, c'est aussi mesurer une forme d'accessibilité : un p95 maîtrisé garde le service utilisable depuis une connexion mobile moyenne ou un appareil modeste, pas seulement depuis la fibre du bureau. En pratique, visez un p95 entre 800 ms et 1,2 s pour les pages web.

Le temps passé en base de données, ensuite, car c'est le suspect numéro un quand le temps de réponse dérive. Rails vous le donne requête par requête, dans le même événement que tout à l'heure :

duration = event.duration                # temps total (ms)
db = payload[:db_runtime]                # dont base de données
views = payload[:view_runtime]           # dont rendu des vues

Les trois se lisent ensemble : duration couvre le traitement complet de la requête, de son arrivée dans le contrôleur jusqu'à la réponse ; db_runtime isole le temps passé à exécuter les requêtes SQL ; view_runtime celui consacré au rendu des templates. Ce qui reste une fois ces deux postes soustraits correspond à votre code Ruby : logique métier, appels à des services externes, sérialisation.

image.png

Si db_runtime représente 80 % de duration, inutile d'optimiser votre code Ruby : c'est une requête N+1 ou un index manquant qu'il faut chercher.

La latence des jobs asynchrones, enfin, la grande oubliée. Vos emails, vos webhooks, la génération de vos documents par Sidekiq et une queue qui s'engorge est invisible depuis le navigateur... Jusqu'à ce qu'un utilisateur attende son code de confirmation depuis vingt minutes. Sidekiq expose la mesure nativement :

Sidekiq::Queue.new("mailers").latency
# => ancienneté (en secondes) du plus vieux job en attente

Seuils typiques : p95 entre 800 ms et 1,2 s pour les pages web, latence de queue sous la minute pour tout ce que l'utilisateur attend activement. À adapter, évidemment : un virement peut patienter dix secondes, un code 2FA non.

Les signaux faibles : quand les métriques parlent sécurité

Reprenons nos codes HTTP une dernière fois, avec la casquette sécurité. Un pic de 401 et de 403 n'est pas un problème de fiabilité : l'application fait exactement son travail en refusant l'accès. Mais des centaines de 401 sur votre page de connexion en dix minutes sont souvent l'indicateur d'une activité malveillante, telle qu'une attaque par force brute ou du credential stuffing. Le compteur mis en place plus haut collecte déjà ces codes ; il ne manque qu'une alerte dessus.

Pour nos clients du secteur financier, ce n'est plus optionnel : le règlement européen DORA, applicable depuis janvier 2025, impose aux entités financières de détecter, classifier et déclarer leurs incidents majeurs liés aux systèmes d'information. Le jour où il faut documenter un incident, l'historique horodaté de vos 401 peut être intéressant. Les articles 4 et 5 de cette série seront entièrement consacrés à cette lecture sécuritaire de vos métriques, jusqu'au premier niveau de réponse : la réponse automatique.

Les indicateurs d'état : dépendances et MFA

Changement de registre pour finir. Les indicateurs vus jusqu'ici se mesurent sur une période : on compte des requêtes, des erreurs, des minutes d'indisponibilité. Il existe toute une autre famille d'indicateurs, dits d'état, qui se constatent à un instant donné, comme un inventaire qu'on vérifie régulièrement. La conformité cyber en regorge : proportion de postes chiffrés, comptes dormants, sauvegardes testées, délai d'application des correctifs. On en retient deux ici, particulièrement parlants pour une application Rails.

La santé des dépendances. Votre Gemfile.lock est un inventaire de code écrit par d'autres, et chaque gem peut être frappée par une CVE. L'audit s'automatise en une commande, à brancher dans votre CI :

bundle-audit check --update

L'outil (bundler-audit) compare vos versions à la base des vulnérabilités connues. Seuil : zéro CVE critique non traitée, et idéalement zéro CVE tout court. Gardez aussi un œil sur la fraîcheur des versions : une dépendance qui n'est plus maintenue est une vulnérabilité potentielle.

La couverture MFA. Le pourcentage d'utilisateurs ayant activé la double authentification, à surveiller en priorité chez les comptes à privilèges. Des réglementations comme DORA imposent d'ailleurs des mécanismes d'authentification forte, ce qui fait de ce taux un indicateur de conformité autant que de sécurité. Si vous utilisez devise-two-factor, la mesure tient en une ligne :

admins = User.where(role: :admin)
admins.where(otp_required_for_login: true).count.fdiv(admins.count) * 100

Seuil : 100 % des comptes administrateurs. Sans exception ni compte de service oublié : c'est précisément celui-là qu'on retrouvera dans le rapport d'incident.

Note : au-delà de la conformité, la MFA a une valeur défensive singulière. Contre certaines attaques comme le credential stuffing, où l'attaquant se présente avec un mot de passe valide récupéré dans une fuite, elle est la seule barrière qui tienne encore : aucune politique de mot de passe ne protège d'un mot de passe déjà compromis.

Reporting et automatisation

Ces indicateurs collectés, une dernière habitude les rend réellement utiles : les consigner. Du point de vue de l'ingénieur, le travail est terminé quand les alertes sont branchées. Du point de vue du RSSI ou de l'auditeur, il ne fait que commencer.

En cybersécurité, et particulièrement en conformité juridique (RGPD, DORA et autres réglementations sectorielles), une règle prévaut : aux yeux des autorités (CNIL, AMF...), un travail qui n'est pas documenté n'a pas été fait. Vous pouvez suivre vos indicateurs avec rigueur pendant des mois, si rien n'en atteste, cela ne pèsera rien lors d'un contrôle.

D'où la dernière étape : un job planifié qui compile chaque semaine ou chaque mois disponibilité, taux d'erreurs, p95, latence des queues, résultat d'audit des dépendances et couverture MFA en un rapport.

La façon de consigner compte autant que le contenu. Un message éphémère dans un canal de discussion n'a guère de valeur probante : datez vos rapports, conservez-les dans un espace dédié et pérenne, et gardez un format stable d'une édition à l'autre pour rendre les évolutions lisibles. Archivés ainsi, ces rapports constituent, mois après mois, une partie intégrante de votre dossier conformité.

En deux mots

Il faut finalement peu de code pour tout cela : Rails instrumente déjà presque l'ensemble de votre application, il ne reste qu'à exploiter ces données. Ces indicateurs sont une base de travail, de quoi couvrir l'essentiel et donner des idées. Selon vos objectifs, on peut aller beaucoup plus loin : SLO et budgets d'erreur, tracing distribué, tests de charge, parcours rejoués de bout en bout. Inutile de tout déployer d'un coup : commencez par l'indicateur le plus pertinent pour vos priorités, ajoutez les autres au fil des semaines.

Quant à notre virement fantôme de 22h13, avec ces indicateurs en place, la latence de la queue de paiement aurait donné l'alerte avant même l'appel du client.

Dans le prochain article, on apprendra à configurer notre application Rails pour logguer correctement : quoi écrire, sous quelle forme et ce qui ne doit jamais finir dans les logs.

— Inès, Responsable de la Sécurité des Systèmes d'Information chez Capsens