Back

Journalisation des requêtes avec les middlewares Node.js

Journalisation des requêtes avec les middlewares Node.js

Lorsque quelque chose casse dans votre API à 2 heures du matin, la première chose que vous cherchez, ce sont vos logs. S’ils sont manquants, incomplets ou noyés dans le bruit, le débogage devient une devinette. La journalisation des requêtes HTTP dans Node.js est l’un de ces fondamentaux qu’il est facile de mal faire—et coûteux d’ignorer.

Cet article couvre le fonctionnement des middlewares de journalisation dans Express, quand utiliser des bibliothèques établies comme Morgan ou Pino, et à quoi ressemble réellement une journalisation prête pour la production.

Points clés à retenir

  • Les middlewares de journalisation dans Express interceptent les requêtes tôt dans la chaîne et écoutent l’événement finish de res pour capturer les données de réponse comme les codes de statut et la durée.
  • Morgan fournit des logs d’accès rapides et lisibles adaptés au développement, tandis que Pino offre une sortie JSON structurée et rapide conçue pour les environnements de production.
  • Utilisez AsyncLocalStorage dans les versions modernes de Node.js pour propager les ID de corrélation à travers les opérations asynchrones sans avoir à les passer manuellement dans chaque appel de fonction.
  • Ne journalisez jamais de données sensibles telles que les en-têtes Authorization, les cookies ou les corps de requête par défaut—utilisez la redaction intégrée ou nettoyez manuellement.

Comment les middlewares de journalisation s’intègrent dans le cycle de vie des requêtes Express

Dans Express, un middleware est une fonction avec la signature (req, res, next). Il intercepte chaque requête avant qu’elle n’atteigne votre gestionnaire de route. Le middleware de journalisation se situe en haut de cette chaîne, enregistrant ce qui est entré et ce qui est sorti.

