Cloudflare Workers OpenTelemetry: OTLP-Kosten 90% senken

Warum Observability am Edge so schwierig ist
In traditionellen Serverumgebungen können Sie Logs in Dateien schreiben, APM-Agenten als Sidecar-Prozesse ausführen und den Kontext über die lange Lebensdauer einer Anfrage hinweg aufrechterhalten. Cloudflare Workers ist jedoch eine völlig andere Welt.
Workers werden in isolierten V8-Kontexten ausgeführt, deren Lebensdauer in Millisekunden gemessen wird. Es gibt kein Dateisystem und keine persistenten Hintergrundprozesse. Wenn Sie mit console.log debuggen und in der Produktion auf einen unerklärlichen Fehler stoßen, gab es bisher außer der Echtzeit-Loganzeige über wrangler tail kaum effektive Möglichkeiten.
Seit Ende 2024 integriert Cloudflare jedoch nativ OpenTelemetry-Unterstützung direkt in Workers. Ohne eine einzige Zeile Code zu ändern, müssen Sie lediglich einige Konfigurationszeilen in der wrangler.jsonc hinzufügen, um verteilte Traces aller Fetch-Anfragen, D1-Abfragen, KV-Lese-/Schreibzugriffe und Durable Object-Aufrufe automatisch zu erfassen.
In diesem Artikel erfahren Sie, wie Sie das native automatische OTel-Tracing konfigurieren, benutzerdefinierte Spans hinzufügen, Daten per OTLP an Axiom und Honeycomb exportieren und Ihre monatlichen Observability-Kosten durch eine effektive Sampling-Strategie um mehr als 90 % senken.
Grundlagen zu den Kernkonzepten von OpenTelemetry
Vor der Konfiguration sollten die drei Kernkonzepte von OTel kurz beleuchtet werden.
Trace
└── Span: Eine einzelne Arbeitseinheit
├── Span: D1-Abfrage (Child Span)
├── Span: KV-Lesezugriff (Child Span)
└── Span: Externer API-Aufruf (Child Span)
Jeder Span enthält Folgendes:
- Start-/Endzeitpunkt
- Dauer (duration)
- Attribute: Key-Value-Metadaten
- Events: Zeitpunktspezifische Aufzeichnungen
- Status: OK / Error
- Trace: Die gesamte Reise einer Anfrage durch das System
- Span: Eine einzelne Arbeitseinheit innerhalb eines Traces (z. B. Datenbankabfrage, API-Aufruf)
- OTLP: OpenTelemetry Protocol – das standardisierte Format für die Übertragung von Telemetriedaten
Bei Workers visualisiert ein Trace den gesamten Ablauf von der Worker-Anfrage über die D1-Abfrage bis hin zum externen API-Aufruf in einem einzigen Wasserfalldiagramm.
Verstehen der Observability-Kostenstruktur
Vor der Konfiguration sollten Sie zunächst die Kostenstruktur verstehen. Bei falscher Konfiguration können die Kosten für Traces die eigentlichen Ausführungskosten von Workers übersteigen.
| Komponente | Kostenfreies Kontingent | Kostenpflichtig |
|---|---|---|
| Cloudflare Workers Dashboard Traces | 20 Mio. Events/Monat | $0.60 pro weitere Mio. |
| Axiom-Datenerfassung | 500 GB/Monat | $1 pro weiteres GB |
| Grafana Cloud Traces | 50 GB/Monat | $0.55 pro weiteres GB |
| Honeycomb | 20 Mio. Events/Monat | ca. $1 pro weitere Mio. |
Hauptproblem: Bei hohem Traffic-Aufkommen können Cloudflare Workers täglich hunderte Millionen Anfragen verarbeiten. Wenn Sie für jede Anfrage einen Trace erfassen, explodiert die Anzahl der monatlichen Events.
Lösung: Head-Sampling. Wenn Sie nur 5 % aller Anfragen samplen, reduzieren Sie das Event-Volumen um 95 % und erhalten dennoch statistisch repräsentative Daten. Konfigurieren Sie das System so, dass Fehler zu 100 % erfasst werden – so bleibt die Debugging-Fähigkeit vollständig erhalten, während die Kosten um mehr als 90 % sinken.
Schritt 1: Natives automatisches Tracing aktivieren
Sie müssen keinen Anwendungscode ändern, sondern lediglich die wrangler.jsonc anpassen.
// wrangler.jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"observability": {
"traces": {
"enabled": true,
// Head Sampling: Nur 5 % aller Anfragen samplen
"head_sampling_rate": 0.05,
// Auch im Cloudflare Dashboard speichern (7 Tage Aufbewahrung)
"persist": true
},
"logs": {
"enabled": true,
"head_sampling_rate": 0.1
}
},
"d1_databases": [
{
"binding": "DB",
"database_name": "my-db",
"database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
],
"kv_namespaces": [
{
"binding": "CACHE",
"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
]
}
Mit dieser Konfiguration allein werden folgende Vorgänge automatisch nachverfolgt:
- Alle Einstiegspunkte von
fetch()-Handlern - D1-Datenbankabfragen (inklusive SQL-Statements)
- KV-Lese- und Schreiboperationen
- R2-Object-Storage-Operationen
- Aufrufe von Durable Objects
- Outbound-
fetch()-HTTP-Anfragen - Queue-Consumer und -Producer
Sobald Sie wrangler deploy ausführen, können Sie die Traces sofort im Cloudflare Dashboard unter Workers → Observability einsehen.
Schritt 2: Anwendungslogik mit benutzerdefinierten Spans nachverfolgen
Während das native automatische Tracing Plattformoperationen abdeckt, erfordert die Nachverfolgung der internen Logik Ihrer Anwendung benutzerdefinierte Spans.
Neu seit 2025: Die native Tracing-API von cloudflare:workers
// src/index.ts
import { tracing } from "cloudflare:workers";
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/d1";
import { users, posts } from "./db/schema";
type Env = {
DB: D1Database;
CACHE: KVNamespace;
};
const app = new Hono<{ Bindings: Env }>();
app.get("/api/posts/:slug", async (c) => {
const slug = c.req.param("slug");
// Benutzerspezifischer Span: Fasst die gesamte Logik zum Abrufen des Beitrags in einem Span zusammen
return tracing.enterSpan("get-post-by-slug", async (span) => {
// Business-Kontext-Attribute zum Span hinzufügen
span.setAttribute("post.slug", slug);
span.setAttribute("app.feature", "blog");
const db = drizzle(c.env.DB);
// KV-Cache-Abfrage (erzeugt automatisch einen Child Span)
const cacheKey = `post:${slug}`;
const cached = await c.env.CACHE.get(cacheKey, "json");
if (cached) {
span.setAttribute("cache.hit", true);
// Span-Event: Bestimmten Zeitpunkt aufzeichnen
span.addEvent("cache-hit", { key: cacheKey });
return c.json(cached);
}
span.setAttribute("cache.hit", false);
// D1-Abfrage (erzeugt automatisch einen Child Span – inklusive SQL-Statement)
const post = await db
.select()
.from(posts)
.where(eq(posts.slug, slug))
.get();
if (!post) {
// Fehlerstatus aufzeichnen
span.setStatus({ code: "ERROR", message: "Post not found" });
return c.json({ error: "Not found" }, 404);
}
span.setAttribute("post.id", post.id);
span.setAttribute("post.published", post.published);
// KV-Caching (Fire-and-Forget)
c.executionCtx.waitUntil(
c.env.CACHE.put(cacheKey, JSON.stringify(post), {
expirationTtl: 300,
})
);
return c.json(post);
});
});
export default app;
Verschachtelte Spans: Tracing komplexer Workflows
// Komplexe Anwendungslogik mit mehreren Schritten
async function processOrder(
env: Env,
ctx: ExecutionContext,
orderId: string
) {
return tracing.enterSpan("process-order", async (orderSpan) => {
orderSpan.setAttribute("order.id", orderId);
// Schritt 1: Lagerbestand prüfen
const inventory = await tracing.enterSpan(
"check-inventory",
async (span) => {
span.setAttribute("order.id", orderId);
const db = drizzle(env.DB);
return db.select().from(inventoryTable)
.where(eq(inventoryTable.orderId, orderId))
.get();
}
);
if (!inventory || inventory.stock < 1) {
orderSpan.setStatus({
code: "ERROR",
message: "Out of stock",
});
throw new Error("Out of stock");
}
// Schritt 2: Zahlungsabwicklung (externe API)
const payment = await tracing.enterSpan(
"process-payment",
async (span) => {
span.setAttribute("payment.provider", "stripe");
// Outbound-fetch erzeugt automatisch einen Span
const res = await fetch("https://api.stripe.com/v1/charges", {
method: "POST",
// ...
});
span.setAttribute("payment.status", res.status);
return res.json();
}
);
orderSpan.setAttribute("payment.id", payment.id);
orderSpan.addEvent("order-completed", {
orderId,
paymentId: payment.id,
});
return payment;
});
}
tracing.enterSpan() schließt den Span automatisch ab, sobald die Callback-Funktion einen Wert zurückgibt oder das Promise aufgelöst wird. Ein manueller Aufruf von span.end() ist nicht erforderlich.
Schritt 3: OTLP-Integration mit externen Dashboards
Um die Aufbewahrungsfrist von 7 Tagen des Cloudflare Dashboards zu überschreiten, müssen Sie die Daten an einen externen OTLP-Endpunkt exportieren.
Axiom-Integration (kostenfrei bis 500 GB/Monat)
// Destinations in wrangler.jsonc hinzufügen
{
"observability": {
"traces": {
"enabled": true,
"head_sampling_rate": 0.05,
"persist": false, // Speicherung im Cloudflare Dashboard deaktivieren
"destinations": ["axiom-traces"]
},
"logs": {
"enabled": true,
"head_sampling_rate": 0.1,
"destinations": ["axiom-logs"]
}
}
}
Hinzufügen einer Destination im Cloudflare Dashboard:
- Workers → Observability → Destinations → Add Destination
- Name:
axiom-traces - Type:
OTLP - Endpoint:
https://api.axiom.co/v1/traces - Headers:
Authorization: Bearer <AXIOM_API_TOKEN>,X-Axiom-Dataset: my-workers
Honeycomb-Integration
Endpoint: https://api.honeycomb.io/v1/traces
Headers:
x-honeycomb-team: <HONEYCOMB_API_KEY>
x-honeycomb-dataset: cloudflare-workers
Grafana Cloud-Integration
Endpoint: https://otlp-gateway-prod-<region>.grafana.net/otlp/v1/traces
Headers:
Authorization: Basic <base64(instanceId:apiToken)>
@microlabs/otel-cf-workers: Anpassung auf Code-Ebene
Wenn Ihnen das native OTel nicht ausreicht und Sie eine feinere Steuerung benötigen:
// src/index.ts – Einsatz von @microlabs/otel-cf-workers
import { instrument, ResolveConfigFn } from "@microlabs/otel-cf-workers";
import { trace, context } from "@opentelemetry/api";
type Env = {
DB: D1Database;
OTEL_EXPORTER_OTLP_ENDPOINT: string;
OTEL_EXPORTER_OTLP_HEADERS: string;
};
const handler = {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const tracer = trace.getTracer("my-worker");
return tracer.startActiveSpan("handle-request", async (span) => {
try {
span.setAttribute("http.method", request.method);
span.setAttribute("http.url", request.url);
const url = new URL(request.url);
const response = await routeRequest(request, env, ctx);
span.setAttribute("http.status_code", response.status);
return response;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
});
},
};
// OTLP-Konfiguration aus Umgebungsvariablen lesen
const config: ResolveConfigFn = (env: Env, trigger) => ({
exporter: {
url: env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: Object.fromEntries(
new URLSearchParams(env.OTEL_EXPORTER_OTLP_HEADERS)
),
},
service: {
name: "my-cloudflare-worker",
version: "1.0.0",
},
});
export default instrument(handler, config);
Schritt 4: Sampling-Strategien – Der Schlüssel zu 90 % Kostenersparnis
Sampling bedeutet nicht einfach nur das pauschale Senken der Rate. Die Kernstrategie besteht darin, reguläre Anfragen zu samplen, während Fehler zu 100 % erfasst werden.
Head-Sampling
Die Entscheidung über das Tracing wird zu Beginn der Anfrage getroffen. Dies ist die einfachste Methode mit minimalem Performance-Overhead.
{
"observability": {
"traces": {
"enabled": true,
"head_sampling_rate": 0.05 // 5% 샘플링
}
}
}
Nachteil: Anfragen, bei denen Fehler auftreten, können mit einer Wahrscheinlichkeit von 95 % verworfen werden.
Tail-Sampling: 100 % Erhalt von Fehlern
Mit @microlabs/otel-cf-workers und einem benutzerdefinierten Sampler lässt sich ein Tail-Sampling realisieren:
// src/sampling.ts – Fehler zu 100 %, reguläre Anfragen zu 5 % samplen
import { Sampler, SamplingResult, SamplingDecision } from "@opentelemetry/sdk-trace-base";
export class ErrorAlwaysSampler implements Sampler {
private baseRate: number;
constructor(baseRate = 0.05) {
this.baseRate = baseRate;
}
shouldSample(
context: any,
traceId: string,
spanName: string,
spanKind: any,
attributes: any,
links: any
): SamplingResult {
// HTTP-Fehlerstatus immer samplen
const statusCode = attributes["http.status_code"];
if (statusCode && statusCode >= 400) {
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
// Wenn Fehlerattribute vorhanden sind, immer samplen
if (attributes["error"] === true) {
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
// Restliche Anfragen mit der Wahrscheinlichkeit baseRate samplen
return {
decision:
Math.random() < this.baseRate
? SamplingDecision.RECORD_AND_SAMPLED
: SamplingDecision.NOT_RECORD,
};
}
toString() {
return `ErrorAlwaysSampler(${this.baseRate})`;
}
}
// Benutzerdefinierten Sampler in der Instrument-Konfiguration anwenden
const config: ResolveConfigFn = (env: Env) => ({
exporter: { url: env.OTEL_ENDPOINT },
service: { name: "my-worker" },
sampler: new ErrorAlwaysSampler(0.05),
});
Kostensimulation nach Sampling-Rate
Angenommen, ein Worker verarbeitet 1 Milliarde Anfragen pro Monat:
| Sampling-Rate | Monatliche Events | Axiom-Kosten | Honeycomb-Kosten |
|---|---|---|---|
| 100% (Kein Sampling) | 1 Mrd. | ~$500/Monat | ~$1.000/Monat |
| 10% | 100 Mio. | ~$50/Monat | ~$100/Monat |
| 5% | 50 Mio. | ~$25/Monat | ~$50/Monat |
| 1% | 10 Mio. | Kostenfrei | ~$10/Monat |
Durch ein Sampling von nur 5 % reduzieren Sie die Kosten um 95 % und erhalten dennoch statistisch repräsentative Stichproben von tausenden Anfragen pro Sekunde.
Schritt 5: Traces in der Praxis – Reale Produktionsszenarien
Langsame D1-Abfragen identifizieren
Mit der folgenden Abfrage in Axiom finden Sie D1-Abfragen, die länger als 100 ms dauern:
// Axiom APL-Abfrage
['cloudflare-workers']
| where ['span.kind'] == "client"
| where ['db.system'] == "cloudflare.d1"
| where duration > 100ms
| summarize count(), avg(duration) by ['db.statement']
| order by avg_duration desc
| limit 20
Fehler-Muster analysieren
// Nur Traces mit 5xx-Fehlern filtern
['cloudflare-workers']
| where ['http.status_code'] >= 500
| where ['span.is_root'] == true
| project _time, ['http.url'], ['http.method'], duration, ['error.message']
| order by _time desc
P99-Latenz nachverfolgen
// P50/P95/P99-Latenz für einen bestimmten Endpunkt
['cloudflare-workers']
| where ['http.route'] == "/api/posts/:slug"
| summarize
p50 = percentile(duration, 50),
p95 = percentile(duration, 95),
p99 = percentile(duration, 99)
by bin(_time, 5m)
| order by _time desc
Schritt 6: Tracing von Durable Objects
Durable Objects besitzen einen komplexeren Lebenszyklus als herkömmliche Workers, weshalb für sie ein gezieltes Tracing erforderlich ist.
Natives automatisches Tracing (Empfohlen)
Wenn Sie in der wrangler.jsonc lediglich observability.traces.enabled = true konfigurieren, werden DO-Aufrufe automatisch als Child Spans des übergeordneten Worker-Traces verknüpft. Paket @microlabs/otel-cf-workers wird hierfür nicht benötigt.
Worker-Anfrage-Trace
└── fetch handler (root span)
├── D1-Abfrage (automatischer Span)
└── Durable Object-Aufruf (automatischer Child Span)
├── DO fetch handler
└── Interne D1-Abfrage in DO
@microlabs/otel-cf-workers: Manuelle Instrumentierung von DO
// src/rate-limiter.ts — Durable Object with OTel
import { instrumentDO } from "@microlabs/otel-cf-workers";
import { trace } from "@opentelemetry/api";
class RateLimiterBase {
private state: DurableObjectState;
constructor(state: DurableObjectState, env: Env) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const tracer = trace.getTracer("rate-limiter-do");
return tracer.startActiveSpan("rate-limit-check", async (span) => {
const key = new URL(request.url).searchParams.get("key") ?? "global";
span.setAttribute("rate_limit.key", key);
const count = (await this.state.storage.get<number>(key)) ?? 0;
const limit = 100;
if (count >= limit) {
span.setAttribute("rate_limit.exceeded", true);
span.setStatus({ code: "ERROR", message: "Rate limit exceeded" });
span.end();
return new Response("Rate limited", { status: 429 });
}
await this.state.storage.put(key, count + 1);
span.setAttribute("rate_limit.count", count + 1);
span.end();
return new Response("OK");
});
}
}
// Durable Object-Klasse mit OTel wrappen
export const RateLimiter = instrumentDO(RateLimiterBase, config);
Schritt 7: Integration in CI/CD und Alerting
Automatische Überprüfung nach dem Deployment in GitHub Actions
# .github/workflows/deploy.yml
- name: Deploy Worker
run: npx wrangler deploy
- name: Verify Observability (Verifizierung der Trace-Erfassung nach Deployment)
run: |
sleep 30 # Warten auf Trace-Erfassung
# Anzahl der Fehler in den letzten 5 Minuten über die Axiom-API prüfen
ERRORS=$(curl -s "https://api.axiom.co/v1/datasets/my-workers/query" \
-H "Authorization: Bearer $AXIOM_TOKEN" \
-d '{"apl":"[\"my-workers\"] | where status >= 500 | where _time > ago(5m) | count"}' \
| jq '.matches[0].data._count // 0')
if [ "$ERRORS" -gt "10" ]; then
echo "❌ Anstieg von Fehlern nach Deployment: $ERRORS Fehler in 5 Min."
exit 1
fi
echo "✅ Fehleranzahl nach Deployment normal: $ERRORS"
Alert-Konfiguration in Axiom
Slack-Benachrichtigung per Axiom Monitor bei Überschreitung des P99-Latenz-Schwellenwerts:
{
"name": "Workers P99 Latency Alert",
"query": {
"apl": "['my-workers'] | where ['span.is_root'] == true | summarize p99 = percentile(duration, 99) by bin(_time, 1m) | where p99 > 2000ms"
},
"frequencyMinutes": 5,
"durationMinutes": 5,
"notifiers": ["slack-webhook"]
}
Leitfaden zur Auswahl von Anbietern
| Szenario | Empfohlener Anbieter | Grund |
|---|---|---|
| Frische Startups, Budgeteinsparung | Axiom | 500 GB/Monat kostenfrei, integrierte Logs & Traces |
| Bestehender Grafana-Stack | Grafana Cloud | Nahtlose Dashboard-Integration, 50 GB/Monat kostenfrei |
| Komplexe Abfragen/Analysen erforderlich | Honeycomb | BubbleUp-Anomalieerkennung, fortgeschrittene Abfragen |
| On-Premise-Anforderungen | SigNoz (Self-Hosted) | Open-Source, Kubernetes-Deployment |
| Nur einfache Logs benötigt | Cloudflare Dashboard | Kostenfrei, keine zusätzliche Konfiguration nötig |
Checkliste zur Kostenoptimierung
Liste der Optimierungen für die reale Produktionsumgebung:
Sampling-Strategie:
-
head_sampling_rate: 0.05(5 % der regulären Anfragen samplen) - 100 % Sampling im Fehlerfall (benutzerdefinierter Sampler)
- 100 % Sampling bei langsamen Anfragen (>1s)
Datenreduktion:
- Unnötige Attribute entfernen (personenbezogene Daten, große Payloads)
- Kardinalität von Span-Namen begrenzen (URL-Parameter normalisieren)
- Duplizierte Speicherung im Cloudflare Dashboard mit
persist: falseverhindern
Kosten-Monitoring:
- Alerts für das monatliche Event-Volumen im Dashboard des Anbieters einrichten
- Wöchentliche Überprüfung der Anfragemengen in Cloudflare Workers Analytics
- Dynamische Anpassung der Sampling-Rate bei steigendem Traffic
Migrationspfad: Von wrangler tail zu OTel
Wenn Sie sich bisher ausschließlich auf wrangler tail und console.log verlassen haben, empfiehlt sich eine schrittweise Umstellung:
Woche 1: observability.traces.enabled: true zur wrangler.jsonc hinzufügen und Traces im Cloudflare Dashboard überprüfen
Woche 2: Axiom Destination hinzufügen und 30 Tage Aufbewahrung kostenfrei nutzen
Woche 3: Kern-Business-Logik mit benutzerdefinierten Spans instrumentieren
Woche 4: Sampling-Raten anpassen und Benachrichtigungen konfigurieren
Sie müssen bestehende console.log-Aufrufe nicht entfernen. Workers-Logs können mit observability.logs.enabled: true ebenfalls gemeinsam exportiert werden.
Fazit
Die OpenTelemetry-Unterstützung in Cloudflare Workers hat sich seit Ende 2024 rasant weiterentwickelt. Durch das native automatische Tracing werden alle Plattformoperationen ohne Codeänderungen erfasst, während die tracing.enterSpan()-API von cloudflare:workers die präzise Instrumentierung von Anwendungslogik ermöglicht.
Die Kosten lassen sich durch Sampling vollständig kontrollieren:
- 5 % Head-Sampling: 95 % Kostenersparnis bei voller statistischer Sichtbarkeit
- 100 % Erfassung von Fehlern: Kein Verlust von Debugging-Fähigkeiten
- Axiom-Freikontingent (500 GB): Kostenfrei bis zu mittleren Produktionsumgebungen
Der Wechsel von einer Blackbox-Edge-Funktion zu einem vollständig beobachtbaren verteilten System erfordert lediglich eine Minute, um fünf Zeilen in die wrangler.jsonc einzufügen.
Verwandter Artikel: Im Artikel Cloudflare Workflows und Durable Execution AI-Agenten finden Sie State-Management-Muster für Durable Objects.