Observability jenseits von Logging: Traces, Metriken und das Verständnis der Produktion

Einführung

„Einfach mehr Logging hinzufügen” ist die Standardantwort, wenn Produktionsprobleme schwer zu debuggen sind. Aber mehr Logs bedeuten oft mehr Rauschen, nicht mehr Erkenntnis.

Echte Observability erfordert einen anderen Ansatz: das System durch Metriken, Traces und strukturierte Logs zu verstehen, die zusammenwirken.

Die drei Säulen

Logs

Was zu einem bestimmten Zeitpunkt passiert ist.

{
  "timestamp": "2024-01-15T10:23:45Z",
  "level": "error",
  "message": "Payment failed",
  "userId": "user_123",
  "paymentId": "pay_456",
  "error": "Card declined",
  "traceId": "abc123"
}

Gut geeignet für: Detailliertes Debugging, Audit-Trails, das Verständnis konkreter Ereignisse.

Schlecht geeignet für: Aggregation, Trends, das Verständnis systemweiten Verhaltens.

Metriken

Numerische Messungen über die Zeit.

http_requests_total{method="POST", path="/api/payments", status="500"} 42
http_request_duration_seconds{quantile="0.99"} 2.5
active_database_connections 45

Gut geeignet für: Alerting, Dashboards, das Verständnis von Trends und Mustern.

Schlecht geeignet für: Das Verständnis, warum etwas passiert ist, das Debuggen konkreter Anfragen.

Traces

Der Weg einer Anfrage durch Ihr System.

Trace: abc123
├── API Gateway (2ms)
├── Auth Service (15ms)
├── Payment Service (450ms)
│   ├── Validate Request (5ms)
│   ├── Check Fraud (200ms)
│   └── Process Payment (240ms)
│       └── External Provider (235ms) ← langsam!
└── Notification Service (25ms)

Gut geeignet für: Das Verständnis von Latenz, das Finden von Engpässen, das Debuggen verteilter Systeme.

Schlecht geeignet für: Aggregation (zu viele Daten), einfache Systeme.

Effektives Logging implementieren

Strukturieren Sie Ihre Logs

Unstrukturierte Logs sind im großen Maßstab nahezu nutzlos:

# Schlecht
console.log(`User ${userId} failed to pay: ${error}`);

# Gut
logger.error('Payment failed', {
  userId,
  paymentId,
  amount,
  currency,
  errorCode: error.code,
  errorMessage: error.message,
  traceId: context.traceId
});

Log-Level sind wichtig

Verwenden Sie Level konsistent:

  • ERROR: Etwas ist fehlgeschlagen, das nicht hätte fehlschlagen sollen
  • WARN: Etwas Unerwartetes, aber Behandeltes
  • INFO: Wichtige Geschäftsereignisse
  • DEBUG: Detaillierte Informationen zum Debuggen (in Produktion deaktiviert)

Kontext einbeziehen

Jedes Log sollte beantworten: wer, was, wann, wo, warum?

function processOrder(order: Order, context: Context) {
  const logContext = {
    orderId: order.id,
    userId: order.userId,
    traceId: context.traceId,
    spanId: context.spanId
  };

  logger.info('Processing order', { ...logContext, amount: order.total });
  
  try {
    // ... Bestellung verarbeiten
    logger.info('Order processed successfully', logContext);
  } catch (error) {
    logger.error('Order processing failed', { 
      ...logContext, 
      error: error.message,
      stack: error.stack 
    });
    throw error;
  }
}

Metriken implementieren

Die vier goldenen Signale

Beginnen Sie mit diesen für jeden Service:

  1. Latenz: Wie lange Anfragen dauern
  2. Traffic: Wie viele Anfragen Sie bearbeiten
  3. Fehler: Wie viele Anfragen fehlschlagen
  4. Sättigung: Wie „voll” Ihr Service ist
// Beispiel mit Prometheus-Client
const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests',
  labelNames: ['method', 'path', 'status'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5]
});

const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'path', 'status']
});

app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    const labels = { 
      method: req.method, 
      path: req.route?.path || 'unknown',
      status: res.statusCode 
    };
    
    httpRequestDuration.observe(labels, duration);
    httpRequestsTotal.inc(labels);
  });
  
  next();
});

Geschäftsmetriken

Überwachen Sie nicht nur die Infrastruktur – überwachen Sie, was für das Geschäft wichtig ist:

const ordersProcessed = new Counter({
  name: 'orders_processed_total',
  help: 'Total orders processed',
  labelNames: ['status', 'payment_method']
});

const orderValue = new Histogram({
  name: 'order_value_dollars',
  help: 'Order value in dollars',
  buckets: [10, 50, 100, 500, 1000, 5000]
});

Warnung zur Kardinalität

