Skip to content

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-cloudflare instead. It builds on this same foundation and adds complete bindings instrumentation.

Terminal window
npm install autotel-edge
  • Vercel Edge Functions / Edge Middleware
  • Netlify Edge Functions
  • Deno Deploy
  • AWS Lambda@Edge (limited AsyncLocalStorage support)
  • Custom edge runtimes (e.g. building your own autotel-vercel)
import { trace, init } from 'autotel-edge';
init({
service: { name: 'my-edge-function' },
exporter: {
url: process.env.OTEL_ENDPOINT ?? 'http://localhost:4318/v1/traces',
},
});
export const handler = trace(async (request: Request) => {
return new Response('Hello World');
});

Wrap a function so its execution becomes a span. Three call shapes:

// Bare function
const handler = trace(async (request: Request) => new Response('OK'));
// With options
const handler = trace(
{
name: 'fetch-handler',
attributesFromArgs: ([request]) => ({ 'http.method': request.method }),
},
async (request: Request) => new Response('OK'),
);
// Factory pattern — receives ctx, returns the handler
const handler = trace((ctx) => async (request: Request) => {
ctx.setAttribute('custom.attr', 'value');
return new Response('OK');
});

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;
},
);

Configure the SDK once, at module load:

init({
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() },
});

init() accepts a function that closes over the runtime environment too:

init((env) => ({
service: { name: env.SERVICE_NAME },
exporter: { url: env.OTEL_ENDPOINT },
}));

Filter which paths are traced and map URL patterns to per-route service names:

init({
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' },
},
},
},
});

Tail sampling decides after the trace finishes, so you can keep 100% of errors and slow requests while sampling everything else at any rate.

import { SamplingPresets } from 'autotel-edge/sampling';
init({
service: { name: 'my-app' },
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 |

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;
});
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 automatically

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 { getExecutionLogger, trace } from 'autotel-edge';
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 });
});

Track product events with the active trace ID attached automatically:

import { publishEvent } from 'autotel-edge/events';
await publishEvent({
name: 'order.completed',
userId: '123',
properties: { orderId: 'abc', amount: 99.99 },
});
// app/api/route.ts (or middleware.ts)
import { trace, init } from 'autotel-edge';
init({
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/hello.ts
import { trace, init } from 'autotel-edge';
init({
service: { name: 'netlify-edge' },
exporter: { url: Deno.env.get('OTEL_ENDPOINT')! },
});
export default trace(async (request: Request) => {
return new Response('Hello from the edge');
});
import { trace, init } from 'https://esm.sh/autotel-edge';
init({
service: { name: 'deno-deploy' },
exporter: { url: Deno.env.get('OTEL_ENDPOINT')! },
});
Deno.serve(trace(async (request) => new Response('OK')));
import { createTraceCollector, assertTraceCreated } from 'autotel-edge/testing';
const collector = createTraceCollector();
await myFunction();
assertTraceCreated(collector, 'myFunction');
  • No Node.js APIs. No fs, net, process (beyond process.env polyfills); use Web APIs (fetch, crypto.subtle) instead.
  • No auto-instrumentations. Edge runtimes don't support @opentelemetry/auto-instrumentations-node. Instrument explicitly with trace() and span().
  • AsyncLocalStorage required. Some runtimes need it enabled (e.g. Cloudflare's compatibility_flags = ["nodejs_compat"]).

| Import | Contents | | ------------------------------- | --------------------------------------- | | autotel-edge | Core + toolkit: trace, span, init, definePlugin, createPluginRunner, shouldInstrumentPath, getServiceForPath, runMiddlewareFinishPipeline | | autotel-edge/sampling | Presets and custom samplers | | autotel-edge/events | publishEvent, subscribers | | autotel-edge/logger | createEdgeLogger, execution logger | | autotel-edge/testing | Trace collectors and assertions |

  • autotel-cloudflare — Cloudflare Workers with full bindings coverage
  • autotel-vercel — coming soon
  • autotel-netlify — coming soon