Zum Inhalt springen

Observability jenseits von Monitoring: Traces, Logs und Metriken

Wie man einen vollständigen Observability-Stack baut, der Traces, strukturierte Logs und Metriken verbindet — schneller als reines Monitoring.

4 Min. Lesezeit
Diagramm der drei Säulen der Observability, das Traces, Logs und Metriken zu einer einheitlichen Ansicht verbindet

Monitoring sagt dir, dass etwas nicht stimmt. Observability sagt dir, warum. Dieser Unterschied ist entscheidend, wenn du um 2 Uhr nachts ein Produktionsproblem debuggst. Wenn deine Dashboards erhöhte Fehlerraten zeigen, du aber keine einzelne fehlgeschlagene Anfrage durch dein System verfolgen kannst, hast du Monitoring ohne Observability.

Die drei Säulen — Traces, Logs und Metriken — sind isoliert betrachtet wenig hilfreich. Ihre eigentliche Stärke liegt in der Korrelation: einen Metrik-Ausschlag mit den Traces zu verknüpfen, die ihn verursacht haben, und diese Traces wiederum mit den Log-Zeilen, die die eigentliche Ursache erklären.

Metriken: der Ausgangspunkt

Metriken sind über die Zeit aggregierte Zahlen. Sie zeigen dir, was auf Systemebene passiert, aber nicht, warum.

tstypescript
// ❌ Only tracking high-level metrics
const requestCount = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
});
 
app.use((req, res, next) => {
  requestCount.inc();
  next();
});
// You know requests increased, but not which endpoints or status codes
tstypescript
// ✅ Metrics with dimensions for drill-down
import { Counter, Histogram } from 'prom-client';
 
const requestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
});
 
const requestErrors = new Counter({
  name: 'http_request_errors_total',
  help: 'HTTP request errors',
  labelNames: ['method', 'route', 'error_type'],
});
 
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
 
  res.on('finish', () => {
    const duration = Number(process.hrtime.bigint() - start) / 1e9;
    const route = req.route?.path ?? 'unknown';
 
    requestDuration
      .labels(req.method, route, String(res.statusCode))
      .observe(duration);
 
    if (res.statusCode >= 400) {
      requestErrors
        .labels(req.method, route, String(res.statusCode))
        .inc();
    }
  });
 
  next();
});

Mit gelabelten Metriken kannst du Fragen wie „Welcher Endpunkt ist langsam?" oder „Welche Fehlercodes nehmen zu?" direkt aus den Metrikdaten beantworten. Verwende Histogramme für Latenzen (sie liefern dir Perzentile), Counter für Summenwerte und Gauges für aktuelle Werte wie die Queue-Tiefe.

Strukturiertes Logging

Unstrukturierte Logs lassen sich im großen Maßstab nicht sinnvoll abfragen. Strukturierte Logs sind durchsuchbar, filterbar und lassen sich mit Traces korrelieren.

tstypescript
// ❌ Unstructured log lines
console.log(`User ${userId} created order ${orderId} - total: $${total}`);
console.log(`ERROR: Payment failed for order ${orderId}`);
// Good luck searching for "all payment failures over $100 in the last hour"
tstypescript
// ✅ Structured JSON logs with consistent fields
import pino from 'pino';
 
const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});
 
// Application code
logger.info({
  event: 'order_created',
  userId,
  orderId,
  total,
  currency: 'USD',
  itemCount: items.length,
});
 
logger.error({
  event: 'payment_failed',
  orderId,
  userId,
  amount: total,
  errorCode: 'card_declined',
  provider: 'stripe',
  traceId: req.headers['x-trace-id'],
});

Strukturierte Logs bieten zwei Vorteile: Du kannst sie abfragen (event=payment_failed AND amount>100) und Trace-IDs einbetten, die Log-Zeilen mit verteilten Traces verknüpfen.

Verteiltes Tracing

Ein Trace verfolgt eine einzelne Anfrage durch jeden Service, den sie durchläuft. Jeder Abschnitt ist ein Span. Die Spans bilden einen Baum, der zeigt, wo die Zeit verbraucht wurde.

tstypescript
import { trace, SpanStatusCode } from '@opentelemetry/api';
 
const tracer = trace.getTracer('order-service');
 
