evlog

evloghqMIT活跃维护

TypeScript 优先的结构化日志与可观测性库,用宽事件和结构化错误替代翻日志碰运气,支持多种运行时。

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

[email protected]发布于 2026-11-05 12:09 UTC1,936star +2 / 7 天72fork同步于 2026-11-10 11:46 UTC

怎么上手

由 README 提炼,执行前请确认
npm install evlog
npx evlog init
硬件要求按 README 说明

README 未说明

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

TypeScript 优先的宽事件日志库,把一次请求的上下文聚合成一条结构化事件

TypeScript 优先的结构化日志与可观测性库,用宽事件和结构化错误替代翻日志碰运气,支持多种运行时。

核心能力

  • 每次请求只发一条宽事件,聚合全部上下文与耗时
  • 用 log.set 逐步累积字段,请求结束时自动 emit
  • createError 支持 why 和 fix 字段,让错误自解释
  • 提供 Nuxt 模块与 Nitro v2/v3 模块,零配置接入
  • 支持按路由 include 与生产环境采样率配置

容易踩的坑

非 Nuxt/Nitro 框架需手动调用 initLogger(toLoggerConfig(config)) 并把 toMiddlewareOptions(config) 传给中间件;配置集中在根目录 evlog.config.ts,Nuxt 中 nuxt.config.ts 的 evlog 键会覆盖该文件;Nitro v2 与 v3 的导入路径不同(evlog/nitro 与 evlog/nitro/v3)

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本[email protected]
  1. [email protected]最新正式版2026-11-05 · 2026-11-05 12:09 UTC

    What's Changed

    Bug Fixes 🐞

    • Register the Nitro v3 error handler and plugin in the Nuxt module when running on Nitro 3, so error responses keep their status code and JSON content type under Nuxt 5 by @danielroe in https://github.com/evloghq/evlog/pull/1007

    Tests 🧪

    Changelog: https://github.com/evloghq/evlog/blob/[email protected]/packages/evlog/CHANGELOG.md#2351 Full Changelog: https://github.com/evloghq/evlog/compare/[email protected]@2.35.1

  2. [email protected]pre-release2026-11-04 · 2026-11-04 22:38 UTC
    查看更新说明

    What's Changed

    Breaking Changes 💥

    Features 🚀

    Bug Fixes 🐞

    Dependency Updates 📦

    Other Changes 🔨

  3. @evlog/[email protected]pre-release2026-11-04 · 2026-11-04 22:38 UTC
    查看更新说明

    What's Changed

    Features 🚀

    Bug Fixes 🐞

    Dependency Updates 📦

    Other Changes 🔨

    Changelog: https://github.com/evloghq/evlog/blob/@evlog/[email protected]/packages/cli/CHANGELOG.md#100-beta2 Full Changelog: https://github.com/evloghq/evlog/compare/@evlog/[email protected]...@evlog/[email protected]

  4. [email protected]2026-11-04 · 2026-11-04 21:43 UTC
    查看更新说明

    What's Changed

    Features 🚀

    Bug Fixes 🐞

    Refactoring 🛠

    Dependency Updates 📦

    Changelog: https://github.com/evloghq/evlog/blob/[email protected]/packages/evlog/CHANGELOG.md#2350 Full Changelog: https://github.com/evloghq/evlog/compare/[email protected]@2.35.0

  5. @evlog/[email protected]2026-11-04 · 2026-11-04 21:43 UTC
    查看更新说明

    What's Changed

    Bug Fixes 🐞

    Dependency Updates 📦

    Changelog: https://github.com/evloghq/evlog/blob/@evlog/[email protected]/packages/cli/CHANGELOG.md#0112 Full Changelog: https://github.com/evloghq/evlog/compare/@evlog/[email protected]...@evlog/[email protected]

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    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
    

    Configure once

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

    // evlog.config.ts
    import { defineEvlog } from 'evlog'
    
    export default defineEvlog({
      service: 'my-app',
      // Optional: only log specific routes (supports glob patterns)
      include: ['/api/**'],
      // Optional: sample only in production
      sampling: process.env.NODE_ENV === 'production'
        ? { rates: { info: 10, warn: 50, debug: 0 } }
        : undefined,
    })
    

    Nuxt, Nitro and TanStack Start load it on their own. Other frameworks import it with initLogger(toLoggerConfig(config)) and pass toMiddlewareOptions(config) to the middleware. See Configuration.

    Nuxt Integration

    The recommended way to use evlog. Zero config, everything just works.

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

    The module loads evlog.config.ts. The evlog key in nuxt.config.ts still works and overrides the file.

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

    Nitro v2

    // nitro.config.ts
    import { defineNitroConfig } from 'nitropack/config'
    import evlog from 'evlog/nitro'
    
    export default defineNitroConfig({
      modules: [
        evlog()
      ],
    })
    

    The module loads evlog.config.ts. 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 a small script, inline options are fine. Anything bigger reads evlog.config.ts, as the worker below does:

    // 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, toLoggerConfig } from 'evlog'
    import config from '../evlog.config'
    
    initLogger(toLoggerConfig(config))
    
    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 a drain in evlog.config.ts, 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 { toLoggerConfig } from 'evlog'
    import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
    import config from '../evlog.config'
    
    initWorkersLogger(toLoggerConfig(config))
    
    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
      }
    })
    

    evlog.config.ts is bundled into the Worker and evaluated there, so process.env reads in it (such as the NODE_ENV check for sampling above) only see your Worker's variables with Cloudflare's nodejs_compat compatibility flag. Without it, set those options inline instead.

    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

    Effect (beta)

    evlog/effect gives every request served by Effect's HttpRouter or HttpApi one wide event. Handlers read it from the Effect context with yield* EvlogLogger, and failures are recorded from the Cause. It needs effect 4 and no AsyncLocalStorage, so it runs on Cloudflare Workers without nodejs_compat.

    // src/index.ts
    import { createServer } from 'node:http'
    import { NodeHttpServer, NodeRuntime } from '@effect/platform-node'
    import { Effect, Layer } from 'effect'
    import { HttpRouter, HttpServerResponse } from 'effect/http'
    import { EvlogLogger, initLoggerLayer, routerLayer } from 'evlog/effect'
    
    const Routes = HttpRouter.add('GET', '/users/:id', Effect.gen(function* () {
      const log = yield* EvlogLogger
      const { id } = yield* HttpRouter.params
      log.set({ user: { id } })
      return HttpServerResponse.jsonUnsafe({ id })
    }))
    
    const Server = HttpRouter.serve(Routes.pipe(Layer.provide(routerLayer())), { disableLogger: true }).pipe(
      Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
      Layer.provide(initLoggerLayer({ env: { service: 'my-api' } })),
    )
    
    NodeRuntime.runMain(Layer.launch(Server))
    

    Jobs, queue messages and Durable Object alarms become their own wide event with .pipe(withLogger('send-digest')) from evlog/effect. Run inside a request, that event shares the request's traceId. See the Effect docs and the example.

    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())
    })
    
    DestinationImport
    Axiomevlog/axiom
    OTLP (Grafana, Datadog, Honeycomb, any OTLP backend)evlog/otlp
    Datadogevlog/datadog
    PostHogevlog/posthog
    Sentryevlog/sentry
    Better Stackevlog/better-stack
    HyperDXevlog/hyperdx
    Sazabievlog/sazabi
    Grafana Lokievlog/loki
    ClickHouseevlog/clickhouse
    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

    FrameworkIntegration
    Nuxtmodules: ['evlog/nuxt']
    Next.jscreateEvlog() factory with import { createEvlog } from 'evlog/next' (docs)
    SvelteKitexport const { handle, handleError } = createEvlogHooks() with import { createEvlogHooks } from 'evlog/sveltekit' (docs)
    Nitro v3modules: [evlog()] with import evlog from 'evlog/nitro/v3'
    Nitro v2modules: [evlog()] with import evlog from 'evlog/nitro'
    TanStack StartNitro v3 module setup (docs)
    React Routerevlog() middleware with import { evlog } from 'evlog/react-router' (docs)
    NestJSEvlogModule.forRoot() with import { EvlogModule } from 'evlog/nestjs' (docs)
    Expressapp.use(evlog()) with import { evlog } from 'evlog/express' (docs)
    Honoapp.use(evlog()) with import { evlog } from 'evlog/hono' (docs)
    Fastifyapp.register(evlog) with import { evlog } from 'evlog/fastify' (docs)
    Elysia.use(evlog()) with import { evlog } from 'evlog/elysia' (docs)
    oRPCwithEvlog(handler) + os.use(evlog()) with import { evlog, withEvlog } from 'evlog/orpc' (docs)
    Effect (beta)Layer.provide(routerLayer()) with import { routerLayer } from 'evlog/effect' (docs)
    evedefineEvlogHook() in agent/hooks/evlog.ts with import { defineEvlogHook, useLogger } from 'evlog/eve' (docs)
    Cloudflare WorkersManual setup with import { initWorkersLogger, createWorkersLogger } from 'evlog/workers' (docs)
    CustomBuild your own with import { createMiddlewareLogger } from 'evlog/toolkit' (guide)
    AnalogNitro v2 module setup
    VinxiNitro v2 module setup
    SolidStartNitro v2 module setup (example)

    CLI

    evlog ships an evlog executable, 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, 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 map
    # or: pnpm evlog 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
    
    CommandWhat it does
    evlog mapScore every entry point and list the three worth fixing first
    evlog map <route-or-file>Explain one entry point in full, with the shape it could take
    evlog map --allEvery entry point as a check matrix
    evlog map --min-score <n>Exit 1 below the threshold — a CI gate
    evlog doctorDiagnose 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: seven framework adapters today, rules still being refined. Expect verdicts and scores to move between releases. The executable runs @evlog/cli when it is installed and fetches it with your package manager otherwise, so evlog itself adds no dependency; add @evlog/cli as a dev dependency for a pinned, instant run 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 map) to score dark entry points. Early, 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.

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

    License

    MIT

    Made by @HugoRCD