Installation
evlog supports multiple environments: Nuxt, Nitro, Cloudflare Workers, and standalone TypeScript.
Nuxt
Install evlog via your preferred package manager:
pnpm add evlog
npm install evlog
yarn add evlog
bun add evlog
Then add it to your Nuxt config using the evlog/nuxt module:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: {
service: 'my-app',
},
// Optional: only log specific routes (supports glob patterns)
include: ['/api/**'],
// Optional: exclude specific routes from logging
exclude: ['/api/_nuxt_icon/**'],
},
})
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
env.service | string | 'app' | Service name shown in logs |
env.environment | string | Auto-detected | Environment name |
include | string[] | undefined | Route patterns to log. Supports glob (/api/**). If not set, all routes are logged |
exclude | string[] | undefined | Route patterns to exclude from logging. Supports glob (/api/_nuxt_icon/**). Exclusions take precedence over inclusions |
pretty | boolean | true in dev | Pretty print with tree formatting |
sampling.rates | object | undefined | Head sampling rates per log level (0-100%). See Sampling |
sampling.keep | array | undefined | Tail sampling conditions to force-keep logs. See Sampling |
transport.enabled | boolean | false | Enable sending client logs to the server. See Client Transport |
transport.endpoint | string | '/api/_evlog/ingest' | API endpoint for client log ingestion |
Route Filtering
Use include and exclude to control which routes are logged. Both support glob patterns.
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
// Log all API and auth routes...
include: ['/api/**', '/auth/**'],
// ...except internal/noisy routes
exclude: [
'/api/_nuxt_icon/**', // Nuxt Icon requests
'/api/_content/**', // Nuxt Content queries
'/api/health', // Health checks
],
},
})
include and exclude, it will be excluded.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:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
sampling: {
rates: {
info: 10, // Keep 10% of info logs
warn: 50, // Keep 50% of warning logs
debug: 5, // Keep 5% of debug logs
error: 100, // Always keep errors (default)
},
},
},
})
error: 100, error logs are never sampled out unless you explicitly set error: 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 even when head sampling would drop them:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
sampling: {
rates: { info: 10 }, // Only 10% of info logs
keep: [
{ duration: 1000 }, // Always keep if duration >= 1000ms
{ status: 400 }, // Always keep if status >= 400
{ path: '/api/critical/**' }, // Always keep critical paths
],
},
},
})
Conditions use >= comparison and follow OR logic (any match = keep).
Custom Tail Sampling Hook
For business-specific conditions (premium users, feature flags, etc.), use the evlog:emit:keep Nitro hook:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
// Always keep logs for premium users
const user = ctx.context.user as { premium?: boolean } | undefined
if (user?.premium) {
ctx.shouldKeep = true
}
})
})
The hook receives a TailSamplingContext with status, duration, path, method, and the full accumulated context.
Log Draining
Send logs to external services like Axiom, Loki, or custom endpoints using the evlog:drain hook. The hook is called in fire-and-forget mode, meaning it never blocks the HTTP response.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', async (ctx) => {
await fetch('https://api.axiom.co/v1/datasets/logs/ingest', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.AXIOM_TOKEN}` },
body: JSON.stringify([ctx.event])
})
})
})
The hook receives a DrainContext with:
event: The completeWideEvent(timestamp, level, service, and all accumulated context)request: Optional request metadata (method,path,requestId)headers: HTTP headers from the original request (useful for correlation with external services)
authorization, cookie, set-cookie, x-api-key, x-auth-token, proxy-authorization) are automatically filtered out and never passed to the drain hook.Using Headers for External Service Correlation
The headers field allows you to correlate logs with external services like PostHog, Sentry, or custom analytics:
export default defineNitroPlugin((nitroApp) => {
const posthog = usePostHog()
nitroApp.hooks.hook('evlog:drain', (ctx) => {
if (!posthog) return
// Extract correlation headers sent from the client
const sessionId = ctx.headers?.['x-posthog-session-id']
const distinctId = ctx.headers?.['x-posthog-distinct-id']
if (!distinctId) return
posthog.capture({
distinctId,
event: 'server_log',
properties: {
...ctx.event,
$session_id: sessionId,
},
})
})
})
Client Transport
Send browser logs to your server for centralized logging. When enabled, client-side log.info(), log.error(), etc. calls are automatically sent to the server via the /api/_evlog/ingest endpoint.
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
transport: {
enabled: true, // Enable client log transport
endpoint: '/api/_evlog/ingest', // default
},
},
})
How it works
- Client calls
log.info({ action: 'click', button: 'submit' }) - Log is sent to
/api/_evlog/ingestvia POST - Server enriches with environment context (service, version, region, etc.)
evlog:drainhook is called withsource: 'client'- External services receive the log (Axiom, Loki, etc.)
service, environment, or version from the client.In your drain hook, you can identify client logs by the source: 'client' field:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', async (ctx) => {
if (ctx.event.source === 'client') {
// Handle client logs specifically
console.log('[CLIENT]', ctx.event)
}
// Send to external service...
})
})
$production override to sample only in production while keeping full visibility in development:export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
},
$production: {
evlog: {
sampling: {
rates: { info: 10, warn: 50, debug: 0 },
keep: [{ duration: 1000 }, { status: 400 }],
},
},
},
})
That's it! You can now use useLogger(event) in any API route.
Nitro
Install evlog via your preferred package manager:
pnpm add evlog
npm install evlog
yarn add evlog
bun add evlog
Then, add evlog as a Nitro plugin (without Nuxt) using the evlog/nitro plugin:
export default defineNitroConfig({
plugins: ['evlog/nitro'],
})
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:
requestIddefaults tocf-raywhen availablerequest.cfis included (colo, country, asn) unless disabled- Use
headerAllowlistto avoid logging sensitive headers
Standalone TypeScript
Install evlog via your preferred package manager:
pnpm add evlog
npm install evlog
yarn add evlog
bun add evlog
Then, use it as any other TypeScript library within your scripts, CLI tools, workers, or apps:
import { initLogger, createRequestLogger } from 'evlog'
// Initialize once at startup
initLogger({
env: {
service: 'my-worker',
environment: 'production',
},
// Optional: sample logs
sampling: {
rates: { info: 10, debug: 5 },
},
})
// Create a logger for each operation
const log = createRequestLogger({ jobId: job.id })
log.set({ source: job.source, target: job.target })
log.set({ recordsSynced: 150 })
log.emit() // Manual emit required in standalone mode
log.emit() manually. In Nuxt/Nitro, this happens automatically at request end.TypeScript Configuration
evlog is written in TypeScript and ships with full type definitions. No additional configuration is required.
Next Steps
- Quick Start - Learn the core concepts and start using evlog