evlog is a free, open source monitoring & observability project written in TypeScript and released under MIT. It has 1,909 GitHub stars, 69 forks and 19 open issues, and was last pushed 19 hours ago. On this registry it ranks #179 of 270 tracked projects in Monitoring & Observability, with 5 head-to-head comparisons available. It gained 33 stars over the last 30 tracked days.

What is evlog?

evlog is an MIT-licensed, TypeScript-first logging library that emits one wide event per operation — full request context plus a duration — for Nuxt and Next.js teams and for the AI coding agents that read their logs.

What it is

evlog is a logging and observability library published on npm as evlog, written in TypeScript, licensed MIT, with 1,854 stars, 63 forks, and 19 open issues on GitHub. Documentation lives at evlog.dev. Its central idea is the wide event: instead of emitting many lines per request, the application builds one comprehensive event with useLogger(event) and accumulates context through repeated log.set({ ... }) calls. The library then writes a single structured JSON record carrying timestamp, level, service, method, path, duration, durationMs, and every field the handler attached, such as user: { id, plan } or cart: { items, total }.

The problem it solves is stated plainly in the README: a single request generates 10 or more log lines, so when production breaks at 3am an engineer sifts scattered lines for a needle of signal. Errors are equally unhelpful, saying only "Something went wrong". evlog replaces that pattern — the ad-hoc console.log trail and the generic throw new Error('Something went wrong') — with one event per operation and with structured errors that carry why and fix fields through createError. It sits in the Infrastructure and Operations / Monitoring and Observability category, in the TypeScript ecosystem, with Nuxt and Next.js named among its topics.

Key capabilities

  • One wide event per operation: useLogger(event) is auto-injected, log.set({ user: { id, plan: 'premium' } }) accumulates context, and a single JSON event is emitted.
  • Structured, self-documenting errors: log.error(error, { step: 'payment' }) attaches the failure step, and createError({ message, status, why, fix }) records the reason and the suggested remedy in the event itself.
  • Automatic duration capture, emitted as both "duration": "1.2s" and "durationMs": 1204.
  • Zero-config Nuxt integration through modules: ['evlog/nuxt'] in nuxt.config.ts, with the service name set by evlog.env.service.
  • Route filtering with include glob patterns, for example include: ['/api/**'].
  • Per-level sampling, configured as sampling: { rates: { info: 10, warn: 50, debug: 0 } }, and scoped to production through the $production block.
  • Repository topics covering otlp, posthog, sentry, nextjs, nuxt, wide-events, error-handling, and observability.

Who uses it and how

  • Nuxt teams use the documented path: add the module, then call useLogger(event) in any API route, as in the README's server/api/checkout.post.ts example.
  • Next.js projects are addressed by the nextjs topic alongside nuxt, matching the tagline's claim of every runtime.
  • Teams whose debugging is driven by AI agents use the single-event output and the why and fix fields so an agent can parse context instead of grepping through noise.
  • High-volume production deployments use the sampling rates to drop debug to zero and thin info to one in ten while keeping half of warn events.
  • Teams already running an observability backend are pointed at by the otlp, posthog, and sentry topics.

Getting started

Install with npm install evlog, then register the module as evlog/nuxt in nuxt.config.ts with a service name; the README describes this as the recommended, zero-config path.

How it compares

No list of paid products that evlog replaces is provided, so the comparison rests on the topics list, which names Sentry, PostHog, and OTLP. Those names belong to error tracking, product analytics, and telemetry transport respectively, while evlog presents itself in its own README as a wide-event logging library for TypeScript runtimes.

When to use it — and when not to

Adopters run this as a library rather than a hosted service, so they own the deployment configuration: the service name, the include route globs, and the sampling rates, with the event sink left to their own stack. Teams not working in a JavaScript or TypeScript runtime, or those wanting a fully managed backend with no configuration, should look elsewhere. The honest caveat is scope: the README excerpt documents Nuxt as the zero-config integration in detail, so the tagline's claim of every runtime is broader than the setup the excerpt shows, and the project carries 19 open issues.

project readme (upstream, from github) — read inline

evlog — Digging through logs is not observability. It's hope

evlog

npm version npm downloads CI TypeScript Documentation license

Digging through logs is not observability. It's hope.

A single request generates 10+ log lines. When production breaks at 3am, you're sifting scattered lines for a needle of signal. Your errors say "Something went wrong", which helps nobody.

evlog is different. One wide event per operation. All the context. Errors that explain why and what to do next.

Why evlog?

The Problem

// server/api/checkout.post.ts

// Scattered logs - impossible to debug
console.log('Request received')
console.log('User:', user.id)
console.log('Cart loaded')
console.log('Payment failed')  // Good luck finding this at 3am

