Edge Runtimes
autotel-edge is a lightweight, vendor-agnostic OpenTelemetry implementation
for edge runtimes. It’s the foundation under
autotel-cloudflare and works directly on
Vercel Edge, Netlify Edge, Deno Deploy, and any runtime with fetch() and
AsyncLocalStorage. There are no Node.js APIs and no auto-instrumentation
libraries. The core SDK ships with sampling, events, logger, and testing
helpers.
| Bundle | Size |
|---|---|
Core (autotel-edge) |
~20KB minified, ~8KB gz |
| All entry points combined | ~25KB minified, ~10KB gz |
Cloudflare Workers users: use
autotel-cloudflareinstead. It builds on this same foundation and adds complete bindings instrumentation.
Installation
Section titled “Installation”npm install autotel-edgeWhen to use this directly
Section titled “When to use this directly”- Vercel Edge Functions / Edge Middleware
- Netlify Edge Functions
- Deno Deploy
- AWS Lambda@Edge (limited
AsyncLocalStoragesupport) - Custom edge runtimes (e.g. building your own
autotel-vercel)
Quick start
Section titled “Quick start”There is no init(). Parse a config once per isolate, register a
WorkerTracerProvider, and give its tracer the parsed head sampler. trace()
and span() then use the global tracer.
import { resourceFromAttributes } from '@opentelemetry/resources'; // add as a direct dependencyimport { parseConfig, WorkerTracerProvider, trace } from 'autotel-edge';
const config = parseConfig({ service: { name: 'my-edge-function' }, exporter: { url: process.env.OTEL_ENDPOINT ?? 'http://localhost:4318/v1/traces', },});
const provider = new WorkerTracerProvider( config.spanProcessors, resourceFromAttributes({ 'service.name': config.service.name }),);provider.register(); // also installs the AsyncLocalStorage context manager// Applies sampling.headSampler; without it every trace is head-sampled.provider.getTracer('autotel-edge').setHeadSampler(config.sampling.headSampler);
export const handler = trace(async (request: Request) => { return new Response('Hello World');});Without exporter or spanProcessors, parseConfig warns once and spans are
dropped. A trace is exported when its local root span ends; for a trace that
continues a remote parent, call
await provider.getTracer('autotel-edge').forceFlush(traceId) (e.g. inside
waitUntil).
Later examples show only the object passed to parseConfig(), or call this
helper that wraps the provider lines above:
import { resourceFromAttributes } from '@opentelemetry/resources';import { WorkerTracerProvider, type parseConfig } from 'autotel-edge';
export function setupTracing(config: ReturnType<typeof parseConfig>) { const provider = new WorkerTracerProvider( config.spanProcessors, resourceFromAttributes({ 'service.name': config.service.name }), ); provider.register(); provider .getTracer('autotel-edge') .setHeadSampler(config.sampling.headSampler); return provider;}Core API
Section titled “Core API”trace(fn) / trace(name, fn) / trace.run(name, operation)
Section titled “trace(fn) / trace(name, fn) / trace.run(name, operation)”trace wraps, trace.run runs. Every trace(...) form returns a wrapper;
trace.run(...) runs the operation immediately with an injected context. Edge
matches core exactly:
// Bare functionconst handler = trace(async (request: Request) => new Response('OK'));
// Reusable function with a stable nameconst handler = trace( 'fetch-handler', async (request: Request) => new Response('OK'),);
// Same wrapper, options form — needed when you want attributesFromArgsconst handler = instrument({ key: 'fetch-handler', fn: async (request: Request) => new Response('OK'), attributesFromArgs: ([request]) => ({ 'http.method': request.method }),});
// One immediate named operation with contextconst result = await trace.run('cache.refresh', async (ctx) => { ctx.setAttribute('cache.region', 'eu'); return refreshCache();});
// Factory pattern — receives ctx, returns the handlerconst handler = withTracing({})((ctx) => async (request: Request) => { ctx.setAttribute('custom.attr', 'value'); return new Response('OK');});span(options, fn)
Section titled “span(options, fn)”Create a named child span around a code block:
import { span } from 'autotel-edge';
const rows = await span( { name: 'database.query', attributes: { table: 'users' } }, async (s) => { const data = await db.query('SELECT * FROM users'); s.setAttribute('rows', data.length); return data; },);parseConfig(config)
Section titled “parseConfig(config)”Parse the config once, at module load, then register the provider as in the quick start:
const config = parseConfig({ service: { name: 'my-edge-function', version: '1.0.0', namespace: 'production', }, exporter: { url: 'https://api.honeycomb.io/v1/traces', headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY }, }, sampling: { tailSampler: SamplingPresets.production() },});createInitialiser() accepts a function that closes over the runtime
environment:
import { createInitialiser } from 'autotel-edge';
const initialise = createInitialiser((env) => ({ service: { name: env.SERVICE_NAME }, exporter: { url: env.OTEL_ENDPOINT },}));const config = initialise(env, trigger); // same shape as parseConfig()'s resultFetch route controls
Section titled “Fetch route controls”Filter which paths are traced and map URL patterns to per-route service names.
Handler wrappers (such as autotel-cloudflare’s) read these; plain trace() does
not. To apply them yourself, use the route matching helpers.
parseConfig({ service: { name: 'edge-app' }, exporter: { url: 'https://otlp.example.com/v1/traces' }, handlers: { fetch: { include: ['/api/**'], exclude: ['/api/internal/**', '/health'], routes: { '/api/auth/**': { service: 'auth-service' }, '/api/**': { service: 'api-service' }, }, }, },});Sampling
Section titled “Sampling”Tail sampling decides after the trace finishes, so you can keep 100% of errors and slow requests while sampling everything else at any rate.
Presets
Section titled “Presets”import { SamplingPresets } from 'autotel-edge/sampling';
parseConfig({ service: { name: 'my-app' }, exporter: { url: 'https://otlp.example.com/v1/traces' }, sampling: { tailSampler: SamplingPresets.production() },});| Preset | Behaviour |
|---|---|
SamplingPresets.development() |
100% sampling |
SamplingPresets.production() |
10% baseline + 100% errors + slow >1s |
SamplingPresets.highTraffic() |
1% baseline + 100% errors + slow >1s |
SamplingPresets.debugging() |
Errors only |
Custom sampler
Section titled “Custom sampler”import { createCustomTailSampler } from 'autotel-edge/sampling';import { SpanStatusCode } from '@opentelemetry/api';
const sampler = createCustomTailSampler((trace) => { const span = trace.localRootSpan; if (span.attributes['http.route']?.toString().startsWith('/api/')) return true; if (span.status.code === SpanStatusCode.ERROR) return true; return false;});Logger
Section titled “Logger”Simple logger
Section titled “Simple logger”import { createEdgeLogger } from 'autotel-edge/logger';
const log = createEdgeLogger('my-service');log.info('Processing request', { userId: '123' });log.error('Request failed', { error });// trace_id and span_id are attached automaticallyExecution logger
Section titled “Execution logger”The execution logger is a request-scoped accumulator: build context across the
request, then emit once. Supports fork() for background work that should keep
its own snapshot:
import { trace } from 'autotel-edge';import { getExecutionLogger } from 'autotel-edge/logger';
export const handler = trace(async (request) => { const log = getExecutionLogger();
log.set({ feature: 'checkout' }); log.info('Processing order', { orderId: '123' });
log.fork('send-email', async () => { await sendEmail('123'); });
log.emitNow(); return Response.json({ ok: true });});Events
Section titled “Events”Track product events with the active trace ID attached automatically:
import { createEdgeSubscribers } from 'autotel-edge/events';
const events = createEdgeSubscribers({ service: 'my-edge-function', transport: async (event) => { await fetch('https://analytics.example.com/events', { method: 'POST', body: JSON.stringify(event), }); },});
events.trackEvent('order.completed', { orderId: 'abc', amount: 99.99 });Runtime notes
Section titled “Runtime notes”Vercel Edge Functions
Section titled “Vercel Edge Functions”// app/api/route.ts (or middleware.ts)import { trace, parseConfig } from 'autotel-edge';import { setupTracing } from './tracing';
setupTracing( parseConfig({ service: { name: 'vercel-edge' }, exporter: { url: process.env.OTEL_ENDPOINT! }, }),);
export const runtime = 'edge';
export const GET = trace(async (request: Request) => { return Response.json({ ok: true });});Netlify Edge Functions
Section titled “Netlify Edge Functions”import { trace, parseConfig } from 'autotel-edge';import { setupTracing } from './tracing.ts';
setupTracing( parseConfig({ service: { name: 'netlify-edge' }, exporter: { url: Deno.env.get('OTEL_ENDPOINT')! }, }),);
export default trace(async (request: Request) => { return new Response('Hello from the edge');});Deno Deploy
Section titled “Deno Deploy”import { trace, parseConfig } from 'https://esm.sh/autotel-edge';import { setupTracing } from './tracing.ts';
setupTracing( parseConfig({ service: { name: 'deno-deploy' }, exporter: { url: Deno.env.get('OTEL_ENDPOINT')! }, }),);
Deno.serve(trace(async (request) => new Response('OK')));Testing
Section titled “Testing”autotel-edge/testing has no runtime helpers yet. Pass an in-memory exporter
to parseConfig(), register the provider as in the quick start, and read the
finished spans back:
import { InMemorySpanExporter } from '@opentelemetry/sdk-trace-base';import { parseConfig } from 'autotel-edge';
const exporter = new InMemorySpanExporter();setupTracing(parseConfig({ service: { name: 'test' }, exporter }));
await myFunction();expect(exporter.getFinishedSpans().map((s) => s.name)).toContain('myFunction');Constraints
Section titled “Constraints”- No Node.js APIs. No
fs,net,process(beyondprocess.envpolyfills); use Web APIs (fetch,crypto.subtle) instead. - No auto-instrumentations. Edge runtimes don’t support
@opentelemetry/auto-instrumentations-node. Instrument explicitly withtrace()andspan(). AsyncLocalStoragerequired. Some runtimes need it enabled (e.g. Cloudflare’scompatibility_flags = ["nodejs_compat"]).
Route matching helpers
Section titled “Route matching helpers”The toolkit exports three functions for building your own middleware. They drive the route controls above and are useful when you wire instrumentation by hand.
import { shouldInstrumentPath, getServiceForPath, runMiddlewareFinishPipeline,} from 'autotel-edge';
// Skip noisy paths before opening a span.const trace = shouldInstrumentPath('/api/users', { exclude: ['/health', '/metrics'],});
// Name the span's service from the route.const service = getServiceForPath('/users/123', { '/users/**': { service: 'users-api' }, '/admin/**': { service: 'admin-api' },});
// After the handler runs, enrich the span and drain async work.await runMiddlewareFinishPipeline( { event: request, status: 200 }, { enrichers: [ (ctx) => span.setAttributes({ 'http.status_code': ctx.status }), ], drains: [(ctx) => analytics.record(ctx)], },);Each enricher and drain runs inside its own try/catch, so one failure does not cascade into the others.
Entry points
Section titled “Entry points”| Import | Contents |
|---|---|
autotel-edge |
Core + toolkit: trace, span, parseConfig, WorkerTracerProvider, definePlugin, createPluginRunner, shouldInstrumentPath, getServiceForPath, runMiddlewareFinishPipeline |
autotel-edge/sampling |
Presets and custom samplers |
autotel-edge/events |
createEdgeSubscribers, getEdgeSubscribers |
autotel-edge/logger |
createEdgeLogger, execution logger |
autotel-edge/testing |
Types only (no runtime helpers yet) |
Vendor packages
Section titled “Vendor packages”autotel-cloudflare: Cloudflare Workers with full bindings coverageautotel-vercel: coming soonautotel-netlify: coming soon
Examples
Section titled “Examples”cloudflare-example: Cloudflare Workers built on autotel-edge.