evlog
Wide-event structured logging with per-framework wire-up, middleware integration, and structured error handling.
What create-faster adds
The setup varies by stack. Each gets the idiomatic evlog integration for its framework.
Next.js
Files added:
src/
├── lib/
│ └── evlog.ts # createEvlog + createInstrumentation
└── instrumentation.ts # defineNodeInstrumentation (Node runtime gate)Modified files:
src/proxy.ts- Returns viaevlogMiddleware()for request-id and timing correlation
evlog.ts
Factory exports for request-scoped logging and server instrumentation:
import { createEvlog } from 'evlog/next';
import { createInstrumentation } from 'evlog/next/instrumentation/create';
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'my-app',
});
export const { register, onRequestError } = createInstrumentation({
service: 'my-app',
});withEvlog()wraps route handlers to emit wide events on request completionuseLogger()provides request-scopedlog.set()for progressive contextcreateError()throws structured errors withwhy,fix,linkfields
instrumentation.ts
Gates evlog initialization to the Node.js runtime so Edge bundles stay clean:
import { defineNodeInstrumentation } from 'evlog/next/instrumentation';
export const { register, onRequestError } = defineNodeInstrumentation(() => import('./lib/evlog'));proxy.ts
When evlog is selected, proxy.ts returns via evlogMiddleware() which sets x-request-id and x-evlog-start headers for timing correlation across the middleware-to-handler chain. Composes cleanly with Better Auth session resolution.
Hono
Modified files:
src/app.ts- AddsinitLogger,evlog()middleware, typed variables, and structured error handler
import { initLogger, parseError } from 'evlog';
import { evlog, type EvlogVariables } from 'evlog/hono';
initLogger({
env: { service: 'my-api' },
});
const app = new Hono<EvlogVariables>();
app.use(evlog());Replaces the default hono/logger middleware. The EvlogVariables type gives typed access to c.get('log') in route handlers.
The error handler uses parseError() for structured JSON responses with why and fix fields.
TanStack Start
The wiring depends on the runtime. On Nitro (the default) the evlog/nitro/v3 module emits the wide event. On Cloudflare Workers there is no Nitro, so a custom Worker entry wraps Start's fetch with withEvlog. Both runtimes share one project-local useLogger(), and the same error middleware. Under cloudflare-static nothing runs per request, so none of these files is generated.
Files added:
src/lib/evlog.ts # useLogger(), the request logger of the current request
src/start.ts # global request and server function middleware that log errors
nitro.config.ts # Nitro: evlog/nitro/v3 module registration
src/server.ts # Cloudflare: Worker entry (always generated), wrapped with withEvlogsrc/lib/evlog.ts
Exports useLogger(), which returns the logger of the current request in route middleware, server routes, server functions and your own code (it throws outside a request). On Workers it is backed by AsyncLocalStorage (the nodejs_compat flag is already set), like useLogger() in Next.js. On Nitro it reads the logger the evlog module attaches to the request (useRequest().context.log from nitro/context).
import { useLogger } from '@/lib/evlog'
useLogger().set({ user: { plan: 'pro' } })It also exports findLogger(), which returns the logger or undefined instead of throwing. The error middlewares use it so that a missing logger never replaces the error being handled.
src/start.ts
Start's createStart instance (generated when a library contributes to it). Start only applies its default server function CSRF protection when this file is absent, so the file always registers createCsrfMiddleware first in requestMiddleware. evlog adds two global middlewares after it:
requestMiddlewarerecords any error thrown while handling a request on the wide event, and returns anEvlogErroras a JSON response that keeps its structureddatafields (why,fix,link).functionMiddlewarerecords errors thrown by server functions, both when a loader calls them during SSR and over RPC.
An error thrown directly inside a route loader, without going through a server function, is not caught by any middleware: the event carries the 500 status but no error. Per-route onError is the only server-side hook for it.
nitro.config.ts
Nitro only. Registers the evlog Nitro v3 module with async context for request-scoped logging. nitro itself comes with the TanStack Start stack, whose nitro() Vite plugin loads this file when building the production server:
import { defineConfig } from 'nitro';
import evlog from 'evlog/nitro/v3';
export default defineConfig({
experimental: {
asyncContext: true,
},
modules: [
evlog({
env: { service: 'my-app' },
}),
],
});Not generated under the cloudflare and cloudflare-static deployments, which build without Nitro.
src/server.ts
Cloudflare only. The Worker entry exists with or without evlog (wrangler.jsonc main points to it); with evlog it wraps Start's fetch with withEvlog, which emits one wide event per request (with cfRay, colo, country and asn) and flushes drains through ctx.waitUntil. The logger is passed to Start as context.log and bound to useLogger():
import handler from '@tanstack/react-start/server-entry'
import { initWorkersLogger, withEvlog } from 'evlog/workers'
import { loggerStorage } from '@/lib/evlog'
initWorkersLogger({ env: { service: 'my-app' } })
export default withEvlog((request, _env, _ctx, log) =>
loggerStorage.run(log, () => handler.fetch(request, { context: { log } })),
)Node
Modified files:
src/index.ts- AddsinitLoggerwith service name
import { initLogger, log } from 'evlog';
initLogger({
env: { service: 'my-worker' },
});
log.info({ action: 'startup' });For standalone scripts and workers, use createLogger() and createRequestLogger() from evlog for manual wide events with log.set() / log.emit().

