Observability im Hosting: Monitoring, Logging und Tracing richtig aufsetzen
Monitoring allein reicht nicht. Moderne Webanwendungen brauchen Observability — die Fähigkeit, aus Daten zu verstehen, warum etwas schiefläuft, nicht nur dass. Dieser Guide zeigt die Open-Source-Toolchain: Prometheus, Grafana, Loki und Jaeger.
Monitoring vs. Observability — Der Unterschied
Monitoring sagt dir, dass ein System down ist. Observability sagt dir, warum — und wie das System sich unter bisher ungesehenen Bedingungen verhält. Drei Säulen:
- Metriken (Metrics): Numerische Messwerte über Zeit (CPU, Request-Latenz, Fehlerrate)
- Logs: Ereignisse mit Zeitstempel (was passiert ist, wann, wo)
- Traces (Distributed Tracing): Requests durch mehrere Services hindurch verfolgen
Monitoring ist eine Teilmenge von Observability. Observability schließt unknowables ein — Probleme, die du noch nicht kennst.
SLOs definieren: Bevor du Observability aufsetzt, definiere Service Level Objectives (SLOs): "99,9% der API-Requests antworten innerhalb 200ms." Ohne SLOs weißt du nicht, welche Alerts wirklich wichtig sind. Folge: Alert-Fatigue.
Prometheus: Metriken sammeln und abfragen
Prometheus scrapt Metriken von deinen Services über HTTP (/metrics-Endpoint) und speichert sie als Zeitreihen. Es gibt kaum ein System, das keinen Prometheus-Metriken-Export unterstützt.
# Beispiel: Node Exporter — Systemmetriken (CPU, RAM, Disk, Netzwerk)
# Installation auf jedem Server:
wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz
tar xzf node_exporter-1.7.0.linux-amd64.tar.gz
./node_exporter
node_exporter läuft auf Port 9100 und exportiert Metriken wie node_cpu_seconds_total, node_memory_MemTotal_bytes, node_filesystem_avail_bytes.
Prometheus-Konfiguration
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
rule_files:
- "alert_rules.yml"
scrape_configs:
# Prometheus itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Node Exporter — jeder Server im Cluster
- job_name: 'node'
static_configs:
- targets: ['server1:9100', 'server2:9100', 'server3:9100']
# Deine Node.js/Express-App (mit prom-client)
- job_name: 'webapp'
metrics_path: /metrics
static_configs:
- targets: ['webapp:3000']Metriken in Node.js/Express exportieren
// server.js (Express)
import express from 'express';
import client from 'prom-client';
// Default-Registry inkl. HTTP-Metriken
const register = new client.Registry();
client.collectDefaultMetrics({ register });
// Custom Metrics
const httpRequestDuration = new client.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.5, 1, 2, 5],
registers: [register],
});
const activeConnections = new client.Gauge({
name: 'http_active_connections',
help: 'Number of active HTTP connections',
registers: [register],
});
app.use((req, res, next) => {
activeConnections.inc();
const start = Date.now();
res.on('finish', () => {
activeConnections.dec();
httpRequestDuration
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe((Date.now() - start) / 1000);
});
next();
});
// /metrics-Endpoint für Prometheus
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});Grafana: Dashboards und Alerting
Grafana visualisiert Prometheus-Metriken und vieles mehr. Datasource hinzufügen: Prometheus → http://prometheus:9090.
Wichtige Panel-Typen für Webhosting
Request Rate
P99 Latency
Error Rate
CPU Usage
Alerting-Regeln
# alert_rules.yml
groups:
- name: webapp_alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status_code=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "Hohe 5xx-Fehlerrate"
description: "Fehlerrate {{ $value | printf \"%.2f\" }}% über 5 Minuten."
- alert: HighLatency
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 10m
labels:
severity: warning
annotations:
summary: "Hohe P99-Latenz"
description: "P99-Latenz {{ $value }}s über 10 Minuten."
- alert: HighMemoryUsage
expr: (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) < 0.15
for: 5m
labels:
severity: warning
annotations:
summary: "Wenig freier Arbeitsspeicher"
description: "Nur {{ $value | printf \"%.1f\" }}% RAM frei auf {{ $labels.instance }}."
- alert: InstanceDown
expr: up == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Instance nicht erreichbar"
description: "{{ $labels.job }} auf {{ $labels.instance }} ist seit 2 Minuten down."Loki: Zentralisierte Logs ohne Elasticsearch
Loki ist Log-Aggregation ohne die Komplexität von Elasticsearch. Es speichert Log-Streams indiziert nach Labeln (nicht nach Fulltext) — dadurch extrem günstig. Drei Komponenten:
- Loki: Log-Speicher und Query-Engine
- Promtail: Log-Sammler (auf jedem Node, auf dem Logs entstehen)
- Grafana: Log-Visualisierung (Datasource: Loki)
# promtail-config.yml (auf jedem Node)
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /var/log/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# Node.js/Express Logs
- job_name: webapp
static_configs:
- targets:
- localhost
labels:
job: webapp
env: production
__path__: /var/log/webapp/*.log
# Nginx Logs
- job_name: nginx
static_configs:
- targets:
- localhost
labels:
job: nginx
__path__: /var/log/nginx/*.log
# Logging format in Node.js (JSON für Loki-Parsing):
# {"level":"error","timestamp":"2026-06-04T10:00:00Z","message":"Connection timeout","requestId":"abc123","service":"api"}Log-Label-Strategie: Halt die Zahl der Labels niedrig. Zu viele Labels führen zu "churn rate"-Problemen in Loki (zu viele Streams). Nutze job, env, service, region — nicht user_id oder request_id als Label.
LogQL: Loki-Metriken aus Logs erzeugen
Das Geniale an Loki: du kannst Logs in Metriken umwandeln. LogQL ist das Query-Language-Äquivalent zu PromQL.
# Fehlerrate aus Logs berechnen (LogQL → Metrik)
sum by (job) (
rate(
{job="webapp"} | json | level="error" [5m]
)
)
# Latenzen aus Access-Logs extrahieren
avg by (path) (
rate(
{job="nginx"} | json | status_code >= 200 | status_code < 400
| unwrap latency_ms [5m]
)
)
# Fehler-Streams mit mehr Kontext
{job="webapp"} | json | level="error" | status_code >= 500 | line_format "{{.timestamp}} {{.message}} {{.requestId}}"Distributed Tracing mit Jaeger
Bei Microservices oder vielen verbundenen Services brauchst du Distributed Tracing — um zu sehen, wo ein Request wie lange braucht. OpenTelemetry ist der Standard für Instrumentation, Jaeger der Backend-Store.
# Node.js mit OpenTelemetry + Jaeger
import { NodeSDK } from '@opentelemetry/sdk-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'webapp',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
traceExporter: new JaegerExporter({
endpoint: 'http://jaeger:14268/api/traces',
}),
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
],
});
sdk.start();
// HTTP-Request-Tracing in Express
// Jeder Request bekommt automatisch einen Trace-Span
// Kinder: Datenbank-Call, External API, Cache Lookup
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((err) => console.error('Error terminating tracing', err))
.finally(() => process.exit(0));
});In Jaeger UI siehst du jeden Request als Trace mit allen Spans: vom HTTP-Endpoint über Datenbankabfragen bis zu Downstream-API-Aufrufen. Das macht Performance-Bottlenecks sichtbar.
Die vollständige Toolchain: Prometheus + Grafana + Loki + Jaeger
| Tool | Einsatzbereich | Speicherort Metriken | Skalierung |
|---|---|---|---|
| Prometheus | Metriken / Alerting | TSDB (lokal) | Horizontal (föderiert) |
| Grafana | Visualisierung + Alerting UI | — (nur Queries) | Stateless, multi-Tenant |
| Loki | Log-Aggregation | Object Store (S3/GCS/MinIO) | Read/Write-Scalable |
| Jaeger | Distributed Tracing | Elasticsearch/Cassandra | Multi-Tenant-Sampler |
| Grafana OnCall | Alert-Routing (PagerDuty-Ersatz) | — | Cloud oder Self-hosted |
Alert-Fatigue vermeiden
Die häufigste Fehlerquelle in Monitoring: zu viele Alerts, zu wenig Kontext. Regeln:
- Alert nur, wenn: human action needed. Automatisch behebbare Probleme → Runbook-Aktion, kein Alert.
- Benachrichtige via Severity: Critical → PagerDuty/SMS sofort; Warning → Slack mit Zeitverzögerung; Info → nur Dashboard.
- Snooze-Protection: Alert-Hold für geplante Wartungsfenster (Grafana Alerting: Mute Timings).
- Every Alert = Toil-Reduktion: Wenn ein Alert mehr als 2× im Monat auftritt → automatisieren oder fixe Dauer-Maßnahme ins Runbook.
OpenTelemetry: Der Standard-Collector's Ansatz
Anstatt jeden Service einzeln zu instrumentieren, nutze den OpenTelemetry Collector — ein zentraler Sidecar, der Metriken, Logs und Traces normalisiert und an Backend-Dienste weiterleitet.
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
http:
prometheus:
config:
scrape_configs:
- job_name: 'webapp'
static_configs:
- targets: ['webapp:3000']
# Logs von überall
journald:
units: ['webapp.service', 'nginx.service']
processors:
batch:
timeout: 10s
memory_limiter:
check_interval: 1s
limit_percentage: 75
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
namespace: "otel"
const_labels:
env: production
loki:
endpoint: http://loki:3100/loki/api/v1/push
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
exporters: [jaeger]
metrics:
receivers: [prometheus, otlp]
exporters: [prometheus]
logs:
receivers: [journald, otlp]
exporters: [loki]Dein Observability-Spickzettel
- Prometheus scrapt Metriken über HTTP — exportiere sie von jedem Service über den
/metrics-Endpoint - Grafana Dashboards = Metriken visualisieren. Nutze P99-Latenz, nicht Durchschnitt — der Median ist im Hosting irrelevant
- SLOs zuerst: Definiere Fehlerbudgets, bevor du Alerts baust. Ziel: MTTR senken, nicht Alert-Count maximieren
- Loki statt Elasticsearch für Logs — Label-basiert, deutlich günstiger, und gut mit Grafana integriert
- OpenTelemetry ist der Zukunftsstandard — instrumentiere einmal, sende an Prometheus/Loki/Jaeger gleichzeitig
- Alert nur bei Action-Bedarf. Alle anderen Events → Runbook automatisieren oder ins Dashboard verlagern
- Runbooks schreiben: Jeder Alert braucht eine klare Response-Prozedur. Ein Alert ohne Runbook ist nur Lärm.