Cloudflare Workers
The evlog/workers adapter instruments Cloudflare Workers and Durable Objects with request-scoped loggers carrying Cloudflare-specific context.
Use withEvlog to get the same middleware pipeline as every other framework integration — route filtering, redaction, enrich, tail sampling, plugins, drains, and automatic emit. Reach for defineWorkerFetch or createWorkersLogger when you'd rather own the emit yourself.
Set up evlog in my Cloudflare Worker
Quick Start
1. Install
pnpm add evlog
bun add evlog
yarn add evlog
npm install evlog
2. Wrap your fetch handler
import { initWorkersLogger, withEvlog } from 'evlog/workers'
initWorkersLogger({
env: { service: 'my-worker' },
})
export default withEvlog(async (request, _env, _ctx, log) => {
log.set({ action: 'handle_request' })
// ... your handler logic
return Response.json({ ok: true })
})
withEvlog emits one wide event per request when the handler returns — no manual log.emit(). It reads ExecutionContext off the third argument, so async drain calls (PostHog, Axiom, …) are registered with waitUntil and stay alive after the response is returned. Streaming responses defer the emit until the body completes.
requestId comes from x-request-id when the caller sends one, falling back to cf-ray. method, path, cf-ray, traceparent and the safe subset of request.cf are captured automatically.
Options
withEvlog accepts the same options as every other framework integration:
import { initWorkersLogger, withEvlog } from 'evlog/workers'
import { createAxiomDrain } from 'evlog/axiom'
initWorkersLogger({ env: { service: 'my-worker' } })
export default withEvlog(
async (request, env, ctx, log) => {
log.set({ route: 'checkout' })
return Response.json({ ok: true })
},
{
drain: createAxiomDrain(),
exclude: ['/health'],
routes: { '/api/**': { service: 'api' } },
redact: true,
enrich: (ctx) => {
ctx.event.colo = ctx.event.colo ?? 'unknown'
},
keep: (ctx) => {
if (ctx.duration > 1000) ctx.shouldKeep = true
},
},
)
Emitting manually
Prefer to own the emit? defineWorkerFetch wires ExecutionContext for you but leaves log.emit() to you:
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
initWorkersLogger({ env: { service: 'my-worker' } })
export default defineWorkerFetch(async (request, _env, _ctx, log) => {
log.set({ action: 'handle_request' })
log.emit()
return Response.json({ ok: true })
})
defineWorkerFetch and createWorkersLogger are the low-level path: they create the logger and leave the lifecycle to you, so include / exclude, routes, redact, enrich, keep and plugins do not apply. Use withEvlog to get them.Wide Events
Build up context progressively, then emit at the end:
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
initWorkersLogger({
env: { service: 'my-worker' },
})
export default defineWorkerFetch(async (request, env, _ctx, log) => {
const url = new URL(request.url)
log.set({ route: url.pathname })
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(url.searchParams.get('userId')).first()
log.set({ user: { id: user.id, plan: user.plan } })
const orders = await env.DB.prepare('SELECT COUNT(*) as count FROM orders WHERE user_id = ?').bind(user.id).first()
log.set({ orders: { count: orders.count } })
log.emit()
return Response.json({ user, orders })
})
14:58:15 INFO [my-worker] GET /api/users 200 in 12ms
├─ orders: count=5
├─ user: id=usr_123 plan=pro
├─ route: /api/users
└─ requestId: 4a8ff3a8-...
Error Handling
Use createError for structured errors and handle them with try/catch:
import { createError, parseError } from 'evlog'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
initWorkersLogger({ env: { service: 'my-worker' } })
export default defineWorkerFetch(async (request, env, _ctx, log) => {
try {
const body = await request.json()
log.set({ payment: { amount: body.amount } })
if (body.amount <= 0) {
throw createError({
status: 400,
message: 'Invalid payment amount',
why: 'The amount must be a positive number',
fix: 'Pass a positive integer in cents',
})
}
log.emit()
return Response.json({ success: true })
} catch (error) {
log.error(error instanceof Error ? error : new Error(String(error)))
log.emit()
const parsed = parseError(error)
return Response.json({
message: parsed.message,
why: parsed.why,
fix: parsed.fix,
}, { status: parsed.status })
}
})
Configuration
See the Configuration reference for all available options (initLogger, middleware options, sampling, silent mode, etc.).
Drain & Enrichers
Configure drain and enrichers via initWorkersLogger options:
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
import { createDrainPipeline } from 'evlog/pipeline'
import type { DrainContext } from 'evlog'
const pipeline = createDrainPipeline<DrainContext>({
batch: { size: 50, intervalMs: 5000 },
})
const drain = pipeline(createAxiomDrain())
const userAgent = createUserAgentEnricher()
initWorkersLogger({
env: { service: 'my-worker' },
drain,
enrich: (ctx) => {
userAgent(ctx)
},
})
Wrangler Configuration
Disable Cloudflare's default invocation logs to avoid duplicates when using evlog:
[observability]
enabled = false
Run Locally
wrangler dev
Next Steps
- Wide Events: Design comprehensive events with context layering
- Adapters: Send logs to Axiom, Sentry, PostHog, and more
- Sampling: Control log volume with head and tail sampling
- Structured Errors: Throw errors with
why,fix, andlinkfields