evlog

Your logs are lying to you.
A single request generates 10+ log lines. When production breaks at 3am, you're grep-ing through noise, praying you'll find signal. Your errors say "Something went wrong" -- thanks, very helpful.
evlog fixes this. One log per request. All context included. Errors that explain themselves.
Why evlog?
The Problem
console.log('Request received')
console.log('User:', user.id)
console.log('Cart loaded')
console.log('Payment failed')
throw new Error('Something went wrong')
The Solution
import { useLogger } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
log.set({ user: { id: user.id, plan: 'premium' } })
log.set({ cart: { items: 3, total: 9999 } })
log.error(error, { step: 'payment' })
})
Output:
{
"timestamp": "2025-01-24T10:23:45.612Z",
"level": "error",
"service": "my-app",
"method": "POST",
"path": "/api/checkout",
"duration": "1.2s",
"user": { "id": "123", "plan": "premium" },
"cart": { "items": 3, "total": 9999 },
"error": { "message": "Card declined", "step": "payment" }
}
Built for AI-Assisted Development
We're in the age of AI agents writing and debugging code. When an agent encounters an error, it needs clear, structured context to understand what happened and how to fix it.
Traditional logs force agents to grep through noise. evlog gives them:
- One event per request with all context in one place
- Self-documenting errors with
why and fix fields
- Structured JSON that's easy to parse and reason about
Your AI copilot will thank you.
Installation
npm install evlog
Nuxt Integration
The recommended way to use evlog. Zero config, everything just works.
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: {
service: 'my-app',
},
include: ['/api/**'],
},
})
Tip: Use $production to enable sampling only in production:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: { env: { service: 'my-app' } },
$production: {
evlog: { sampling: { rates: { info: 10, warn: 50, debug: 0 } } },
},
})
That's it. Now use useLogger(event) in any API route:
import { useLogger, createError } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const user = await requireAuth(event)
log.set({ user: { id: user.id, plan: user.plan } })
const cart = await getCart(user.id)
log.set({ cart: { items: cart.items.length, total: cart.total } })
try {
const payment = await processPayment(cart, user)
log.set({ payment: { id: payment.id, method: payment.method } })
} catch (error) {
log.error(error, { step: 'payment' })
throw createError({
message: 'Payment failed',
status: 402,
why: error.message,
fix: 'Try a different payment method or contact your bank',
})
}
const order = await createOrder(cart, user)
log.set({ order: { id: order.id, status: order.status } })
return order
})
The wide event emitted at the end contains everything:
{
"timestamp": "2026-01-24T10:23:45.612Z",
"level": "info",
"service": "my-app",
"method": "POST",
"path": "/api/checkout",
"duration": "1.2s",
"user": { "id": "user_123", "plan": "premium" },
"cart": { "items": 3, "total": 9999 },
"payment": { "id": "pay_xyz", "method": "card" },
"order": { "id": "order_abc", "status": "created" },
"status": 200
}
Nitro Integration
Works with any framework powered by Nitro: Nuxt, Analog, Vinxi, SolidStart, TanStack Start, and more.
Nitro v3
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
modules: [
evlog({ env: { service: 'my-api' } })
],
})
Nitro v2
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [
evlog({ env: { service: 'my-api' } })
],
})
Then use useLogger in any route. Import from evlog/nitro/v3 (v3) or evlog/nitro (v2):
import { defineEventHandler } from 'h3'
import { useLogger } from 'evlog/nitro'
import { createError } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const documentId = getRouterParam(event, 'id')
log.set({ document: { id: documentId } })
const body = await readBody(event)
log.set({ export: { format: body.format, includeComments: body.includeComments } })
const document = await db.documents.findUnique({ where: { id: documentId } })
if (!document) {
throw createError({
message: 'Document not found',
status: 404,
why: `No document with ID "${documentId}" exists`,
fix: 'Check the document ID and try again',
})
}
log.set({ document: { id: documentId, title: document.title, pages: document.pages.length } })
try {
const exportResult = await generateExport(document, body.format)
log.set({ export: { format: body.format, size: exportResult.size, pages: exportResult.pages } })
return { url: exportResult.url, expiresAt: exportResult.expiresAt }
} catch (error) {
log.error(error, { step: 'export-generation' })
throw createError({
message: 'Export failed',
status: 500,
why: `Failed to generate ${body.format} export: ${error.message}`,
fix: 'Try a different format or contact support',
})
}
})
Output when the export completes:
{
"timestamp": "2025-01-24T14:32:10.123Z",
"level": "info",
"service": "document-api",
"method": "POST",
"path": "/api/documents/doc_123/export",
"duration": "2.4s",
"document": { "id": "doc_123", "title": "Q4 Report", "pages": 24 },
"export": { "format": "pdf", "size": 1240000, "pages": 24 },
"status": 200
}
Standalone TypeScript
For scripts, workers, or any TypeScript project:
import { initLogger, log, createRequestLogger } from 'evlog'
initLogger({
env: {
service: 'migration-script',
environment: 'production',
},
})
log.info('migration', 'Starting database migration')
log.info({ action: 'migration', tables: ['users', 'orders'] })
const migrationLog = createRequestLogger({ action: 'full-migration' })
migrationLog.set({ tables: ['users', 'orders', 'products'] })
migrationLog.set({ rowsProcessed: 15000 })
migrationLog.emit()
import { initLogger, createRequestLogger, createError } from 'evlog'
initLogger({
env: {
service: 'sync-worker',
environment: process.env.NODE_ENV,
},
})
async function processSyncJob(job: Job) {
const log = createRequestLogger({ jobId: job.id, type: 'sync' })
try {
log.set({ source: job.source, target: job.target })
const result = await performSync(job)
log.set({ recordsSynced: result.count })
return result
} catch (error) {
log.error(error, { step: 'sync' })
throw error
} finally {
log.emit()
}
}
Cloudflare Workers
Use the Workers adapter for structured logs and correct platform severity.
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
initWorkersLogger({
env: { service: 'edge-api' },
})
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
}
},
}
Disable invocation logs to avoid duplicate request logs:
[observability.logs]
invocation_logs = false
Notes:
requestId defaults to cf-ray when available
request.cf is included (colo, country, asn) unless disabled
- Use
headerAllowlist to avoid logging sensitive headers
Hono
import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
initLogger({ env: { service: 'hono-api' } })
const app = new Hono<EvlogVariables>()
app.use(evlog())
app.get('/api/users', (c) => {
const log = c.get('log')
log.set({ users: { count: 42 } })
return c.json({ users: [] })
})
See the full hono example for a complete working project.
Express
import express from 'express'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/express'
initLogger({ env: { service: 'express-api' } })
const app = express()
app.use(evlog())
app.get('/api/users', (req, res) => {
req.log.set({ users: { count: 42 } })
res.json({ users: [] })
})
Use useLogger() to access the logger from anywhere in the call stack without passing req.
See the full express example for a complete working project.
Fastify
import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/fastify'
initLogger({ env: { service: 'fastify-api' } })
const app = Fastify({ logger: false })
await app.register(evlog)
app.get('/api/users', async (request) => {
request.log.set({ users: { count: 42 } })
return { users: [] }
})
request.log is the evlog wide-event logger (shadows Fastify's built-in pino logger on the request). Use useLogger() to access the logger from anywhere in the call stack.
See the full fastify example for a complete working project.
Elysia
import { Elysia } from 'elysia'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/elysia'
initLogger({ env: { service: 'elysia-api' } })
const app = new Elysia()
.use(evlog())
.get('/api/users', ({ log }) => {
log.set({ users: { count: 42 } })
return { users: [] }
})
.listen(3000)
Use useLogger() to access the logger from anywhere in the call stack.
See the full elysia example for a complete working project.
React Router
import { initLogger } from 'evlog'
import { evlog, loggerContext } from 'evlog/react-router'
initLogger({ env: { service: 'react-router-api' } })
export const middleware: Route.MiddlewareFunction[] = [
evlog(),
]
import { loggerContext } from 'evlog/react-router'
export async function loader({ params, context }: Route.LoaderArgs) {
const log = context.get(loggerContext)
log.set({ users: { count: 42 } })
return { users: [] }
}
Use context.get(loggerContext) in loaders/actions, or useLogger() from anywhere in the call stack. Requires v8_middleware: true in react-router.config.ts.
See the full react-router example for a complete working project.
NestJS
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
@Module({
imports: [EvlogModule.forRoot()],
})
export class AppModule {}
import { useLogger } from 'evlog/nestjs'
const log = useLogger()
log.set({ users: { count: 42 } })
EvlogModule.forRoot() registers a global middleware that creates a request-scoped logger for every request. Use useLogger() to access it anywhere in the call stack, or req.log directly. Supports forRootAsync() for async configuration.
See the full nestjs example for a complete working project.
Browser
Use the log API on the client side for structured browser logging:
import { log } from 'evlog/browser'
log.info('checkout', 'User initiated checkout')
log.error({ action: 'payment', error: 'validation_failed' })
In Nuxt, log is auto-imported -- no import needed in Vue components:
<script setup>
log.info('checkout', 'User initiated checkout')
</script>
Client logs output to the browser console with colored tags in development.
Client Transport
To send client logs to the server for centralized logging, enable the transport:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
transport: {
enabled: true,
},
},
})
When enabled:
- Client logs are sent to
/api/_evlog/ingest via POST
- Server enriches with environment context (service, version, etc.)
evlog:drain hook is called with source: 'client'
- External services receive the log
Structured Errors
Errors should tell you what happened, why, and how to fix it.
import { useLogger, createError } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
log.set({ repo: { owner: 'acme', name: 'my-project' } })
try {
const result = await syncWithGitHub()
log.set({ sync: { commits: result.commits, files: result.files } })
return result
} catch (error) {
log.error(error, { step: 'github-sync' })
throw createError({
message: 'Failed to sync repository',
status: 503,
why: 'GitHub API rate limit exceeded',
fix: 'Wait 1 hour or use a different token',
link: 'https://docs.github.com/en/rest/rate-limit',
cause: error,
})
}
})
Console output (development):
Error: Failed to sync repository
Why: GitHub API rate limit exceeded
Fix: Wait 1 hour or use a different token
More info: https://docs.github.com/en/rest/rate-limit
Enrichment Hook
Use the evlog:enrich hook to add derived context after emit, before drain.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
ctx.event.deploymentId = process.env.DEPLOYMENT_ID
})
})
Built-in Enrichers
import {
createGeoEnricher,
createRequestSizeEnricher,
createTraceContextEnricher,
createUserAgentEnricher,
} from 'evlog/enrichers'
export default defineNitroPlugin((nitroApp) => {
const enrich = [
createUserAgentEnricher(),
createGeoEnricher(),
createRequestSizeEnricher(),
createTraceContextEnricher(),
]
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
for (const enricher of enrich) enricher(ctx)
})
})
Each enricher adds a specific field to the event:
createUserAgentEnricher() | event.userAgent | { raw, browser?: { name, version? }, os?: { name, version? }, device?: { type } } |
createGeoEnricher() | event.geo | { country?, region?, regionCode?, city?, latitude?, longitude? } |
createRequestSizeEnricher() | event.requestSize | { requestBytes?, responseBytes? } |
createTraceContextEnricher() | event.traceContext + event.traceId + event.spanId | { traceparent?, tracestate?, traceId?, spanId? } |
All enrichers accept an optional { overwrite?: boolean } option. By default (overwrite: false), user-provided data on the event takes precedence over enricher-computed values. Set overwrite: true to always replace existing fields.
Cloudflare geo note: Only cf-ipcountry is a real Cloudflare HTTP header. The cf-region, cf-city, cf-latitude, cf-longitude headers are NOT standard -- they are properties of request.cf. For full geo data on Cloudflare, write a custom enricher that reads request.cf, or use a Workers middleware to forward cf properties as custom headers.
Custom Enrichers
The evlog:enrich hook receives an EnrichContext with these fields:
interface EnrichContext {
event: WideEvent
request?: {
method?: string
path?: string
requestId?: string
}
headers?: Record<string, string>
response?: {
status?: number
headers?: Record<string, string>
}
}
Example custom enricher:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
ctx.event.deploymentId = process.env.DEPLOYMENT_ID
ctx.event.region = process.env.FLY_REGION
const tenantId = ctx.headers?.['x-tenant-id']
if (tenantId) {
ctx.event.tenantId = tenantId
}
})
})
AI SDK Integration
Capture token usage, tool calls, model info, and streaming metrics from the Vercel AI SDK into wide events. Requires ai >= 6.0.0.
import { streamText } from 'ai'
import { createAILogger } from 'evlog/ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const ai = createAILogger(log)
const result = streamText({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
messages,
onFinish: ({ text }) => saveConversation(text),
})
return result.toTextStreamResponse()
})
The middleware captures: inputTokens, outputTokens, cacheReadTokens, reasoningTokens, model, provider, finishReason, toolCalls, steps, msToFirstChunk, msToFinish, tokensPerSecond.
For embeddings: ai.captureEmbed({ usage }).
Adapters
Send your logs to external observability platforms with built-in adapters.
Axiom
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
Set environment variables:
NUXT_AXIOM_TOKEN=xaat-your-token
NUXT_AXIOM_DATASET=your-dataset
OTLP (OpenTelemetry)
Works with Grafana, Datadog, Honeycomb, and any OTLP-compatible backend.
import { createOTLPDrain } from 'evlog/otlp'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
})
Set environment variables:
NUXT_OTLP_ENDPOINT=http://localhost:4318
Datadog
import { createDatadogDrain } from 'evlog/datadog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createDatadogDrain())
})
Set environment variables:
NUXT_DATADOG_API_KEY=your-api-key
NUXT_DATADOG_SITE=datadoghq.eu
You can also use standard Datadog names: DD_API_KEY and DD_SITE.
Wide events are sent with a short message line (method, path, level) and full context under the evlog attribute (facets like @evlog.path). See the Datadog adapter docs.
PostHog
import { createPostHogDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
})
Set environment variables:
NUXT_POSTHOG_API_KEY=phc_your-key
NUXT_POSTHOG_HOST=https://us.i.posthog.com
Sentry
import { createSentryDrain } from 'evlog/sentry'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createSentryDrain())
})
Set environment variables:
NUXT_SENTRY_DSN=https://public@o0.ingest.sentry.io/123
Better Stack
import { createBetterStackDrain } from 'evlog/better-stack'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createBetterStackDrain())
})
Set environment variables:
NUXT_BETTER_STACK_SOURCE_TOKEN=your-source-token
Multiple Destinations
Send logs to multiple services:
import { createAxiomDrain } from 'evlog/axiom'
import { createOTLPDrain } from 'evlog/otlp'
export default defineNitroPlugin((nitroApp) => {
const axiom = createAxiomDrain()
const otlp = createOTLPDrain()
nitroApp.hooks.hook('evlog:drain', async (ctx) => {
await Promise.allSettled([axiom(ctx), otlp(ctx)])
})
})
Custom Adapters
Build your own adapter for any destination:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', async (ctx) => {
await fetch('https://your-service.com/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ctx.event),
})
})
})
See the full documentation for adapter configuration options, troubleshooting, and advanced patterns.
Drain Pipeline
For production use, wrap your drain adapter with createDrainPipeline to get batching, retry with backoff, and buffer overflow protection.
Without a pipeline, each event triggers a separate network call. The pipeline buffers events and sends them in batches, reducing overhead and handling transient failures automatically.
import type { DrainContext } from 'evlog'
import { createDrainPipeline } from 'evlog/pipeline'
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
const pipeline = createDrainPipeline<DrainContext>({
batch: { size: 50, intervalMs: 5000 },
retry: { maxAttempts: 3, backoff: 'exponential', initialDelayMs: 1000 },
onDropped: (events, error) => {
console.error(`[evlog] Dropped ${events.length} events:`, error?.message)
},
})
const drain = pipeline(createAxiomDrain())
nitroApp.hooks.hook('evlog:drain', drain)
nitroApp.hooks.hook('close', () => drain.flush())
})
How it works
- Events are buffered in memory as they arrive
- A batch is flushed when either the batch size is reached or the interval expires (whichever comes first)
- If the drain function fails, the batch is retried with the configured backoff strategy
- If all retries are exhausted,
onDropped is called with the lost events
- If the buffer exceeds
maxBufferSize, the oldest events are dropped to prevent memory leaks
Options
batch.size | 50 | Maximum events per batch |
batch.intervalMs | 5000 | Max time (ms) before flushing a partial batch |
retry.maxAttempts | 3 | Total attempts (including first) |
retry.backoff | 'exponential' | 'exponential' | 'linear' | 'fixed' |
retry.initialDelayMs | 1000 | Base delay for first retry |
retry.maxDelayMs | 30000 | Upper bound for any retry delay |
maxBufferSize | 1000 | Max buffered events before dropping oldest |
onDropped | -- | Callback when events are dropped |
Returned drain function
The function returned by pipeline(drain) is hook-compatible and exposes:
drain(ctx) -- Push a single event into the buffer
drain.flush() -- Force-flush all buffered events (call on server shutdown)
drain.pending -- Number of events currently buffered
API Reference
initLogger(config)
Initialize the logger. Required for standalone usage, automatic with Nuxt/Nitro plugins.
initLogger({
enabled: boolean
env: {
service: string
environment: string
version?: string
commitHash?: string
region?: string
},
pretty?: boolean
silent?: boolean
stringify?: boolean
include?: string[]
sampling?: {
rates?: {
info?: number
warn?: number
debug?: number
error?: number
}
keep?: Array<{
status?: number
duration?: number
path?: string
}>
}
})
Sampling
At scale, logging everything can become expensive. evlog supports two sampling strategies:
Head Sampling (rates)
Random sampling based on log level, decided before the request completes:
initLogger({
sampling: {
rates: {
info: 10,
warn: 50,
debug: 0,
},
},
})
Tail Sampling (keep)
Force-keep logs based on request outcome, evaluated after the request completes. Useful to always capture slow requests or critical paths:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
sampling: {
rates: { info: 10 },
keep: [
{ duration: 1000 },
{ status: 400 },
{ path: '/api/critical/**' },
],
},
},
})
Custom Tail Sampling Hook
For business-specific conditions (premium users, feature flags), use the evlog:emit:keep Nitro hook:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
if (ctx.context.user?.premium) {
ctx.shouldKeep = true
}
})
})
Pretty Output Format
In development, evlog uses a compact tree format:
16:45:31.060 INFO [my-app] GET /api/checkout 200 in 234ms
|- user: id=123 plan=premium
|- cart: items=3 total=9999
+- payment: id=pay_xyz method=card
In production (pretty: false), logs are emitted as JSON for machine parsing.
log
Simple logging API.
log.info('tag', 'message')
log.info({ key: 'value' })
log.error('tag', 'message')
log.warn('tag', 'message')
log.debug('tag', 'message')
createRequestLogger(options)
Create a request-scoped logger for wide events.
const log = createRequestLogger({
method: 'POST',
path: '/checkout',
requestId: 'req_123',
})
log.set({ user: { id: '123' } })
log.error(error, { step: 'x' })
log.emit()
log.getContext()
initWorkersLogger(options?)
Initialize evlog for Cloudflare Workers (object logs + correct severity).
import { initWorkersLogger } from 'evlog/workers'
initWorkersLogger({
env: { service: 'edge-api' },
})
createWorkersLogger(request, options?)
Create a request-scoped logger for Workers. Auto-extracts cf-ray, request.cf, method, and path.
import { createWorkersLogger } from 'evlog/workers'
const log = createWorkersLogger(request, {
requestId: 'custom-id',
headers: ['x-request-id'],
})
log.set({ user: { id: '123' } })
log.emit({ status: 200 })
createError(options)
Create a structured error with HTTP status support. Import from evlog directly to avoid conflicts with Nuxt/Nitro's createError.
Note: createEvlogError is also available as an auto-imported alias in Nuxt/Nitro to avoid conflicts.
import { createError } from 'evlog'
createError({
message: string
status?: number
why?: string
fix?: string
link?: string
cause?: Error
internal?: Record<string, unknown>
})
internal — Optional context for support, auditing, or debugging (IDs, gateway codes, raw diagnostics). It is stored on EvlogError and exposed as error.internal in server code. It is not included in JSON error responses, toJSON(), or parseError() results. When the error is passed to log.error() (or thrown in integrations that record errors on the wide event), internal is copied into the emitted event under error.internal.
parseError(error)
Parse a caught error into a flat structure with all evlog fields. Auto-imported in Nuxt.
import { parseError } from 'evlog'
try {
await $fetch('/api/checkout')
} catch (err) {
const error = parseError(err)
console.log(error.message)
console.log(error.status)
console.log(error.why)
console.log(error.fix)
console.log(error.link)
toast.add({
title: error.message,
description: error.why,
color: 'error',
})
}
Framework Support
| Nuxt | modules: ['evlog/nuxt'] |
| Next.js | createEvlog() factory with import { createEvlog } from 'evlog/next' (example) |
| SvelteKit | export const { handle, handleError } = createEvlogHooks() with import { createEvlogHooks } from 'evlog/sveltekit' (example) |
| Nitro v3 | modules: [evlog()] with import evlog from 'evlog/nitro/v3' |
| Nitro v2 | modules: [evlog()] with import evlog from 'evlog/nitro' |
| TanStack Start | Nitro v3 module setup (example) |
| React Router | evlog() middleware with import { evlog } from 'evlog/react-router' (example) |
| NestJS | EvlogModule.forRoot() with import { EvlogModule } from 'evlog/nestjs' (example) |
| Express | app.use(evlog()) with import { evlog } from 'evlog/express' (example) |
| Hono | app.use(evlog()) with import { evlog } from 'evlog/hono' (example) |
| Fastify | app.register(evlog) with import { evlog } from 'evlog/fastify' (example) |
| Elysia | .use(evlog()) with import { evlog } from 'evlog/elysia' (example) |
| Cloudflare Workers | Manual setup with import { initLogger, createRequestLogger } from 'evlog' (example) |
| Custom | Build your own with import { createMiddlewareLogger } from 'evlog/toolkit' (guide) |
| Analog | Nitro v2 module setup |
| Vinxi | Nitro v2 module setup |
| SolidStart | Nitro v2 module setup (example) |
Agent Skills
evlog provides Agent Skills to help AI coding assistants understand and implement proper logging patterns in your codebase.
Installation
npx skills add https://www.evlog.dev
What it does
Once installed, your AI assistant will:
- Review your logging code and suggest wide event patterns
- Help refactor scattered
console.log calls into structured events
- Guide you to use
createError() for self-documenting errors
- Ensure proper use of
useLogger(event) in Nuxt/Nitro routes
Examples
Add logging to this endpoint
Review my logging code
Help me set up logging for this service
Philosophy
Inspired by Logging Sucks by Boris Tane.
- Wide Events: One log per request with all context
- Structured Errors: Errors that explain themselves
- Request Scoping: Accumulate context, emit once
- Pretty for Dev, JSON for Prod: Human-readable locally, machine-parseable in production
License
MIT
Made by @HugoRCD