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.

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.
// ❌ 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// ✅ 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.
// ❌ 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"// ✅ 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.
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.
// 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:
- Metrik-Alert: Die P99-Latenz für
/api/ordersüberschreitet 2s - Trace-Suche: Traces zu
/api/ordersmit einer Dauer > 2s in den letzten 10 Minuten werden gesucht - Span-Analyse: Der Span
processPaymentliegt konstant bei 1,8s (normal 200ms) - 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.
# ❌ 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
- Metriken zeigen, was passiert — verwende gelabelte Histogramme und Counter mit Dimensionen, um tiefer einzusteigen
- Strukturierte Logs erklären, warum — verwende JSON-Logging mit konsistenten Feldnamen und Trace-IDs
- Traces zeigen, wo die Zeit verbraucht wird — instrumentiere Service-Grenzen und teure Operationen als Spans
- Korrelation ist der Multiplikator — verknüpfe Metriken → Traces → Logs über Trace-IDs, die über Services hinweg propagiert werden
- Alarmiere bei Symptomen, nicht bei Ursachen — Fehlerraten und Latenz-Perzentile zählen mehr als die CPU-Auslastung
- Verwende die
for-Klausel — verlange anhaltende Verstöße, bevor alarmiert wird, um Rauschen durch kurzzeitige Ausschläge zu vermeiden


