TL;DR: Observability ist das Fundament der operativen Sicherheit in einer SaaS-Anwendung. Ohne sie debuggen Sie blind. Dieser Leitfaden behandelt die drei Säulen der Observability — Logs, Metriken und Traces — implementiert für SaaS-Anwendungen auf Cloudflare Workers und TanStack Start, mit praktischen Mustern für Produktionsmonitoring, Alarmierung und Root-Cause-Analyse.
Die drei Säulen der Observability
| Säule | Sie beantwortet | Speicher | Aufbewahrung | Kosten |
|---|---|---|---|---|
| Logs | „Was ist passiert?" | Workers Analytics Engine | 7–30 Tage | Niedrig |
| Metriken | „Wie ist der Trend?" | Analytics Engine + Grafana | 3–12 Monate | Mittel |
| Traces | „Warum ist es passiert?" | Workers Trace (Tail Worker) | 1–7 Tage | Niedrig (gesampelt) |
Logging: Strukturiertes Logging am Edge
Log-Format
Strukturiertes Logging erfasst jeden Request in einem konsistenten JSON-Format mit Level, Nachricht, Request-ID, Dauer und Metadaten. Dieser strukturierte Ansatz ermöglicht Filtering, Alarmierung und Debugging über alle Services hinweg:
// src/lib/logger.ts
type LogLevel = "debug" | "info" | "warn" | "error"
interface LogEntry {
timestamp: string
level: LogLevel
message: string
requestId: string
userId?: string
duration?: number
error?: string
metadata?: Record<string, unknown>
}
function createLogger(request: Request) {
const requestId = request.headers.get("cf-ray") ?? crypto.randomUUID()
return {
info: (msg: string, meta?: Record<string, unknown>) =>
log({ level: "info", message: msg, requestId, metadata: meta }),
warn: (msg: string, meta?: Record<string, unknown>) =>
log({ level: "warn", message: msg, requestId, metadata: meta }),
error: (msg: string, error?: Error, meta?: Record<string, unknown>) =>
log({
level: "error",
message: msg,
requestId,
error: error?.message,
stack: error?.stack,
metadata: meta,
}),
getRequestId: () => requestId,
}
}
Der Logger hängt eine eindeutige requestId (aus Cloudflares cf-ray-Header) an jeden Log-Eintrag an, wodurch alle Logs eines einzelnen Requests korreliert werden können. Die createLogger-Factory bettet diese ID ein, sodass Entwickler sie nie manuell übergeben müssen.
Logs an die Analytics Engine senden
Cloudflare Workers Analytics Engine bietet abfragbaren, langfristigen Speicher für Ihre Log-Daten. Jeder Log-Eintrag wird als Datenpunkt mit Blobs (Strings), Doubles (numerische Werte) und Indizes für effizientes Filtern geschrieben:
// src/lib/logger.ts — Schreiben in Workers Analytics Engine
function log(entry: LogEntry) {
// In Analytics Engine schreiben für abfragbaren Speicher
context.env.ANALYTICS.writeDataPoint({
blobs: [
entry.level,
entry.message,
entry.requestId,
entry.userId ?? "",
entry.error ?? "",
JSON.stringify(entry.metadata ?? {}),
],
doubles: [entry.duration ?? 0],
indexes: [entry.requestId],
})
// Auch an Workers-Konsole für wrangler tail ausgeben
console.log(JSON.stringify(entry))
}
Durch das Schreiben sowohl in die Analytics Engine als auch in console.log erhalten Sie das Beste aus beiden Welten: persistenten, abfragbaren Speicher für Dashboards und Echtzeit-Streaming über wrangler tail für interaktives Debugging.
Metriken: Was Sie messen sollten
SaaS-Metriken-Taxonomie
| Kategorie | Metriken | Messmethode |
|---|---|---|
| Performance | Request-Latenz (p50/p95/p99), Antwortgröße, Cache-Hit-Rate | Analytics Engine |
| Business | Signups, MRR, Churn-Rate, Conversion-Rate | Datenbankabfragen |
| Infrastruktur | Worker-CPU-Zeit, Speicher, Subrequest-Anzahl | Workers Runtime API |
| User Experience | LCP, CLS, INP, TTFB | Web Vitals API + Analytics |
Performance-Metriken erfassen
Request-Level-Metriken — Latenz, Statuscodes und Cache-Hit-Rates — sind das Fundament jeder Observability-Praxis. Diese Middleware erfasst Zeitdaten für jeden Request und zeichnet sie in der Analytics Engine zur späteren Analyse auf:
// server/middleware/metrics.ts
export const metricsMiddleware = createServerFn({ method: "GET" }).handler(
async ({ request, context }) => {
const startTime = Date.now()
// Nach Abschluss des Requests Metriken aufzeichnen
context.waitUntil(
context.env.ANALYTICS.writeDataPoint({
blobs: [
request.url,
request.method,
String(context.response?.status ?? 500),
],
doubles: [
Date.now() - startTime, // Dauer
request.cf?.colo ?? 0, // Edge-Standort
],
indexes: [request.url.split("?")[0]], // Pfad
})
)
}
)
Die Verwendung von context.waitUntil stellt sicher, dass die Metrik auch nach dem Senden der Antwort an den Client aufgezeichnet wird, sodass keine Latenz zu user-facing Requests hinzugefügt wird, während gleichzeitig umfassende Daten gesammelt werden.
Metriken abfragen
Sobald Metriken erfasst sind, erfordert ihre Abfrage SQL-Zugriff auf die Analytics Engine. Dieses Muster extrahiert Percentile-Aufschlüsselungen nach API-Pfad und gibt Ihnen einen klaren Überblick darüber, welche Endpunkte optimiert werden müssen:
// Analytics Engine für Dashboard-Daten abfragen
export const getApiLatency = createServerFn({ method: "GET" }).handler(
async ({}, { context }) => {
const result = await context.env.ANALYTICS.query({
sql: `
SELECT
blob3 as path,
quantile(0.50)(double1) as p50,
quantile(0.95)(double1) as p95,
quantile(0.99)(double1) as p99,
count() as requests
FROM analytics_engine_table
WHERE timestamp > now() - INTERVAL '1' DAY
GROUP BY blob3
ORDER BY p99 DESC
LIMIT 20
`,
})
return result.rows
}
)
Die quantile-Funktionen berechnen Percentile-Latenzen direkt in der Datenbank und vermeiden so den Aufwand, rohe Daten zur Verarbeitung in die Anwendung zu ziehen. Die Filterung auf die letzten 24 Stunden hält Abfragen schnell und auf die aktuelle Performance fokussiert.
Tracing: Verteiltes Request-Tracing
Einen Tail Worker einrichten
Ein Tail Worker empfängt Events von Ihrem primären Worker, nachdem jeder Request abgeschlossen wurde, was ihn ideal zum Samplen von Trace-Daten macht, ohne die Request-Performance zu beeinträchtigen. Deployen Sie ihn als separaten Worker, der Event-Streams verarbeitet:
// tail-worker.ts — als separater Worker deployed
export default {
async tail(events: TraceEvent[]) {
for (const event of events) {
if (event.duration && event.event.request) {
await ANALYTICS.writeDataPoint({
blobs: [
event.event.request.method,
event.event.request.url,
String(event.exceptions?.[0]?.message ?? ""),
],
doubles: [
event.duration ?? 0,
event.event.request.cf?.botManagement?.score ?? 0,
],
indexes: [event.event.request.url.split("?")[0]],
})
// Bei Exceptions Alarm auslösen
if (event.exceptions?.length > 0 || event.crons?.length > 0) {
await sendIncidentAlert(event)
}
}
}
},
}
Der Tail Worker erfasst Dauer, Exceptions und Bot-Scores für jeden Request. Wenn Exceptions erkannt werden, kann er sofort Incident Alerts auslösen und verwandelt damit Ihre Observability-Pipeline in ein automatisiertes Incident-Response-System.
Trace-Kontext injizieren
Für Requests, die mit externen Services interagieren (Stripe, Resend, OpenAI), ermöglicht das Propagieren einer Trace-ID die Korrelation der Server-Funktionsausführung mit nachgelagerten API-Calls:
// Trace-Kontext von Server-Funktion an nachgelagerte Services übergeben
export const handleWebhook = createServerFn({ method: "POST" }).handler(
async ({ request, context }) => {
const traceId = crypto.randomUUID()
// Trace-ID an nachgelagerte Calls übergeben
await fetch("https://api.stripe.com/v1/events", {
headers: {
"X-Trace-Id": traceId,
},
})
// Mit Trace-Kontext loggen
context.logger.info("Webhook erhalten", { traceId, eventType: event.type })
}
)
Der X-Trace-Id-Header wird an nachgelagerte Services übergeben und dieselbe Trace-ID in Log-Einträgen eingefügt. Dies erzeugt einen Ende-zu-Ende-Trace, der den eingehenden Request, Ihre Server-Funktionslogik und alle externen API-Calls verbindet.
Alarmierung: Wann jemand geweckt werden sollte
Alarm-Schweregrade
| Level | Reaktionszeit | Beispiel |
|---|---|---|
| Kritisch | 15 Minuten | p99-Latenz > 5s für 5 Minuten |
| Hoch | 1 Stunde | Fehlerrate > 1 % für 10 Minuten |
| Mittel | 24 Stunden | Speichernutzung > 80 % |
| Niedrig | 7 Tage | Deprecated API-Calls erkannt |
Alarm-Konfiguration
Alarme sollten von den Metriken getrieben werden, die Sie bereits erfassen, nicht von separater Monitoring-Infrastruktur. Diese Server-Funktion fragt die Analytics Engine nach Fehlerraten und Latenz-Perzentilen und wertet sie gegen Schwellenwerte aus, um zu bestimmen, ob Handlungsbedarf besteht:
// server/monitoring/alerts.ts
import { createServerFn } from "@tanstack/react-start"
export const checkAlerts = createServerFn({ method: "GET" }).handler(
async ({}, { context }) => {
const alerts = []
// 1. Fehlerrate prüfen
const errorRate = await context.env.ANALYTICS.query({
sql: `
SELECT count() as total,
countIf(blob0 = 'error') as errors
FROM analytics_engine_table
WHERE timestamp > now() - INTERVAL '5' MINUTE
`,
})
const rate = Number(errorRate.rows[0]?.errors ?? 0) /
Math.max(Number(errorRate.rows[0]?.total ?? 1), 1)
if (rate > 0.01) {
alerts.push({
severity: "high",
message: `Fehlerrate ${(rate * 100).toFixed(1)}% > 1%`,
})
}
// 2. Latenz prüfen
const latency = await context.env.ANALYTICS.query({
sql: `
SELECT quantile(0.99)(double1) as p99
FROM analytics_engine_table
WHERE timestamp > now() - INTERVAL '5' MINUTE
`,
})
const p99 = Number(latency.rows[0]?.p99 ?? 0)
if (p99 > 5000) {
alerts.push({
severity: "critical",
message: `p99-Latenz ${p99}ms > 5000ms`,
})
}
return alerts
}
)
Der Alert-Checker läuft auf einem Cron-Schedule und wertet sowohl Fehlerraten- als auch Latenz-Schwellenwerte aus. Durch das Abfragen der letzten 5 Minuten werden Spitzen schnell erkannt, während falsch-positive Ergebnisse durch kurze transiente Fehler vermieden werden.
Dashboard: Observability-Daten visualisieren
Das ideale SaaS-Observability-Dashboard hat drei Ansichten:
Operative Ansicht (Echtzeit)
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Request-Rate │ Fehlerrate │ p50-Latenz │ p99-Latenz │
│ 1.234/min │ 0,02 % │ 45ms │ 210ms │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ Aktive User │ Signups (24h)│ Churn (30d) │ MRR │
│ 847 │ 23 │ 3,2 % │ $12.450 │
└──────────────┴──────────────┴──────────────┴──────────────┘
Business-Ansicht (wöchentlich)
Kohorten-Retention-Tabelle
Signup-Trichter (Besuch → Signup → Subscribe → Aktiv)
Feature-Adoption-Heatmap
Top-User-Aktionen
Debug-Ansicht (on-demand)
Request Trace Explorer
Error Log Stream
Worker-CPU-Profil
Datenbank-Performance
Automatisierte Incident-Response
Wenn Alarme ausgelöst werden, protokolliert der Incident-Response-Handler das Event, benachrichtigt das Team über Slack und E-Mail (bei hoher Priorität) und versucht, wo möglich, automatische Behebung. Diese Server-Funktion zentralisiert die Response-Logik:
// server/monitoring/incident.ts
export const handleIncident = createServerFn({ method: "POST" }).handler(
async ({ data }: { data: { severity: string; message: string } }) => {
// 1. Incident loggen
await logIncident(data)
// 2. Benachrichtigen (nur für hohe+ Priorität)
if (["high", "critical"].includes(data.severity)) {
await sendSlackNotification(
`[${data.severity.toUpperCase()}] ${data.message}`
)
await sendEmail({
to: "ops@tanstackship.com",
subject: `[${data.severity}] ${data.message}`,
})
}
// 3. Wo möglich automatisch beheben
if (data.message.includes("rate limit")) {
// Rate-Limit-Fenster hochskalieren
console.log("Auto-Remediation: Rate-Limit überschritten")
}
}
)
Der Incident-Handler nutzt eine schwellenwertbasierte Benachrichtigungsstrategie: niedrige und mittlere Alarme werden still protokolliert, während hohe und kritische Alarme sofortige Benachrichtigungen auslösen. Automatische Gegenmaßnahmen — wie das Anpassen von Rate-Limits — sind in denselben Flow integriert, damit die Wiederherstellung ohne menschliches Eingreifen erfolgt.
Observability-Budget
Allocieren Sie Ihre Observability-Ressourcen sinnvoll:
| Komponente | % des Observability-Budgets | Warum |
|---|---|---|
| Request-Logging (gesampelt) | 20 % | Operative Kerndaten |
| Error-/Exception-Logging | 30 % | Höchster Debugging-Wert |
| Business-Metriken | 25 % | Umsatz und Wachstum |
| Performance-Metriken | 15 % | User Experience |
| Verteilte Traces | 10 % | Komplexe Probleme debuggen |
Produktions-Observability-Checkliste
- Alle Requests erzeugen strukturierte JSON-Logs
- Logs enthalten Request-ID, User-ID und Dauer
- Error-Logs enthalten Stack Traces
- Alle kritischen Business-Events werden als Metriken erfasst
- Worker-Performance-Metriken erfasst (CPU, Speicher, Subrequests)
- Alarme für p99-Latenz > 5s und Fehlerrate > 1 % konfiguriert
- Tail Worker erfasst Trace-Daten für gesampelte Requests
- Dashboard deckt operative, Business- und Debug-Ansichten ab
- Logs haben automatisierte Aufbewahrungs- und Archivierungspolicy
- Zugriff auf Logs wird auditiert und rollenbasiert eingeschränkt
Fazit
Observability ist für eine Produktions-SaaS-Anwendung keine Option. Die drei Säulen — Logs, Metriken und Traces — erfüllen jeweils unterschiedliche Zwecke, und eine ausgereifte Observability-Praxis braucht alle drei.
Für TanStack Start auf Cloudflare Workers bieten die nativen Observability-Tools (Workers Analytics Engine, Tail Workers, Trace Workers) eine solide Grundlage, ohne dass Drittanbieter-Services erforderlich sind. Sie können operative Daten vollständig innerhalb des Cloudflare-Ökosystems erfassen, abfragen und alarmieren.
Der Schlüssel liegt darin, einfach zu beginnen: zuerst strukturierte Logs, dann Metriken für die wichtigsten geschäftlichen und performance-bezogenen Indikatoren, dann Traces zum Debuggen komplexer Probleme. Fügen Sie Alarmierung erst hinzu, wenn Sie genügend Daten haben, um aussagekräftige Schwellenwerte zu definieren. Für eine breitere Sicherheitsperspektive lesen Sie unsere Best Practices für SaaS-Sicherheit, und wie Sie Performance-Metriken mit User Experience verknüpfen, erfahren Sie im Core Web Vitals Optimization Guide. Der Leitfaden Real User Monitoring behandelt die Erfassung von Felddaten aus echten Nutzern, während der Cloudflare D1 Production Guide erklärt, wie Sie Ihre Datenbank-Performance überwachen.