Seien Sie vorsichtig mit Label-Werten. Hohe Kardinalität bringt Metriksysteme zum Absturz:

// Schlecht - userId hat unbegrenzte Werte
httpRequests.inc({ userId: user.id }); 

// Gut - begrenzte Wertemenge
httpRequests.inc({ userType: user.type }); // 'free', 'premium', 'enterprise'

Verteiltes Tracing implementieren

Kontext weitergeben

Geben Sie den Trace-Kontext durch Ihren gesamten Anfragefluss weiter:

// HTTP-Client
async function callService(url: string, context: Context) {
  return fetch(url, {
    headers: {
      'X-Trace-Id': context.traceId,
      'X-Span-Id': context.spanId,
      'X-Parent-Span-Id': context.parentSpanId
    }
  });
}

// Message Queue
async function publishMessage(queue: string, message: any, context: Context) {
  await queue.publish({
    ...message,
    _traceContext: {
      traceId: context.traceId,
      spanId: generateSpanId(),
      parentSpanId: context.spanId
    }
  });
}

Schlüsseloperationen instrumentieren

Konzentrieren Sie das Tracing auf:

  • Aufrufe externer Services
  • Datenbankabfragen
  • Cache-Operationen
  • Message-Queue-Operationen
  • Wichtige Geschäftslogik
async function processPayment(payment: Payment, context: Context) {
  return tracer.startSpan('processPayment', { parent: context.span }, async (span) => {
    span.setAttributes({
      'payment.id': payment.id,
      'payment.amount': payment.amount,
      'payment.currency': payment.currency
    });

    try {
      const result = await paymentProvider.charge(payment);
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    }
  });
}

Die Säulen verbinden

Die wahre Stärke entsteht durch die Verbindung von Logs, Metriken und Traces:

Trace-ID überall

Fügen Sie die Trace-ID in alle Logs und Metriken ein:

logger.info('Payment processed', {
  traceId: context.traceId,  // Verknüpft mit Trace
  paymentId: payment.id
});

paymentDuration.observe(
  { traceId: context.traceId },  // Verknüpft mit Trace
  duration
);

Exemplare

Verknüpfen Sie Metriken mit konkreten Traces:

// Wenn Sie einen Latenzanstieg sehen, klicken Sie durch, um
// die tatsächlichen Traces zu sehen, die ihn verursacht haben
httpLatency.observe(
  { method: 'POST', path: '/payments' },
  duration,
  { traceId: context.traceId }  // Exemplar
);

Alerting-Strategie

Bei Symptomen alarmieren, nicht bei Ursachen

# Schlecht - alarmiert bei der Ursache
- alert: HighCPU
  expr: cpu_usage > 80%

# Gut - alarmiert beim Symptom
- alert: HighLatency
  expr: http_request_duration_seconds{quantile="0.99"} > 2

Mehrere Signale nutzen

- alert: PaymentServiceDegraded
  expr: |
    (
      rate(payment_errors_total[5m]) / rate(payment_requests_total[5m]) > 0.01
    ) and (
      histogram_quantile(0.99, rate(payment_duration_seconds_bucket[5m])) > 5
    )
  annotations:
    summary: "Payment service is degraded - high errors AND high latency"

Praktische Tipps

Einfach anfangen

Versuchen Sie nicht, alles auf einmal zu instrumentieren:

  1. Die vier goldenen Signale zu jedem Service hinzufügen
  2. Strukturiertes Logging mit Trace-IDs hinzufügen
  3. Tracing für externe Aufrufe hinzufügen
  4. Basierend auf dem tatsächlichen Debugging-Bedarf erweitern

Dashboards nützlich gestalten

Ein gutes Dashboard beantwortet: „Ist das System gerade gesund?”

Enthalten Sie:

  • Anfragerate und Fehlerrate
  • Latenz-Perzentile (p50, p95, p99)
  • Sättigungsmetriken (Queue-Tiefe, Connection Pool)
  • Wichtige Geschäftsmetriken

Ihre Observability üben

Führen Sie Game Days durch, bei denen Sie:

  1. Ausfälle künstlich herbeiführen
  2. Versuchen, nur mit Observability-Tools zu diagnostizieren
  3. Lücken in der Instrumentierung identifizieren

Fazit

Gute Observability bedeutet nicht, mehr Daten zu sammeln – es bedeutet, die richtigen Daten zu sammeln und sie leicht nutzbar zu machen.

Konzentrieren Sie sich auf:

  • Strukturierte, kontextreiche Logs
  • Die vier goldenen Signale für Metriken
  • Traces für das Verständnis des Anfrageflusses
  • Die Verbindung aller drei mit Trace-IDs

Wenn in der Produktion etwas schiefgeht, sollten Sie in Minuten, nicht Stunden, vom Alarm zur Grundursache gelangen können.