


Logs JSON structurés, trace ID propagé, OpenTelemetry, alertes P1/P2/P3 avec runbooks, audit log append-only : la boîte à outils pour diagnostiquer un incident fintech en 5 minutes.
Sur une plateforme financière, "ça marche en dev" ne suffit pas. Voici comment j'instrumente logs, métriques et traces pour qu'un incident à 3 h du matin trouve sa cause en 5 minutes.
Nexus est une plateforme d'investissement financier. Quand un utilisateur fait un dépôt, une dizaine d'opérations s'enchaînent : auth, scoring KYC, vérification solde, appel à la passerelle de paiement, webhook de confirmation, mise à jour du compte, notification. Si l'une casse, l'utilisateur voit "transaction échouée" — mais sans observabilité, on ne sait pas où ça a cassé.
L'observabilité n'est pas un luxe sur ce genre de plateforme. C'est ce qui permet à un incident de 3 h du matin d'être diagnostiqué en 5 minutes au lieu de 5 heures. Cet article condense ce que j'ai mis en place.
Ce qui s'est passé. Texte structuré, lisible par humain et machine. Granularité : une ligne par événement notable.
Combien, à quelle fréquence. Séries temporelles agrégées. Granularité : un point par minute typiquement.
Le chemin d'une requête à travers plusieurs services. Une trace = un ID unique propagé. Granularité : un span par étape.
Les trois sont complémentaires. Une métrique alerte ("le taux d'erreur explose"), un log montre la cause ("Stripe a renvoyé 503"), une trace montre où dans la chaîne ("entre auth-service et payment-service, latence 8s").
Un log non-structuré est inutilisable pour la corrélation. Tous les logs Nexus sont en JSON, avec une structure stable.
| 1 | // src/utils/logger.ts |
| 2 | import winston from "winston"; |
| 3 | |
| 4 | export const logger = winston.createLogger({ |
| 5 | level: process.env.LOG_LEVEL ?? "info", |
| 6 | format: winston.format.combine( |
| 7 | winston.format.timestamp(), |
| 8 | winston.format.errors({ stack: true }), |
| 9 | winston.format.json(), |
| 10 | ), |
| 11 | defaultMeta: { service: "nexus-api", env: process.env.NODE_ENV }, |
| 12 | transports: [new winston.transports.Console()], |
| 13 | }); |
Une ligne typique :
| 1 | { |
| 2 | "timestamp": "2026-03-12T14:32:08.412Z", |
| 3 | "level": "info", |
| 4 | "service": "nexus-api", |
| 5 | "env": "production", |
| 6 | "trace_id": "abc123", |
| 7 | "user_id": "u_8jhq...", |
| 8 | "operation": "deposit.initiate", |
| 9 | "amount_xof": 50000, |
| 10 | "gateway": "mtn_momo", |
| 11 | "msg": "Deposit initiated" |
| 12 | } |
Le trace_id est la clé. Tous les logs d'une même requête le partagent. Loki ou Elasticsearch indexent ce champ, et une recherche trace_id="abc123" reconstruit toute l'histoire.
Quatre niveaux, et c'est tout. Un cinquième tue la lisibilité.
| Niveau | Quand | Exemple |
|---|---|---|
| ERROR | Quelque chose a vraiment cassé et un humain doit savoir | Webhook signature invalide |
| WARN | Anomalie qui ne casse pas mais qui doit être surveillée | Retry après 3 échecs |
| INFO | Événement notable du business | Paiement confirmé, KYC validé |
| DEBUG | Détails techniques utiles en investigation | Payload reçu, query SQL |
Règle de discipline : DEBUG est désactivé en prod par défaut. Activable temporairement via une variable d'env sans redéploiement. Si vos logs prod sont à 80 % DEBUG, vous n'avez pas de logs — vous avez du bruit.
Sans trace ID propagé, vous ne pouvez pas suivre une requête entre services. Sur Nexus, le gateway génère un trace ID à chaque requête entrante et le propage en header HTTP x-trace-id.
| 1 | // src/middlewares/traceContext.ts |
| 2 | import { randomUUID } from "node:crypto"; |
| 3 | import { AsyncLocalStorage } from "node:async_hooks"; |
| 4 | |
| 5 | const storage = new AsyncLocalStorage<{ traceId: string }>(); |
| 6 | |
| 7 | export function traceContext(req, res, next) { |
| 8 | const traceId = req.header("x-trace-id") ?? randomUUID(); |
| 9 | res.setHeader("x-trace-id", traceId); |
| 10 | storage.run({ traceId }, () => next()); |
| 11 | } |
| 12 | |
| 13 | export function currentTraceId(): string | undefined { |
| 14 | return storage.getStore()?.traceId; |
| 15 | } |
Le logger inclut automatiquement trace_id dans chaque log :
| 1 | const log = (level, msg, meta = {}) => |
| 2 | logger.log({ level, message: msg, trace_id: currentTraceId(), ...meta }); |
Chaque appel HTTP downstream propage le header. La trace devient ininterrompue.
Deux familles distinctes, deux dashboards distincts.
| 1 | // src/observability/metrics.ts |
| 2 | import prom from "prom-client"; |
| 3 | |
| 4 | export const depositCounter = new prom.Counter({ |
| 5 | name: "nexus_deposit_total", |
| 6 | help: "Total deposits initiated", |
| 7 | labelNames: ["gateway", "currency", "status"], |
| 8 | }); |
| 9 | |
| 10 | export const depositLatency = new prom.Histogram({ |
| 11 | name: "nexus_deposit_seconds", |
| 12 | help: "Deposit completion time", |
| 13 | labelNames: ["gateway"], |
| 14 | buckets: [0.5, 1, 2, 5, 10, 30], |
| 15 | }); |
Une alerte business ("volume de dépôts divisé par 3 dans la dernière heure") signale plus tôt qu'une alerte technique ("le service paiement répond plus lentement"). Les deux comptent.
Pour les requêtes qui traversent plusieurs services, OpenTelemetry trace chaque span.
| 1 | // src/observability/tracing.ts |
| 2 | import { NodeSDK } from "@opentelemetry/sdk-node"; |
| 3 | import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; |
| 4 | import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; |
| 5 | |
| 6 | const sdk = new NodeSDK({ |
| 7 | traceExporter: new OTLPTraceExporter({ url: process.env.OTLP_ENDPOINT }), |
| 8 | instrumentations: [getNodeAutoInstrumentations()], |
| 9 | }); |
| 10 | |
| 11 | sdk.start(); |
Avec auto-instrumentation, chaque appel HTTP, requête SQL, query Redis devient un span automatiquement. Pour les opérations métier, on ajoute des spans manuels :
| 1 | import { trace } from "@opentelemetry/api"; |
| 2 | |
| 3 | const tracer = trace.getTracer("nexus-api"); |
| 4 | |
| 5 | async function processDeposit(input: DepositInput) { |
| 6 | return tracer.startActiveSpan("deposit.process", async (span) => { |
| 7 | span.setAttribute("amount", input.amount); |
| 8 | span.setAttribute("gateway", input.gateway); |
| 9 | try { |
| 10 | const result = await doProcess(input); |
| 11 | span.setStatus({ code: 1 }); // OK |
| 12 | return result; |
| 13 | } catch (error) { |
| 14 | span.recordException(error); |
| 15 | span.setStatus({ code: 2, message: error.message }); |
| 16 | throw error; |
| 17 | } finally { |
| 18 | span.end(); |
| 19 | } |
| 20 | }); |
| 21 | } |
Visualisation dans Tempo ou Jaeger : on voit la cascade complète, où chaque étape a passé combien de ms, et où ça a cassé.
Tracer 100 % des requêtes en production coûte cher en stockage et en bande passante. Sampling :
| 1 | const sampler = new ParentBasedSampler({ |
| 2 | root: new TraceIdRatioBasedSampler(0.1), |
| 3 | remoteParentSampled: new AlwaysOnSampler(), |
| 4 | }); |
Coût stockage divisé par 10 sans perdre les signaux qui comptent.
Trois canaux d'alerte, trois urgences distinctes :
| 1 | # prometheus/alerts.yml |
| 2 | groups: |
| 3 | - name: nexus.p1 |
| 4 | rules: |
| 5 | - alert: PaymentServiceDown |
| 6 | expr: up{service="payment-service"} == 0 |
| 7 | for: 1m |
| 8 | labels: { severity: p1 } |
| 9 | annotations: |
| 10 | summary: "Payment service is down" |
| 11 | runbook: "https://wiki.nexus/runbooks/payment-down" |
Chaque alerte P1 a un runbook : 5 étapes à suivre dans l'ordre. Pas de "réfléchir à 3 h du matin" — exécuter le runbook, vérifier, escalader si pas résolu en 15 min.
Un seul dashboard ouvert sur un grand écran à l'open space. Trois blocs :
Si tout est vert, on n'y regarde pas. Si quelque chose passe au rouge, tout le monde voit en même temps. Pas besoin d'attendre l'alerte Slack.
Les logs applicatifs ne suffisent pas pour l'audit fintech. Un log séparé, append-only, conservé 7 ans minimum :
| 1 | CREATE TABLE audit_log ( |
| 2 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), |
| 3 | actor_type VARCHAR(20) NOT NULL, |
| 4 | actor_id UUID NULL, |
| 5 | action VARCHAR(80) NOT NULL, |
| 6 | resource VARCHAR(80) NOT NULL, |
| 7 | resource_id UUID NULL, |
| 8 | before JSONB NULL, |
| 9 | after JSONB NULL, |
| 10 | trace_id VARCHAR(64) NOT NULL, |
| 11 | at TIMESTAMP NOT NULL DEFAULT NOW() |
| 12 | ); |
| 13 | |
| 14 | -- Refus de UPDATE/DELETE via trigger |
| 15 | CREATE FUNCTION audit_immutable() RETURNS trigger AS $$ |
| 16 | BEGIN RAISE EXCEPTION 'audit_log is append-only'; END; |
| 17 | $$ LANGUAGE plpgsql; |
| 18 | CREATE TRIGGER no_update BEFORE UPDATE ON audit_log |
| 19 | FOR EACH ROW EXECUTE FUNCTION audit_immutable(); |
| 20 | CREATE TRIGGER no_delete BEFORE DELETE ON audit_log |
| 21 | FOR EACH ROW EXECUTE FUNCTION audit_immutable(); |
Cet audit est ce qu'un régulateur ou un auditeur va lire. Il doit être propre, complet, immuable.
| Piège | Symptôme | Correction |
|---|---|---|
| Logs en texte plat | Recherche impossible | JSON structuré, indexé |
| Pas de trace ID propagé | Requête introuvable inter-service | UUID propagé en header partout |
| Trop de niveaux de log | Bruit ingérable | 4 niveaux max, DEBUG off en prod |
| Alertes sans runbook | Panique nocturne | Chaque P1 a son runbook |
| Sampling 100 % | Coût stockage exorbitant | Sampling intelligent par criticité |
| Pas d'audit séparé | Risque conformité | Table audit_log append-only, 7 ans |
| Dashboards trop détaillés | Personne ne regarde | Un dashboard santé, 5 secondes |
| Métriques uniquement techniques | Drift business invisible | Métriques business + techniques distinctes |
L'observabilité n'est pas un sujet d'infra. C'est un sujet de résilience opérationnelle. Une plateforme fintech sans observabilité claire est une bombe à retardement : le premier incident sérieux durera 6 heures, perdra des transactions, et entamera la confiance des utilisateurs.
Quatre fondamentaux pour démarrer : logs JSON structurés avec trace ID propagé, métriques business + techniques distinctes, traces distribuées sur les opérations critiques, audit log append-only séparé.
Le coût est réel (~2 semaines de setup initial, ~20 % d'overhead de code). Le bénéfice est inestimable la première fois qu'un incident à 3 h du matin se diagnostique en 5 minutes.
Si ce sujet ressemble à un problème réel dans votre produit, je peux intervenir sur le diagnostic, l'architecture, le backend, l'interface et les automatisations qui rendent une plateforme exploitable.
Réactions des lecteurs
Aucun commentaire pour l'instant
Soyez le premier à réagir à cet article.