Betrieb & Architektur
Monitoring & Observability: Warum Fehler oft sichtbar sind – aber niemand hinschaut
Das System läuft. Die Tests sind grün. Der Go-Live war erfolgreich. Und dann passiert es: Der Kunde meldet, dass Bestellungen nicht durchkommen. Der Support sieht nichts in den Logs. Die Entwickler starten eine Fehlersuche, die Stunden dauert. Das Problem war die ganze Zeit sichtbar – es hat nur niemand hingeschaut, weil niemand wusste, wo.
Monitoring vs. Observability: Der Unterschied zählt
Monitoring beantwortet vordefinierte Fragen: Ist der Server erreichbar? Wie hoch ist die CPU-Auslastung? Wie viele Requests pro Sekunde? Sie definieren vorher, was Sie wissen wollen, und richten Dashboards und Alerts dafür ein.
Observability beantwortet Fragen, die Sie noch nicht kennen. Wenn ein Nutzer meldet „Bestellungen dauern manchmal 30 Sekunden“, können Sie mit guter Observability die Ursache finden, ohne vorher einen spezifischen Alert dafür eingerichtet zu haben. Durch strukturierte Logs, Traces und Metriken navigieren Sie sich von Symptom zur Ursache.
| Aspekt | Monitoring | Observability |
|---|---|---|
| Fragt | „Ist es kaputt?“ | „Warum ist es kaputt?“ |
| Ansatz | Vordefinierte Checks & Schwellwerte | Explorative Analyse unbekannter Probleme |
| Daten | Metriken, Uptime, Fehlerrate | Logs, Traces, Metriken – korreliert |
| Reicht für | Einfache Systeme, bekannte Fehlermodi | Verteilte Systeme, unbekannte Fehlermodi |
Für die meisten Projekte gilt: Sie brauchen beides. Monitoring als erste Verteidigungslinie (Alerting, Dashboards), Observability als Werkzeug zur Fehleranalyse (Structured Logging, Tracing).
Die drei Säulen: Logs, Metriken, Traces
1. Structured Logging
Der häufigste Fehler in Logging-Architekturen: unstrukturierte Textzeilen, die kein Mensch und keine Maschine zuverlässig parsen kann.
// Schlecht: Freitext-Logging
$logger->info("Bestellung 4711 von Kunde 89 angelegt, Betrag 249.90 EUR");
// Gut: Structured Logging (JSON)
$logger->info('order.created', [
'order_id' => 4711,
'customer_id' => 89,
'amount' => 249.90,
'currency' => 'EUR',
'items_count' => 3,
'channel' => 'web',
'duration_ms' => 127
]);
Warum JSON statt Freitext:
- Maschinen können filtern:
jq '.[] | select(.amount > 1000)' - Dashboards können aggregieren: Durchschnittliche Bestelldauer pro Channel
- Alerts können gezielt auslösen: Wenn
duration_ms > 5000bei mehr als 10 Bestellungen pro Minute - Korrelation wird möglich: Alle Logs mit
order_id=4711über alle Services hinweg finden
Context Enrichment: Logs brauchbar machen
class ContextLogger
{
private array $context = [];
public function withContext(array $context): self
{
$clone = clone $this;
$clone->context = array_merge($this->context, $context);
return $clone;
}
public function info(string $event, array $data = []): void
{
$entry = array_merge($this->context, $data, [
'event' => $event,
'timestamp' => gmdate('c'),
'level' => 'info',
]);
error_log(json_encode($entry, JSON_UNESCAPED_UNICODE));
}
}
// Verwendung: Kontext einmal setzen, alle Logs erben ihn
$logger = (new ContextLogger())
->withContext(['request_id' => bin2hex(random_bytes(8))])
->withContext(['user_id' => $currentUser->id])
->withContext(['tenant_id' => $currentTenant->id]);
$logger->info('order.created', ['order_id' => 4711]);
// Ergebnis: {"request_id":"a3f2...","user_id":89,"tenant_id":5,"event":"order.created","order_id":4711,...}
request_id, die durch alle Log-Einträge eines Requests mitläuft, ist der wichtigste einzelne Kontextparameter. Damit können Sie jeden Request komplett nachvollziehen – über alle Services hinweg.
2. Metriken: Die vier goldenen Signale
Google hat im SRE-Buch vier Metriken definiert, die für jedes System relevant sind. In der Praxis decken sie die meisten Probleme ab:
| Signal | Was es misst | Beispiel-Alert |
|---|---|---|
| Latenz | Wie lange dauern Requests? | p95-Latenz > 2s seit 5 Minuten |
| Traffic | Wie viele Requests kommen rein? | Traffic-Einbruch > 50% gegenüber Vorwoche |
| Fehlerrate | Wie viele Requests scheitern? | 5xx-Rate > 1% seit 10 Minuten |
| Saturation | Wie voll ist das System? | DB-Connections > 80% des Pools |
class MetricsCollector
{
private array $timings = [];
private array $counters = [];
public function timing(string $metric, float $durationMs): void
{
$this->timings[$metric][] = $durationMs;
}
public function increment(string $metric, array $tags = []): void
{
$key = $metric . ':' . implode(',', array_map(
fn($k, $v) => "$k=$v", array_keys($tags), $tags
));
$this->counters[$key] = ($this->counters[$key] ?? 0) + 1;
}
public function getPercentile(string $metric, int $percentile): ?float
{
$values = $this->timings[$metric] ?? [];
if (empty($values)) return null;
sort($values);
$index = (int) ceil(count($values) * $percentile / 100) - 1;
return $values[$index];
}
}
// Verwendung in Middleware
$start = microtime(true);
$response = $handler->handle($request);
$duration = (microtime(true) - $start) * 1000;
$metrics->timing('http.request.duration', $duration);
$metrics->increment('http.request.total', [
'method' => $request->getMethod(),
'status' => $response->getStatusCode(),
'route' => $request->getUri()->getPath()
]);
3. Distributed Tracing
In verteilten Systemen reicht ein einzelner Log-Eintrag nicht. Eine Bestellung durchläuft API-Gateway, Order-Service, Payment-Service, Notification-Service. Tracing verbindet diese Schritte zu einer nachvollziehbaren Kette:
// Vereinfachtes Tracing-Konzept
class Tracer
{
public function startSpan(string $name, ?string $parentId = null): Span
{
return new Span(
traceId: $parentId ? $this->currentTraceId : bin2hex(random_bytes(16)),
spanId: bin2hex(random_bytes(8)),
parentId: $parentId,
name: $name,
startTime: microtime(true)
);
}
}
// Verwendung
$rootSpan = $tracer->startSpan('http.request');
$dbSpan = $tracer->startSpan('db.query', $rootSpan->spanId);
$result = $db->query("SELECT ...");
$dbSpan->finish();
$apiSpan = $tracer->startSpan('external.payment', $rootSpan->spanId);
$paymentResult = $paymentClient->charge($amount);
$apiSpan->finish();
$rootSpan->finish();
// Ergebnis: Ein Trace mit drei Spans, zeitlich zueinander geordnet
// http.request [=========================] 450ms
// db.query [====] 45ms
// external.payment [===========] 280ms
In der Praxis übernehmen Bibliotheken wie OpenTelemetry das Span-Management. Der entscheidende Architekturpunkt: Die trace_id muss über HTTP-Header (traceparent) zwischen Services weitergereicht werden.
Alerting ohne Alert-Fatigue
Das häufigste Problem in Monitoring-Setups ist nicht zu wenig Alerting, sondern zu viel. Teams, die 50 Alerts am Tag bekommen, ignorieren alle – auch die kritischen.
Severity-Stufen klar definieren
| Stufe | Bedeutung | Reaktion | Kanal |
|---|---|---|---|
| Critical | System/Feature komplett ausgefallen | Sofort reagieren, auch nachts | SMS/Anruf, PagerDuty |
| Warning | Degradiert, aber funktionsfähig | Innerhalb der Geschäftszeiten beheben | Slack/Teams-Channel |
| Info | Auffällig, aber nicht dringend | Im nächsten Sprint prüfen | Dashboard, Ticket |
Regeln gegen Alert-Fatigue
- Jeder Alert muss eine Handlung auslösen. Wenn die Reaktion immer „ignorieren“ ist, gehört der Alert weg
- Schwellwerte auf Basis realer Daten setzen, nicht auf Bauchgefühl. Zwei Wochen Baseline messen, dann Schwellwerte definieren
- Rate statt Absolutwert: „5xx-Rate > 1%“ ist stabiler als „mehr als 10 Fehler“ (bei 1.000 Requests sind 10 Fehler normal, bei 100 nicht)
- Zeitfenster beachten: Ein einzelner Timeout ist kein Alert. Fünf Timeouts in drei Minuten schon
- Deduplication: Dieselbe Ursache sollte einen Alert erzeugen, nicht 200
// Alert-Konfiguration (konzeptionell)
$alerts = [
[
'name' => 'high_error_rate',
'condition' => 'rate(http_errors_total[5m]) / rate(http_requests_total[5m]) > 0.01',
'severity' => 'critical',
'for' => '5m', // Muss 5 Minuten lang gelten
'summary' => 'Fehlerrate über 1% seit 5 Minuten',
'runbook' => '/runbooks/high-error-rate.md'
],
[
'name' => 'slow_responses',
'condition' => 'histogram_quantile(0.95, http_request_duration_seconds) > 2',
'severity' => 'warning',
'for' => '10m',
'summary' => 'p95-Latenz über 2 Sekunden seit 10 Minuten',
'runbook' => '/runbooks/slow-responses.md'
],
[
'name' => 'db_connection_pool',
'condition' => 'db_connections_active / db_connections_max > 0.8',
'severity' => 'warning',
'for' => '5m',
'summary' => 'DB Connection Pool über 80% ausgelastet',
'runbook' => '/runbooks/db-pool-exhaustion.md'
]
];
Tool-Entscheidungen: Stack-Auswahl als Architektur
Die Wahl des Monitoring-Stacks ist eine Architekturentscheidung mit langfristigen Konsequenzen. Drei gängige Ansätze:
Elastic Stack (Elasticsearch, Logstash/Beats, Kibana)
Früher als „ELK Stack“ bekannt, heute erweitert um Beats und Fleet. Der Klassiker für Log-Aggregation. Stark bei Volltextsuche und Log-Exploration.
- Stärke: Mächtige Suche, flexible Dashboards, großes Ökosystem
- Schwäche: Hoher Ressourcenverbrauch (Elasticsearch braucht RAM), Betriebsaufwand
- Passt wenn: Viele verschiedene Log-Quellen, hohe Log-Volumina, Team hat ELK-Erfahrung
Prometheus + Grafana
Der Standard für Metriken. Prometheus sammelt, Grafana visualisiert.
- Stärke: Pull-basiert (kein Agent nötig), mächtige Query-Sprache (PromQL), bewährt in Kubernetes-Umgebungen
- Schwäche: Kein Log-Management (braucht Ergänzung wie Loki), langfristige Datenspeicherung erfordert Zusatz-Tools
- Passt wenn: Metriken-fokussiertes Monitoring, Container-Umgebungen, Budget für Betrieb vorhanden
Managed Services (Datadog, New Relic, Elastic Cloud)
- Stärke: Kein Betriebsaufwand, schneller Start, oft Logs+Metriken+Tracing in einem Tool
- Schwäche: Kosten skalieren mit Datenvolumen (können schnell erheblich werden), Vendor Lock-in, Daten liegen extern
- Passt wenn: Kleines Team ohne DevOps-Kapazität, schnelle Ergebnisse nötig, Budget vorhanden
OpenTelemetry: Der Vendor-neutrale Standard
OpenTelemetry (OTel) definiert ein einheitliches Format für Logs, Metriken und Traces. Der Vorteil: Sie instrumentieren Ihren Code einmal und können das Backend jederzeit wechseln – von Jaeger zu Datadog, von Prometheus zu New Relic.
// OpenTelemetry-Konzept: Instrumentation einmal, Backend austauschbar
// PHP: opentelemetry/sdk Paket
$tracerProvider = new TracerProvider(
new SimpleSpanProcessor(
new OtlpHttpExporter('http://collector:4318/v1/traces')
)
);
$tracer = $tracerProvider->getTracer('meine-app');
$span = $tracer->spanBuilder('order.process')
->setAttribute('order.id', $orderId)
->setAttribute('customer.id', $customerId)
->startSpan();
try {
$result = $this->processOrder($orderId);
$span->setStatus(StatusCode::STATUS_OK);
} catch (\Exception $e) {
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
$span->recordException($e);
throw $e;
} finally {
$span->end();
}
Architektur-Empfehlung: Wenn Sie heute ein neues System mit längerer Lebensdauer bauen, ist OpenTelemetry oft die beste vendor-neutrale Grundlage. Die Backend-Entscheidung (selbst gehostet vs. Managed) können Sie dann unabhängig davon treffen und später ändern. Für sehr kleine Monolithen kann Structured Logging + ein einfacher Health-Endpoint aber erstmal ausreichen – OTel bringt dort mehr Struktur als unmittelbaren Nutzen.
Praxis: Monitoring-Architektur für PHP-Anwendungen
Die meisten PHP-Anwendungen sind keine Microservice-Landschaften, sondern monolithische oder modular-monolithische Systeme hinter PHP-FPM. Das verändert die Monitoring-Anforderungen:
PHP-spezifische Metriken
- PHP-FPM Pool-Status: Aktive/idle Worker, Queue-Länge, max_children erreicht
- OPcache-Hitrate: Unter 95% deutet auf Konfigurationsprobleme hin
- Memory pro Request: Steigt der Memory-Verbrauch über die Zeit? (Memory Leaks in Long-Running-Processes)
- Slow Queries: MySQL/PostgreSQL Slow Query Log auswerten
// PHP-FPM Status als Metrik-Quelle
// In der PHP-FPM-Config: pm.status_path = /fpm-status
function collectFpmMetrics(): array
{
$status = json_decode(
file_get_contents('http://127.0.0.1:9000/fpm-status?json=&full'),
true
);
return [
'fpm.active_processes' => $status['active processes'],
'fpm.idle_processes' => $status['idle processes'],
'fpm.total_processes' => $status['total processes'],
'fpm.max_children_reached' => $status['max children reached'],
'fpm.slow_requests' => $status['slow requests'],
'fpm.listen_queue' => $status['listen queue'],
'fpm.listen_queue_max' => $status['max listen queue'],
];
}
Health-Check-Endpoint
// /api/health - Liefert Systemstatus für Load Balancer und Monitoring
function healthCheck(PDO $db, Redis $redis): array
{
$checks = [];
// Datenbank
try {
$start = microtime(true);
$db->query("SELECT 1");
$checks['database'] = [
'status' => 'ok',
'response_ms' => round((microtime(true) - $start) * 1000, 2)
];
} catch (\Exception $e) {
$checks['database'] = ['status' => 'error', 'message' => 'Connection failed'];
}
// Redis/Cache
try {
$start = microtime(true);
$redis->ping();
$checks['cache'] = [
'status' => 'ok',
'response_ms' => round((microtime(true) - $start) * 1000, 2)
];
} catch (\Exception $e) {
$checks['cache'] = ['status' => 'error', 'message' => 'Connection failed'];
}
// Gesamtstatus
$allOk = !in_array('error', array_column($checks, 'status'));
return [
'status' => $allOk ? 'healthy' : 'degraded',
'checks' => $checks,
'timestamp' => gmdate('c'),
'version' => getenv('APP_VERSION') ?: 'unknown'
];
}
- Liveness (
/health/live) – Der Prozess lebt. Antwortet er nicht, wird er neu gestartet. - Readiness (
/health/ready) – Das System kann Traffic annehmen. Während Cache aufgewärmt wird oder Migrationen laufen: alive, aber nicht ready. - Degraded – Das System lebt und nimmt Traffic an, aber ein Teil hängt (z.B. Cache ausgefallen, ein externer Service nicht erreichbar). Kein Neustart nötig, aber Aufmerksamkeit schon.
Typische Monitoring-Fehler
1. Alles loggen, nichts finden
Systeme, die jede Zeile Code loggen, erzeugen Terabytes an Daten, in denen niemand etwas findet. Logging-Volumen korreliert nicht mit Logging-Qualität.
Besser: Log-Level konsequent einsetzen. DEBUG nur in Entwicklung. INFO für geschäftsrelevante Ereignisse (Bestellung angelegt, Zahlung eingegangen). WARNING für degradierte Zustände. ERROR für Fehler, die Aufmerksamkeit brauchen.
2. Dashboards bauen, aber nicht anschauen
20 Grafana-Dashboards erstellt, keines wird regelmäßig geprüft. Das ist Monitoring-Theater.
Besser: Ein einziges Haupt-Dashboard mit den vier goldenen Signalen. Dieses Dashboard wird täglich angeschaut. Alles andere sind Detail-Dashboards für die Fehleranalyse.
3. Monitoring erst nach dem Vorfall einrichten
Klassischer Ablauf: Outage → Postmortem → „Wir brauchen Monitoring“ → hastig Dashboards bauen → nächster Outage in einem anderen Bereich → wiederholen.
Besser: Monitoring ist Teil der Definition of Done. Ein Feature ohne Metriken und Alerting ist nicht fertig.
4. Nur Infrastruktur monitoren, nicht Geschäftslogik
CPU, RAM, Disk – alles grün. Aber es werden keine Bestellungen mehr angelegt. Infrastruktur-Monitoring sieht keine Business-Probleme.
Besser: Business-Metriken messen: Bestellungen pro Stunde, Zahlungseingänge, neue Registrierungen. Wenn diese Metriken einbrechen, wissen Sie sofort, dass etwas nicht stimmt – auch wenn die Server grün sind.
Entscheidungshilfe: Was brauchen Sie?
| Situation | Empfehlung |
|---|---|
| Einzelner Server, PHP-Monolith | Structured Logging + Health-Endpoint + einfaches Dashboard (Grafana oder Managed) |
| Mehrere Services, moderate Last | Structured Logging + Prometheus/Grafana + Alerting-Regeln |
| Microservices, hohe Last | OpenTelemetry + Distributed Tracing + Metriken + Log-Aggregation |
| Regulierte Branche (Finanzen, Gesundheit) | Alles oben + Audit-Logging + Retention-Policies + Compliance-Dashboards |
Checkliste: Monitoring & Observability
- Structured Logging eingeführt? JSON statt Freitext,
request_iddurchgehend - Vier goldene Signale gemessen? Latenz, Traffic, Fehlerrate, Saturation
- Health-Endpoint vorhanden? Liveness + Readiness, prüft echte Abhängigkeiten
- Alerting eingerichtet? Severity-Stufen definiert, Runbooks verlinkt
- Alert-Fatigue vermieden? Jeder Alert löst eine Handlung aus, keine „Noise“-Alerts
- Business-Metriken? Nicht nur CPU und RAM, sondern auch Bestellungen, Zahlungen, Registrierungen
- Dashboards gepflegt? Ein Haupt-Dashboard, das täglich genutzt wird
- Datenvolumen im Griff? Log-Level korrekt, Kosten bei Managed Services geprüft
Monitoring-Architektur planen?
Ich analysiere Ihre bestehende Systemlandschaft und entwickle eine Monitoring-Strategie, die zu Teamgröße, Budget und Komplexität passt – ohne Over-Engineering.
Kostenlose Erstanalyse anfragen