Was macht Beobachtbarkeit und Instrumentierung?
Überblick
Code, den Sie nicht beobachten können, ist Code, den Sie nicht betreiben können. Beobachtbarkeit ist die Fähigkeit, von außen die Frage "Was macht das System und warum?" zu beantworten, und zwar mithilfe der Telemetriedaten, die der Code ausstößt. Instrumentierung ist kein nachträglicher Zusatz nach dem Start – sie wird zeitgleich mit der Funktion geschrieben, genauso wie Tests. Wenn eine Funktion ohne Telemetrie ausgeliefert wird, wird der erste von einem Benutzer gemeldete Fehler zur Archäologie anstatt zu einer Abfrage.
Wann zu verwenden
- Bei der Entwicklung jeder Funktion, die in Produktion laufen wird
- Beim Hinzufügen eines neuen Dienstes, Endpunkts, Hintergrundjobs oder einer externen Integration
- Ein Produktionsvorfall hat zu lange gedauert, um diagnostiziert zu werden ("wir konnten nicht sagen, was passiert ist")
- Beim Einrichten oder Überprüfen von Alarmierungsregeln
- Bei der Überprüfung eines PRs, der E/A, Wiederholungen, Warteschlangen oder dienstübergreifende Aufrufe hinzufügt
NICHT für:
- Die Diagnose eines gerade stattfindenden Fehlers – verwenden Sie die Fertigkeit
debugging-and-error-recovery(Beobachtbarkeit ist das, was diese Fertigkeit beim nächsten Mal schnell macht) - Profiling und Optimierung gemessener Langsamkeit – verwenden Sie die Fertigkeit
performance-optimization - Checklisten für die Überwachung am Starttag und Rollback-Auslöser – siehe die Fertigkeit
shipping-and-launch; diese Fertigkeit deckt die Instrumentierung ab, die diese speist
Prozess
1. Definieren Sie "funktioniert", bevor Sie instrumentieren
Telemetrie ohne eine Frage ist Rauschen. Bevor Sie eine Instrumentierung hinzufügen, schreiben Sie 2–4 Fragen auf, die ein Bereitschaftsingenieur zu dieser Funktion stellen wird:
FUNKTION: Zahlungswiederholung beim Checkout
FRAGEN, DIE DER BEREITSCHAFTSINGENIEUR STELLEN WIRD:
1. Welcher Anteil der Zahlungen ist beim ersten Versuch erfolgreich im Vergleich zu nach einer Wiederholung?
2. Wenn eine Zahlung endgültig fehlschlägt, warum? (Anbieterfehler? Zeitüberschreitung? Validierung?)
3. Ist der Zahlungsanbieter langsamer als üblich?
→ Jedes Signal unten muss helfen, eine dieser Fragen zu beantworten.
Wenn Sie die Fragen nicht nennen können, sind Sie nicht bereit zu instrumentieren – Sie werden alles protokollieren und nichts lernen.
2. Wählen Sie das richtige Signal für jede Frage
| Signal | Beantwortet | Kostenprofil | Beispiel |
|---|---|---|---|
| Strukturiertes Log | "Was ist in diesem speziellen Fall passiert?" | Pro Ereignis; wächst mit dem Datenverkehr | payment_failed mit Anbieterfehlercode |
| Metrik | "Wie oft / wie schnell, im Aggregat?" | Fest pro Serie; billig abzufragen | p99-Latenz von Anbieteraufrufen |
| Trace | "Wo ging Zeit zwischen Diensten verloren?" | Pro Anfrage; normalerweise gesampelt | Ein langsamer Checkout, aufgeschlüsselt nach Hop |
Faustregel: Metriken sagen Ihnen, dass etwas falsch ist, Traces sagen Ihnen wo, Logs sagen Ihnen warum.
3. Strukturiertes Logging
Protokollieren Sie Ereignisse, keine Prosa. Jede Log-Zeile ist ein JSON-Objekt mit einem stabilen Ereignisnamen und maschinenlesbaren Feldern:
// SCHLECHT: String-Interpolation — nicht abfragbar, inkonsistent
logger.info(`Zahlung ${id} fehlgeschlagen für Benutzer ${userId} nach ${n} Wiederholungen`);
// GUT: stabiler Ereignisname + strukturierte Felder
logger.warn({
event: 'payment_failed',
paymentId: id,
provider: 'stripe',
errorCode: err.code,
attempt: n,
}, 'zahlung fehlgeschlagen');
Log-Level — verwenden Sie sie konsistent:
| Level | Bedeutung | Aktion des Bereitschaftsingenieurs |
|---|---|---|
error | Invariante verletzt; jemand muss möglicherweise handeln | Untersuchen |
warn | Beeinträchtigt, aber behandelt (Wiederholung erfolgreich, Fallback verwendet) | Trends beobachten |
info | Bedeutendes Geschäftsereignis (Bestellung aufgegeben, Job beendet) | Keine |
debug | Diagnostisches Detail | Standardmäßig in Produktion ausgeschaltet |
Korrelations-IDs sind obligatorisch. Generieren (oder akzeptieren) Sie eine Anforderungs-ID an der Systemgrenze und hängen Sie sie an jede Log-Zeile, jeden Span und jeden ausgehenden Aufruf an. Ohne sie können Sie eine einzelne Anfrage aus verschachtelten Logs nicht rekonstruieren:
// Express: Child-Logger pro Anfrage, ID wird nachgelagert weitergegeben
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
req.log = logger.child({ requestId: req.id });
res.setHeader('x-request-id', req.id);
next();
});
Wenn mehrere Einstiegspunkte in ein Log schreiben, benennen Sie den Einstiegspunkt. Eine Korrelations-ID identifiziert einen Durchlauf; sie sagt nicht, welcher Codepfad ihn gestartet hat. Derselbe Job, der von einem Scheduler, einem Replay-Endpunkt und einem manuellen CLI-Aufruf erreicht wird, erzeugt austauschbare Zeilen in einer Senke. Die Zuordnung einer Zeile fällt daher auf die Eliminierung zurück – Querlesen des Scheduler-Verlaufs, der Prozesstabelle, eines Deployment-Logs – und diese Argumentation gilt nur, solange diese externen Aufzeichnungen zufällig noch existieren. Stempeln Sie den Einstiegspunkt dort ab, wo der Durchlauf beginnt, neben der Korrelations-ID, und geben Sie beide auf die gleiche Weise weiter:
// Ein Helfer für jeden Einstiegspunkt: Der eigene Logger des Durchlaufs trägt beide Felder.
// `entryPoint`, nicht `source` — ECS reserviert `source.*` für Netzwerkfelder.
export const runLog = (entryPoint: 'scheduler' | 'replay_endpoint' | 'cli', runId: string) =>
logger.child({ entryPoint, requestId: runId });
// scheduler tick -> runLog('scheduler', crypto.randomUUID())
// POST /jobs/:id/replay -> runLog('replay_endpoint', req.id)
// CLI invocation -> runLog('cli', process.env.RUN_ID ?? crypto.randomUUID())
Beide Felder müssen dieselben Grenzen überschreiten wie die Korrelations-ID – Warteschlangen-Metadaten, HTTP-Header – oder ein Worker leitet den Einstiegspunkt ab und rät. Ein Feld, das lediglich mit einem Einstiegspunkt korreliert, ist ein Hinweis, keine Zuordnung: Alles, was den Job aufrufen kann, kann ihn reproduzieren.
Protokollieren Sie niemals Geheimnisse, Token, Passwörter oder vollständige personenbezogene Daten (PII). Dies ist eine harte Regel aus der Fertigkeit security-and-hardening – Telemetriepipelines sind ein klassischer Datenleckpfad. Erlauben Sie Felder auf eine Whitelist; protokollieren Sie keine vollständigen Anforderungskörper.
4. Metriken
Instrumentieren Sie für anforderungsgesteuerte Dienste RED auf jedem Endpunkt und jeder externen Abhängigkeit: Rate (Anfragen/s), Errors (Fehlerrate), Duration (Latenz-Histogramm, nicht Durchschnitt). Verwenden Sie für Ressourcen (Warteschlangen, Pools, Hosts) USE: Utilization (Auslastung), Saturation (Sättigung), Errors (Fehler).
Wie beim Tracing ist der anbieterneutrale Weg die OpenTelemetry-Metriken-API (gleiches SDK und gleicher Kontext wie Schritt 5). Das folgende Beispiel verwendet prom-client von Prometheus – eine gängige Backend-Wahl, nicht die einzige; die RED-/USE- und Kardinalitätsregeln sind in beiden Fällen identisch.
import { Histogram } from 'prom-client';
const httpDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP-Anfragedauer',
labelNames: ['method', 'route', 'status_class'], // '2xx', nicht '200'
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
Kardinalität ist die Fehlerart. Jede eindeutige Label-Kombination ist eine separate Zeitreihe. Labels müssen aus kleinen, festen Mengen stammen (Routenvorlage, Statusklasse, Anbietername). Verwenden Sie niemals Benutzer-IDs, rohe URLs, Fehlermeldungen oder andere unbegrenzte Werte als Labels – diese gehören in Logs und Traces.
OK als Label: route="/api/tasks/:id" status_class="5xx" provider="stripe"
NIEMALS ein Label: user_id, email, request_id, vollständige URL, Fehlermeldungstext
Verfolgen Sie niemals Durchschnitte, immer Perzentile: Ein Durchschnitt verbirgt die 1 % der Benutzer, die eine schlechte Zeit haben. Verwenden Sie Histogramme und lesen Sie p50/p95/p99.
5. Verteiltes Tracing
Verwenden Sie OpenTelemetry – es ist der anbieterneutrale Standard, und die Auto-Instrumentierung deckt HTTP, gRPC und gängige DB-Clients mit nahezu null Code ab:
// tracing.ts — muss vor allem anderen importiert werden
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'checkout-service',
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Fügen Sie manuelle Spans nur um sinnvolle interne Arbeitseinheiten herum hinzu (z. B. applyDiscounts, chargeProvider) und hängen Sie die Attribute an, nach denen der Bereitschaftsdienst filtern wird. Geben Sie den Kontext über jede asynchrone Grenze hinweg weiter – HTTP-Header, Warteschlangen-Metadaten – oder der Trace stirbt an der Lücke. Samplen Sie standardmäßig kopfgesteuert mit einer niedrigen Rate; behalten Sie 100 % der Fehler bei, wenn Ihr Backend Tail-Sampling unterstützt.
6. Alarmierung
Alarmieren Sie auf Symptome, die Benutzer spüren, nicht auf Ursachen:
SYMPTOM (seitenwürdig): URSACHE (Dashboard, keine Seite):
Fehlerrate > 1 % für 5 Min CPU bei 85 %
p99-Latenz > 2s ein Pod neu gestartet
Warteschlangenalter > 10 Min Festplatte bei 70 %
Ursachenbasierte Alarme lösen aus, wenn nichts falsch ist, und übersehen Fehler, die Sie nicht vorhergesehen haben. Symptombasierte Alarme lösen genau dann aus, wenn Benutzer beeinträchtigt sind, unabhängig von der Ursache.
Regeln für jeden Alarm, den Sie erstellen:
- Er muss umsetzbar sein. Wenn die Antwort "ignorieren, es heilt sich selbst" ist, löschen Sie den Alarm.
- Er verlinkt auf ein Runbook – selbst drei Zeilen: was es bedeutet, erste auszuführende Abfrage, Eskalationspfad.
- Er hat einen Schwellenwert und eine Dauer, die durch das SLO oder durch historische Daten gerechtfertigt sind, nicht durch eine Vermutung.
- Verwenden Sie nur zwei Schweregrade: Seite (benutzerorientiert, jetzt handeln) und Ticket (Beeinträchtigung, diese Woche handeln). Eine dritte Stufe wird zu Rauschen, das Menschen darin trainiert, alles zu ignorieren.
Runbooks schreiben
Regel 2 oben erfordert, dass jeder Alarm auf ein Runbook verlinkt. Die Aufgabe eines Runbooks ist es, drei Fragen zu beantworten, ohne dass der Leser nachdenken muss: Was passiert, was zuerst überprüft werden soll und wen man anrufen soll, wenn das das Problem nicht löst. Speichern Sie es in docs/runbooks/ benannt nach dem Alarm.
Minimal lebensfähiges Runbook (drei Zeilen):
# Runbook: Hohe Fehlerrate auf /api/tasks
**Bedeutet:** DB-Verbindungspool wahrscheinlich erschöpft oder ein fehlerhaftes Deployment.
**Erste Überprüfung:** `SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';`
— wenn Anzahl > Pool-Limit, siehe Schritt 2. (Ersetzen Sie dies durch das Äquivalent für Ihre Datenbank.)
**Eskalation an:** #db-oncall oder Engineering-Bereitschaftsrotation.
Wann über drei Zeilen hinaus erweitern: Fügen Sie Schritte nur hinzu, wenn die erste Überprüfung allein nicht ausreicht, um zu entscheiden. Ein Runbook mit fünf Schritten, das die drei häufigsten Ursachen abdeckt, ist besser als ein Dokument mit zwanzig Schritten, das jeden Randfall abdeckt und nur überflogen wird.
Halten Sie Runbooks aktuell. Aktualisieren Sie das Runbook als Teil des Abschlusses jedes Vorfalls, in dem es verwendet wurde – ein veraltetes Runbook schafft falsches Vertrauen. Wenn ein Schritt falsch war oder fehlte, korrigieren Sie ihn, bevor Sie den Vorfall als abgeschlossen markieren.
7. Überprüfen Sie die Telemetrie selbst
Instrumentierung ist Code; sie kann falsch sein. Bevor Sie die Arbeit als erledigt bezeichnen, lösen Sie die Pfade aus und sehen Sie sich die tatsächliche Ausgabe an:
- Erzwingen Sie einen Fehler im Staging → finden Sie ihn in den Logs anhand der
requestId, bestätigen Sie, dass die Felder strukturiert sind (nicht[object Object]) - Senden Sie Testverkehr → bestätigen Sie, dass Metrikreihen mit den erwarteten Labels und sinnvollen Werten erscheinen
- Verfolgen Sie eine Anfrage dienstübergreifend in der Tracing-Benutzeroberfläche → keine unterbrochenen Spans
- Lösen Sie jeden neuen Alarm einmal aus (senken Sie den Schwellenwert vorübergehend) → bestätigen Sie, dass er den richtigen Kanal erreicht und der Runbook-Link funktioniert
Häufige Rechtfertigungen
| Rechtfertigung | Realität |
|---|---|
| "Ich füge Logging hinzu, nachdem es funktioniert" | "Nachher" wird zu "nach dem ersten Vorfall", was der teuerste Moment ist, um festzustellen, dass Sie blind sind. Instrumentieren Sie, während Sie bauen. |
| "Mehr Logs = mehr Beobachtbarkeit" | Unstrukturiertes Rauschen macht Vorfälle langsamer, nicht schneller. Drei abfragbare Ereignisse schlagen dreihundert Prosa-Zeilen. |
| "console.log ist für den Anfang in Ordnung" | Unstrukturierte Ausgabe kann nicht gefiltert, korreliert oder alarmiert werden. Der strukturierte Logger kostet einmal fünf zusätzliche Minuten. |
| "Wir können uns die Dashboards einfach ansehen, wenn etwas kaputt geht" | Dashboards, die ohne definierte Fragen erstellt wurden, zeigen Ihnen alles außer der Antwort. Beginnen Sie mit Fragen des Bereitschaftsdienstes. |
| "Alarmieren Sie auf alles Wichtige, wir passen es später an" | Ein lauter Pager trainiert Leute, ihn zu ignorieren. Die Anpassung findet nie statt; der verpasste echte Alarm schon. |
| "Benutzer-ID als Metrik-Label erleichtert das Debugging" | Es bringt auch Ihr Metrik-Backend zum Absturz. Lookups mit hoher Kardinalität gehören in Logs und Traces. |
| "Tracing ist übertrieben für unsere zwei Dienste" | Zwei Dienste bedeuten bereits dienstübergreifende Latenzfragen, die Logs nicht beantworten können. Auto-Instrumentierung macht die Kosten trivial. |
Rote Flaggen
- Ein Funktions-PR mit Wiederholungen, Warteschlangen oder externen Aufrufen und null neuer Telemetrie
- Log-Zeilen, die durch String-Interpolation anstelle von strukturierten Feldern erstellt wurden
- Keine Korrelations-/Anfrage-ID – jede Log-Zeile ist eine Waise
- Ein Log-Stream, der von einem Scheduler, einem Webhook und manuellen Ausführungen gespeist wird, ohne ein Feld, das benennt, welcher die Zeile produziert hat
- Metriken, die mit Benutzer-IDs, rohen URLs oder Fehlermeldungstext gekennzeichnet sind (Kardinalitätsbombe)
- Latenz als Durchschnitt ohne Perzentile verfolgt
- Alarme, die täglich auslösen und ohne Aktion bestätigt werden
- Alarme auf Ursachen (CPU, Speicher), die Menschen anpiepsen, während die benutzerorientierte Fehlerrate unbeaufsichtigt bleibt
- Geheimnisse, Token oder vollständige Anforderungskörper, die in Logs erscheinen
- "Es funktioniert auf meinem Rechner" als einziger Beweis dafür, dass eine Produktionsfunktion gesund ist
Überprüfung
Nach der Instrumentierung einer Funktion bestätigen Sie:
- Die Bereitschaftsfragen für diese Funktion sind aufgeschrieben, und jedes Signal ist einer zugeordnet
- Die gesamte Log-Ausgabe ist strukturiert (JSON), mit stabilen Ereignisnamen und einer Korrelations-ID auf jeder Zeile
- Jede Log-Senke, die von mehr als einem Einstiegspunkt beschrieben wird, trägt ein Einstiegspunkt-Feld, das dort gesetzt wird, wo der Durchlauf beginnt, und mit der Korrelations-ID weitergegeben wird, anstatt nachgelagert abgeleitet zu werden
- Keine Geheimnisse, Token oder ungeschwärzte personenbezogene Daten in irgendeiner Log-Zeile (Stichprobenartige Überprüfung der tatsächlichen Ausgabe)
- RED-Metriken existieren für jeden neuen Endpunkt und jede externe Abhängigkeit mit begrenzten Label-Sets
- Latenz ist ein Histogramm; p95/p99 sind abfragbar
- Eine einzelne Anfrage kann in der Tracing-Benutzeroberfläche durchgängig verfolgt werden, ohne unterbrochene Spans
- Jeder neue Alarm ist symptom-basiert, hat einen Runbook-Link und wurde einmal testweise ausgelöst
- Ein induzierter Fehler im Staging wurde allein durch Telemetrie lokalisiert, ohne den Quellcode zu lesen
Für die Kurzversion dieser Liste, einschließlich des Instrumentierungs-Gates vor dem Start, siehe ../../references/observability-checklist.md.