Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions apps/docs/content/0.landing.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,22 +427,18 @@ Wide events and structured errors for TypeScript. One log per request, full cont

#cloudflare
```ts [src/worker.ts]
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'

initWorkersLogger({ env: { service: 'checkout-worker' } })

export default {
async fetch(request, env) {
const log = createWorkersLogger(request)

const { cartId } = await request.json()
const cart = await env.DB.findCart(cartId)
log.set({ cart: { items: cart.items.length, total: cart.total } })
export default defineWorkerFetch(async (request, env, _ctx, log) => {
const { cartId } = await request.json()
const cart = await env.DB.findCart(cartId)
log.set({ cart: { items: cart.items.length, total: cart.total } })

log.emit()
return Response.json({ orderId: cart.id })
},
}
log.emit()
return Response.json({ orderId: cart.id })
})
```

#bun
Expand Down
120 changes: 60 additions & 60 deletions apps/docs/content/4.frameworks/12.cloudflare-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ The `evlog/workers` adapter provides factory functions for creating request-scop
Set up evlog in my Cloudflare Worker.

- Install evlog: pnpm add evlog
- Import initWorkersLogger and createWorkersLogger from 'evlog/workers'
- Import initWorkersLogger and defineWorkerFetch from 'evlog/workers'
- Call initWorkersLogger({ env: { service: 'my-worker' } }) at the top level
- In the fetch handler, create a logger with createWorkersLogger(request)
- In the fetch handler, use **defineWorkerFetch** (recommended) or createWorkersLogger(request, { executionCtx: ctx })
- Use log.set() to accumulate context throughout the request
- Call log.emit() manually before returning the response (no middleware lifecycle)

Expand All @@ -37,54 +37,55 @@ bun add evlog
### 2. Initialize and create request loggers

```typescript [src/worker.ts]
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'

initWorkersLogger({
env: { service: 'my-worker' },
})

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const log = createWorkersLogger(request)

log.set({ action: 'handle_request' })
export default defineWorkerFetch(async (request, _env, _ctx, log) => {
log.set({ action: 'handle_request' })

// ... your handler logic
// ... your handler logic

log.emit()
return Response.json({ ok: true })
},
}
log.emit()
return Response.json({ ok: true })
})
```

`createWorkersLogger(request)` automatically extracts `method`, `path`, and `cf-ray` from the request.
`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.

::callout{icon="i-lucide-info" color="info"}
You must call `log.emit()` manually before returning a response. Workers don't have a request lifecycle hook to auto-emit.
You must call `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:

```typescript [src/worker.ts]
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const log = createWorkersLogger(request)
const url = new URL(request.url)
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'

log.set({ route: url.pathname })
initWorkersLogger({
env: { service: 'my-worker' },
})

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 } })
export default defineWorkerFetch(async (request, env, _ctx, log) => {
const url = new URL(request.url)

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.set({ route: url.pathname })

log.emit()
return Response.json({ user, orders })
},
}
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 })
})
```

```bash [Terminal output]
Expand All @@ -101,39 +102,38 @@ Use `createError` for structured errors and handle them with try/catch:

```typescript [src/worker.ts]
import { createError, parseError } from 'evlog'

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const log = createWorkersLogger(request)

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 })
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
Expand Down
8 changes: 8 additions & 0 deletions apps/docs/content/4.frameworks/14.astro.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ Adapters: https://www.evlog.dev/adapters
This is a guide-level integration. It uses the generic `createRequestLogger` API rather than a framework-specific module.
::

::callout{icon="i-lucide-cloud" color="warning"}
On **Cloudflare Workers** (including Astro with `@astrojs/cloudflare`), set `waitUntil` on `createRequestLogger` to your `ExecutionContext#waitUntil` (properly bound), or use [`defineWorkerFetch`](/frameworks/cloudflare-workers) / [`createWorkersLogger`](/frameworks/cloudflare-workers) with `{ executionCtx }` on a **Worker `fetch` entry**. Otherwise async drains may never finish after the response is returned.

For Astro **middleware** (not the raw Worker handler), there is no `defineWorkerFetch`; you still pass `waitUntil` from the adapter-exposed context.

The exact way to read `ctx` from Astro middleware depends on your adapter version — check the [Cloudflare adapter docs](https://docs.astro.build/en/guides/integrations-guide/cloudflare/).
::

## Quick Start

### 1. Install
Expand Down
30 changes: 13 additions & 17 deletions examples/workers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'

initWorkersLogger({
env: { service: 'workers-example' },
})

export default {
async fetch(request: Request) {
const log = createWorkersLogger(request)

try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
},
}
export default defineWorkerFetch(async (request, _env, _ctx, log) => {
try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
})
60 changes: 45 additions & 15 deletions packages/evlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,30 +336,40 @@ async function processSyncJob(job: Job) {

## Cloudflare Workers

Use the Workers adapter for structured logs and correct platform severity.
Use the Workers adapter for structured logs and correct platform severity. With `initWorkersLogger({ drain })`, use **`defineWorkerFetch`** so async drains are registered with `waitUntil` automatically (Cloudflare only passes `ExecutionContext` as the third `fetch` argument — there is no global).

```typescript
// src/index.ts
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'

initWorkersLogger({
env: { service: 'edge-api' },
})

export default defineWorkerFetch(async (request, _env, _ctx, log) => {
try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
})
```

If you keep a raw `export default { fetch }`, pass `{ executionCtx: ctx }` to `createWorkersLogger` or `waitUntil` on `createRequestLogger`.

```typescript
// Lower-level (equivalent)
import { createWorkersLogger } from 'evlog/workers'

export default {
async fetch(request: Request) {
const log = createWorkersLogger(request)

try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
async fetch(request: Request, _env: unknown, ctx: ExecutionContext) {
const log = createWorkersLogger(request, { executionCtx: ctx })
// ...
},
}
```
Expand All @@ -373,6 +383,7 @@ invocation_logs = false
```

Notes:
- Prefer **`defineWorkerFetch`** so you do not have to pass `executionCtx` yourself when using a drain
- `requestId` defaults to `cf-ray` when available
- `request.cf` is included (colo, country, asn) unless disabled
- Use `headerAllowlist` to avoid logging sensitive headers
Expand Down Expand Up @@ -1185,18 +1196,37 @@ initWorkersLogger({
})
```

### `defineWorkerFetch(handler)`

Recommended for Workers when using **`initWorkersLogger({ drain })`**. Wraps your handler so `createWorkersLogger` always receives `executionCtx` — you do not pass `ctx` into the factory yourself. Cloudflare does not expose `ExecutionContext` globally (only as `fetch`’s third argument), so this is the “automatic” option for plain Workers scripts.

```typescript
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'

initWorkersLogger({ env: { service: 'edge-api' }, drain })

export default defineWorkerFetch(async (request, env, ctx, log) => {
log.emit({ status: 200 })
return new Response('ok')
})
```

### `createWorkersLogger(request, options?)`

Create a request-scoped logger for Workers. Auto-extracts `cf-ray`, `request.cf`, method, and path.

```typescript
import { createWorkersLogger } from 'evlog/workers'

// ctx is the third argument to fetch(request, env, ctx)
const log = createWorkersLogger(request, {
requestId: 'custom-id', // Override cf-ray (default: cf-ray header)
headers: ['x-request-id'], // Headers to include (default: none)
executionCtx: ctx, // With initWorkersLogger({ drain }), registers async drain via waitUntil
})

// Or pass waitUntil directly: waitUntil: ctx.waitUntil.bind(ctx)

log.set({ user: { id: '123' } })
log.emit({ status: 200 })
```
Expand Down
Loading
Loading