Your CDN serves 80% of your traffic, but you only observe the origin. When cache hit ratio drops, TTFB spikes in Southeast Asia, or an edge function silently fails, traditional monitoring leaves you blind. A proper observability stack brings logs, metrics, and traces together at the edge — so you can debug what happened, where, and why in seconds.
Edge infrastructure is fundamentally different from centralized cloud workloads. Requests terminate in 300+ Points of Presence, logic executes in isolates or containers within 50ms of the user, and state is ephemeral. If your observability pipeline was designed for origin servers, it will miss the most critical hop. This guide shows how to build a production-grade CDN observability stack that captures high-cardinality edge signals without drowning in cost or cardinality.
Why Edge Observability Fails With Legacy Monitoring
Most teams start with CDN-provided dashboards and origin APM. That covers availability, but not explainability. A spike in cache_status=MISS in Singapore could be a cache invalidation bug, an origin latency regression, a TLS negotiation failure, or a bot flood bypassing cache. Without correlated logs and traces, you are guessing.
Three edge-specific challenges break traditional stacks:
- High cardinality: Every PoP, ASN, cache key, and edge function version multiplies metric dimensions. A naive Prometheus setup can create millions of active series overnight.
- Log volume: A modest 10k RPS site generates ~864M log lines per day. Shipping raw access logs to Elasticsearch is prohibitively expensive and slow.
- Short-lived execution: Edge Workers and Functions run for 1-30ms. Without distributed tracing, you cannot see if latency came from KV lookup, origin fetch, or image optimization.
The solution is not more dashboards. It is a unified pipeline that treats logs, metrics, and traces as complementary signals, sampled and aggregated at the edge before centralization. If you are still relying on basic uptime checks, read our primer on CDN monitoring and observability fundamentals before implementing this stack.
The Three Pillars at the Edge
Observability is often described as three pillars, but at the CDN layer each pillar answers a different operational question. You need all three, but you will use them in different workflows.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
1. Logs: The Ground Truth for Every Request
Logs are your discrete, high-fidelity record of what happened. For CDNs, that means structured access logs enriched with edge context: pop, cache_status (HIT, MISS, STALE, BYPASS), edge_compute_duration, origin_ttfb, tls_version, http_version, client_asn, and waf_action.
Do not ship unstructured Nginx-style logs. Emit JSON from the edge and filter aggressively before egress. Drop health checks, favicon requests, and known bot traffic at the collector. Keep hot logs for 7-14 days for debugging, then archive to object storage in Parquet for audit.
Effective log pipelines use Vector or Fluent Bit deployed as close to the PoP as possible — often via log push from your CDN provider to your collector. Enrich logs with GeoIP and ASN at ingest, not query time, to keep queries fast. For teams running logic at the edge, correlate logs with edge computing workloads by injecting request_id and function_version into every log line.
cache_key and surrogate_key to your log schema on day one. When you later debug cache invalidation issues, you will be able to pinpoint which purge tag or key pattern caused a MISS storm without replaying traffic.2. Metrics: Your Early Warning System
Metrics are aggregated, numeric signals optimized for alerting and trend detection. At the edge, the golden signals are: request rate, error rate (4xx/5xx by PoP), latency (p50/p95/p99 TTFB by region), cache hit ratio, origin offload, and edge compute duration.
Collect metrics via Prometheus exposition or OTel metrics, but control cardinality ruthlessly. Never use url or cache_key as a label. Instead, aggregate by pop, cache_status, status_code_class, country, and function_name. Use histograms for TTFB with buckets tuned for edge latencies: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2] seconds.
Store metrics in Prometheus with Thanos/Cortex or managed Grafana Mimir for long-term retention. Derive key SLOs from these metrics: cache hit ratio > 92%, p95 edge TTFB < 120ms, and 5xx < 0.1% per PoP. Unlike logs, metrics should be retained for 13 months to support capacity planning and seasonal comparison, especially for ecommerce traffic peaks.
3. Traces: Connecting Edge to Origin
Traces show the causal chain for a single request across edge and origin. A trace for a cache MISS might contain spans for: edge.request → edge.worker.auth → kv.get → origin.fetch → image.optimize. With W3C Trace Context propagation, you can follow a request from Cloudflare Workers, Fastly Compute, or AWS Lambda@Edge all the way to your API.
Traces are essential for debugging tail latency. If p99 latency spikes only for HTTP/3 clients in Brazil, a trace will reveal whether the bottleneck is QUIC handshake, edge KV, or origin. For modern protocols, correlate trace data with your HTTP/2 and HTTP/3 performance to isolate transport-layer issues.
Sample intelligently. Head-based sampling at 1% is cheap but misses rare errors. Implement tail-based sampling in the OpenTelemetry Collector: keep 100% of traces with errors, cache MISS with origin latency > 500ms, or WAF blocks, and 1% of successful HITs. This keeps trace volume under 5% of log volume while preserving debuggability.
Reference Architecture: From PoP to Grafana
A resilient stack separates collection, processing, and storage. Edge PoPs push telemetry to a regional collector tier, which does filtering, sampling, and enrichment before forwarding to centralized storage. This avoids backpressure at the edge and keeps egress costs predictable.
Do not scrape edge PoPs with Prometheus. Edge nodes are ephemeral and often behind NAT. Use log push, OTLP export, and Prometheus remote-write from the collector tier. This also ensures you capture data even when a PoP is unhealthy and cannot be scraped.
Choosing Your Storage: Signals vs. Cost
Not all telemetry belongs in the same database. The table below maps each signal to its ideal storage and query pattern.
| Signal | Question It Answers | Storage | Retention & Sampling |
|---|---|---|---|
| Logs Structured Access Logs |
Why did this specific request MISS? Which WAF rule fired? | Loki, ClickHouse, or S3 + Athena | 7–14 days hot, 90 days archive; filter >50% at edge |
| Metrics Aggregated Counters & Histograms |
Is cache hit ratio degrading in EU? Is p95 TTFB breaching SLO? | Prometheus + Mimir / Thanos | 13 months; aggregate by PoP, status class, region |
| Traces Distributed Spans |
Where did latency accumulate for this cache MISS chain? | Tempo, Jaeger, Honeycomb | 30 days; tail-sample 1–5%, keep 100% errors |
| Events Deploys & Purges |
Did a new worker version or purge cause the spike? | Loki annotations / Grafana | 90 days; correlate with metric anomalies |
Implementation: Code and Configuration
Start small: instrument one edge function and one PoP group, then expand. Below are production-tested snippets for the collector and edge.
Vector + OpenTelemetry Collector for Log Enrichment
Use Vector to receive CDN log push (e.g., Cloudflare Logpush or Fastly logging endpoint), parse JSON, enrich, and forward. This example drops health checks and derives a metric for cache hit ratio.
vector.toml — edge log pipeline[sources.cdn_logs]
type = "http_server"
address = "0.0.0.0:8080"
encoding = "json"
[transforms.filter]
type = "filter"
inputs = ["cdn_logs"]
condition = '.path != "/healthz" && .user_agent != "ELB-HealthChecker"'
[transforms.enrich]
type = "remap"
inputs = ["filter"]
source = '''
.pop = get_env_var("POP") ?? .pop
.cache_status = upcase(.cache_status)
# Derive metric-friendly fields
.status_class = to_string(to_int(.status) / 100) + "xx"
'''
[sinks.loki]
type = "loki"
inputs = ["enrich"]
endpoint = "http://loki:3100"
labels = { pop="{{pop}}", cache_status="{{cache_status}}", status_class="{{status_class}}" }
[sinks.prometheus]
type = "prometheus_remote_write"
inputs = ["enrich"]
endpoint = "http://mimir:9009/api/v1/push"
Edge Worker Tracing with OpenTelemetry
For Cloudflare Workers, Fastly Compute, or Lambda@Edge, propagate W3C trace context and create spans for KV and origin calls. Keep span attributes low-cardinality.
worker.js — OTel tracing at the edge (simplified)import { trace, context } from '@opentelemetry/api';
export default {
async fetch(request, env) {
const tracer = trace.getTracer('edge-worker');
return tracer.startActiveSpan('edge.request', async (span) => {
span.setAttribute('http.method', request.method);
span.setAttribute('cdn.pop', env.POP);
span.setAttribute('cdn.cache_status', 'MISS'); // set after cache lookup
// Trace KV lookup separately
const kvSpan = tracer.startSpan('kv.get', {}, context.active());
const data = await env.CACHE_KV.get(request.url);
kvSpan.end();
if (!data) {
const originSpan = tracer.startSpan('origin.fetch');
const res = await fetch(request);
originSpan.setAttribute('origin.ttfb_ms', res.headers.get('x-origin-ttfb'));
originSpan.end();
span.setAttribute('cdn.cache_status', 'MISS');
span.end();
return res;
}
span.setAttribute('cdn.cache_status', 'HIT');
span.end();
return new Response(data);
});
}
}
Prometheus Alerting for SLOs
Define burn-rate alerts on metrics, not logs. This alert fires when cache hit ratio drops below SLO for 10 minutes.
alerts.yml — SLO burn rategroups:
- name: cdn.slo
rules:
- alert: CacheHitRatioLow
expr: |
sum(rate(cdn_requests_total{cache_status="HIT"}[5m]))
/
sum(rate(cdn_requests_total[5m])) < 0.92
for: 10m
labels: { severity: warning }
annotations:
summary: "Cache hit ratio {{ $value | humanizePercentage }} below 92% in {{ $labels.pop }}"
runbook: "Check recent purges and origin latency"
Secure the entire pipeline with mTLS between edge and collector, and ensure your SSL/TLS configuration does not strip trace headers. Many WAFs drop traceparent by default — allowlist it.
From Data to Action: SLOs, Dashboards, and Runbooks
Observability without action is overhead. Build three Grafana dashboards:
- Edge Overview: Global hit ratio, p50/p95 TTFB by continent, 5xx by PoP, and WAF block rate. Use for NOC triage.
- Cache Debugging: Log panel filtered by
cache_statusandsurrogate_key, correlated with purge events. Use for invalidation and origin debugging. - Edge Compute: Function duration histogram, cold start rate, KV hit ratio, and trace waterfall for slow requests.
Link logs to traces via trace_id — Grafana Loki and Tempo do this natively with derived fields. When an alert fires, the on-call engineer should click from metric spike → exemplar trace → full logs for that request_id in under 30 seconds.
Finally, test your stack with chaos. Purge a high-traffic surrogate key, deploy a canary worker that adds 50ms latency, and verify that sampling, alerting, and correlation work before a real incident.
Adopting this stack incrementally is more sustainable than a big bang migration. Start with structured logs and hit-ratio metrics this sprint, add tail-based tracing next sprint, and layer SLOs once data is reliable. The payoff is immediate: faster MTTR, fewer false positives, and the ability to confidently ship edge logic knowing you can observe its impact globally.