nejdřív se rozbije viditelnost
Edge a serverless často schytají vinu za problémy, které samy nevytvořily. Skoky v latenci, náhodné timeouty, drift p95, který se objeví jen v jednom regionu, cache missy, které zmizí dřív, než je někdo stihne zreprodukovat, to všechno se hodí do škatulky „riziko architektury“. Reálný problém přitom bývá jinde: většina týmů nasadí tyhle runtime s observabilitou navrženou pro Kubernetes pod, který žije tři dny.
Tenhle model se rozpadne rychle. Route handler v Next.js 15 běžící v edge runtime má krátký život, omezené API, divné hranice exekuce a mnohem menší prostor pro invazivní profiling, na který byli lidi zvyklí z warm Node procesu. Go 1.22 backend za ním má opačný profil: stabilní lifetime procesu, lepší profiling hooky, jednodušší propagaci contextu a dost CPU času na to, aby vůbec vyemitoval použitelnou telemetry. Když obě vrstvy instrumentujete stejně, dostanete to horší z obou světů: nafouklý účet, chybějící trace a dashboard plný průměrů, které zdvořile schovají průšvihy.
Pattern, který se nám ve steezr osvědčil nejvíc, napříč zákaznickými portály, AI document pipelines i pár interními systémy s edge auth a regionálním routováním, je docela jednoduchý. Měřte cold starty explicitně, profily sbírejte přes agresivní sampling místo permanentního profilingu všude a u distribuovaných trace dělejte tail sampling podle latence, status code a důležitosti route, místo abyste všechno head-samplovali na 10 % a doufali, že zajímavé selhání zrovna prošlo.
Je taky potřeba přijmout, že observabilita na edge je z definice ztrátová. To je v pohodě. Dobré systémy umí fungovat s užitečnou ztrátou. Špatné systémy předstírají, že si můžou nechat každou event navždy, a pak přijde první faktura a finance observabilitu seškrtají. Cíl je držet signály, které rychle odpoví na produkční otázky, ne archivovat každý span, který vygeneruje bot mlátící do preview deploymentu.
traceujte cold starty napřímo
Cold starty si zaslouží telemetry první kategorie, protože kontaminují všechno kolem sebe. Když vaše edge function stráví 180 ms bootem, vinu schytá downstream fetch do Go API, synthetic checky ukazují na špatnou službu a incident review se změní v soutěž o nejlepší odhad.
V Next.js 15 to instrumentujte v instrumentation.ts a v route nebo middleware entrypointu, kde execution opravdu začíná. Klíč je process-local marker, který existuje jen při první invokaci daného isolate, a pak span attribute, podle kterého se dá později filtrovat. Edge runtime vám ne vždy dá bohaté process primitivy, ale globalThis většinou stačí.
1// instrumentation.ts2import { trace, context, SpanStatusCode } from '@opentelemetry/api'34const tracer = trace.getTracer('next-edge')56if (!(globalThis as any).__boot_ts) {7 ;(globalThis as any).__boot_ts = Date.now()8 ;(globalThis as any).__cold = true9}1011export async function register() {}1213export async function markInvocation<T>(name: string, fn: () => Promise<T>) {14 return tracer.startActiveSpan(name, async span => {15 const cold = !!(globalThis as any).__cold16 const bootTs = (globalThis as any).__boot_ts as number17 span.setAttribute('runtime.name', 'nextjs-edge')18 span.setAttribute('deployment.environment', process.env.NODE_ENV || 'unknown')19 span.setAttribute('serverless.cold_start', cold)20 if (cold) {21 span.setAttribute('serverless.init_duration_ms', Date.now() - bootTs)22 ;(globalThis as any).__cold = false23 }24 try {25 return await fn()26 } catch (err: any) {27 span.recordException(err)28 span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message })29 throw err30 } finally {31 span.end()32 }33 })34}
Pak uvnitř route handleru:
1import { markInvocation } from '@/instrumentation'23export const runtime = 'edge'45export async function GET(req: Request) {6 return markInvocation('edge.products.GET', async () => {7 const t0 = performance.now()8 const res = await fetch(`${process.env.API_BASE_URL}/products`, {9 headers: { 'x-request-id': crypto.randomUUID() }10 })11 const body = await res.text()12 const duration = performance.now() - t013 return new Response(body, {14 status: res.status,15 headers: {16 'content-type': res.headers.get('content-type') || 'application/json',17 'x-upstream-duration-ms': duration.toFixed(1)18 }19 })20 })21}
Attribute serverless.cold_start=true je důležitější, než si lidi myslí. Dejte ji na root span, rozbijte podle ní latency grafy a půlka záhad zmizí. Na Go straně otiskněte do stejné trace stáří containeru nebo uptime procesu, ať odlišíte edge cold start od churnu v backendu. Bez toho budete týdny nadávat na Postgres, i když se ve skutečnosti jen bootoval čerstvý isolate ve Frankfurtu.
tail sampling, nebo se v tom utopíte
Head sampling je v pohodě u low-volume systémů a průměrný všude jinde. Kritické edge cesty potřebují tail sampling, protože trace je zajímavá až ve chvíli, kdy ji vidíte celou: status code, retrye, celkové trvání, cache miss, jestli uživatel skončil na checkoutu, nebo na nějaké neškodné marketingové route.
OpenTelemetry Collector dává dost kontroly na to, aby byl užitečný, aniž by se z toho stal science project. Pipeline držte nudnou. Přijímejte OTLP přes gRPC a HTTP, batchujte, trochu obohaťte attributes a pak dělejte tail sampling podle explicitních pravidel. Tohle je config collectoru, který jsme používali jako výchozí bod:
1receivers:2 otlp:3 protocols:4 grpc:5 endpoint: 0.0.0.0:43176 http:7 endpoint: 0.0.0.0:431889processors:10 memory_limiter:11 check_interval: 1s12 limit_mib: 51213 spike_limit_mib: 1281415 batch:16 send_batch_size: 204817 timeout: 2s1819 attributes/edge:20 actions:21 - key: service.namespace22 value: edge-platform23 action: upsert2425 tail_sampling:26 decision_wait: 8s27 num_traces: 10000028 expected_new_traces_per_sec: 250029 policies:30 - name: errors31 type: status_code32 status_code:33 status_codes: [ERROR]34 - name: slow-traces35 type: latency36 latency:37 threshold_ms: 75038 - name: cold-starts39 type: string_attribute40 string_attribute:41 key: serverless.cold_start42 values: ["true"]43 - name: checkout-routes44 type: string_attribute45 string_attribute:46 key: http.route47 values: ["/api/checkout", "/api/login"]48 - name: probabilistic-baseline49 type: probabilistic50 probabilistic:51 sampling_percentage: 25253exporters:54 otlp/lightstep:55 endpoint: ingest.lightstep.com:44356 headers:57 lightstep-access-token: ${LIGHTSTEP_ACCESS_TOKEN}58 debug:59 verbosity: basic6061service:62 pipelines:63 traces:64 receivers: [otlp]65 processors: [memory_limiter, attributes/edge, tail_sampling, batch]66 exporters: [otlp/lightstep, debug]
Pár praktických poznámek. decision_wait: 8s je dost dlouhé pro většinu request chainů, které skáčou z edge do API, do Redis a do Postgres, a zároveň dost krátké na to, aby collector nežral paměť donekonečna. Pravidlo cold-starts zachrání trace, které byste při head samplingu skoro jistě neviděli. Základních 2 % drží dost normálního provozu pro srovnání. U low-traffic služeb to zvedněte, u hlučných anonymních route to stáhněte.
Jestli na frontendu a edge vrstvě používáte Sentry pro tracing, držte client-side head sampling nízko a per-route. Něco jako tohle funguje, aniž byste pálili peníze:
1Sentry.init({2 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,3 tracesSampler: samplingContext => {4 const name = samplingContext.name || ''5 const attrs = samplingContext.attributes || {}6 if (attrs['serverless.cold_start'] === true) return 1.07 if (name.includes('/api/checkout')) return 0.58 if (name.includes('/api/login')) return 0.39 return 0.0210 },11 profilesSampleRate: 0.012})
Ne, 100 % u cold startů není drahé, pokud jsou cold starty vzácné. Pokud vzácné nejsou, máte problém v deploymentu a traffic shapingu, a trace vám to pomůžou dokázat.
profily chtějí disciplínu
Flamegraphy jsou pořád nejrychlejší způsob, jak zabít špatné domněnky, hlavně u Go služeb, kde všichni přísahají, že bottleneck je síťová latence, a pprof mezitím ukáže 37 % CPU v JSON encodingu nebo regex schovaný ve validaci requestu. Go vrstvu profilujte vždycky. Edge vrstvu profilujte opatrně, a jen pokud to vaše platforma umí bez hacků.
Go 1.22 vám dá základ skoro zadarmo. Vystrčte net/http/pprof na interní port, zamkněte ho, během incidentů stahujte krátké CPU profily a kde to dává smysl, zapněte continuous profiling, Pyroscope, Parca, Grafana Cloud Profiles, vyberte si. Overhead sample-based profilingu bývá v pohodě, když collection intervaly držíte při zemi.
1import (2 _ "net/http/pprof"3 "net/http"4 "log"5)67func startDebugServer() {8 go func() {9 mux := http.NewServeMux()10 mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {11 w.WriteHeader(http.StatusOK)12 _, _ = w.Write([]byte("ok"))13 })14 mux.Handle("/debug/pprof/", http.DefaultServeMux)15 if err := http.ListenAndServe("127.0.0.1:6060", mux); err != nil {16 log.Printf("pprof server failed: %v", err)17 }18 }()19}
Pak si z podu nebo VM stáhněte 30sekundový profil:
1go tool pprof -http=:0 http://127.0.0.1:6060/debug/pprof/profile?seconds=30
Když profil ukáže dominanci runtime.mallocgc a encoding/json, nepřidávejte další collectory a další dashboardy. Opravte allocation churn nebo přepněte hot pathy na jsoniter či předpočítané response buffery tam, kde to má reálný dopad. Když je obří wait time v database/sql, flamegraph to za vás nevyřeší, ale aspoň uřízne špatný flamewar ve Slacku.
Profiling v Next.js edge je horší, protože jste uvnitř hostovaného isolate s omezenou introspekcí. Built-in observabilita od Vercelu se zlepšila, ale pro analýzu CPU na úrovni kódu se většinou víc dozvíte, když stejnou logiku reprodukujete v Node runtime nebo drahou práci přesunete do Go, kde existuje pprof a nelže. Není to argument o čistotě architektury. Je to argument o nástrojích. Rozpočet na observabilitu utrácejte tam, kde je instrumentation surface skutečná.
Čemu bych se vyhnul, je always-on profiling s vysokou frekvencí napříč každou backend službou. Tým tu myšlenku miluje zhruba dva týdny, než vystřelí retention náklady a nikdo nedokáže vysvětlit, proč potřebují profily pro cron worker, který běží šestkrát denně.
propagujte context čistě
Distribuované tracingy umírají ve chvíli, kdy se propagace contextu začne flákat. Edge systémy se v tomhle rozbijí rychle, protože request může projít browser kódem, middleware, route handlery, fetch hranicemi, API gatewayí a pak ještě Go službou, která si pouští gorutiny pro paralelní lookupy. Jeden chybějící hop v headeru a z trace je dekorativní konfety.
Držte se W3C Trace Contextu, tedy traceparent a tracestate, pokud nemáte opravdu dobrý důvod dělat něco jiného. V Next.js 15 edge handlerech tyhle headery přeposílejte explicitně při každém interním fetchi. Nespoléhejte na to, že to platforma dělá konzistentně za vás.
1const traceparent = req.headers.get('traceparent')2const tracestate = req.headers.get('tracestate')34const upstream = await fetch(`${process.env.API_BASE_URL}/v1/session`, {5 headers: {6 'traceparent': traceparent || '',7 'tracestate': tracestate || '',8 'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID()9 }10})
V Go použijte OTel HTTP middleware a přestaňte si ručně bastlit napůl rozbité span setupy, pokud vás teda baví debugovat chybějící parent ID ve dvě ráno.
1import (2 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"3 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"4 "go.opentelemetry.io/otel/sdk/resource"5 sdktrace "go.opentelemetry.io/otel/sdk/trace"6)78handler := otelhttp.NewHandler(apiMux, "go-api")910srv := &http.Server{11 Addr: ":8080",12 Handler: handler,13}
Pak přidejte attributes, které při triage opravdu pomůžou: region, deployment ID, cache hit, tenant ID, pokud to dovolí váš privacy model, edge colo, bucket upstream timeoutu. Vanity attributes, na které se nikdo nikdy neptá, klidně vynechte. Pořád vídám spany nacpané padesáti tagy jen proto, že si někdo přečetl blog od vendora a nadchl se. Jen to nafukuje storage a zpomaluje dotazy.
Nejlepší trace attribute je ta, podle které budete během incidentu filtrovat. U edge systémů je to typicky serverless.cold_start, cloud.region, http.route, http.status_code, cache.hit a deploy marker typu service.version nebo Git SHA. Všechno ostatní si musí obhájit existenci.
retence bez výčitek
Rozpočet na observabilitu potřebuje reálná čísla. Jinak tým buď sbírá málo a letí naslepo, nebo si nechá všechno 30 dní a pak se tváří překvapeně, že telemetry je najednou jedna z pěti největších infra položek.
Rozumný start pro produkt střední velikosti, řekněme 50 milionů requestů měsíčně, Next.js edge frontend, pár Go API, PostgreSQL a Redis, může vypadat takhle. Metrics držte 30 dní jen u high-cardinality service dashboardů, a to ještě jen pokud to backend zvládá, jinak po 7 dnech downsamplujte. Trace držte v plné věrnosti u nasamplovaného důležitého provozu 7 dní, pak už jen index-only nebo agregované pohledy do 30 dní. Profily držte 3 až 7 dní, pokud zrovna aktivně neřešíte výkon. Logy kratší, než by si všichni přáli, typicky 7 dní pro aplikační logy, delší jen pro audit eventy a security-relevant streamy.
Hrubý budget pro trace. Počítejme, že tail sampling zachová 100 % chyb, 100 % cold startů, 100 % checkout a auth route a 2 % zbytku. Hodně týmů skončí po složení všech pravidel někde mezi 3 až 6 % efektivní retence. Když má uložená trace v průměru 12 KB po kompresi a měsíčně jich zachováte 2,5 milionu, jste zhruba na 30 GB čistého trace payloadu ještě před indexačním overheadem. Účet nedělají byty na drátu, ale indexace a query engine. Plánujte podle ceny za indexované spany u vendora, ne jen podle storage na disku.
U Sentry držte sampling transakcí agresivní na hot business cestách a skoro nulový jinde. U Lightstepu nebo jiného OTLP backendu stlačte retenci drahých raw trace dolů a pokud to vendor umí, exportujte z collectoru dlouhověké RED metrics přes span-to-metrics. Pokud self-hostujete, stacky postavené na ClickHouse umí být rychle nákladově efektivní, ale bez debat potřebují někoho v týmu, kdo rozumí merge chování, TTL a tomu, proč vám naivní schema zničí query performance.
Jedno praktické pravidlo, kterému věřím víc než jakékoli pricing kalkulačce: když se dva týdny nikdo nepodíval na nějakou třídu telemetry, zkraťte retenci nebo snižte sample rate. Data v observabilitě si na sebe musí vydělat.
Právě tady je vidět seniorní úsudek. Kritické auth failure si zaslouží štědrou retenci. Hlučný veřejný search endpoint, do kterého střílí crawlery, ne. Peníze utrácejte za trace, které vysvětlí bolest uživatelů a riziko pro revenue, a zbytek si nechte jen tak dlouho, aby šly poznat trendy.
