Logging von Requests mit Node.js-Middleware
Wenn in Ihrer API um 2 Uhr nachts etwas kaputtgeht, greifen Sie als Erstes zu Ihren Logs. Wenn diese fehlen, unvollständig sind oder in Rauschen untergehen, wird das Debugging zum Ratespiel. HTTP-Request-Logging in Node.js gehört zu jenen Grundlagen, die leicht falsch umgesetzt werden – und die zu ignorieren teuer werden kann.
Dieser Artikel behandelt, wie Request-Logging-Middleware in Express funktioniert, wann etablierte Bibliotheken wie Morgan oder Pino eingesetzt werden sollten und wie produktionsreifes Logging tatsächlich aussieht.
Wichtigste Erkenntnisse
- Logging-Middleware in Express fängt Requests früh in der Chain ab und lauscht auf das
finish-Event vonres, um Response-Daten wie Statuscodes und Dauer zu erfassen. - Morgan bietet schnellen, menschenlesbaren Zugriff auf Logs, die für die Entwicklung geeignet sind, während Pino schnelle, strukturierte JSON-Ausgaben liefert, die für Produktionsumgebungen konzipiert sind.
- Verwenden Sie
AsyncLocalStoragein modernem Node.js, um Correlation-IDs über asynchrone Operationen hinweg zu propagieren, ohne sie manuell durch jeden Funktionsaufruf durchreichen zu müssen. - Loggen Sie niemals sensible Daten wie
Authorization-Header, Cookies oder Request-Bodies standardmäßig – nutzen Sie eingebaute Redaction-Funktionen oder bereinigen Sie manuell.
Wie Logging-Middleware in den Express-Request-Lifecycle passt
In Express ist Middleware eine Funktion mit der Signatur (req, res, next). Sie fängt jeden Request ab, bevor er Ihren Route-Handler erreicht. Logging-Middleware sitzt am Anfang dieser Chain und protokolliert, was hereingekommen und was hinausgegangen ist.
[Client] → [Logging-Middleware] → [Auth-Middleware] → [Route-Handler] → [Response]
Die zentrale Erkenntnis: Sie können die vollständige Response – Statuscode, Dauer – nicht loggen, bevor die Response abgeschlossen ist. Deshalb lauscht Logging-Middleware auf das finish-Event von res, anstatt sofort zu loggen.
import crypto from 'node:crypto'
// Express-Middleware für Request-Logging
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)
Beachten Sie, dass crypto.randomUUID() den Import des Moduls node:crypto erfordert (oder die Verwendung des globalen crypto, das ab Node.js 19+ verfügbar ist). Außerdem ist das logger-Objekt hier ein Platzhalter – Sie würden es durch Ihre tatsächliche Logging-Bibliotheksinstanz ersetzen (wie Pino oder console).
Dieses Muster funktioniert auf jedem Node.js-HTTP-Server, nicht nur mit Express.
Traditionelles Access-Logging mit Morgan
Morgan ist die klassische Express-Request-Logging-Middleware. Mit zwei Zeilen haben Sie Access-Logs im Apache-Stil:
import morgan from 'morgan'
app.use(morgan('combined'))
// Ausgabebeispiel:
// ::1 - - [01/Jan/2025:00:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234
Morgan ist für die Entwicklung und einfache Deployments in Ordnung. Die Ausgabe ist menschenlesbar, aber nicht einfach maschinell parsbar – was zum Problem wird, wenn Sie Logs an Datadog, Loki oder ein anderes strukturiertes Log-System senden.
Strukturiertes Logging in Node.js mit Pino
Für die Produktion bedeutet strukturiertes Logging die Ausgabe von JSON. Jede Log-Zeile wird zu einem abfragbaren Datensatz. Pino ist hier die Standardwahl – es ist deutlich schneller als Winston oder Bunyan, mit minimalem Overhead.
Das Package pino-http lässt sich direkt als Express-Middleware einsetzen:
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 schreibt nach stdout. Ihre Infrastruktur (Docker, systemd, ein Log-Shipper) übernimmt das Routing dieser Zeilen dorthin, wo sie benötigt werden.
Discover how at OpenReplay.com.
Correlation-IDs und Request-Context
Wenn Sie einen Request über mehrere asynchrone Operationen hinweg verfolgen, benötigen Sie eine konsistente Request-ID, die an jede Log-Zeile angehängt wird. In modernem Node.js verwenden Sie dafür AsyncLocalStorage – nicht das veraltete domain-Modul oder Low-Level-Async-Hooks.
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)
})
Jeder Logger-Aufruf innerhalb des asynchronen Kontexts dieses Requests kann nun die requestId abrufen, ohne sie manuell durch jede Funktion durchreichen zu müssen. So würden Sie sie abrufen:
// In einem beliebigen nachgelagerten Modul
import { requestContext } from './context.js'
function doWork() {
const { requestId } = requestContext.getStore()
logger.info({ requestId, msg: 'doing work' })
}
Verantwortungsvolles Logging
Ein paar Regeln, die in der Produktion wichtig sind:
- Loggen Sie niemals
Authorization-Header, Cookies oder API-Tokens. Verwenden Sie Pinosredact-Option oder bereinigen Sie manuell vor dem Schreiben. - Seien Sie vorsichtig mit Client-IPs hinter Proxies.
req.socket.remoteAddressgibt Ihnen die IP des Proxys. Wenn Ihre App hinter einem Reverse-Proxy läuft, konfigurieren Sie Express’trust proxy-Einstellung korrekt und behandeln SieX-Forwarded-For-Header mit Vorsicht. - Loggen Sie Request-Bodies nicht standardmäßig. Sie können groß, binär sein oder personenbezogene Daten enthalten. Loggen Sie sie selektiv, mit Größenbeschränkungen.
Wahl zwischen Morgan und Pino
| Morgan | Pino (pino-http) | |
|---|---|---|
| Ausgabeformat | Text (Apache-Stil) | JSON (strukturiert) |
| Performance | Gut | Exzellent |
| Log-Redaction | Manuell | Eingebaut |
| Produktionsreif | Eingeschränkt | Ja |
| Setup-Zeit | ~2 Min. | ~5 Min. |
Verwenden Sie Morgan für die lokale Entwicklung, wenn Sie lesbare Ausgaben bevorzugen. Verwenden Sie Pino für alles, was in die Produktion geht.
Fazit
Gute Logging-Middleware ist unsichtbar, bis Sie sie brauchen – und dann ist sie alles. Beginnen Sie mit pino-http, geben Sie strukturiertes JSON nach stdout aus und lassen Sie Ihre Infrastruktur Routing und Speicherung übernehmen. Kombinieren Sie es mit Correlation-IDs über AsyncLocalStorage, damit Sie jeden Request End-to-End verfolgen können. Halten Sie sensible Daten von Anfang an aus Ihren Logs heraus, und Sie haben ein Observability-Fundament, das mit Ihrer Anwendung skaliert.
FAQs
Ja, Sie können beide parallel betreiben. Ein gängiges Muster ist die Verwendung von Morgan in der Entwicklung für lesbare Konsolenausgaben und Pino in der Produktion für strukturierte JSON-Logs. Verwenden Sie eine Umgebungsvariable, um bedingt das eine oder andere anzuwenden, damit Sie doppelte Log-Einträge in einer einzelnen Umgebung vermeiden.
Pino schreibt designbedingt JSON nach stdout. Verwenden Sie einen Log-Shipper wie Fluent Bit, Filebeat oder den Datadog-Agent, um stdout-Ausgaben von Ihrem Container oder Prozess zu sammeln und an Ihre Logging-Plattform weiterzuleiten. Dies hält Ihren Anwendungscode von jeder spezifischen Log-Destination entkoppelt.
AsyncLocalStorage propagiert den Kontext automatisch über die gesamte asynchrone Call-Chain hinweg, ohne Funktionssignaturen zu ändern. Das manuelle Durchreichen einer Request-ID durch jede Funktion ist fehleranfällig und überfrachtet Ihren Code. AsyncLocalStorage ist in Node.js 16 und höher stabil und der empfohlene Ansatz für request-bezogenen Kontext.
Mit Pino ist der Overhead typischerweise vernachlässigbar, da es schnelle Serialisierung verwendet und Logs effizient nach stdout schreibt. Morgan ist ebenfalls leichtgewichtig für die meisten Workloads. Das `finish`-Event-Listener-Muster bedeutet, dass das Logging läuft, nachdem die Response an das Betriebssystem übergeben wurde, sodass es normalerweise keine Client-Latenz beeinflusst. Vermeiden Sie synchrone Datei-Schreibvorgänge oder schwere Serialisierung in Ihrem Logging-Pfad.
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.