Skip to content

feat(api): structured JSON logging for Cloud Run with pino #45

Description

@arenier

De quoi il s'agit

apps/api/src/main.ts utilise le Logger Nest en sortie texte. Cloud Run capture stdout/stderr
dans Cloud Logging, qui sait lire une ligne JSON et son champ severity pour classer autre
chose que de l'INFO (WARNING, ERROR). En texte, tout ressort en INFO : le seul outil de
diagnostic une fois déployé (ADR 0004) devient aveugle aux erreurs — un 500 se noie dans les logs de
requête.

Cette issue remplace le logging app-wide par du JSON structuré. Elle a été détachée de #26,
qui ne garde que le minimum couplé à son filtre d'exception (logguer une erreur rattrapée en
severity: ERROR). Ici, c'est l'axe observabilité complet.

Décision : pino (via nestjs-pino)

Log app-wide structuré, avec redaction, corrélation de requête et logging de requête automatique :
un logger maison réinventerait tout ça. pino est le choix — rapide, JSON natif, redact
intégré, intégration Nest éprouvée par nestjs-pino.

Ce choix ne contredit pas l'ethos « écrit à la main plutôt qu'une lib » d'environment.ts : là,
une poignée de variables ne justifie pas une lib de schéma ; ici, le JSON structuré + redaction +
trace + logging de requête est un vrai périmètre où pino gagne sa place. C'est l'arbitrage
qu'on s'était réservé en PR sur #26, tranché.

Périmètre

  • nestjs-pino branché : NestFactory.create(..., { bufferLogs: true }) puis
    app.useLogger(app.get(Logger)), pour que main.ts et tous les providers loggent via pino.
  • Mapping niveau → severity attendu par Cloud Logging. pino émet des niveaux numériques
    (30 info, 40 warn, 50 error, 60 fatal) ; un formatters.level les traduit en
    { severity: 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' }, avec messageKey: 'message' et
    timestamp ISO. Cloud Logging lit alors severity et message de la ligne JSON.
  • Corrélation de requête : dériver le trace id de l'en-tête X-Cloud-Trace-Context injecté par
    Cloud Run, exposé sous logging.googleapis.com/trace (clé reconnue par Cloud Logging) — chaque
    log d'une requête est ainsi rattaché à sa trace.
  • Redaction, non négociable (redact de pino, remove: true) : jamais de clé de provider
    (GEMINI_API_KEY, OPENROUTER_API_KEY, en-tête authorization), jamais de bytes d'image. Les
    serializers de requête/réponse ne loggent pas le corps.
  • Dev vs prod : en development, transport pino-pretty lisible ; en production, JSON brut sur
    stdout (aucun transport). Sélection par NODE_ENV (déjà validé par environment.ts).
  • Logging de requête : autoLogging de nestjs-pino activé, mais serializers configurés pour ne
    garder que méthode, chemin, statut, durée et trace id — pas le corps, pas les en-têtes sensibles.

Articulation avec #26

#26 pose le filtre d'exception et lui fait logguer une erreur rattrapée en severity: ERROR
(minimum vital pour tenir sa promesse « pas de stack au client »). Ordre : #26 d'abord. Quand la
présente issue passe, le filtre logge à travers le logger pino injecté au lieu de son émission
minimale — un point de câblage, pas une réécriture. La forme severity: ERROR attendue par #26 est
exactement ce que le mapping ci-dessus produit.

Dépendances outillage

pino, nestjs-pino et pino-pretty (dev) épinglés à l'exact — jamais en plage, comme tout
l'outillage (CLAUDE.md, et #21 pour l'automatisation des bumps). pino-pretty en devDependencies
uniquement (il ne doit pas partir dans l'image de prod).

Hors périmètre

  • Pas de métriques ni de tracing distribué (OpenTelemetry) — corrélation par trace id suffit au MVP.
  • Pas de sink externe (Datadog, etc.) : Cloud Logging est le seul outil (ADR 0004).
  • Pas de nouvel ADR : applique l'ADR 0004 (diagnostic Cloud Run). Le choix pino est tracé ici et
    dans le corps de la PR — pas une décision structurante d'architecture.

Critères d'acceptation

  • Chaque log applicatif est une ligne JSON avec un champ severity ; un logger.error(...)
    produit severity: ERROR, un warn WARNING, un log/info INFO.
  • Le trace id de X-Cloud-Trace-Context apparaît sous logging.googleapis.com/trace dans les
    logs d'une requête portant cet en-tête.
  • Aucun log ne contient de clé de provider, d'en-tête authorization, ni de bytes d'image —
    assertion de test sur le payload émis (destination pino capturée en mémoire).
  • En development, la sortie est lisible (pino-pretty) ; en production, JSON brut sur stdout.
  • main.ts et les providers loggent via pino (useLogger + bufferLogs), plus aucun Logger
    Nest texte résiduel.
  • pino-pretty est en devDependencies, absent de l'image de prod.
  • yarn check vert.

Découpage en tâches (TDD, test rouge d'abord)

  1. Config pino — un module/factory qui construit les options pino (formatters level → severity,
    messageKey, timestamp ISO, redact) ; test sur le mapping de severity et sur la redaction (une clé
    et des bytes d'image n'apparaissent pas dans la ligne émise, via une destination capturée).
  2. Intégration Nestnestjs-pino dans AppModule, bufferLogs + useLogger dans main.ts ;
    test d'intégration qu'un log passe bien par pino.
  3. Corrélation de requête — extraction du trace id depuis X-Cloud-Trace-Context ; test qu'une
    requête avec l'en-tête produit logging.googleapis.com/trace.
  4. Dev vs prodpino-pretty conditionné à NODE_ENV=development ; test de sélection.
  5. Filtre (si feat(api): durcir la frontière HTTP — CORS et filtre d'exception global #26 déjà mergé) — router le log d'erreur du filtre à travers le logger pino.
  6. Doc — ligne dans CLAUDE.md (section Outillage / Commandes) sur le logging structuré ;
    .env.example inchangé (aucune variable nouvelle requise).

Références

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions