Cloudflare Workers
The evlog/workers adapter provides factory functions for creating request-scoped loggers with Cloudflare-specific context. Unlike framework integrations, Workers require manual log.emit() calls since there is no middleware lifecycle to hook into.
Set up evlog in my Cloudflare Worker
Quick Start
1. Install
pnpm add evlog
bun add evlog
yarn add evlog
npm install evlog
2. Initialize and create request loggers
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
initWorkersLogger({
env: { service: 'my-worker' },
})
export default defineWorkerFetch(async (request, _env, _ctx, log) => {
log.set({ action: 'handle_request' })
// ... your handler logic
log.emit()
return Response.json({ ok: true })
})
defineWorkerFetch passes ExecutionContext into createWorkersLogger for you, so async drain calls (PostHog, Axiom, …) stay alive via waitUntil after the response is returned. Use raw export default { fetch } + createWorkersLogger(request, { executionCtx: ctx }) only if you prefer not to use the wrapper.
createWorkersLogger still auto-extracts method, path, and cf-ray from the request.
log.emit() manually before returning a response. Workers don't have a request lifecycle hook to auto-emit. With defineWorkerFetch, async drain work is tied to waitUntil automatically; with a raw { fetch } handler, pass { executionCtx: ctx } to createWorkersLogger.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