Zum Inhalt springen

Best Practices für Logging in Produktionssystemen

Strukturiertes Logging, Log-Level und Correlation-IDs — die Muster, die aus lauter Log-Ausgabe eine durchsuchbare, verwertbare Observability-Schicht machen.

3 Min. Lesezeit
Strukturierte JSON-Log-Ausgabe in einem Terminal mit Correlation-IDs von Requests

Die meisten Produktions-Logs fallen in zwei Kategorien: zu viel Rauschen, um nützlich zu sein, oder zu wenig Information, um überhaupt etwas zu debuggen. Gutes Logging ist eine Designentscheidung, kein nachträglicher Einfall. Es erfordert, für jede Log-Anweisung das richtige Format, das richtige Level und den richtigen Kontext zu wählen.

Strukturierte Logs statt Klartext

Klartext-Logs sind in einem Terminal für Menschen lesbar und überall sonst nutzlos. Log-Aggregationstools wie Datadog, Elastic und CloudWatch brauchen strukturierte Daten zum Filtern, Durchsuchen und für Alerts.

tstypescript
// ❌ Plain text — impossible to parse or filter at scale
console.log(`User ${userId} placed order ${orderId} for $${total}`);
// "User cust_123 placed order ord_456 for $99.50"
 
// ✅ Structured JSON — every field is searchable
logger.info("Order placed", {
  userId: "cust_123",
  orderId: "ord_456",
  total: 99.5,
  currency: "USD",
  itemCount: 3,
});
// {"level":"info","msg":"Order placed","userId":"cust_123","orderId":"ord_456","total":99.5,"currency":"USD","itemCount":3,"timestamp":"2020-02-19T14:30:00Z"}

Mit strukturierten Logs kannst du "zeig mir alle Bestellungen über 100 $ von Nutzer cust_123" abfragen, ohne Regex-Klimmzüge.

Log-Level, die etwas bedeuten

Die meisten Teams verwenden Log-Level uneinheitlich. Definiere klare Semantik und setze sie im Code-Review durch.

LevelWann verwendenBeispiel
errorEtwas ist fehlgeschlagen und braucht menschliche AufmerksamkeitZahlungsabbuchung fehlgeschlagen, Datenbankverbindung verloren
warnEtwas Unerwartetes, aber behandeltRate Limit nähert sich, veraltete API aufgerufen
infoBedeutsame Business-EreignisseBestellung aufgegeben, Nutzer registriert, Deployment gestartet
debugInterne Details zur FehlersucheCache Hit/Miss, Query-Timing, Zwischenzustand
tstypescript
// ❌ Wrong levels — error used for non-errors, info used for noise
logger.error("User not found"); // This is expected behavior, not an error
logger.info(`Cache key: ${key}`); // Debug-level detail flooding production
 
// ✅ Correct levels — each level has clear semantics
logger.warn("User not found", { userId, endpoint: "/api/profile" });
logger.debug("Cache lookup", { key, hit: false, latencyMs: 2 });

Setze in Produktion das Mindestlevel auf info. Aktiviere debug vorübergehend, wenn du ein bestimmtes Problem untersuchst. Lass debug niemals dauerhaft laufen — es erzeugt zu viel Volumen und kostet echtes Geld beim Log-Speicher.

Correlation-IDs für Request-Tracing

Wenn ein einzelner Nutzer-Request mehrere Services berührt, verbinden Correlation-IDs die gesamte Kette.

tstypescript
import { randomUUID } from "crypto";
 
function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
  const requestId = req.headers["x-request-id"] ?? randomUUID();
 
  // Attach to response for client debugging
  res.setHeader("x-request-id", requestId);
 
  // Attach to request context for downstream logging
  req.requestId = requestId;
 
  next();
}
tstypescript
// Every log in the request lifecycle includes the same correlationId
function createRequestLogger(requestId: string) {
  return {
    info: (msg: string, data?: Record<string, unknown>) =>
      logger.info(msg, { ...data, requestId }),
    warn: (msg: string, data?: Record<string, unknown>) =>
      logger.warn(msg, { ...data, requestId }),
    error: (msg: string, data?: Record<string, unknown>) =>
      logger.error(msg, { ...data, requestId }),
  };
}
 