async function createOrder(userId: string, items: CartItem[]) {
  return tracer.startActiveSpan('createOrder', async (span) => {
    try {
      span.setAttribute('user.id', userId);
      span.setAttribute('order.item_count', items.length);
 
      // Child span: validate inventory
      const inventory = await tracer.startActiveSpan(
        'validateInventory',
        async (childSpan) => {
          try {
            const result = await inventoryService.check(items);
            childSpan.setAttribute('inventory.available', result.allAvailable);
            return result;
          } finally {
            childSpan.end();
          }
        }
      );
 
      if (!inventory.allAvailable) {
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: 'Items out of stock',
        });
        throw new Error('Items out of stock');
      }
 
      // Child span: process payment
      const payment = await tracer.startActiveSpan(
        'processPayment',
        async (childSpan) => {
          try {
            const result = await paymentService.charge(userId, total);
            childSpan.setAttribute('payment.provider', 'stripe');
            childSpan.setAttribute('payment.amount', total);
            return result;
          } finally {
            childSpan.end();
          }
        }
      );
 
      span.setAttribute('order.id', payment.orderId);
      return { orderId: payment.orderId, status: 'confirmed' };
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}

Der Trace aus diesem Code zeigt: createOrder (350ms) → validateInventory (50ms) + processPayment (280ms). Ist die Zahlung langsam, siehst du genau, wo der Engpass liegt, und kannst in die Spans des Payment-Service eintauchen.

Die drei Säulen korrelieren

Der eigentliche Nutzen entsteht erst, wenn man Metriken, Logs und Traces miteinander verknüpft. Ein Metrik-Alert führt zu Traces, und diese führen zu konkreten Log-Zeilen.

tstypescript
// Middleware that connects all three pillars
import { trace, context } from '@opentelemetry/api';
 
function observabilityMiddleware(req: Request, res: Response, next: NextFunction) {
  const span = trace.getActiveSpan();
  const traceId = span?.spanContext().traceId ?? 'no-trace';
 
  // Attach trace ID to logger — all log lines include it
  req.log = logger.child({ traceId, requestId: req.id });
 
  // Attach trace ID to response headers — clients can report it
  res.setHeader('X-Trace-Id', traceId);
 
  const start = process.hrtime.bigint();
 
  res.on('finish', () => {
    const duration = Number(process.hrtime.bigint() - start) / 1e9;
 
    // Metric with trace exemplar
    requestDuration
      .labels(req.method, req.route?.path ?? 'unknown', String(res.statusCode))
      .observe(duration);
 
    // Structured log with trace correlation
    req.log.info({
      event: 'request_completed',
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration,
    });
  });
 
  next();
}

Der Korrelationsablauf:

  1. Metrik-Alert: Die P99-Latenz für /api/orders überschreitet 2s
  2. Trace-Suche: Traces zu /api/orders mit einer Dauer > 2s in den letzten 10 Minuten werden gesucht
  3. Span-Analyse: Der Span processPayment liegt konstant bei 1,8s (normal 200ms)
  4. Log-Drilldown: Logs werden nach traceId gefiltert — es zeigen sich payment_timeout-Fehler vom EU-Endpunkt von Stripe

Alarmierung anhand der richtigen Signale

Alarmiere bei Symptomen (Auswirkungen, die Nutzer spüren), nicht bei Ursachen (CPU-Auslastung). Verwende die RED-Methode für Services und die USE-Methode für Ressourcen.

ymlyaml
# ❌ Alerting on causes — noisy and often irrelevant
groups:
  - name: infrastructure
    rules:
      - alert: HighCPU
        expr: cpu_usage > 80
        # CPU at 81% with no user impact → false alarm at 3am
 
# ✅ Alerting on symptoms — user-facing impact
groups:
  - name: service-health
    rules:
      - alert: HighErrorRate
        expr: |
          sum(rate(http_request_errors_total{status_code=~"5.."}[5m]))
          /
          sum(rate(http_requests_total[5m])) > 0.01
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Error rate exceeds 1% for 5 minutes"
 
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            rate(http_request_duration_seconds_bucket[5m])
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P99 latency exceeds 2s for 5 minutes"

Die Klausel for: 5m verhindert, dass bei kurzzeitigen Ausschlägen alarmiert wird. Eine einzelne langsame Anfrage ist Rauschen. Fünf Minuten erhöhter Latenz sind dagegen ein echtes Problem.

Die wichtigsten Erkenntnisse

  1. Metriken zeigen, was passiert — verwende gelabelte Histogramme und Counter mit Dimensionen, um tiefer einzusteigen
  2. Strukturierte Logs erklären, warum — verwende JSON-Logging mit konsistenten Feldnamen und Trace-IDs
  3. Traces zeigen, wo die Zeit verbraucht wird — instrumentiere Service-Grenzen und teure Operationen als Spans
  4. Korrelation ist der Multiplikator — verknüpfe Metriken → Traces → Logs über Trace-IDs, die über Services hinweg propagiert werden
  5. Alarmiere bei Symptomen, nicht bei Ursachen — Fehlerraten und Latenz-Perzentile zählen mehr als die CPU-Auslastung
  6. Verwende die for-Klausel — verlange anhaltende Verstöße, bevor alarmiert wird, um Rauschen durch kurzzeitige Ausschläge zu vermeiden
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX