12 — Observability

Teil D — Wie man betreibt. Zurück zur Übersicht.

Lernziel

Nach diesem Kapitel kannst du Monitoring von Observability unterscheiden, kennst die drei Säulen und die vier goldenen Signale und kannst begründen, warum auf Symptome und nicht auf Ursachen alarmiert wird. Der KFZ-Rechner liefert Metriken, wird überwacht und meldet sich bei Problemen.

12.1 Monitoring oder Observability

Die Begriffe werden oft synonym verwendet, meinen aber Unterschiedliches.

Monitoring Observability
Beantwortet bekannte Fragen unvorhergesehene Fragen
Beispiel „Ist die CPU über 80 Prozent?„ „Warum sind ausgerechnet Anfragen aus Filiale 12 langsam?“
Vorgehen vorher festgelegte Prüfungen Daten reichhaltig genug, um nachträglich zu forschen
Findet die Probleme, die man erwartet hat auch die anderen

Monitoring ist nicht überholt — Observability ist die Erweiterung. Man braucht beides: feste Prüfungen für Bekanntes und genug Kontext, um Unbekanntes untersuchen zu können.

Aus Kapitel 2 wieder aufgegriffen: Das ist der Zweite Weg, die Rückmeldung. Ohne Messung bleibt er eine Absichtserklärung.

12.2 Die drei Säulen

Säule Beantwortet Beispiel
Metriken Was passiert? Wie viel? Wie schnell? 340 Anfragen/min, 1,2 % Fehler, p95 bei 180 ms
Logs Was genau ist passiert? „Berechnung für Vertrag 4711 abgebrochen: Division durch null„
Traces Wo genau hat es gehakt? Anfrage 78 ms in der Anwendung, 1.240 ms in der Datenbank

Metriken sind billig, aggregiert und gut für Alarme. Logs sind teuer, detailliert und gut für die Ursachensuche. Traces zeigen den Weg einer einzelnen Anfrage durch mehrere Systeme — und sind bei verteilten Anwendungen unverzichtbar.

Der übliche Ablauf einer Fehlersuche: Ein Alarm auf einer Metrik meldet, dass etwas nicht stimmt. Ein Trace zeigt, wo. Die Logs sagen, warum.

12.3 Die vier goldenen Signale

Aus dem Betriebshandbuch von Google, und als Einstieg schwer zu schlagen. Wer nur vier Dinge misst, misst diese:

Signal Frage Beim KFZ-Rechner
Latenz Wie lange dauert eine Anfrage? Antwortzeit der Berechnung
Verkehr Wie viel Last liegt an? Anfragen pro Minute
Fehler Wie viel geht schief? Anteil der Antworten mit Status 5xx
Sättigung Wie voll ist das System? Speicher, Verbindungspool, Warteschlange

Latenz getrennt nach erfolgreichen und fehlgeschlagenen Anfragen messen.

Ein Systemfehler wird oft sehr schnell zurückgegeben. Mischt man beides, sinkt die Durchschnittslatenz genau dann, wenn das System kaputtgeht — und die Kurve sieht aus, als hätte sich etwas verbessert.

Überhaupt ist der Durchschnitt bei Latenzen die falsche Kennzahl. Bei 1.000 Anfragen mit 50 ms und 10 Anfragen mit 8 Sekunden liegt der Mittelwert bei rund 130 ms — unauffällig. Zehn Anwender haben trotzdem acht Sekunden gewartet. Deshalb misst man Perzentile: p50, p95, p99. Das p99 sagt: Ein Prozent der Anwender hat es mindestens so schlecht erwischt.

12.4 Brauchbare Logs

Schlecht:
  ERROR: something went wrong

Besser:
  2026-07-29T14:32:07Z ERROR Prämienberechnung fehlgeschlagen
  vertrag=4711 sf_klasse=-3 fehler=ValueError anfrage_id=a7f3c9

Am besten - strukturiert:
  {"zeit":"2026-07-29T14:32:07Z","stufe":"ERROR",
   "text":"Prämienberechnung fehlgeschlagen","vertrag":"4711",
   "sf_klasse":-3,"fehler":"ValueError","anfrage_id":"a7f3c9"}

Strukturierte Logs sind maschinell auswertbar. Statt in Textmengen zu suchen, filtert man nach Feldern: alle Fehler des Typs ValueError der letzten Stunde, gruppiert nach Vertrag.

Die Korrelations-ID

Der wichtigste Einzelkniff: Jede Anfrage bekommt beim Eintritt eine eindeutige Kennung, die durch alle Systeme mitwandert und in jeder Logzeile steht.

import uuid
from contextvars import ContextVar
 