// In a route handler:
app.post("/api/orders", async (req, res) => {
  const log = createRequestLogger(req.requestId);
 
  log.info("Order creation started", { userId: req.userId });
 
  const order = await createOrder(req.body, log);
  log.info("Order created", { orderId: order.id });
 
  await chargePayment(order, log);
  log.info("Payment charged", { orderId: order.id });
 
  res.json(order);
});

Wenn etwas fehlschlägt, suche nach requestId, um die vollständige Request-Timeline über alle Services hinweg zu sehen.

Was man loggen sollte (und was nicht)

Immer loggen

  • Request-Start und -Ende (mit Dauer)
  • Business-Ereignisse (Bestellung aufgegeben, Zahlung verarbeitet, Nutzeraktion)
  • Fehler und Exceptions mit vollständigen Stack Traces
  • Externe API-Aufrufe (mit Antwortzeit und Status)

Niemals loggen

  • Passwörter, Tokens, API-Keys oder Secrets
  • Vollständige Kreditkartennummern oder Sozialversicherungsnummern
  • Request-Bodies mit sensiblen Nutzerdaten
  • Health-Check-Requests (sie überfluten den echten Traffic)
tstypescript
// ❌ Logging sensitive data
logger.info("User login", { email, password: req.body.password });
 
// ✅ Redact sensitive fields
logger.info("User login", { email, passwordProvided: !!req.body.password });

Fehler-Logging mit Kontext

Ein Fehler-Log ohne Kontext ist fast nutzlos. Nimm alles auf, was zum Reproduzieren des Problems nötig ist.

tstypescript
// ❌ Bare error message — who was affected? What were the inputs?
logger.error("Payment failed");
 
// ✅ Full context — actionable without opening a debugger
try {
  await paymentGateway.charge(amount, paymentMethodId);
} catch (error) {
  logger.error("Payment charge failed", {
    userId,
    orderId,
    amount,
    paymentMethodId,
    gateway: "stripe",
    errorCode: error.code,
    errorMessage: error.message,
    stack: error.stack,
  });
  throw error;
}

Das Ziel ist, dass jemand, der das Log um 3 Uhr nachts liest, versteht, was passiert ist, wer betroffen war und was zu untersuchen ist — ohne neues Logging deployen zu müssen.

Performance-Logging

Protokolliere langsame Operationen proaktiv. Warte nicht darauf, dass Nutzer Latenz melden.

tstypescript
function withTiming<T>(
  name: string,
  fn: () => Promise<T>,
  log: Logger,
  thresholdMs = 1000,
): Promise<T> {
  const start = performance.now();
 
  return fn().finally(() => {
    const durationMs = Math.round(performance.now() - start);
 
    if (durationMs > thresholdMs) {
      log.warn("Slow operation detected", { operation: name, durationMs });
    } else {
      log.debug("Operation completed", { operation: name, durationMs });
    }
  });
}
 
// Usage
const users = await withTiming("getActiveUsers", () => getActiveUsers(), log);

Dieses Muster deckt Performance-Regressionen auf, bevor sie zu Incidents werden.

Die wichtigsten Punkte

  1. Nutze strukturierte JSON-Logs — Klartext ist im großen Maßstab nicht abfragbar
  2. Definiere Log-Level konsistent — error bedeutet "braucht menschliche Aufmerksamkeit", nicht "etwas Unerwartetes ist passiert"
  3. Correlation-IDs verbinden einen gesamten Request-Lebenszyklus über Services hinweg
  4. Logge niemals sensible Daten — Passwörter, Tokens und PII sind Sicherheitsrisiken in Log-Speichern
  5. Nimm vollständigen Kontext in Fehler-Logs auf — mach Debugging um 3 Uhr nachts ohne Codeänderungen möglich
  6. Protokolliere langsame Operationen proaktiv — Performance-Schwellenwerte erkennen Regressionen frühzeitig
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX