# autotel-cloudflare

**The #1 OpenTelemetry package for Cloudflare Workers** - complete bindings coverage, native CF OTel integration, advanced sampling, zero vendor lock-in.

[![npm version](https://badge.fury.io/js/autotel-cloudflare.svg)](https://www.npmjs.com/package/autotel-cloudflare)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/autotel-cloudflare)](https://bundlephobia.com/package/autotel-cloudflare)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

## Features

- ✅ **Cloudflare native tracing (automatic)** - `trace()`/`span()` nest inside Cloudflare's native trace waterfall when enabled; no duplicate spans, no exporter code
- ✅ **Native Cloudflare OTel integration** - Works with `wrangler.toml` destinations
- ✅ **Complete bindings coverage** - KV, R2, D1, DO, AI, Vectorize, Hyperdrive, and more
- ✅ **Multiple API styles** - `instrument()`, `wrapModule()`, `wrapDurableObject()`, functional
- ✅ **Advanced sampling** - Adaptive tail sampling (10% baseline, 100% errors/slow)
- ✅ **Events integration** - Product analytics with trace correlation
- ✅ **Zero vendor lock-in** - OTLP compatible, works with any backend
- ✅ **Tree-shakeable** - Import only what you need
- ✅ **TypeScript native** - Full type safety

## DX Direction

The package direction is to make Cloudflare observability feel the same across Workers, Queues, Durable Objects, alarms, and Workflows:

- use Cloudflare-native wrappers to create the root span
- use `trace(fn)` for inferred-name wrappers and `trace(name, ctx => result)` for immediate named work; use `withTracing({ name })((ctx) => fn)` for reusable named business logic (`instrument(handler, config)` is the Worker handler adapter)
- prefer span attributes and one execution snapshot over scattered info logs

See [docs/CLOUDFLARE-DX.md](../../docs/CLOUDFLARE-DX.md) for the design target and review rules.

## Cloudflare native tracing (automatic)

Cloudflare Workers ship [native tracing](https://developers.cloudflare.com/workers/observability/traces/): enable it in Wrangler and Cloudflare instruments fetch/KV/R2/D1/handlers and exports OTLP to any backend. No exporter code. autotel integrates **automatically**: the same `trace()`/`span()`/`enterSpan()` code nests inside Cloudflare's native waterfall when native tracing is on, and falls back to autotel's own OTLP pipeline everywhere else (other edge runtimes, native off, local `wrangler dev`).

```toml
# wrangler.toml — that's the only setup
[observability.traces]
enabled = true
```

When native tracing is active, autotel **defers to the platform**: it does not proxy-instrument bindings (no duplicate spans) and does not run its own exporter. It routes your custom spans to `tracing.enterSpan()` and surfaces the `cf-ray` id as `ctx.correlationId` + a `correlation.id` span attribute. Controlled by `nativeTracing: 'auto' | 'on' | 'off'` (default `'auto'`).

### Same code, both modes: captured evidence

This is real output from [`apps/cloudflare-example`](../../apps/cloudflare-example) (`node scripts/capture-evidence.mjs`). Identical business logic, two runtimes:

```text
# autotel OTLP pipeline (local dev / non-Workers / nativeTracing:'off')
• GET /orders
  • order.price            order.id=ORD-1 order.total=120
    • order.subtotal       order.subtotal=100
      • KV MY_KV: get       db.system.name=cloudflare-kv db.operation.name=get   ← autotel-instrumented
    • order.total          order.total=120
  • user.create            user.duplicate=true
    • db.checkDuplicate
  • payment.charge [ERROR]  exception.message="card declined"

# Cloudflare native tracing (autotel → ctx.tracing.enterSpan)
• order.price [OK]       correlation.id=8f1c2d3e…-LHR  order.id=ORD-1 order.total=120
  • order.subtotal       correlation.id=8f1c2d3e…-LHR  order.subtotal=100
  • order.total          correlation.id=8f1c2d3e…-LHR  order.total=120
• user.create            correlation.id=8f1c2d3e…-LHR  user.duplicate=true
  • db.checkDuplicate
• payment.charge [ERROR] error=true exception.message="card declined"  correlation.id=8f1c2d3e…-LHR
```

Note the native tree has **no `KV MY_KV: get` span**. Cloudflare emits that natively (your custom spans nest above it on deploy), so there are no duplicates.

Full details, degradation map, and the forward-compatible trace-id story: [docs/CLOUDFLARE-NATIVE-TRACING.md](../../docs/CLOUDFLARE-NATIVE-TRACING.md).

## Installation

```bash
npm install autotel-cloudflare
# or
pnpm add autotel-cloudflare
# or
yarn add autotel-cloudflare
```

## Quickstart: Structured logging in a Cloudflare Worker

For just structured logs (no tracing setup required), import from `autotel-cloudflare/logger`:

```ts
import { createEdgeLogger } from 'autotel-cloudflare/logger';

const log = createEdgeLogger('my-worker', { level: 'info' });

export default {
  async fetch() {
    log.info({ user_id: 'u1' }, 'request handled');
    return new Response('ok');
  },
};
```

`autotel-cloudflare/logger` is the edge-clean entry: it re-exports `createEdgeLogger` from `autotel-edge/logger` plus the Cloudflare execution-logger helpers (`createWorkersLogger`, `getRequestLogger`, `getQueueLogger`). The root `autotel-cloudflare` import pulls in tracing, `AsyncLocalStorage`, and the wrappers. Only use it when you want spans.

## Quick Start

### 1. Configure Cloudflare Native OTel (wrangler.toml)

```toml
[observability.traces]
enabled = true
destinations = ["honeycomb"]  # Configure in CF dashboard
head_sampling_rate = 1.0      # Let autotel handle sampling
```

### 2. Instrument Your Worker

```typescript
import { wrapModule, trace } from 'autotel-cloudflare';

// Zero-boilerplate function tracing
const processOrder = trace(async (orderId: string, kv: KVNamespace) => {
  const order = await kv.get(orderId); // Auto-instrumented!
  return order;
});

export default wrapModule(
  {
    service: { name: 'my-worker' },
    instrumentBindings: true, // Auto-instrument KV, R2, D1, etc.
    sampling: 'adaptive', // 10% baseline, 100% errors/slow
  },
  {
    async fetch(req, env, ctx) {
      return Response.json(await processOrder('123', env.ORDERS_KV));
    },
  },
);
```

## API Styles

### Style 1: wrapModule (Recommended)

Inspired by workers-honeycomb-logger:

```typescript
import { wrapModule } from 'autotel-cloudflare';

const handler = {
  async fetch(req, env, ctx) {
    return new Response('Hello');
  },
};

export default wrapModule({ service: { name: 'my-worker' } }, handler);
```

### Style 2: instrument

```typescript
import { instrument } from 'autotel-cloudflare';

export default instrument(
  {
    async fetch(req, env, ctx) {
      return new Response('Hello');
    },
  },
  { service: { name: 'my-worker' } },
);
```

### Fetch Route Controls

Filter which fetch routes are instrumented and map route patterns to service names:

```typescript
import { wrapModule } from 'autotel-cloudflare';

export default wrapModule(
  {
    service: { name: 'edge-gateway' },
    handlers: {
      fetch: {
        include: ['/api/**'],
        exclude: ['/api/internal/**', '/health'],
        routes: {
          '/api/auth/**': { service: 'auth-service' },
          '/api/**': { service: 'api-service' },
        },
      },
    },
  },
  {
    async fetch(req) {
      return new Response('ok');
    },
  },
);
```

### Style 3: Functional API (Unique)

Zero-boilerplate function tracing:

```typescript
import { trace, span, withTracing } from 'autotel-cloudflare';

// Automatic trace name inference
export const createUser = trace(async (data: UserData) => {
  return await db.insert(data);
});

// Factory pattern for context access
export const processPayment = withTracing({ name: 'payment.process' })(
  (ctx) => async (amount: number) => {
    ctx.setAttribute('amount', amount);

    await span('validate.card', () => validateCard());
    await span('charge.card', () => chargeCard(amount));

    return { success: true };
  },
);
```

## Request Logger Bootstrap

Use `createWorkersLogger()` for request-scoped snapshots with Cloudflare context pre-filled.

```typescript
import { wrapModule, createWorkersLogger } from 'autotel-cloudflare';

export default wrapModule(
  { service: { name: 'checkout-worker' } },
  {
    async fetch(request) {
      const log = createWorkersLogger(request, {
        headers: ['x-request-id'],
      });

      log.info('checkout.started');
      log.set({ checkout: { stage: 'validated' } });
      log.emitNow({ status: 200 });

      return new Response('ok');
    },
  },
);
```

## Complete Bindings Coverage

### Auto-Instrumented Bindings

All bindings are automatically instrumented when `instrumentBindings: true`:

```typescript
// KV
await env.MY_KV.get('key')           // → Span: "KV MY_KV: get"
await env.MY_KV.put('key', 'value')  // → Span: "KV MY_KV: put"

// R2
await env.MY_R2.get('file.txt')      // → Span: "R2 MY_R2: get"
await env.MY_R2.put('file.txt', data) // → Span: "R2 MY_R2: put"

// D1
await env.MY_D1.prepare('SELECT * FROM users').all()  // → Span: "D1 MY_D1: all"

// Durable Objects
await env.MY_DO.get(id).fetch(req)   // → Span: "DO MY_DO: fetch"

// Workers AI
await env.AI.run('@cf/meta/llama', { prompt: '...' })  // → Span: "AI: run"

// Vectorize
await env.VECTOR.query(vector)       // → Span: "Vectorize VECTOR: query"

// Service Bindings
await env.MY_SERVICE.fetch(req)      // → Span: "Service MY_SERVICE: fetch"

// Queue
await env.MY_QUEUE.send({ data })    // → Span: "Queue MY_QUEUE: send"

// Analytics Engine
await env.ANALYTICS.writeDataPoint({ ... })  // → Span: "Analytics: writeDataPoint"
```

**Supported Bindings:**

- ✅ KV (get, put, delete, list, getWithMetadata)
- ✅ R2 (head, get, put, delete, list, createMultipartUpload)
- ✅ D1 (prepare, batch, exec, dump)
- ✅ Durable Objects (fetch, alarm)
- ✅ Workflows (get, create, getInstance)
- ✅ Workers AI (run)
- ✅ Vectorize (insert, query, getByIds, deleteByIds, upsert)
- ✅ Hyperdrive (all queries)
- ✅ Service Bindings (fetch)
- ✅ Queue (send, sendBatch)
- ✅ Analytics Engine (writeDataPoint)
- ✅ Email (send, forward)

## Sampling Strategies

### Adaptive Sampling (Recommended)

```typescript
import { SamplingPresets } from 'autotel-cloudflare/sampling';

wrapModule(
  {
    service: { name: 'my-worker' },
    sampling: {
      tailSampler: SamplingPresets.production(),
      // 10% baseline, 100% errors, 100% slow requests (>1s)
    },
  },
  handler,
);
```

### Available Presets

```typescript
// Development - 100% sampling
sampling: {
  tailSampler: SamplingPresets.development();
}

// Production - 10% baseline, all errors, slow >1s
sampling: {
  tailSampler: SamplingPresets.production();
}

// High traffic - 1% baseline, all errors, slow >1s
sampling: {
  tailSampler: SamplingPresets.highTraffic();
}

// Debugging - errors only
sampling: {
  tailSampler: SamplingPresets.debugging();
}

// Or use shorthand
sampling: 'adaptive'; // Same as SamplingPresets.production()
sampling: 'error-only'; // Same as SamplingPresets.debugging()
```

### Custom Sampling

```typescript
import { createCustomTailSampler } from 'autotel-cloudflare/sampling';

const customSampler = createCustomTailSampler((trace) => {
  const span = trace.localRootSpan;

  // Always sample /api/* endpoints
  if (span.attributes['http.route']?.toString().startsWith('/api/')) {
    return true;
  }

  // Sample all errors
  if (span.status.code === SpanStatusCode.ERROR) {
    return true;
  }

  // Sample slow requests
  const duration = (span.endTime[0] - span.startTime[0]) / 1_000_000;
  if (duration > 1000) {
    return true;
  }

  return Math.random() < 0.1; // 10% of everything else
});
```

## Durable Objects

### Instrument Durable Object Class

```typescript
import { wrapDurableObject } from 'autotel-cloudflare';

class Counter implements DurableObject {
  async fetch(request: Request) {
    // Auto-traced with span "Counter: fetch"
    const count = (await this.state.storage.get('count')) || 0;
    await this.state.storage.put('count', count + 1);
    return new Response(String(count + 1));
  }

  async alarm() {
    // Auto-traced with span "Counter: alarm"
    console.log('Alarm triggered');
  }
}

export default wrapDurableObject({ service: { name: 'counter-do' } }, Counter);
```

## Events Integration

Track product events with automatic trace correlation:

```typescript
import { publishEvent } from 'autotel-cloudflare/events';

wrapModule(
  {
    service: { name: 'my-worker' },
    // Configure event subscribers
    subscribers: [
      async (event) => {
        // Send to your analytics platform
        await fetch('https://analytics.example.com/events', {
          method: 'POST',
          body: JSON.stringify(event),
        });
      },
    ],
  },
  {
    async fetch(req, env, ctx) {
      // Track user events
      await publishEvent({
        name: 'order.completed',
        userId: '123',
        properties: {
          orderId: 'abc',
          amount: 99.99,
        },
        // Automatically includes current trace ID
      });

      return new Response('OK');
    },
  },
);
```

## Configuration

### Complete Example

```typescript
import { wrapModule, SamplingPresets } from 'autotel-cloudflare';

export default wrapModule(
  {
    // Service identification
    service: {
      name: 'my-worker',
      version: '1.0.0',
      namespace: 'production',
    },

    // Auto-instrument bindings
    instrumentBindings: true,

    // Global instrumentations
    instrumentation: {
      instrumentGlobalFetch: true, // Trace all fetch() calls
      instrumentGlobalCache: true, // Trace cache API
      disabled: false, // Set true to disable all tracing
    },

    // Sampling strategy
    sampling: {
      tailSampler: SamplingPresets.production(),
    },

    // Handler-specific config
    handlers: {
      fetch: {
        postProcess: (span, { request, response }) => {
          // Add custom attributes
          const url = new URL(request.url);
          if (url.pathname.startsWith('/api/')) {
            span.setAttribute('api.endpoint', url.pathname);
          }
        },
      },
    },
  },
  handler,
);
```

### Dynamic Configuration

```typescript
// Configuration can be a function
export default wrapModule(
  (env, trigger) => ({
    service: { name: env.SERVICE_NAME || 'my-worker' },
    exporter: {
      url: env.OTEL_ENDPOINT,
      headers: { 'x-api-key': env.API_KEY },
    },
    sampling: {
      tailSampler:
        env.ENVIRONMENT === 'production'
          ? SamplingPresets.production()
          : SamplingPresets.development(),
    },
  }),
  handler,
);
```

## Entry Points (Tree-Shaking)

```typescript
// Main export (everything — pulls AsyncLocalStorage, needs nodejs_compat)
import { wrapModule, trace, instrument } from 'autotel-cloudflare';

// Tree-shakeable entry points
import { instrumentKV, instrumentR2 } from 'autotel-cloudflare/bindings';
import { instrumentDO } from 'autotel-cloudflare/handlers';
import { SamplingPresets } from 'autotel-cloudflare/sampling';
import { publishEvent } from 'autotel-cloudflare/events';
import { createEdgeLogger } from 'autotel-cloudflare/logger';
import { createTraceCollector } from 'autotel-cloudflare/testing';
```

## Cloudflare compatibility

Workers run on V8 isolates, not Node. Anything that transitively imports `node:async_hooks` (used by `AsyncLocalStorageContextManager` for span context) or `node:buffer` requires `compatibility_flags = ["nodejs_compat"]` in your `wrangler.toml`. The `/logger` subpath stays clear of those.

| Entry                            | Edge-safe without `nodejs_compat`? | Notes                                                                                                                              |
| -------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `autotel-cloudflare`             | No: needs `nodejs_compat`          | Pulls `AsyncLocalStorage` (span context) and `Buffer` transitively via `autotel-edge` root                                         |
| `autotel-cloudflare/logger`      | Yes                                | Edge-clean. `createEdgeLogger`, `createWorkersLogger`, `getRequestLogger`, `getQueueLogger`, `getWorkflowLogger`, `getActorLogger` |
| `autotel-cloudflare/bindings`    | No: needs `nodejs_compat`          | Wrappers create spans, require context manager                                                                                     |
| `autotel-cloudflare/handlers`    | No: needs `nodejs_compat`          | DO/Workflow wrappers create spans                                                                                                  |
| `autotel-cloudflare/sampling`    | Yes                                | Pure samplers, no runtime imports                                                                                                  |
| `autotel-cloudflare/events`      | No: needs `nodejs_compat`          | Pulls trace context for correlation                                                                                                |
| `autotel-cloudflare/testing`     | Yes (test-only)                    | Collectors live in test runtime; do not ship to Workers                                                                            |
| `autotel-cloudflare/actors`      | No: needs `nodejs_compat`          | Builds on DO wrappers                                                                                                              |
| `autotel-cloudflare/agents`      | No: needs `nodejs_compat`          | Builds on DO wrappers                                                                                                              |
| `autotel-cloudflare/parse-error` | Yes                                | Pure error normaliser                                                                                                              |

Rule of thumb: if you only need logs, import `autotel-cloudflare/logger` and skip `nodejs_compat`. If you want spans, add `nodejs_compat` and import from the root.

## See also

- [autotel-edge](../autotel-edge): vendor-agnostic foundation re-exported by this package
- [autotel](../autotel): Node.js entry, full SDK with auto-instrumentation
- [autotel-drizzle](../autotel-drizzle): Drizzle ORM spans (Node only)

## Testing

```typescript
import {
  createTraceCollector,
  assertTraceCreated,
} from 'autotel-cloudflare/testing';

describe('my worker', () => {
  it('creates traces', async () => {
    const collector = createTraceCollector();

    await myFunction();

    assertTraceCreated(collector, 'myFunction');
  });
});
```

## Examples

See [apps/cloudflare-example](../../apps/cloudflare-example) for a complete working example with:

- ✅ All bindings instrumented (KV, R2, D1, etc.)
- ✅ Multiple handler types (fetch, scheduled, queue, email)
- ✅ Durable Objects
- ✅ Custom spans and attributes
- ✅ Error handling
- ✅ Sampling strategies
- ✅ Events tracking

## License

Apache-2.0 © [Jag Reehal](https://github.com/jagreehal)

## Links

- [GitHub Repository](https://github.com/jagreehal/autotel)
- [Documentation](https://github.com/jagreehal/autotel#readme)
- [Issues](https://github.com/jagreehal/autotel/issues)
- [autotel-edge](../autotel-edge) - Vendor-agnostic foundation
