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
30 changes: 30 additions & 0 deletions apps/docs/content/2.logging/2.wide-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,36 @@ export default defineEventHandler(async (event) => {
`useLogger` doesn't create a logger, it retrieves the one the framework middleware already attached to the event. The middleware handles creation and emission automatically. In Nuxt, `useLogger` is auto-imported.
::

## After emit: sealing and background work

When the wide event is **emitted** (automatically at the end of the request, or when you call `log.emit()` yourself), that logger instance is **sealed**. Further `set`, `error`, `info`, and `warn` calls do **not** update the event that was already sent to your drains. They are ignored and evlog prints a **`[evlog]` warning** to the console with the keys that were dropped. This also applies when **head sampling** discards the event (`emit()` returned `null`): the logger is still sealed for that unit of work.

This matters for **async work that outlives the handler** (fire-and-forget promises, `setTimeout`, tasks started but not awaited). On many runtimes, `AsyncLocalStorage` keeps returning the same request logger, so `useLogger()` still succeeds even though the HTTP response — and the wide event — are already finished. Without warnings, that looks like silent data loss.

### `log.fork(label, fn)`

For intentional background work that should produce **its own** wide event, use **`log.fork(label, fn)`** when your integration provides it (Express, Fastify, NestJS, SvelteKit, React Router, Next.js `withEvlog`, Elysia). Inside `fn`, `useLogger()` resolves to a **child** logger. When `fn` completes (or throws), the child emits an event with:

- **`operation`**: the `label` you passed
- **`_parentRequestId`**: the parent request’s `requestId` (for correlation in queries and dashboards)

The parent wide event may be emitted **before** the child event; they are two separate events ordered by time.

**Not available yet:** Hono (no `useLogger` without `c.get('log')` + ALS) and Nitro/Nuxt `useLogger(event)` — use the post-emit warnings to catch mistakes; a different API may arrive later for event-scoped forks.

```typescript [server/routes/checkout.post.ts]
import { evlog, useLogger } from 'evlog/express'

// Inside a route after evlog middleware:
const log = req.log
log.set({ order_dispatched: true })

log.fork?.('process_order', async () => {
const child = useLogger()
child.set({ inventory_checked: true })
})
```

## Anatomy of a Wide Event

A well-designed wide event contains context from multiple layers. The examples below show what to add inside your handler or script. They assume `log` is already created via `createLogger`, `createRequestLogger`, or `useLogger`.
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/3.core-concepts/0.lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ evlog events follow a pipeline from creation to delivery. The pipeline differs s
| **Enrich** | Via global drain | Via global drain | Via hooks or callbacks |
| **Drain** | Via global drain | Via global drain | Via hooks or callbacks |

After **`emit`** (including when sampling returns no output), the request logger is **sealed**: later `set` / `error` / `info` / `warn` calls are ignored with a console warning. For background work that needs its own event, use **`log.fork()`** where your integration supports it. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

## Request Logging Pipeline

For framework-managed request logging, every request follows this pipeline. The middleware creates the logger and `useLogger(event)` retrieves it:
Expand Down
17 changes: 17 additions & 0 deletions apps/docs/content/4.frameworks/02.nextjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,23 @@ All fields are merged into a single wide event emitted when the handler complete
└─ requestId: a1b2c3d4-...
```

## Background work (`log.fork`)

Inside `withEvlog`, `useLogger()` returns a logger with **`fork`** for child wide events. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

```typescript [app/api/orders/route.ts]
import { withEvlog, useLogger } from '@/lib/evlog'

export const POST = withEvlog(async () => {
const log = useLogger()
log.fork!('enqueue', async () => {
const child = useLogger()
child.set({ job: 'queued' })
})
return Response.json({ ok: true })
})
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields that help developers debug in both logs and API responses:
Expand Down
17 changes: 17 additions & 0 deletions apps/docs/content/4.frameworks/03.sveltekit.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ export const GET: RequestHandler = async ({ params }) => {

Both `event.locals.log` and `useLogger()` return the same logger instance. `useLogger()` uses `AsyncLocalStorage` to propagate the logger across async boundaries.

## Background work (`log.fork`)

Use `locals.log.fork(label, fn)` for a child wide event. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

```typescript [src/routes/api/orders/+server.ts]
import { useLogger } from 'evlog/sveltekit'
import type { RequestHandler } from './$types'

export const POST: RequestHandler = async ({ locals }) => {
locals.log.fork!('process', async () => {
const log = useLogger()
log.set({ step: 'done' })
})
return new Response(JSON.stringify({ ok: true }))
}
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields. The `handleError` hook captures thrown errors automatically:
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/4.frameworks/04.nitro.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ One request, one log line with all context:
└─ requestId: a1b2c3d4-...
```

Nitro uses **`useLogger(event)`** (event-bound scope), not `AsyncLocalStorage`, so **`log.fork()` is not available** here yet. Post-emit warnings still apply if code calls `set()` after the wide event was emitted. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

## Error Handling

`createError` produces structured errors with `why`, `fix`, and `link` fields that help both humans and AI agents understand what went wrong.
Expand Down
17 changes: 17 additions & 0 deletions apps/docs/content/4.frameworks/06.nestjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@ export class UsersController {

Both `req.log` and `useLogger()` return the same logger instance. `useLogger()` uses `AsyncLocalStorage` to propagate the logger across async boundaries.

## Background work (`log.fork`)

Use `req.log.fork(label, fn)` (or the logger from `useLogger()` in the same request) for child wide events. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

```typescript [src/orders.controller.ts]
import { useLogger } from 'evlog/nestjs'

@Post()
create(@Req() req: Express.Request) {
req.log.fork!('enqueue', async () => {
const log = useLogger()
log.set({ queued: true })
})
return { ok: true }
}
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields. Create a NestJS exception filter to log and format errors:
Expand Down
19 changes: 19 additions & 0 deletions apps/docs/content/4.frameworks/07.express.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ app.get('/users/:id', async (req, res) => {

Both `req.log` and `useLogger()` return the same logger instance. `useLogger()` uses `AsyncLocalStorage` to propagate the logger across async boundaries.

## Background work (`log.fork`)

Fire-and-forget async work that finishes **after** the response can no longer update the request wide event (the logger is sealed after emit). Use **`req.log.fork(label, fn)`** so `useLogger()` inside `fn` targets a **child** logger that emits its own event with `operation` and `_parentRequestId`. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

```typescript [src/index.ts]
import { evlog, useLogger } from 'evlog/express'

app.use(evlog())

app.post('/orders', (req, res) => {
req.log.set({ orderId: 'ord_1' })
req.log.fork!('fulfill_order', async () => {
const log = useLogger()
log.set({ step: 'inventory_ok' })
})
res.json({ ok: true })
})
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields. Express uses a 4-argument error handler middleware:
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/4.frameworks/08.hono.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ All fields are merged into a single wide event emitted when the request complete
└─ requestId: 4a8ff3a8-...
```

Hono does not attach **`log.fork()`** yet (access the logger via `c.get('log')` only). If you schedule async work after the response, post-emit **`[evlog]` warnings** still help you notice stale `set()` calls. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields:
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/content/4.frameworks/09.fastify.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ app.get('/users/:id', async (request) => {

Both `request.log` and `useLogger()` return the same logger instance. `useLogger()` uses `AsyncLocalStorage` to propagate the logger across async boundaries.

## Background work (`log.fork`)

Use `request.log.fork(label, fn)` for async work that should emit a **separate** child wide event after the response. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

```typescript [src/index.ts]
import { evlog, useLogger } from 'evlog/fastify'

app.post('/orders', async (request, reply) => {
request.log.fork!('fulfill', async () => {
const log = useLogger()
log.set({ step: 'ok' })
})
return { ok: true }
})
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields. Fastify captures thrown errors via `onError`:
Expand Down
18 changes: 18 additions & 0 deletions apps/docs/content/4.frameworks/10.elysia.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ app.get('/users/:id', async ({ params }) => {

Both `log` in context and `useLogger()` return the same logger instance. `useLogger()` uses `AsyncLocalStorage` to propagate the logger across async boundaries.

## Background work (`log.fork`)

Use `log.fork(label, fn)` from the route context for a child wide event. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

```typescript [src/index.ts]
import { evlog, useLogger } from 'evlog/elysia'

app
.use(evlog())
.post('/orders', ({ log }) => {
log.fork!('ship', async () => {
const l = useLogger()
l.set({ shipped: true })
})
return { ok: true }
})
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields. Elysia captures thrown errors via `onError`:
Expand Down
19 changes: 19 additions & 0 deletions apps/docs/content/4.frameworks/11.react-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,25 @@ export async function loader({ params, context }: Route.LoaderArgs) {
}
```

## Background work (`log.fork`)

The logger from `loggerContext` supports `fork` for child wide events. See [Wide events — After emit](/logging/wide-events#after-emit-sealing-and-background-work).

```typescript [app/routes/orders.tsx]
import { loggerContext } from 'evlog/react-router'
import { useLogger } from 'evlog/react-router'
import type { Route } from './+types/orders'

export async function action({ context }: Route.ActionArgs) {
const log = context.get(loggerContext)
log.fork!('background', async () => {
const child = useLogger()
child.set({ step: 'complete' })
})
return { ok: true }
}
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields:
Expand Down
34 changes: 34 additions & 0 deletions packages/evlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,40 @@ log.emit() // Emit final event
log.getContext() // Get current context
```

### Wide event lifecycle and `log.fork()`

The framework emits **one wide event per HTTP request** when the response finishes (or on error). After `emit()` runs — including when head sampling drops the event (`emit()` returns `null`) — that logger instance is **sealed**: further `set`, `error`, `info`, and `warn` calls are ignored and emit a **`[evlog]` console warning** listing dropped keys. A second `emit()` is ignored with a warning. This avoids silent data loss when async work (unawaited promises, `setTimeout`, etc.) still resolves `useLogger()` to the same logger via `AsyncLocalStorage` after the response has already been logged.

**`log.fork(label, fn)`** runs work under a **child** request logger: inside `fn`, `useLogger()` returns the child. When `fn` settles, the child emits its **own** wide event with `operation` set to `label` and `_parentRequestId` set to the parent’s `requestId` (query and dashboard correlation). The parent event may be emitted **before** the child event; they are two separate events ordered by time.

`fork` is attached by integrations that use `AsyncLocalStorage` for `useLogger()`. Standalone `createLogger()` instances do not have `fork`.

| Integration | `log.fork()` |
|-------------|----------------|
| Express, Fastify, NestJS, SvelteKit, React Router, Elysia | Yes |
| Next.js `withEvlog` | Yes |
| Hono (`c.get('log')` only) | Not yet |
| Nitro / Nuxt `useLogger(event)` | Not yet — use post-emit warnings; see [Wide events](https://evlog.dev/logging/wide-events) |

```typescript
import { evlog, useLogger } from 'evlog/express'

app.post('/checkout', (req, res) => {
const log = req.log
log.set({ order_dispatched: true })

log.fork!('process_order', async () => {
const childLog = useLogger()
childLog.set({ inventory_checked: true })
// child emits automatically when this async function completes
})

res.json({ ok: true })
})
```

Use optional chaining if `fork` might be absent: `log.fork?.('task', async () => { ... })`.

### `initWorkersLogger(options?)`

Initialize evlog for Cloudflare Workers (object logs + correct severity).
Expand Down
18 changes: 15 additions & 3 deletions packages/evlog/src/elysia/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'
import { Elysia } from 'elysia'
import type { RequestLogger } from '../types'
import { createMiddlewareLogger, type BaseEvlogOptions } from '../shared/middleware'
import { attachForkToLogger } from '../shared/fork'
import { extractSafeHeaders } from '../shared/headers'
import { filterSafeHeaders } from '../utils'

Expand Down Expand Up @@ -79,17 +80,28 @@ export function evlog(options: EvlogElysiaOptions = {}) {

return new Elysia({ name: 'evlog' })
.derive({ as: 'global' }, ({ request, path, headers }) => {
const { logger, finish, skipped } = createMiddlewareLogger({
const middlewareOpts = {
method: request.method,
path,
requestId: headers['x-request-id'] || crypto.randomUUID(),
// It's recommended to use context.headers instead of context.request.headers
// because Elysia has fast path for getting headers on Bun
headers: filterSafeHeaders(headers as Record<string, string>),
...options,
})
}
const { logger, finish, skipped } = createMiddlewareLogger(middlewareOpts)

if (!skipped) activeLoggers.add(logger)
if (!skipped) {
attachForkToLogger(storage, logger, middlewareOpts, {
onChildEnter: (child) => {
activeLoggers.add(child)
},
onChildExit: (child) => {
activeLoggers.delete(child)
},
})
activeLoggers.add(logger)
}
storage.enterWith(logger)
requestState.set(request, { finish, skipped, logger })

Expand Down
7 changes: 5 additions & 2 deletions packages/evlog/src/express/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Request, Response, NextFunction, RequestHandler } from 'express'
import type { RequestLogger } from '../types'
import { createMiddlewareLogger, type BaseEvlogOptions } from '../shared/middleware'
import { attachForkToLogger } from '../shared/fork'
import { extractSafeNodeHeaders } from '../shared/headers'
import { createLoggerStorage } from '../shared/storage'

Expand Down Expand Up @@ -38,19 +39,21 @@ declare module 'express-serve-static-core' {
*/
export function evlog(options: EvlogExpressOptions = {}): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {
const { logger, finish, skipped } = createMiddlewareLogger({
const middlewareOpts = {
method: req.method,
path: new URL(req.originalUrl || req.url || '/', 'http://localhost').pathname,
requestId: req.get('x-request-id') || crypto.randomUUID(),
headers: extractSafeNodeHeaders(req.headers),
...options,
})
}
const { logger, finish, skipped } = createMiddlewareLogger(middlewareOpts)

if (skipped) {
next()
return
}

attachForkToLogger(storage, logger, middlewareOpts)
req.log = logger

res.on('finish', () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/evlog/src/fastify/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FastifyPluginCallback } from 'fastify'
import { createMiddlewareLogger, type BaseEvlogOptions } from '../shared/middleware'
import { attachForkToLogger } from '../shared/fork'
import { extractSafeNodeHeaders } from '../shared/headers'
import { createLoggerStorage } from '../shared/storage'

Expand Down Expand Up @@ -30,19 +31,22 @@ const evlogPlugin: FastifyPluginCallback<EvlogFastifyOptions> = (fastify, option
const headers = extractSafeNodeHeaders(request.headers)
const path = new URL(request.url, 'http://localhost').pathname

const { logger, finish, skipped } = createMiddlewareLogger({
const middlewareOpts = {
method: request.method,
path,
requestId: headers['x-request-id'] || crypto.randomUUID(),
headers,
...options,
})
}
const { logger, finish, skipped } = createMiddlewareLogger(middlewareOpts)

if (skipped) {
done()
return
}

attachForkToLogger(storage, logger, middlewareOpts)

// Shadow Fastify's built-in pino logger with evlog's request-scoped logger
const req = request as any
req.log = logger
Expand Down
Loading
Loading