Observability-Stack: Metriken, Logs und Traces
Baue einen vollständigen Observability-Stack mit Prometheus, strukturiertem JSON-Logging und OpenTelemetry — und verbinde die Signale sinnvoll.

Observability bedeutet, anhand der Ausgaben eines Systems zu verstehen, was darin passiert. Drei Signale bilden das Fundament: Metriken sagen dir, dass etwas nicht stimmt, Logs sagen dir, was schiefgelaufen ist, und Traces zeigen dir, wo es über Servicegrenzen hinweg schiefgelaufen ist. Jedes Signal ist für sich genommen nützlich, aber ihre eigentliche Stärke entfalten sie erst im Zusammenspiel.
Die meisten Teams starten mit einem einzelnen Signal und ergänzen die anderen nach und nach. Wer alle drei von Anfang an richtig einrichtet, erspart sich monatelanges Nachrüsten.
Strukturiertes Logging: die Grundlage
Logs sind deine erste Anlaufstelle bei der Fehlersuche. Unstrukturierte Text-Logs (console.log("something happened")) sind im großen Maßstab fast nutzlos – sie lassen sich weder durchsuchen noch aggregieren noch korrelieren.
// ❌ Unstructured logging — impossible to query at scale
console.log("User logged in");
console.log("Order created for user 123");
console.log("Payment failed: insufficient funds");
// grep for "payment"? Returns every line with "payment"
// Find all errors for user 123? Good luck// ✅ Structured JSON logging
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
formatters: {
level(label: string) {
return { level: label };
},
},
timestamp: pino.stdTimeFunctions.isoTime,
// Add base context to every log line
base: {
service: "api-gateway",
version: process.env.APP_VERSION ?? "unknown",
environment: process.env.NODE_ENV ?? "development",
},
});
// Request-scoped child logger
function createRequestLogger(
req: Request
): pino.Logger {
return logger.child({
requestId: req.headers["x-request-id"] as string,
userId: req.user?.id,
method: req.method,
path: req.path,
userAgent: req.headers["user-agent"],
});
}
// Usage with context
app.post("/api/orders", async (req, res) => {
const log = createRequestLogger(req);
log.info(
{ itemCount: req.body.items.length },
"Order creation started"
);
try {
const order = await createOrder(req.body);
log.info(
{
orderId: order.id,
total: order.total,
duration: Date.now() - req.startTime,
},
"Order created successfully"
);
res.status(201).json(order);
} catch (error) {
log.error(
{
error: (error as Error).message,
stack: (error as Error).stack,
body: req.body,
},
"Order creation failed"
);
res.status(500).json({ error: "Order creation failed" });
}
});
// Output (one JSON object per line):
// {"level":"info","time":"2024-05-10T10:30:00.000Z",
// "service":"api-gateway","requestId":"abc-123",
// "userId":"user-456","method":"POST",
// "path":"/api/orders","itemCount":3,
// "msg":"Order creation started"}Prometheus-Metriken: das Systemverhalten messen
Metriken sind numerische Messwerte, die über die Zeit erfasst werden. Sie beantworten Fragen zu deinem System wie „wie viele“, „wie schnell“ und „wie viel“.
import {
collectDefaultMetrics,
register,
Histogram,
Counter,
Gauge,
} from "prom-client";
// Collect Node.js runtime metrics
collectDefaultMetrics();
// HTTP request metrics
const httpRequestDuration = new Histogram({
name: "http_request_duration_seconds",
help: "Duration of HTTP requests in seconds",
labelNames: ["method", "route", "status_code"],
buckets: [
0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
],
});
const httpRequestsTotal = new Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["method", "route", "status_code"],
});
// Business metrics
const ordersCreated = new Counter({
name: "orders_created_total",
help: "Total number of orders created",
labelNames: ["status"],
});
const activeConnections = new Gauge({
name: "active_connections",
help: "Number of active client connections",
});
// Metrics middleware
function metricsMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const start = process.hrtime.bigint();
res.on("finish", () => {
const durationNs =
Number(process.hrtime.bigint() - start);
const durationSec = durationNs / 1e9;
const route =
req.route?.path ?? req.path ?? "unknown";
const labels = {
method: req.method,
route,
status_code: String(res.statusCode),
};
httpRequestDuration.observe(labels, durationSec);
httpRequestsTotal.inc(labels);
});
next();
}
// Expose /metrics endpoint for Prometheus scraping
app.get("/metrics", async (req, res) => {
res.set("Content-Type", register.contentType);
res.send(await register.metrics());
});
app.use(metricsMiddleware);Verteiltes Tracing mit OpenTelemetry
Traces verfolgen eine Anfrage über Servicegrenzen hinweg. Jeder Span steht dabei für eine einzelne Arbeitseinheit innerhalb des Traces.
import {
NodeTracerProvider,
} from "@opentelemetry/sdk-trace-node";
import {
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import {
OTLPTraceExporter,
} from "@opentelemetry/exporter-trace-otlp-http";
import {
HttpInstrumentation,
} from "@opentelemetry/instrumentation-http";
import {
ExpressInstrumentation,
} from "@opentelemetry/instrumentation-express";
import {
PgInstrumentation,
} from "@opentelemetry/instrumentation-pg";
import {
registerInstrumentations,
} from "@opentelemetry/instrumentation";
import { Resource } from "@opentelemetry/resources";
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
// Initialize tracing — must run before app code
const provider = new NodeTracerProvider({
resource: new Resource({
[ATTR_SERVICE_NAME]: "order-service",
[ATTR_SERVICE_VERSION]:
process.env.APP_VERSION ?? "1.0.0",
}),
});
provider.addSpanProcessor(
new SimpleSpanProcessor(
new OTLPTraceExporter({
url:
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
"http://localhost:4318/v1/traces",
})
)
);
provider.register();
// Auto-instrument libraries
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
new PgInstrumentation(),
],
});Die drei Signale verbinden
Die eigentliche Stärke entsteht erst, wenn man die Signale miteinander korreliert. Eine Trace-ID in deinen Logs erlaubt es dir, von einer Log-Zeile direkt zum vollständigen Trace zu springen. Labels an deinen Metriken lassen dich von einer Anomalie bis zu den konkreten betroffenen Anfragen vordringen.
import { trace, context } from "@opentelemetry/api";
// Enrich logs with trace context
function createCorrelatedLogger(
req: Request
): pino.Logger {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
return logger.child({
requestId: req.headers["x-request-id"],
traceId: spanContext?.traceId,
spanId: spanContext?.spanId,
userId: req.user?.id,
method: req.method,
path: req.path,
});
}
// Now logs contain trace IDs:
// {"level":"error","traceId":"abc123def456...",
// "spanId":"789xyz...","msg":"Payment failed"}
//
// Click the traceId in your log viewer →
// see the full distributed trace in Jaeger/Tempo
// Create custom spans for business operations
const tracer = trace.getTracer("order-service");
async function processPayment(
orderId: string,
amount: number
) {
return tracer.startActiveSpan(
"process-payment",
async (span) => {
span.setAttribute("order.id", orderId);
span.setAttribute("payment.amount", amount);
try {
const result =
await paymentGateway.charge(amount);
span.setAttribute(
"payment.transaction_id",
result.transactionId
);
span.setStatus({ code: 0 }); // OK
return result;
} catch (error) {
span.setStatus({
code: 2,
message: (error as Error).message,
});
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
}
);
}Alert-Regeln: Metriken handlungsfähig machen
Metriken ohne Alerts sind nur Diagramme, die sich niemand ansieht. Definiere SLO-basierte Alerts, die den Bereitschaftsdienst benachrichtigen, sobald sich die Nutzererfahrung verschlechtert.
# prometheus-alerts.yml
groups:
- name: api-slos
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
> 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate exceeds 1% SLO"
description: >-
Error rate is {{ $value | humanizePercentage }}
over the last 5 minutes
- alert: HighLatency
expr: |
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m]))
by (le)
) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "P95 latency exceeds 500ms SLO"
- alert: OrderCreationSpike
expr: |
rate(orders_created_total{status="failed"}[5m])
> 0.1
for: 3m
labels:
severity: critical
annotations:
summary: "Order creation failures spiking"Das Wichtigste in Kürze
Strukturiertes JSON-Logging mit anfragebezogenem Kontext (Request-ID, User-ID, Trace-ID) macht aus unlesbarem Fließtext durchsuchbare Daten – jede Log-Zeile sollte ein strukturiertes Ereignis sein, das sich filtern, aggregieren und über Services hinweg korrelieren lässt. Prometheus-Metriken mit gelabelten Histogrammen, Countern und Gauges quantifizieren das Systemverhalten über die Zeit: Instrumentiere HTTP-Anfragen, Datenbankabfragen und Business-Operationen mit aussagekräftigen Labels und stelle einen /metrics-Endpunkt zum Scrapen bereit. Verteiltes Tracing mit OpenTelemetry verfolgt Anfragen als Abfolge von Spans über Servicegrenzen hinweg – instrumentiere HTTP-Clients, Express und Datenbank-Treiber beim Start automatisch und ergänze eigene Spans für geschäftskritische Operationen, die Sichtbarkeit brauchen. Verbinde alle drei Signale, indem du Trace-IDs in Log-Zeilen aufnimmst und Metrik-Labels mit Trace-Attributen abgleichst – so entsteht ein Debugging-Workflow, in dem Metriken Anomalien erkennen, Logs den Kontext liefern und Traces exakt den Service und die Operation bestimmen, die fehlgeschlagen sind.