[Client] → [Middleware de journalisation] → [Middleware d'authentification] → [Gestionnaire de route] → [Réponse]

L’élément clé : vous ne pouvez pas journaliser la réponse complète—code de statut, durée—tant que la réponse n’est pas terminée. C’est pourquoi le middleware de journalisation écoute l’événement finish de res plutôt que de journaliser immédiatement.

import crypto from 'node:crypto'

// Middleware Express pour la journalisation des requêtes
const logRequests = (req, res, next) => {
  const start = Date.now()

  res.on('finish', () => {
    logger.info({
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: Date.now() - start,
      requestId: req.headers['x-request-id'] ?? crypto.randomUUID(),
    })
  })

  next()
}

app.use(logRequests)

Notez que crypto.randomUUID() nécessite d’importer le module node:crypto (ou d’utiliser l’objet global crypto disponible dans Node.js 19+). De plus, l’objet logger ici est un espace réservé—vous le remplaceriez par votre instance réelle de bibliothèque de journalisation (comme Pino ou console).

Ce pattern fonctionne avec n’importe quel serveur HTTP Node.js, pas seulement Express.

Journalisation d’accès traditionnelle avec Morgan

Morgan est le middleware classique de journalisation des requêtes Express. Deux lignes et vous avez des logs d’accès de style Apache :

import morgan from 'morgan'

app.use(morgan('combined'))
// Exemple de sortie :
// ::1 - - [01/Jan/2025:00:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234

Morgan convient au développement et aux déploiements simples. Sa sortie est lisible par l’humain mais pas facilement analysable par machine—ce qui devient un problème lorsque vous envoyez des logs vers Datadog, Loki ou tout système de logs structurés.

Journalisation structurée dans Node.js avec Pino

Pour la production, la journalisation structurée signifie émettre du JSON. Chaque ligne de log devient un enregistrement interrogeable. Pino est le choix standard ici—il est significativement plus rapide que Winston ou Bunyan, avec un overhead minimal.

Le package pino-http s’intègre comme middleware Express :

import express from 'express'
import pinoHttp from 'pino-http'

const app = express()

app.use(pinoHttp({
  level: process.env.LOG_LEVEL ?? 'info',
  redact: ['req.headers.authorization', 'req.headers.cookie'],
}))

app.get('/', (req, res) => {
  req.log.info('handling root request')
  res.send('ok')
})

Pino écrit vers stdout. Votre infrastructure (Docker, systemd, un agent de transfert de logs) gère le routage de ces lignes vers leur destination.

ID de corrélation et contexte de requête

Lorsque vous tracez une requête à travers plusieurs opérations asynchrones, vous avez besoin d’un ID de requête cohérent attaché à chaque ligne de log. Dans les versions modernes de Node.js, utilisez AsyncLocalStorage pour cela—pas le module domain déprécié ou les hooks asynchrones de bas niveau.

import { AsyncLocalStorage } from 'node:async_hooks'
import crypto from 'node:crypto'

export const requestContext = new AsyncLocalStorage()

app.use((req, res, next) => {
  const requestId = req.headers['x-request-id'] ?? crypto.randomUUID()
  requestContext.run({ requestId }, next)
})

Tout appel de logger à l’intérieur du contexte asynchrone de cette requête peut maintenant récupérer le requestId sans avoir à le passer manuellement dans chaque fonction. Voici comment vous le récupéreriez :

// Dans n'importe quel module en aval
import { requestContext } from './context.js'

function doWork() {
  const { requestId } = requestContext.getStore()
  logger.info({ requestId, msg: 'doing work' })
}

Journaliser de manière responsable

Quelques règles qui comptent en production :

  • Ne journalisez jamais les en-têtes Authorization, les cookies ou les tokens API. Utilisez l’option redact de Pino ou nettoyez manuellement avant d’écrire.
  • Soyez prudent avec les IP des clients derrière des proxies. req.socket.remoteAddress vous donnera l’IP du proxy. Si votre application est derrière un reverse proxy, configurez correctement le paramètre trust proxy d’Express et traitez les en-têtes X-Forwarded-For avec précaution.
  • Ne journalisez pas les corps de requête par défaut. Ils peuvent être volumineux, binaires ou contenir des données personnelles. Journalisez-les de manière sélective, avec des limites de taille.

Choisir entre Morgan et Pino

MorganPino (pino-http)
Format de sortieTexte (style Apache)JSON (structuré)
PerformanceBonneExcellente
Redaction de logsManuelleIntégrée
Prêt productionLimitéOui
Temps de setup~2 min~5 min

Utilisez Morgan pour le développement local si vous préférez une sortie lisible. Utilisez Pino pour tout ce qui part en production.

Conclusion

Un bon middleware de journalisation est invisible jusqu’à ce que vous en ayez besoin—et alors il devient tout. Commencez avec pino-http, émettez du JSON structuré vers stdout, et laissez votre infrastructure gérer le routage et le stockage. Associez-le à des ID de corrélation via AsyncLocalStorage pour pouvoir tracer n’importe quelle requête de bout en bout. Gardez les données sensibles hors de vos logs dès le premier jour, et vous aurez une fondation d’observabilité qui évolue avec votre application.

FAQ

Oui, vous pouvez les exécuter côte à côte. Un pattern courant consiste à utiliser Morgan en développement pour une sortie console lisible et Pino en production pour des logs JSON structurés. Utilisez une variable d'environnement pour appliquer conditionnellement l'un ou l'autre afin d'éviter les entrées de log en double dans un même environnement.

Pino écrit du JSON vers stdout par conception. Utilisez un agent de transfert de logs comme Fluent Bit, Filebeat ou l'agent Datadog pour collecter la sortie stdout de votre conteneur ou processus et la transférer vers votre plateforme de journalisation. Cela maintient le code de votre application découplé de toute destination de log spécifique.

AsyncLocalStorage propage automatiquement le contexte à travers toute la chaîne d'appels asynchrones sans modifier les signatures de fonction. Passer manuellement un ID de requête dans chaque fonction est sujet aux erreurs et encombre votre code. AsyncLocalStorage est stable dans Node.js 16 et versions ultérieures et constitue l'approche recommandée pour le contexte limité aux requêtes.

Avec Pino, l'overhead est généralement négligeable car il utilise une sérialisation rapide et écrit les logs efficacement vers stdout. Morgan est également léger pour la plupart des charges de travail. Le pattern d'écouteur d'événement `finish` signifie que la journalisation s'exécute après que la réponse a été transmise au système d'exploitation, donc elle n'affecte généralement pas la latence côté client. Évitez les écritures de fichiers synchrones ou la sérialisation lourde dans votre chemin de journalisation.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay