# evlog replaces scattered log lines with one wide event per request

> evlog is a TypeScript logging library whose unit of output is one wide event per operation rather than a line, with framework integrations for Nuxt, Nitro, Next.js, SvelteKit, Hono, Fastify and more, and a config file that turns on route filtering and per-level sampling.

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

- Repository: https://github.com/evloghq/evlog
- Website: https://evlog.dev
- Stars: 1,934 · Forks: 71
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/evloghq-evlog

## One event, not one line

The argument in the README is about units. A single request generates ten or more log lines, and when production breaks at three in the morning you are sifting scattered lines for one signal, while the error itself says something went wrong.

evlog's answer is that the unit of output is one wide event per operation, carrying all accumulated context and an error that explains why and what to do next. The example it contrasts is a checkout route: four console.log calls followed by a thrown Error with a useless message.

```typescript
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' })
})
```

Each log.set merges a field into the event rather than printing a line, and the emit happens automatically at the end of the request with duration attached. The resulting JSON carries timestamp, level, service, method, path, duration as a string, durationMs, then whatever you set, and a structured error object.

The consequence for a query tool is straightforward: one document per request means you filter by request rather than by correlation id, and a trace of what a handler touched is a single record rather than a timeline you have to reconstruct.

## Errors carry why and fix

The second half of the design is the error object, and it is the part that most directly addresses the 3am problem.

In the Nuxt example, the payment failure is logged and then rethrown as a framework error with four fields beyond the usual ones.

```typescript
    throw createError({
      message: 'Payment failed',
      status: 402,
      why: error.message,
      fix: 'Try a different payment method or contact your bank',
    })
```

status is 402, why carries the underlying message, and fix is a sentence aimed at whoever reads the event. The README is explicit that errors should explain why and what to do next, and calls these self-documenting errors with why and fix fields.

It also frames the library around AI-assisted development, arguing that an agent debugging a failure needs clear structured context and would otherwise grep through noise. That is a design rationale rather than a feature, but it explains why the output is JSON with predictable keys: something is meant to read it and reason about it, not just a human tailing a file.

If you adopt this, the discipline that matters is writing the fix field. A library can carry the field; only your code knows the remedy.

## One config file, route filtering and per-level sampling

Every option lives in evlog.config.ts at the root of your app, and npx evlog init writes it for you.

```typescript
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-app',
  include: ['/api/**'],
  sampling: process.env.NODE_ENV === 'production'
    ? { rates: { info: 10, warn: 50, debug: 0 } }
    : undefined,
})
```

Three options carry real decisions. service names your application and appears in every event, so it is the field you filter on when several services write to one backend. include restricts logging to matching routes and supports glob patterns, which is how you keep health checks and static routes out of your log volume. sampling takes a rate per level, and the example sets it only in production so development logs everything.

The rates deserve a second look. In the shipped example, info is 10, warn is 50 and debug is 0, so debug is fully disabled in production. That can silence debug output for anyone who copies the example without adjusting it. Set these deliberately per environment rather than copying them.

Nuxt, Nitro and TanStack Start load the config file on their own. Other frameworks import it with initLogger(toLoggerConfig(config)) and pass toMiddlewareOptions(config) to the middleware, which means the manual path is three calls rather than a module entry.

## Framework integrations and the import path that differs

Nuxt is described as the recommended way to use evlog, at zero config.

```typescript
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
})
```

The module loads evlog.config.ts, and a legacy evlog key in nuxt.config.ts still works and overrides the file, which matters if you are migrating an existing setup.

Nitro covers any framework powered by Nitro, and the README names Nuxt, Analog, Vinxi, SolidStart and TanStack Start among them. The import path is the detail that trips people up: for Nitro v3 you import evlog from 'evlog/nitro/v3', and for Nitro v2 you import from 'evlog/nitro'. Both register the same way through a modules array.

Beyond the two documented integrations in the README, the repository's examples directory is the best catalogue of the real reach. The listed example applications include browser, bun-script, elysia, express, fastify, hono, nestjs, nextjs, orpc, react-router, solidstart, sveltekit, tanstack-start, vite and workers, plus a telemetry-playground and three skeletons named for community adapters, enrichers and frameworks.

Those skeletons are the interesting part for anyone extending evlog: they show the extension points as separate directories rather than documenting them in prose.

## A pnpm and turbo monorepo behind one npm package

