Getting Started

Installation

Install evlog in your Nuxt, Nitro, or standalone TypeScript project.

evlog supports multiple environments: Nuxt, Nitro, 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
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.

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) => {
    // 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.

server/plugins/evlog-axiom.ts
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 complete WideEvent (timestamp, level, service, and all accumulated context)
  • request: Optional request metadata (method, path, requestId)

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:
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'],
})

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 is written in TypeScript and ships with full 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