Dashboard Get started

Cloudflare Workers

Workers run in V8 isolates — there's no NODE_OPTIONS preload hook, so instrumentation is a small helper instead of a fully automatic agent. la init writes it for you.

Lightarch itself is built on Cloudflare Workers and uses this exact pattern to instrument its own services.

Setup

bash
la init --dsn la_live_YOUR_TOKEN_HERE

In a Workers project (detected via wrangler.toml / wrangler.jsonc), this writes la-instrumentation.ts — a fetch-based span helper — and .dev.vars for local development. For production, store the DSN as a secret:

bash
echo "la_live_YOUR_TOKEN_HERE" | wrangler secret put LA_DSN

Use in your Worker

src/index.ts
import { newTrace, sendSpan } from './la-instrumentation'

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const { traceId, spanId } = newTrace()
    const start = Date.now()
    const url = new URL(request.url)

    try {
      const response = await handleRequest(request, env)
      sendSpan(ctx, {
        name: `${request.method} ${url.pathname}`,
        traceId, spanId, startMs: start, endMs: Date.now(),
        attrs: { 'http.method': request.method, 'http.status_code': response.status },
        isError: response.status >= 500,
      })
      return response
    } catch (err) {
      sendSpan(ctx, {
        name: `${request.method} ${url.pathname}`,
        traceId, spanId, startMs: start, endMs: Date.now(),
        attrs: { 'error.message': (err as Error).message },
        isError: true,
      })
      throw err
    }
  },
}
ctx.waitUntil() inside the helper sends the span after your response is returned — it adds zero latency to your Worker's response time.

Screenshot needed

Traces page showing a Cloudflare Worker trace: service='my-worker', spans for 'GET /api/users' with 12ms duration, environment='production'.

Prefer to write the helper by hand? Here's the whole thing
src/la-instrumentation.ts
const INGEST_URL = 'https://la-ingest-gateway.lightarch.workers.dev/v1/traces'

export function newTrace() {
  const rand = (n: number) => {
    const a = new Uint8Array(n); crypto.getRandomValues(a)
    return Array.from(a).map(b => b.toString(16).padStart(2, '0')).join('')
  }
  return { traceId: rand(16), spanId: rand(8) }
}

export function sendSpan(ctx: ExecutionContext, span: {
  name: string; traceId: string; spanId: string
  startMs: number; endMs: number
  attrs?: Record<string, string | number | boolean>; isError?: boolean
}): void {
  ctx.waitUntil(
    fetch(INGEST_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'x-la-dsn': LA_DSN },
      body: JSON.stringify({ resourceSpans: [{ resource: { attributes: [
        { key: 'service.name', value: { stringValue: 'my-worker' } },
      ]}, scopeSpans: [{ spans: [{
        traceId: span.traceId, spanId: span.spanId, name: span.name,
        startTimeUnixNano: String(span.startMs * 1_000_000),
        endTimeUnixNano:   String(span.endMs * 1_000_000),
        status: { code: span.isError ? 2 : 1 },
        attributes: Object.entries(span.attrs ?? {}).map(([k, v]) => ({
          key: k,
          value: typeof v === 'number' ? { intValue: v }
               : typeof v === 'boolean' ? { boolValue: v }
               : { stringValue: String(v) },
        })),
      }]}]}]}),
    }).catch(() => {})
  )
}