anfrage_id: ContextVar[str] = ContextVar("anfrage_id", default="-")
 
 
def middleware(request):
    # Vorhandene ID übernehmen oder neue erzeugen
    kennung = request.headers.get("X-Request-ID") or str(uuid.uuid4())[:8]
    anfrage_id.set(kennung)
    antwort = verarbeite(request)
    antwort.headers["X-Request-ID"] = kennung
    return antwort

Damit lässt sich der Weg einer einzelnen Anfrage über Reverse Proxy, Anwendung und Datenbank hinweg vollständig zusammensetzen. Ohne diese Kennung ist die Fehlersuche in verteilten Systemen Ratearbeit.

Keine personenbezogenen Daten in Logs.

Namen, Adressen, Geburtsdaten, Vertragsinhalte, vollständige Anfragekörper — all das gehört nicht in ein Protokoll. Logs werden zentral gesammelt, breit lesbar gemacht, lange aufbewahrt und selten gelöscht. Damit entsteht faktisch eine zweite, schlecht geschützte Datenhaltung.

Statt der Daten protokolliert man Verweise: eine Vertragsnummer statt der Vertragsdaten, eine Kundennummer statt des Namens. Wer den Zusammenhang wirklich braucht, kann ihn über das Fachsystem herstellen — protokolliert und berechtigt.

Das gilt auch für Fehlermeldungen mit vollständigem Stacktrace: Dort landen erschreckend häufig ganze Eingabedaten.

12.5 Alarmierung

Die schwierigste Disziplin. Zu wenige Alarme, und Störungen fallen Anwendern zuerst auf. Zu viele, und niemand schaut mehr hin.

Auf Symptome alarmieren, nicht auf Ursachen

Schlecht (Ursache):        Besser (Symptom):
  CPU über 80 %              p95 der Antwortzeit über 2 s
  Speicher über 90 %         Fehlerrate über 1 %
  Festplatte zu 85 % voll    Anfragen schlagen fehl

Eine CPU-Auslastung von 90 Prozent ist kein Problem, solange die Anwender nichts merken — es kann auch schlicht bedeuten, dass die Maschine gut ausgelastet ist. Umgekehrt kann alles im grünen Bereich sein, während die Anwendung wegen eines Sperrproblems in der Datenbank steht.

Die Regel: Alarmiert wird auf das, was Anwender spüren. Ursachenmetriken bleiben in den Diagrammen, damit man sie bei der Analyse zur Hand hat.

Eine Ausnahme bestätigt die Regel: Vorlaufende Größen mit sicherem Ausgang. Ein Zertifikat, das in vierzehn Tagen abläuft, oder ein Dateisystem, das bei aktueller Wachstumsrate in drei Tagen voll ist — dort ist die Ursache selbst der Alarm, weil das Symptom zu spät käme.

Jeder Alarm braucht eine Handlung

Prüffrage für jeden Alarm: Was genau soll die Person tun, die um drei Uhr nachts geweckt wird?

Gibt es keine sinnvolle Antwort, ist es kein Alarm, sondern ein Diagramm. Diagramme wecken niemanden.

Alarmmüdigkeit ist eine ernste Betriebsgefahr: Wer in der Woche vierzig Meldungen bekommt, von denen achtunddreißig folgenlos sind, entwickelt zwangsläufig die Gewohnheit, sie wegzuklicken. Und klickt irgendwann auch die zwei echten weg.

12.6 Lab: Der KFZ-Rechner wird messbar

src/metriken.py
"""Prometheus-Metriken für den KFZ-Rechner."""
from prometheus_client import Counter, Histogram, Gauge
 
ANFRAGEN = Counter(
    "kfz_anfragen_gesamt",
    "Anzahl der Berechnungsanfragen",
    ["ergebnis"],                      # erfolg | fehler
)
 
DAUER = Histogram(
    "kfz_berechnungsdauer_sekunden",
    "Dauer einer Prämienberechnung",
    buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5),
)
 
DB_VERBINDUNGEN = Gauge(
    "kfz_db_verbindungen_aktiv",
    "Aktuell belegte Datenbankverbindungen",
)
src/server.py
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
 
from src.metriken import ANFRAGEN, DAUER
from src.praemie import berechne_praemie
 
 
@app.route("/api/praemie")
def api_praemie():
    with DAUER.time():
        try:
            betrag = berechne_praemie(
                int(request.args["sf"]),
                int(request.args["alter"]),
            )
            ANFRAGEN.labels(ergebnis="erfolg").inc()
            return {"betrag": str(betrag)}
        except (ValueError, KeyError) as fehler:
            ANFRAGEN.labels(ergebnis="fehler").inc()
            app.logger.warning(
                "Berechnung fehlgeschlagen",
                extra={"fehler": type(fehler).__name__},
            )
            return {"fehler": "ungültige Eingabe"}, 400
 
 