throw new Error('Something went wrong')

The Solution

// server/api/checkout.post.ts
import { useLogger } from 'evlog'

// One comprehensive event per request
export default defineEventHandler(async (event) => {
  const log = useLogger(event)  // Auto-injected by evlog

  log.set({ user: { id: user.id, plan: 'premium' } })
  log.set({ cart: { items: 3, total: 9999 } })
  log.error(error, { step: 'payment' })

  // Emits ONE event with ALL context + duration (automatic)
})

Output:

{
  "timestamp": "2025-01-24T10:23:45.612Z",
  "level": "error",
  "service": "my-app",
  "method": "POST",
  "path": "/api/checkout",
  "duration": "1.2s",
  "durationMs": 1204,
  "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.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],

  evlog: {
    env: {
      service: 'my-app',
    },
    // Optional: only log specific routes (supports glob patterns)
    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:

// server/api/checkout.post.ts
import { useLogger, createError } from 'evlog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  // Authenticate user and add to wide event
  const user = await requireAuth(event)
  log.set({ user: { id: user.id, plan: user.plan } })

  // Load cart and add to wide event
  const cart = await getCart(user.id)
  log.set({ cart: { items: cart.items.length, total: cart.total } })

  // Process payment
  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',
    })
  }

  // Create order
  const order = await createOrder(cart, user)
  log.set({ order: { id: order.id, status: order.status } })

  return order
  // log.emit() called automatically at request end
})

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",
  "durationMs": 1204,
  "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

// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  modules: [
    evlog({ env: { service: 'my-api' } })
  ],
})

Nitro v2

// nitro.config.ts
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). Usage is identical to the Nuxt section above; see the Nitro docs for the full route example.

Standalone TypeScript

For scripts, workers, or any TypeScript project:

// scripts/migrate.ts
import { initLogger, log, createRequestLogger } from 'evlog'

// Initialize once at script start
initLogger({
  env: {
    service: 'migration-script',
    environment: 'production',
  },
})

// Simple logging
log.info('migration', 'Starting database migration')
log.info({ action: 'migration', tables: ['users', 'orders'] })

// Or use request logger for a logical operation
const migrationLog = createRequestLogger({ action: 'full-migration' })

migrationLog.set({ tables: ['users', 'orders', 'products'] })
migrationLog.set({ rowsProcessed: 15000 })
migrationLog.emit()
// workers/sync-job.ts
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. With initWorkersLogger({ drain }), use defineWorkerFetch so async drains are registered with waitUntil automatically (Cloudflare only passes ExecutionContext as the third fetch argument, and there is no global).

// src/index.ts
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'

initWorkersLogger({
  env: { service: 'edge-api' },
})

export default defineWorkerFetch(async (request, _env, _ctx, log) => {
  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
  }
})

If you keep a raw export default { fetch }, pass { executionCtx: ctx } to createWorkersLogger or waitUntil on createRequestLogger.

// Lower-level (equivalent)
import { createWorkersLogger } from 'evlog/workers'

export default {
  async fetch(request: Request, _env: unknown, ctx: ExecutionContext) {
    const log = createWorkersLogger(request, { executionCtx: ctx })
    // ...
  },
}

Disable invocation logs to avoid duplicate request logs:

# wrangler.toml
[observability.logs]
invocation_logs = false

Notes:

  • Prefer defineWorkerFetch so you do not have to pass executionCtx yourself when using a drain
  • requestId defaults to cf-ray when available
  • request.cf is included (colo, country, asn) unless disabled
  • Use headerAllowlist to avoid logging sensitive headers

Structured Errors

Errors should tell you what happened, why, and how to fix it.

// server/api/repos/sync.post.ts
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

More

The docs cover everything the README summarizes:

Adapters

Send your logs to external observability platforms with built-in drain adapters, wired from a Nitro plugin:

// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
Destination Import
Axiom evlog/axiom
OTLP (Grafana, Datadog, Honeycomb, any OTLP backend) evlog/otlp
Datadog evlog/datadog
PostHog evlog/posthog
Sentry evlog/sentry
Better Stack evlog/better-stack
HyperDX evlog/hyperdx
File system (NDJSON) evlog/fs
Memory (any runtime, Workers included) evlog/memory
Browser to server (batched HTTP) evlog/http

