Getting Started

Install evlog

Quick Start
Install evlog in your Nuxt, Nitro, Cloudflare Workers, or standalone TypeScript project. Configure sampling, log draining, and client transport.

evlog supports multiple environments: Nuxt, Nitro, Cloudflare Workers, and standalone TypeScript.

Nuxt

Install evlog via your preferred package manager:

pnpm add evlog

Then add it to your Nuxt config using the evlog/nuxt module:

nuxt.config.ts
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

OptionTypeDefaultDescription
env.servicestring'app'Service name shown in logs
env.environmentstringAuto-detectedEnvironment name
includestring[]undefinedRoute patterns to log. Supports glob (/api/**). If not set, all routes are logged
excludestring[]undefinedRoute patterns to exclude from logging. Supports glob (/api/_nuxt_icon/**). Exclusions take precedence over inclusions
routesRecord<string, RouteConfig>undefinedRoute-specific service configuration. Allows setting different service names for different routes using glob patterns
prettybooleantrue in devPretty print with tree formatting
sampling.ratesobjectundefinedHead sampling rates per log level (0-100%). See Sampling
sampling.keeparrayundefinedTail sampling conditions to force-keep logs. See Sampling
transport.enabledbooleanfalseEnable sending client logs to the server. See Client Transport
transport.endpointstring'/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.

nuxt.config.ts
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
    ],
  },
})
Exclusions take precedence. If a path matches both include and exclude, it will be excluded.

Route-Based Service Configuration

In multi-service architectures, configure different service names for different routes:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    env: {
      service: 'default-service', // Fallback for unmatched routes
    },
    routes: {
      '/api/auth/**': { service: 'auth-service' },
      '/api/payment/**': { service: 'payment-service' },
      '/api/booking/**': { service: 'booking-service' },
    },
  },
})

All logs from matching routes will automatically include the configured service name. This is especially useful when:

  • Running multiple microservices behind a single Nuxt server
  • Organizing logs by business domain (auth, payment, inventory)
  • Differentiating between API versions (/api/v1/**, /api/v2/**)

You can also override the service name per handler using useLogger(event, 'service-name'). See Quick Start - Service Identification for details.

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:

nuxt.config.ts
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)
      },
    },
  },
})
Errors are always logged by default. Even if you don't specify 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:

nuxt.config.ts
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:

server/plugins/evlog-custom.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    // Access user from accumulated context
    const user = ctx.context.user as { premium?: boolean } | undefined

    // Force-keep logs for premium users regardless of sampling rate
    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.

server/plugins/evlog-axiom.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', async (ctx) => {
    // Send wide event to Axiom (fire-and-forget, never blocks response)
    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 complete WideEvent (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)
Security: Sensitive headers (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:

server/plugins/evlog-posthog.ts
export default defineNitroPlugin((nitroApp) => {
  const posthog = usePostHog()

  nitroApp.hooks.hook('evlog:drain', (ctx) => {
    if (!posthog) return

    // Extract correlation headers sent from the client
    // These headers are safe (sensitive headers like Authorization are filtered)
    const sessionId = ctx.headers?.['x-posthog-session-id']
    const distinctId = ctx.headers?.['x-posthog-distinct-id']

    if (!distinctId) return

    // Correlate server logs with client sessions in PostHog
    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.

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    transport: {
      enabled: true,  // Enable client log transport
      endpoint: '/api/_evlog/ingest',  // default
    },
  },
})

How it works

  1. Client calls log.info({ action: 'click', button: 'submit' })
  2. Log is sent to /api/_evlog/ingest via POST
  3. Server enriches with environment context (service, version, region, etc.)
  4. evlog:drain hook is called with source: 'client'
  5. External services receive the log (Axiom, Loki, etc.)
Client logs are automatically enriched with the server's environment context. You don't need to send service, environment, or version from the client.

In your drain hook, you can identify client logs by the source: 'client' field:

server/plugins/evlog-drain.ts
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...
  })
})
Tip: Use Nuxt's $production override to sample only in production while keeping full visibility in development:
nuxt.config.ts
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

Then, add evlog as a Nitro plugin (without Nuxt) using the evlog/nitro plugin:

nitro.config.ts
export default defineNitroConfig({
  plugins: ['evlog/nitro'],
})

Cloudflare Workers

Use the Workers adapter for structured logs and correct platform severity.

src/index.ts
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:

wrangler.toml
[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

Standalone TypeScript

Install evlog via your preferred package manager:

pnpm add evlog

Then, use it as any other TypeScript library within your scripts, CLI tools, workers, or apps:

scripts/sync-job.ts
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
In standalone mode, you must call log.emit() manually. In Nuxt/Nitro, this happens automatically at request end.

TypeScript Configuration

evlog ships with full TypeScript type definitions. No additional configuration is required.

evlog requires TypeScript 5.0 or higher for optimal type inference.

Next Steps

  • Quick Start - Learn the core concepts and start using evlog
Copyright © 2026