The repository is not the package. The root package.json is named evlog-monorepo, marked private, with type module, packageManager pnpm@11.28.3 and turbo.json driving the tasks.

Many tasks run through turbo, including dev, docs, build, lint, bench and test, while scripts such as the sandbox and content checks invoke their tools directly. The published library lives in packages/, and packages/cli/bin/evlog.mjs is what the cli script runs, which is the binary behind npx evlog init.

The e2e setup is unusually concrete for a logging library. Three scripts spin up a Docker Compose sandbox from packages/evlog/test/e2e/docker-compose.yml, seed it, and run the end-to-end tests against two services.

Loki and ClickHouse as the e2e backends tell you the project expects real ingestion paths to be exercised rather than mocked. There is also a telemetry CLI entry that sets EVLOG_TELEMETRY and an endpoint environment variable before running the CLI, so the CLI itself can report usage when you opt in.

Alongside that are the files a fast-moving project accumulates: AGENTS.md, CLAUDE.md, CODE_OF_CONDUCT.md, SECURITY.md, a.changeset/ directory for version bumps, renovate.json for dependency updates, and skills/ and.agents/ directories that suggest the project ships its own agent-facing instructions.

## What to check before you depend on it

The honest limitations are visible in the shape of the project.

The design assumes a request context. useLogger(event) takes a framework event object, and the automatic emit happens at request end. In a background job, a queue consumer or a scheduled task there is no request, which is why the standalone path exists with initLogger and createRequestLogger and a migration script example. You can log there, but the automatic duration and request fields are a different story.

The type-first claim is a real dependency. Output is JSON with predictable keys, which is good for agents and for query tools, but it also means your log queries become coupled to a schema that a fast-moving 2.x project is still changing. evlog@2.32.0 shipped on 2026-10-13, followed by 2.31.0 on 2026-10-11, a fast cadence for something your dashboards query.

The sampling model is a blunt instrument too. A per-level rate either keeps or discards an event, so a high-volume info path and the one error you cared about in the same request are governed by the same document. If you need request-scoped sampling rather than level-scoped, that is not what this config does.

MIT licensing is stated in the README and the repository carries a LICENSE file, with no complications.

## Conclusion

Adopt evlog if you run a TypeScript backend, especially on Nuxt or Nitro, and your current logs are the scattered-console variety that becomes unreadable during an incident. Do not adopt it for a polyglot estate or a Node-free runtime without checking the example list, since the integrations are framework-shaped rather than universal. Verify first that your framework is covered by one of the documented integrations, that npx evlog init writes evlog.config.ts where your app root expects it, and that the sampling rates in production do not silently discard the level you need, since the example ships debug at 0. Release cadence is fast, evlog@2.32.0 on 2026-10-13 with the last push on 2026-10-15, so pin an exact version and read the changelog rather than tracking latest.

## FAQ

### How do I install and configure evlog?

Run npm install evlog, then run npx evlog init to write evlog.config.ts at the root of your app. In that file defineEvlog takes service, an optional include of route globs and an optional sampling object with per-level rates.

### How do I use evlog with Nuxt?

Add the module to nuxt.config.ts as modules: ['evlog/nuxt']. The module loads evlog.config.ts, and an evlog key in nuxt.config.ts still works and overrides the file. Then call useLogger(event) in any API route.

### What is the difference between the Nitro v2 and v3 import paths?

For Nitro v3 you import evlog from 'evlog/nitro/v3', and for Nitro v2 you import from 'evlog/nitro'. Both are registered in the modules array of the nitro config, and usage of useLogger is identical.

### Does evlog work outside a web framework?

Yes, through a standalone TypeScript path. Call initLogger once at script start with an env block, then use log.info or createRequestLogger for a logical operation, as shown in the migration script example.

### How does sampling work in evlog?

The sampling option takes a rates object per level, and the README's example applies it only in production with info at 10, warn at 50 and debug at 0, meaning debug events are dropped entirely in production.

## Sources

- [evloghq/evlog on GitHub](https://github.com/evloghq/evlog)
- [License: MIT](https://github.com/evloghq/evlog/blob/main/LICENSE)
- [Project website](https://evlog.dev)
- [README](https://github.com/evloghq/evlog/blob/main/README.md)
- [Releases](https://github.com/evloghq/evlog/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/evloghq-evlog