Each adapter's configuration and environment variables are documented at evlog.dev/integrate/adapters/overview. For production volume, wrap a drain with createDrainPipeline from evlog/pipeline for batching, retry with backoff, and buffer overflow protection (see the drain pipeline docs), and use sampling (head rates and tail keep rules) to control cost.

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   // What happened
  status?: number   // HTTP status code (default: 500)
  why?: string      // Why it happened
  fix?: string      // How to fix it
  link?: string     // Documentation URL
  cause?: Error     // Original error
  internal?: Record<string, unknown>  // Backend-only; never in HTTP body or toJSON()
})

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)

  // Direct access to all fields
  console.log(error.message)  // "Payment failed"
  console.log(error.status)   // 402
  console.log(error.why)      // "Card declined"
  console.log(error.fix)      // "Try another card"
  console.log(error.link)     // "https://docs.example.com/..."

  // Use with toast
  toast.add({
    title: error.message,
    description: error.why,
    color: 'error',
  })
}

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.

Framework Support

Framework Integration
Nuxt modules: ['evlog/nuxt']
Next.js createEvlog() factory with import { createEvlog } from 'evlog/next' (docs)
SvelteKit export const { handle, handleError } = createEvlogHooks() with import { createEvlogHooks } from 'evlog/sveltekit' (docs)
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 (docs)
React Router evlog() middleware with import { evlog } from 'evlog/react-router' (docs)
NestJS EvlogModule.forRoot() with import { EvlogModule } from 'evlog/nestjs' (docs)
Express app.use(evlog()) with import { evlog } from 'evlog/express' (docs)
Hono app.use(evlog()) with import { evlog } from 'evlog/hono' (docs)
Fastify app.register(evlog) with import { evlog } from 'evlog/fastify' (docs)
Elysia .use(evlog()) with import { evlog } from 'evlog/elysia' (docs)
oRPC withEvlog(handler) + os.use(evlog()) with import { evlog, withEvlog } from 'evlog/orpc' (docs)
eve defineEvlogHook() in agent/hooks/evlog.ts with import { defineEvlogHook, useLogger } from 'evlog/eve' (docs)
Cloudflare Workers Manual setup with import { initWorkersLogger, createWorkersLogger } from 'evlog/workers' (docs)
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)

CLI

@evlog/cli is a separate package, still early, that scores what your app can tell you when something goes wrong. It reads your project on disk, with no traffic and no instrumentation, and finds every entry point, and names the ones to fix first. Worth trying once you have anything wired; hand the report to an agent if you like.

npx @evlog/cli map
# or: pnpm dlx @evlog/cli map
▀▀█ █▀▀   score /100              your-app · Nuxt
  █ █▀█   ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱    29 entry points scanned
  ▀ ▀▀▀   good                    ▂▂▂▃▃▃▃▃▄▆▆▆▆▆███████████████

FIX FIRST
1. ANY    /api/auth/:all* A — touches auth and logs nothing
   server/api/auth/[...all].ts:1 · evlog.dev/learn/wide-events
Command What it does
evlog map Score every entry point and list the three worth fixing first
evlog map Explain one entry point in full, with the shape it could take
evlog map --all Every entry point as a check matrix
evlog map --min-score Exit 1 below the threshold — a CI gate
evlog doctor Diagnose the install: Node, workspace, evlog version, local logs

Same code in, same verdict out, with the file and line for every finding, which also makes it something you can hand to an agent: run it, fix the list, run it again.

Early days: the CLI is tested and safe to run on any project, but it is young: four framework adapters today, rules still being refined. Expect verdicts and scores to move between releases; pin it as a dev dependency when you gate CI on the number.

Docs: CLI · evlog map · Rules · Scoring · CI

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
  • Optionally run evlog map (npx @evlog/cli map) to score dark entry points. It is a separate early CLI package, worth trying

Examples

Add logging to this endpoint
Review my logging code
Raise my evlog map score
Help me set up logging for this service

Philosophy

Inspired by Logging Sucks by Boris Tane.

  1. Wide Events: One log per request with all context
  2. Structured Errors: Errors that explain themselves
  3. Request Scoping: Accumulate context, emit once
  4. Pretty for Dev, JSON for Prod: Human-readable locally, machine-parseable in production

Sponsors

evlog is built and maintained in the open. Financial contributions pay for maintainer time, infrastructure, and contributor bounties, and every expense is public on Open Collective.

evlog backers on Open Collective

Become a sponsor: sponsor tiers get your logo in this README and on the collective page.

License

MIT

Made by @HugoRCD

Frequently asked questions

Is evlog free to use?

evlog is open source under the MIT licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does evlog do?

Digging through logs is not observability. It's hope — wide events, structured errors, TypeScript-first, every runtime.

What is evlog written in?

evlog is primarily written in TypeScript. Its source is publicly available at https://github.com/evloghq/evlog, and it has 1,909 GitHub stars.