@app.route("/metrics")
def metrics():
    return generate_latest(), 200, {"Content-Type": CONTENT_TYPE_LATEST}
 
 
@app.route("/health")
def health():
    """Lebt der Prozess?"""
    return {"status": "ok"}
 
 
@app.route("/ready")
def ready():
    """Kann der Dienst Anfragen bearbeiten - inklusive Abhängigkeiten?"""
    if not datenbank_erreichbar():
        return {"status": "nicht bereit"}, 503
    return {"status": "bereit"}

Die Trennung von /health und /ready ist wichtiger, als sie aussieht. Health beantwortet: Lebt der Prozess? Ist die Antwort nein, muss neu gestartet werden. Readiness beantwortet: Kann der Dienst gerade arbeiten? Ist die Antwort nein — etwa weil die Datenbank hängt — soll er keinen Verkehr bekommen, aber auch nicht neu gestartet werden.

Wer beides zusammenlegt, bekommt eine hübsche Fehlerkaskade: Die Datenbank ist kurz nicht erreichbar, sämtliche Instanzen werden für ungesund erklärt und neu gestartet, und der Neustart erzeugt einen Ansturm auf die ohnehin überlastete Datenbank.

Prometheus und Alarmregeln

prometheus.yml
scrape_configs:
  - job_name: kfz-rechner
    scrape_interval: 15s
    static_configs:
      - targets: ["kfz-rechner:8080"]
alarme.yml
groups:
  - name: kfz-rechner
    rules:
      - alert: HoheFehlerrate
        expr: |
          sum(rate(kfz_anfragen_gesamt{ergebnis="fehler"}[5m]))
          / sum(rate(kfz_anfragen_gesamt[5m])) > 0.05
        for: 5m
        labels:
          severity: kritisch
        annotations:
          summary: "Fehlerrate über 5 Prozent"
          beschreibung: "Seit 5 Minuten schlagen mehr als 5 % der Berechnungen fehl."
          runbook: "https://creutz.spdns.de/doku.php/edv:devops:13_incident_management"

      - alert: LangsameAntworten
        expr: |
          histogram_quantile(0.95,
            sum(rate(kfz_berechnungsdauer_sekunden_bucket[5m])) by (le)
          ) > 1
        for: 10m
        labels:
          severity: warnung
        annotations:
          summary: "p95 der Berechnungsdauer über 1 Sekunde"

      - alert: DienstNichtErreichbar
        expr: up{job="kfz-rechner"} == 0
        for: 2m
        labels:
          severity: kritisch
        annotations:
          summary: "KFZ-Rechner antwortet nicht"

Aufgaben:

  1. Bring Prometheus und Grafana als Container zum Laufen und lege ein Dashboard mit den vier goldenen Signalen an.
  2. Erzeuge künstlich Fehler und beobachte, wann der Alarm auslöst.
  3. Verändere for: 5m auf for: 30s. Was passiert bei kurzen Lastspitzen?
  4. Formuliere für jeden der drei Alarme in einem Satz, was die geweckte Person tun soll. Fällt dir zu einem nichts ein, lösche ihn.
  5. Ergänze eine Messung der Sättigung — etwa der belegten Datenbankverbindungen.

12.7 Anti-Pattern

Anti-Pattern Folge Abhilfe
Alarm auf Ursachen viele Meldungen ohne Anwenderwirkung auf Symptome alarmieren
Durchschnittslatenz Ausreißer verschwinden Perzentile
Alarme ohne Handlungsanweisung Ratlosigkeit um drei Uhr nachts Runbook verlinken
Zu viele Alarme Ermüdung, echte Meldungen gehen unter ausdünnen, for-Zeiten nutzen
Dashboard mit vierzig Diagrammen niemand liest es ein Übersichtsbild, Details dahinter
Personenbezogene Daten in Logs Datenschutzverstoß Verweise statt Inhalte
Unstrukturierte Logs nicht auswertbar JSON, feste Felder
/health prüft auch die Datenbank Neustartkaskade Health und Readiness trennen
Erst überwachen, wenn es brennt Störung ohne Datenlage Instrumentierung von Anfang an

12.8 Reflexionsfrage

Wie viele Alarme habt ihr in der vergangenen Woche bekommen, und bei wie vielen davon musste tatsächlich jemand handeln?

Liegt das Verhältnis unter einem Drittel, ist die Alarmierung ein Problem für sich — unabhängig davon, wie gut die Überwachung funktioniert.


Weiter mit Kapitel 13 — Incident-Management