[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-74987":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":23,"hasPages":23,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":38,"readmeContent":39,"aiSummary":40,"trendingCount":16,"starSnapshotCount":16,"syncStatus":41,"lastSyncTime":42,"discoverSource":43},74987,"evlog","HugoRCD\u002Fevlog","HugoRCD","Digging through logs is not observability. It's hope. — wide events, structured errors, TypeScript-first, every runtime.","https:\u002F\u002Fevlog.dev",null,"TypeScript",1471,46,3,9,0,68,90,161,204,18.02,"MIT License",false,"main",[26,27,28,29,30,31,32,33,34,35,36,37],"axiom","debugging","error-handling","logging","nextjs","nuxt","observability","otlp","posthog","sentry","typescript","wide-events","2026-06-12 02:03:31","\u003Cp align=\"center\">\n  \u003Cimg src=\"https:\u002F\u002Fraw.githubusercontent.com\u002FHugoRCD\u002Fevlog\u002Fmain\u002Fassets\u002Fevlog-banner.gif\" width=\"100%\" alt=\"evlog — Digging through logs is not observability. It's hope\" \u002F>\n\u003C\u002Fp>\n\n# evlog\n\n[![npm version](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002Fevlog?color=black)](https:\u002F\u002Fnpmjs.com\u002Fpackage\u002Fevlog)\n[![npm downloads](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fdm\u002Fevlog?color=black)](https:\u002F\u002Fnpm.chart.dev\u002Fevlog)\n[![CI](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Factions\u002Fworkflow\u002Fstatus\u002FHugoRCD\u002Fevlog\u002Fci.yml?branch=main&color=black)](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Factions\u002Fworkflows\u002Fci.yml)\n[![TypeScript](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FTypeScript-black?logo=typescript&logoColor=white)](https:\u002F\u002Fwww.typescriptlang.org\u002F)\n[![Documentation](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FDocumentation-black?logo=readme&logoColor=white)](https:\u002F\u002Fevlog.dev)\n[![license](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Flicense\u002FHugoRCD\u002Fevlog?color=black)](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Fblob\u002Fmain\u002FLICENSE)\n\n**Digging through logs is not observability. It's hope.**\n\nA 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\" — thanks, very helpful.\n\n**evlog is different.** One wide event per operation. All the context. Errors that explain *why* and what to do next.\n\n## Why evlog?\n\n### The Problem\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fcheckout.post.ts\n\n\u002F\u002F Scattered logs - impossible to debug\nconsole.log('Request received')\nconsole.log('User:', user.id)\nconsole.log('Cart loaded')\nconsole.log('Payment failed')  \u002F\u002F Good luck finding this at 3am\n\nthrow new Error('Something went wrong')\n```\n\n### The Solution\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fcheckout.post.ts\nimport { useLogger } from 'evlog'\n\n\u002F\u002F One comprehensive event per request\nexport default defineEventHandler(async (event) => {\n  const log = useLogger(event)  \u002F\u002F Auto-injected by evlog\n\n  log.set({ user: { id: user.id, plan: 'premium' } })\n  log.set({ cart: { items: 3, total: 9999 } })\n  log.error(error, { step: 'payment' })\n\n  \u002F\u002F Emits ONE event with ALL context + duration (automatic)\n})\n```\n\nOutput:\n\n```json\n{\n  \"timestamp\": \"2025-01-24T10:23:45.612Z\",\n  \"level\": \"error\",\n  \"service\": \"my-app\",\n  \"method\": \"POST\",\n  \"path\": \"\u002Fapi\u002Fcheckout\",\n  \"duration\": \"1.2s\",\n  \"user\": { \"id\": \"123\", \"plan\": \"premium\" },\n  \"cart\": { \"items\": 3, \"total\": 9999 },\n  \"error\": { \"message\": \"Card declined\", \"step\": \"payment\" }\n}\n```\n\n### Built for AI-Assisted Development\n\nWe'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.\n\nTraditional logs force agents to grep through noise. evlog gives them:\n- **One event per request** with all context in one place\n- **Self-documenting errors** with `why` and `fix` fields\n- **Structured JSON** that's easy to parse and reason about\n\nYour AI copilot will thank you.\n\n---\n\n## Installation\n\n```bash\nnpm install evlog\n```\n\n## Nuxt Integration\n\nThe recommended way to use evlog. Zero config, everything just works.\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['evlog\u002Fnuxt'],\n\n  evlog: {\n    env: {\n      service: 'my-app',\n    },\n    \u002F\u002F Optional: only log specific routes (supports glob patterns)\n    include: ['\u002Fapi\u002F**'],\n  },\n})\n```\n\n> **Tip:** Use `$production` to enable [sampling](#sampling) only in production:\n> ```typescript\n> export default defineNuxtConfig({\n>   modules: ['evlog\u002Fnuxt'],\n>   evlog: { env: { service: 'my-app' } },\n>   $production: {\n>     evlog: { sampling: { rates: { info: 10, warn: 50, debug: 0 } } },\n>   },\n> })\n> ```\n\nThat's it. Now use `useLogger(event)` in any API route:\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fcheckout.post.ts\nimport { useLogger, createError } from 'evlog'\n\nexport default defineEventHandler(async (event) => {\n  const log = useLogger(event)\n\n  \u002F\u002F Authenticate user and add to wide event\n  const user = await requireAuth(event)\n  log.set({ user: { id: user.id, plan: user.plan } })\n\n  \u002F\u002F Load cart and add to wide event\n  const cart = await getCart(user.id)\n  log.set({ cart: { items: cart.items.length, total: cart.total } })\n\n  \u002F\u002F Process payment\n  try {\n    const payment = await processPayment(cart, user)\n    log.set({ payment: { id: payment.id, method: payment.method } })\n  } catch (error) {\n    log.error(error, { step: 'payment' })\n\n    throw createError({\n      message: 'Payment failed',\n      status: 402,\n      why: error.message,\n      fix: 'Try a different payment method or contact your bank',\n    })\n  }\n\n  \u002F\u002F Create order\n  const order = await createOrder(cart, user)\n  log.set({ order: { id: order.id, status: order.status } })\n\n  return order\n  \u002F\u002F log.emit() called automatically at request end\n})\n```\n\nThe wide event emitted at the end contains **everything**:\n\n```json\n{\n  \"timestamp\": \"2026-01-24T10:23:45.612Z\",\n  \"level\": \"info\",\n  \"service\": \"my-app\",\n  \"method\": \"POST\",\n  \"path\": \"\u002Fapi\u002Fcheckout\",\n  \"duration\": \"1.2s\",\n  \"user\": { \"id\": \"user_123\", \"plan\": \"premium\" },\n  \"cart\": { \"items\": 3, \"total\": 9999 },\n  \"payment\": { \"id\": \"pay_xyz\", \"method\": \"card\" },\n  \"order\": { \"id\": \"order_abc\", \"status\": \"created\" },\n  \"status\": 200\n}\n```\n\n## Nitro Integration\n\nWorks with **any framework powered by Nitro**: Nuxt, Analog, Vinxi, SolidStart, TanStack Start, and more.\n\n### Nitro v3\n\n```typescript\n\u002F\u002F nitro.config.ts\nimport { defineConfig } from 'nitro'\nimport evlog from 'evlog\u002Fnitro\u002Fv3'\n\nexport default defineConfig({\n  modules: [\n    evlog({ env: { service: 'my-api' } })\n  ],\n})\n```\n\n### Nitro v2\n\n```typescript\n\u002F\u002F nitro.config.ts\nimport { defineNitroConfig } from 'nitropack\u002Fconfig'\nimport evlog from 'evlog\u002Fnitro'\n\nexport default defineNitroConfig({\n  modules: [\n    evlog({ env: { service: 'my-api' } })\n  ],\n})\n```\n\nThen use `useLogger` in any route. Import from `evlog\u002Fnitro\u002Fv3` (v3) or `evlog\u002Fnitro` (v2):\n\n```typescript\n\u002F\u002F routes\u002Fapi\u002Fdocuments\u002F[id]\u002Fexport.post.ts\n\u002F\u002F Nitro v3: import { defineHandler } from 'nitro\u002Fh3' + import { useLogger } from 'evlog\u002Fnitro\u002Fv3'\n\u002F\u002F Nitro v2: import { defineEventHandler } from 'h3' + import { useLogger } from 'evlog\u002Fnitro'\nimport { defineEventHandler } from 'h3'\nimport { useLogger } from 'evlog\u002Fnitro'\nimport { createError } from 'evlog'\n\nexport default defineEventHandler(async (event) => {\n  const log = useLogger(event)\n\n  \u002F\u002F Get document ID from route params\n  const documentId = getRouterParam(event, 'id')\n  log.set({ document: { id: documentId } })\n\n  \u002F\u002F Parse request body for export options\n  const body = await readBody(event)\n  log.set({ export: { format: body.format, includeComments: body.includeComments } })\n\n  \u002F\u002F Load document from database\n  const document = await db.documents.findUnique({ where: { id: documentId } })\n  if (!document) {\n    throw createError({\n      message: 'Document not found',\n      status: 404,\n      why: `No document with ID \"${documentId}\" exists`,\n      fix: 'Check the document ID and try again',\n    })\n  }\n  log.set({ document: { id: documentId, title: document.title, pages: document.pages.length } })\n\n  \u002F\u002F Generate export\n  try {\n    const exportResult = await generateExport(document, body.format)\n    log.set({ export: { format: body.format, size: exportResult.size, pages: exportResult.pages } })\n\n    return { url: exportResult.url, expiresAt: exportResult.expiresAt }\n  } catch (error) {\n    log.error(error, { step: 'export-generation' })\n\n    throw createError({\n      message: 'Export failed',\n      status: 500,\n      why: `Failed to generate ${body.format} export: ${error.message}`,\n      fix: 'Try a different format or contact support',\n    })\n  }\n  \u002F\u002F log.emit() called automatically - outputs one comprehensive wide event\n})\n```\n\nOutput when the export completes:\n\n```json\n{\n  \"timestamp\": \"2025-01-24T14:32:10.123Z\",\n  \"level\": \"info\",\n  \"service\": \"document-api\",\n  \"method\": \"POST\",\n  \"path\": \"\u002Fapi\u002Fdocuments\u002Fdoc_123\u002Fexport\",\n  \"duration\": \"2.4s\",\n  \"document\": { \"id\": \"doc_123\", \"title\": \"Q4 Report\", \"pages\": 24 },\n  \"export\": { \"format\": \"pdf\", \"size\": 1240000, \"pages\": 24 },\n  \"status\": 200\n}\n```\n\n## Standalone TypeScript\n\nFor scripts, workers, or any TypeScript project:\n\n```typescript\n\u002F\u002F scripts\u002Fmigrate.ts\nimport { initLogger, log, createRequestLogger } from 'evlog'\n\n\u002F\u002F Initialize once at script start\ninitLogger({\n  env: {\n    service: 'migration-script',\n    environment: 'production',\n  },\n})\n\n\u002F\u002F Simple logging\nlog.info('migration', 'Starting database migration')\nlog.info({ action: 'migration', tables: ['users', 'orders'] })\n\n\u002F\u002F Or use request logger for a logical operation\nconst migrationLog = createRequestLogger({ action: 'full-migration' })\n\nmigrationLog.set({ tables: ['users', 'orders', 'products'] })\nmigrationLog.set({ rowsProcessed: 15000 })\nmigrationLog.emit()\n```\n\n```typescript\n\u002F\u002F workers\u002Fsync-job.ts\nimport { initLogger, createRequestLogger, createError } from 'evlog'\n\ninitLogger({\n  env: {\n    service: 'sync-worker',\n    environment: process.env.NODE_ENV,\n  },\n})\n\nasync function processSyncJob(job: Job) {\n  const log = createRequestLogger({ jobId: job.id, type: 'sync' })\n\n  try {\n    log.set({ source: job.source, target: job.target })\n\n    const result = await performSync(job)\n    log.set({ recordsSynced: result.count })\n\n    return result\n  } catch (error) {\n    log.error(error, { step: 'sync' })\n    throw error\n  } finally {\n    log.emit()\n  }\n}\n```\n\n## Cloudflare Workers\n\nUse 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 — there is no global).\n\n```typescript\n\u002F\u002F src\u002Findex.ts\nimport { defineWorkerFetch, initWorkersLogger } from 'evlog\u002Fworkers'\n\ninitWorkersLogger({\n  env: { service: 'edge-api' },\n})\n\nexport default defineWorkerFetch(async (request, _env, _ctx, log) => {\n  try {\n    log.set({ route: 'health' })\n    const response = new Response('ok', { status: 200 })\n    log.emit({ status: response.status })\n    return response\n  } catch (error) {\n    log.error(error as Error)\n    log.emit({ status: 500 })\n    throw error\n  }\n})\n```\n\nIf you keep a raw `export default { fetch }`, pass `{ executionCtx: ctx }` to `createWorkersLogger` or `waitUntil` on `createRequestLogger`.\n\n```typescript\n\u002F\u002F Lower-level (equivalent)\nimport { createWorkersLogger } from 'evlog\u002Fworkers'\n\nexport default {\n  async fetch(request: Request, _env: unknown, ctx: ExecutionContext) {\n    const log = createWorkersLogger(request, { executionCtx: ctx })\n    \u002F\u002F ...\n  },\n}\n```\n\nDisable invocation logs to avoid duplicate request logs:\n\n```toml\n# wrangler.toml\n[observability.logs]\ninvocation_logs = false\n```\n\nNotes:\n- Prefer **`defineWorkerFetch`** so you do not have to pass `executionCtx` yourself when using a drain\n- `requestId` defaults to `cf-ray` when available\n- `request.cf` is included (colo, country, asn) unless disabled\n- Use `headerAllowlist` to avoid logging sensitive headers\n\n## Hono\n\n```typescript\n\u002F\u002F src\u002Findex.ts\nimport { Hono } from 'hono'\nimport { initLogger } from 'evlog'\nimport { evlog, type EvlogVariables } from 'evlog\u002Fhono'\n\ninitLogger({ env: { service: 'hono-api' } })\n\nconst app = new Hono\u003CEvlogVariables>()\napp.use(evlog())\n\napp.get('\u002Fapi\u002Fusers', (c) => {\n  const log = c.get('log')\n  log.set({ users: { count: 42 } })\n  return c.json({ users: [] })\n})\n```\n\nSee the full [hono example](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Ftree\u002Fmain\u002Fexamples\u002Fhono) for a complete working project.\n\n## Express\n\n```typescript\n\u002F\u002F src\u002Findex.ts\nimport express from 'express'\nimport { initLogger } from 'evlog'\nimport { evlog, useLogger } from 'evlog\u002Fexpress'\n\ninitLogger({ env: { service: 'express-api' } })\n\nconst app = express()\napp.use(evlog())\n\napp.get('\u002Fapi\u002Fusers', (req, res) => {\n  req.log.set({ users: { count: 42 } })\n  res.json({ users: [] })\n})\n```\n\nUse `useLogger()` to access the logger from anywhere in the call stack without passing `req`.\n\nSee the full [express example](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Ftree\u002Fmain\u002Fexamples\u002Fexpress) for a complete working project.\n\n## Fastify\n\n```typescript\n\u002F\u002F src\u002Findex.ts\nimport Fastify from 'fastify'\nimport { initLogger } from 'evlog'\nimport { evlog, useLogger } from 'evlog\u002Ffastify'\n\ninitLogger({ env: { service: 'fastify-api' } })\n\nconst app = Fastify({ logger: false })\nawait app.register(evlog)\n\napp.get('\u002Fapi\u002Fusers', async (request) => {\n  request.log.set({ users: { count: 42 } })\n  return { users: [] }\n})\n```\n\n`request.log` is the evlog wide-event logger (shadows Fastify's built-in pino logger on the request). Use `useLogger()` to access the logger from anywhere in the call stack.\n\nSee the full [fastify example](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Ftree\u002Fmain\u002Fexamples\u002Ffastify) for a complete working project.\n\n## Elysia\n\n```typescript\n\u002F\u002F src\u002Findex.ts\nimport { Elysia } from 'elysia'\nimport { initLogger } from 'evlog'\nimport { evlog, useLogger } from 'evlog\u002Felysia'\n\ninitLogger({ env: { service: 'elysia-api' } })\n\nconst app = new Elysia()\n  .use(evlog())\n  .get('\u002Fapi\u002Fusers', ({ log }) => {\n    log.set({ users: { count: 42 } })\n    return { users: [] }\n  })\n  .listen(3000)\n```\n\nUse `useLogger()` to access the logger from anywhere in the call stack.\n\nSee the full [elysia example](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Ftree\u002Fmain\u002Fexamples\u002Felysia) for a complete working project.\n\n## React Router\n\n```typescript\n\u002F\u002F app\u002Froot.tsx\nimport { initLogger } from 'evlog'\nimport { evlog, loggerContext } from 'evlog\u002Freact-router'\n\ninitLogger({ env: { service: 'react-router-api' } })\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  evlog(),\n]\n\n\u002F\u002F app\u002Froutes\u002Fapi.users.$id.tsx\nimport { loggerContext } from 'evlog\u002Freact-router'\n\nexport async function loader({ params, context }: Route.LoaderArgs) {\n  const log = context.get(loggerContext)\n  log.set({ users: { count: 42 } })\n  return { users: [] }\n}\n```\n\nUse `context.get(loggerContext)` in loaders\u002Factions, or `useLogger()` from anywhere in the call stack. Requires `v8_middleware: true` in `react-router.config.ts`.\n\nSee the full [react-router example](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Ftree\u002Fmain\u002Fexamples\u002Freact-router) for a complete working project.\n\n## NestJS\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module } from '@nestjs\u002Fcommon'\nimport { EvlogModule } from 'evlog\u002Fnestjs'\n\n@Module({\n  imports: [EvlogModule.forRoot()],\n})\nexport class AppModule {}\n\n\u002F\u002F In any controller or service:\nimport { useLogger } from 'evlog\u002Fnestjs'\nconst log = useLogger()\nlog.set({ users: { count: 42 } })\n```\n\n`EvlogModule.forRoot()` registers a global middleware that creates a request-scoped logger for every request. Use `useLogger()` to access it anywhere in the call stack, or `req.log` directly. Supports `forRootAsync()` for async configuration.\n\nSee the full [nestjs example](https:\u002F\u002Fgithub.com\u002FHugoRCD\u002Fevlog\u002Ftree\u002Fmain\u002Fexamples\u002Fnestjs) for a complete working project.\n\n## Browser\n\nUse the `log` API on the client side for structured browser logging:\n\n```typescript\nimport { log } from 'evlog\u002Fclient'\n\nlog.info('checkout', 'User initiated checkout')\nlog.error({ action: 'payment', error: 'validation_failed' })\n```\n\nIn Nuxt, `log` is auto-imported -- no import needed in Vue components:\n\n```vue\n\u003Cscript setup>\nlog.info('checkout', 'User initiated checkout')\n\u003C\u002Fscript>\n```\n\nClient logs output to the browser console with colored tags in development.\n\n### Client Transport\n\nTo send client logs to the server for centralized logging, enable the transport:\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['evlog\u002Fnuxt'],\n  evlog: {\n    transport: {\n      enabled: true,  \u002F\u002F Send client logs to server\n    },\n  },\n})\n```\n\nWhen enabled:\n1. Client logs are sent to `\u002Fapi\u002F_evlog\u002Fingest` via POST\n2. Server enriches with environment context (service, version, etc.)\n3. `evlog:drain` hook is called with `source: 'client'`\n4. External services receive the log\n\nFor a **framework-agnostic** batched HTTP drain (e.g. vanilla JS or custom endpoints), use `createHttpLogDrain` from [`evlog\u002Fhttp`](https:\u002F\u002Fwww.evlog.dev\u002Fextend\u002Fdrain-pipeline#http-drain-browser-to-server). The legacy import path `evlog\u002Fbrowser` is deprecated and will be removed in the next major release.\n\n## Structured Errors\n\nErrors should tell you **what** happened, **why**, and **how to fix it**.\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Frepos\u002Fsync.post.ts\nimport { useLogger, createError } from 'evlog'\n\nexport default defineEventHandler(async (event) => {\n  const log = useLogger(event)\n\n  log.set({ repo: { owner: 'acme', name: 'my-project' } })\n\n  try {\n    const result = await syncWithGitHub()\n    log.set({ sync: { commits: result.commits, files: result.files } })\n    return result\n  } catch (error) {\n    log.error(error, { step: 'github-sync' })\n\n    throw createError({\n      message: 'Failed to sync repository',\n      status: 503,\n      why: 'GitHub API rate limit exceeded',\n      fix: 'Wait 1 hour or use a different token',\n      link: 'https:\u002F\u002Fdocs.github.com\u002Fen\u002Frest\u002Frate-limit',\n      cause: error,\n    })\n  }\n})\n```\n\nConsole output (development):\n\n```\nError: Failed to sync repository\nWhy: GitHub API rate limit exceeded\nFix: Wait 1 hour or use a different token\nMore info: https:\u002F\u002Fdocs.github.com\u002Fen\u002Frest\u002Frate-limit\n```\n\n## Enrichment Hook\n\nUse the `evlog:enrich` hook to add derived context after emit, before drain.\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-enrich.ts\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:enrich', (ctx) => {\n    ctx.event.deploymentId = process.env.DEPLOYMENT_ID\n  })\n})\n```\n\n### Built-in Enrichers\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-enrich.ts\nimport {\n  createGeoEnricher,\n  createRequestSizeEnricher,\n  createTraceContextEnricher,\n  createUserAgentEnricher,\n} from 'evlog\u002Fenrichers'\n\nexport default defineNitroPlugin((nitroApp) => {\n  const enrich = [\n    createUserAgentEnricher(),\n    createGeoEnricher(),\n    createRequestSizeEnricher(),\n    createTraceContextEnricher(),\n  ]\n\n  nitroApp.hooks.hook('evlog:enrich', (ctx) => {\n    for (const enricher of enrich) enricher(ctx)\n  })\n})\n```\n\nEach enricher adds a specific field to the event:\n\n| Enricher | Event Field | Shape |\n|----------|-------------|-------|\n| `createUserAgentEnricher()` | `event.userAgent` | `{ raw, browser?: { name, version? }, os?: { name, version? }, device?: { type } }` |\n| `createGeoEnricher()` | `event.geo` | `{ country?, region?, regionCode?, city?, latitude?, longitude? }` |\n| `createRequestSizeEnricher()` | `event.requestSize` | `{ requestBytes?, responseBytes? }` |\n| `createTraceContextEnricher()` | `event.traceContext` + `event.traceId` + `event.spanId` | `{ traceparent?, tracestate?, traceId?, spanId? }` |\n\nAll enrichers accept an optional `{ overwrite?: boolean }` option. By default (`overwrite: false`), user-provided data on the event takes precedence over enricher-computed values. Set `overwrite: true` to always replace existing fields.\n\n> **Cloudflare geo note:** Only `cf-ipcountry` is a real Cloudflare HTTP header. The `cf-region`, `cf-city`, `cf-latitude`, `cf-longitude` headers are NOT standard -- they are properties of `request.cf`. For full geo data on Cloudflare, write a custom enricher that reads `request.cf`, or use a Workers middleware to forward `cf` properties as custom headers.\n\n### Custom Enrichers\n\nThe `evlog:enrich` hook receives an `EnrichContext` with these fields:\n\n```typescript\ninterface EnrichContext {\n  event: WideEvent        \u002F\u002F The emitted wide event (mutable -- modify it directly)\n  request?: {             \u002F\u002F Request metadata\n    method?: string\n    path?: string\n    requestId?: string\n  }\n  headers?: Record\u003Cstring, string>  \u002F\u002F Safe HTTP headers (sensitive headers filtered)\n  response?: {            \u002F\u002F Response metadata\n    status?: number\n    headers?: Record\u003Cstring, string>\n  }\n}\n```\n\nExample custom enricher:\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-enrich.ts\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:enrich', (ctx) => {\n    \u002F\u002F Add deployment metadata\n    ctx.event.deploymentId = process.env.DEPLOYMENT_ID\n    ctx.event.region = process.env.FLY_REGION\n\n    \u002F\u002F Extract data from headers\n    const tenantId = ctx.headers?.['x-tenant-id']\n    if (tenantId) {\n      ctx.event.tenantId = tenantId\n    }\n  })\n})\n```\n\n## Audit Logs\n\nAudit logs are not a parallel system: they are a typed `audit` field on the wide event plus a few helpers. Add 1 enricher + 1 drain wrapper + `log.audit()` and you get tamper-evident, redact-aware, force-kept audit events through the same pipeline.\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog.ts\nimport { auditEnricher, auditOnly, signed } from 'evlog'\nimport { createAxiomDrain } from 'evlog\u002Faxiom'\nimport { createFsDrain } from 'evlog\u002Ffs'\n\nexport default defineNitroPlugin((nitroApp) => {\n  const enrich = [auditEnricher({ tenantId: ctx => ctx.headers?.['x-tenant-id'] })]\n  const audits = auditOnly(signed(createFsDrain({ path: '.audit\u002F' }), { strategy: 'hash-chain' }), { await: true })\n  const main = createAxiomDrain()\n\n  nitroApp.hooks.hook('evlog:enrich', async ctx => { for (const e of enrich) await e(ctx) })\n  nitroApp.hooks.hook('evlog:drain', async ctx => { await Promise.all([main(ctx), audits(ctx)]) })\n})\n```\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Finvoice\u002F[id]\u002Frefund.post.ts\nimport { auditDiff } from 'evlog'\n\nexport default defineEventHandler(async (event) => {\n  const log = useLogger(event)\n  const before = await db.invoice.get(id)\n  const after = await db.invoice.refund(id)\n\n  log.audit?.({\n    action: 'invoice.refund',\n    actor: { type: 'user', id: user.id, email: user.email },\n    target: { type: 'invoice', id: after.id },\n    outcome: 'success',\n    changes: auditDiff(before, after),\n  })\n})\n```\n\n| Symbol | Kind | Purpose |\n|--------|------|---------|\n| `log.audit(fields)` \u002F `log.audit.deny(reason, fields)` | method | Sugar over `log.set({ audit })` + force-keep |\n| `audit(fields)` | function | Standalone for jobs \u002F scripts |\n| `withAudit({ action, target })(fn)` | wrapper | Auto-emit success \u002F failure \u002F denied |\n| `defineAuditAction(name, opts?)` | factory | Typed action registry |\n| `auditDiff(before, after)` | helper | Redact-aware JSON Patch for `changes` |\n| `mockAudit()` | test util | Capture and assert audits in tests |\n| `auditEnricher({ tenantId? })` | enricher | Auto-fill `req`\u002F`trace`\u002F`ip`\u002F`ua`\u002F`tenantId` context |\n| `auditOnly(drain, { await? })` | wrapper | Routes only events with `event.audit` |\n| `signed(drain, { strategy: 'hmac' \\| 'hash-chain', ... })` | wrapper | Tamper-evident integrity |\n| `auditRedactPreset` | preset | Strict PII for audit events |\n\n`AuditFields` is exported and merges with `BaseWideEvent` — augment it with `declare module` if you need extra typed fields. Audit events are always force-kept by tail sampling and get a deterministic `idempotencyKey` so retries are safe across drains.\n\nSee [the Audit Logs guide](https:\u002F\u002Fevlog.dev\u002Fuse-cases\u002Faudit\u002Foverview) for compliance, GDPR, and recipe details.\n\n## AI SDK Integration\n\nCapture token usage, tool calls, model info, and streaming metrics from the [Vercel AI SDK](https:\u002F\u002Fai-sdk.dev) into wide events. Requires `ai >= 6.0.0`.\n\n```typescript\nimport { streamText } from 'ai'\nimport { createAILogger } from 'evlog\u002Fai'\n\nexport default defineEventHandler(async (event) => {\n  const log = useLogger(event)\n  const ai = createAILogger(log)\n\n  const result = streamText({\n    model: ai.wrap('anthropic\u002Fclaude-sonnet-4.6'),  \u002F\u002F string or model object\n    messages,\n    onFinish: ({ text }) => saveConversation(text),  \u002F\u002F no conflict\n  })\n\n  return result.toTextStreamResponse()\n})\n```\n\nThe middleware captures: `inputTokens`, `outputTokens`, `cacheReadTokens`, `reasoningTokens`, `model`, `provider`, `finishReason`, `toolCalls`, `steps`, `msToFirstChunk`, `msToFinish`, `tokensPerSecond`.\n\nFor embeddings: `ai.captureEmbed({ usage })`.\n\nThe same metadata is also exposed as a public API for custom analytics, billing, or user-facing dashboards:\n\n```typescript\nconst ai = createAILogger(log, {\n  cost: { 'claude-sonnet-4.6': { input: 3, output: 15 } },\n})\n\nawait generateText({ model: ai.wrap('anthropic\u002Fclaude-sonnet-4.6'), prompt })\n\nconst metadata = ai.getMetadata()       \u002F\u002F structured snapshot (AIMetadata)\nconst cost = ai.getEstimatedCost()      \u002F\u002F dollars, or undefined\n\nai.onUpdate((metadata) => {             \u002F\u002F incremental updates per step\n  pushToClient({ tokens: metadata.totalTokens, cost: metadata.estimatedCost })\n})\n```\n\n## Adapters\n\nSend your logs to external observability platforms with built-in adapters.\n\n### Axiom\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport { createAxiomDrain } from 'evlog\u002Faxiom'\n\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())\n})\n```\n\nSet environment variables:\n\n```bash\nNUXT_AXIOM_TOKEN=xaat-your-token\nNUXT_AXIOM_DATASET=your-dataset\n```\n\n### OTLP (OpenTelemetry)\n\nWorks with Grafana, Datadog, Honeycomb, and any OTLP-compatible backend.\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport { createOTLPDrain } from 'evlog\u002Fotlp'\n\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:drain', createOTLPDrain())\n})\n```\n\nSet environment variables:\n\n```bash\nNUXT_OTLP_ENDPOINT=http:\u002F\u002Flocalhost:4318\n```\n\n### Datadog\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport { createDatadogDrain } from 'evlog\u002Fdatadog'\n\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:drain', createDatadogDrain())\n})\n```\n\nSet environment variables:\n\n```bash\nNUXT_DATADOG_API_KEY=your-api-key\n# Optional — defaults to datadoghq.com\nNUXT_DATADOG_SITE=datadoghq.eu\n```\n\nYou can also use standard Datadog names: `DD_API_KEY` and `DD_SITE`.\n\nWide events are sent with a short **`message` line** (method, path, level) and full context under the **`evlog`** attribute (facets like `@evlog.path`). See the [Datadog adapter docs](https:\u002F\u002Fwww.evlog.dev\u002Fintegrate\u002Fadapters\u002Fdatadog).\n\n### PostHog\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport { createPostHogDrain } from 'evlog\u002Fposthog'\n\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:drain', createPostHogDrain())\n})\n```\n\nSet environment variables:\n\n```bash\nNUXT_POSTHOG_API_KEY=phc_your-key\nNUXT_POSTHOG_HOST=https:\u002F\u002Fus.i.posthog.com  # Optional: for EU or self-hosted\n```\n\n### Sentry\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport { createSentryDrain } from 'evlog\u002Fsentry'\n\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:drain', createSentryDrain())\n})\n```\n\nSet environment variables:\n\n```bash\nNUXT_SENTRY_DSN=https:\u002F\u002Fpublic@o0.ingest.sentry.io\u002F123\n```\n\n### Better Stack\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport { createBetterStackDrain } from 'evlog\u002Fbetter-stack'\n\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:drain', createBetterStackDrain())\n})\n```\n\nSet environment variables:\n\n```bash\nNUXT_BETTER_STACK_SOURCE_TOKEN=your-source-token\n```\n\n### Multiple Destinations\n\nSend logs to multiple services:\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport { createAxiomDrain } from 'evlog\u002Faxiom'\nimport { createOTLPDrain } from 'evlog\u002Fotlp'\n\nexport default defineNitroPlugin((nitroApp) => {\n  const axiom = createAxiomDrain()\n  const otlp = createOTLPDrain()\n\n  nitroApp.hooks.hook('evlog:drain', async (ctx) => {\n    await Promise.allSettled([axiom(ctx), otlp(ctx)])\n  })\n})\n```\n\n### Custom Adapters\n\nBuild your own adapter for any destination:\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:drain', async (ctx) => {\n    await fetch('https:\u002F\u002Fyour-service.com\u002Flogs', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\u002Fjson' },\n      body: JSON.stringify(ctx.event),\n    })\n  })\n})\n```\n\n> See the [full documentation](https:\u002F\u002Fwww.evlog.dev\u002Fintegrate\u002Fadapters\u002Foverview) for adapter configuration options, troubleshooting, and advanced patterns.\n\n## Drain Pipeline\n\nFor production use, wrap your drain adapter with `createDrainPipeline` to get **batching**, **retry with backoff**, and **buffer overflow protection**.\n\nWithout a pipeline, each event triggers a separate network call. The pipeline buffers events and sends them in batches, reducing overhead and handling transient failures automatically.\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-drain.ts\nimport type { DrainContext } from 'evlog'\nimport { createDrainPipeline } from 'evlog\u002Fpipeline'\nimport { createAxiomDrain } from 'evlog\u002Faxiom'\n\nexport default defineNitroPlugin((nitroApp) => {\n  const pipeline = createDrainPipeline\u003CDrainContext>({\n    batch: { size: 50, intervalMs: 5000 },\n    retry: { maxAttempts: 3, backoff: 'exponential', initialDelayMs: 1000 },\n    onDropped: (events, error) => {\n      console.error(`[evlog] Dropped ${events.length} events:`, error?.message)\n    },\n  })\n\n  const drain = pipeline(createAxiomDrain())\n\n  nitroApp.hooks.hook('evlog:drain', drain)\n  nitroApp.hooks.hook('close', () => drain.flush())\n})\n```\n\n### How it works\n\n1. Events are buffered in memory as they arrive\n2. A batch is flushed when either the **batch size** is reached or the **interval** expires (whichever comes first)\n3. If the drain function fails, the batch is retried with the configured **backoff strategy**\n4. If all retries are exhausted, `onDropped` is called with the lost events\n5. If the buffer exceeds `maxBufferSize`, the oldest events are dropped to prevent memory leaks\n\n### Options\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `batch.size` | `50` | Maximum events per batch |\n| `batch.intervalMs` | `5000` | Max time (ms) before flushing a partial batch |\n| `retry.maxAttempts` | `3` | Total attempts (including first) |\n| `retry.backoff` | `'exponential'` | `'exponential'` \\| `'linear'` \\| `'fixed'` |\n| `retry.initialDelayMs` | `1000` | Base delay for first retry |\n| `retry.maxDelayMs` | `30000` | Upper bound for any retry delay |\n| `maxBufferSize` | `1000` | Max buffered events before dropping oldest |\n| `onDropped` | -- | Callback when events are dropped |\n\n### Returned drain function\n\nThe function returned by `pipeline(drain)` is hook-compatible and exposes:\n\n- **`drain(ctx)`** -- Push a single event into the buffer\n- **`drain.flush()`** -- Force-flush all buffered events (call on server shutdown)\n- **`drain.pending`** -- Number of events currently buffered\n\n## API Reference\n\n### `initLogger(config)`\n\nInitialize the logger. Required for standalone usage, automatic with Nuxt\u002FNitro plugins.\n\n```typescript\ninitLogger({\n  enabled: boolean       \u002F\u002F Optional. Enable\u002Fdisable all logging (default: true)\n  env: {\n    service: string      \u002F\u002F Service name\n    environment: string  \u002F\u002F 'production' | 'development' | 'test'\n    version?: string     \u002F\u002F App version\n    commitHash?: string  \u002F\u002F Git commit\n    region?: string      \u002F\u002F Deployment region\n  },\n  pretty?: boolean       \u002F\u002F Pretty print (default: true in dev)\n  silent?: boolean       \u002F\u002F Suppress console output (default: false). Events still go to drains.\n  stringify?: boolean    \u002F\u002F JSON.stringify output (default: true, false for Workers)\n  include?: string[]     \u002F\u002F Route patterns to log (glob), e.g. ['\u002Fapi\u002F**']\n  sampling?: {\n    rates?: {            \u002F\u002F Head sampling (random per level)\n      info?: number      \u002F\u002F 0-100, default 100\n      warn?: number      \u002F\u002F 0-100, default 100\n      debug?: number     \u002F\u002F 0-100, default 100\n      error?: number     \u002F\u002F 0-100, default 100 (always logged unless set to 0)\n    }\n    keep?: Array\u003C{       \u002F\u002F Tail sampling (force keep based on outcome)\n      status?: number    \u002F\u002F Keep if status >= value\n      duration?: number  \u002F\u002F Keep if duration >= value (ms)\n      path?: string      \u002F\u002F Keep if path matches glob pattern\n    }>\n  }\n})\n```\n\n### Sampling\n\nAt scale, logging everything can become expensive. evlog supports two sampling strategies:\n\n#### Head Sampling (rates)\n\nRandom sampling based on log level, decided before the request completes:\n\n```typescript\ninitLogger({\n  sampling: {\n    rates: {\n      info: 10,   \u002F\u002F Keep 10% of info logs\n      warn: 50,   \u002F\u002F Keep 50% of warning logs\n      debug: 0,   \u002F\u002F Disable debug logs\n      \u002F\u002F error defaults to 100% (always logged)\n    },\n  },\n})\n```\n\n#### Tail Sampling (keep)\n\nForce-keep logs based on request outcome, evaluated after the request completes. Useful to always capture slow requests or critical paths:\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['evlog\u002Fnuxt'],\n  evlog: {\n    sampling: {\n      rates: { info: 10 },  \u002F\u002F Only 10% of info logs\n      keep: [\n        { duration: 1000 },           \u002F\u002F Always keep if duration >= 1000ms\n        { status: 400 },              \u002F\u002F Always keep if status >= 400\n        { path: '\u002Fapi\u002Fcritical\u002F**' }, \u002F\u002F Always keep critical paths\n      ],\n    },\n  },\n})\n```\n\n#### Custom Tail Sampling Hook\n\nFor business-specific conditions (premium users, feature flags), use the `evlog:emit:keep` Nitro hook:\n\n```typescript\n\u002F\u002F server\u002Fplugins\u002Fevlog-custom.ts\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {\n    \u002F\u002F Always keep logs for premium users\n    if (ctx.context.user?.premium) {\n      ctx.shouldKeep = true\n    }\n  })\n})\n```\n\n### Pretty Output Format\n\nIn development, evlog uses a compact tree format:\n\n```\n16:45:31.060 INFO [my-app] GET \u002Fapi\u002Fcheckout 200 in 234ms\n  |- user: id=123 plan=premium\n  |- cart: items=3 total=9999\n  +- payment: id=pay_xyz method=card\n```\n\nIn production (`pretty: false`), logs are emitted as JSON for machine parsing.\n\n### `log`\n\nSimple logging API.\n\n```typescript\nlog.info('tag', 'message')     \u002F\u002F Tagged log\nlog.info({ key: 'value' })     \u002F\u002F Wide event\nlog.error('tag', 'message')\nlog.warn('tag', 'message')\nlog.debug('tag', 'message')\n```\n\n### `createRequestLogger(options)`\n\nCreate a request-scoped logger for wide events.\n\n```typescript\nconst log = createRequestLogger({\n  method: 'POST',\n  path: '\u002Fcheckout',\n  requestId: 'req_123',\n})\n\nlog.set({ user: { id: '123' } })  \u002F\u002F Add context\nlog.error(error, { step: 'x' })   \u002F\u002F Log error with context\nlog.emit()                         \u002F\u002F Emit final event\nlog.getContext()                   \u002F\u002F Get current context\n```\n\n### Wide event lifecycle and `log.fork()`\n\nThe framework emits **one wide event per HTTP request** when the response finishes (or on error). After `emit()` runs — including when head sampling drops the event (`emit()` returns `null`) — that logger instance is **sealed**: further `set`, `error`, `info`, and `warn` calls are ignored and emit a **`[evlog]` console warning** listing dropped keys. A second `emit()` is ignored with a warning. This avoids silent data loss when async work (unawaited promises, `setTimeout`, etc.) still resolves `useLogger()` to the same logger via `AsyncLocalStorage` after the response has already been logged.\n\n**`log.fork(label, fn)`** runs work under a **child** request logger: inside `fn`, `useLogger()` returns the child. When `fn` settles, the child emits its **own** wide event with `operation` set to `label` and `_parentRequestId` set to the parent’s `requestId` (query and dashboard correlation). The parent event may be emitted **before** the child event; they are two separate events ordered by time.\n\n`fork` is attached by integrations that use `AsyncLocalStorage` for `useLogger()`. Standalone `createLogger()` instances do not have `fork`.\n\n| Integration | `log.fork()` |\n|-------------|----------------|\n| Express, Fastify, NestJS, SvelteKit, React Router, Elysia | Yes |\n| Next.js `withEvlog` | Yes |\n| Hono (`c.get('log')` only) | Not yet |\n| Nitro \u002F Nuxt `useLogger(event)` | Not yet — use post-emit warnings; see [Wide events](https:\u002F\u002Fevlog.dev\u002Flearn\u002Fwide-events) |\n\n```typescript\nimport { evlog, useLogger } from 'evlog\u002Fexpress'\n\napp.post('\u002Fcheckout', (req, res) => {\n  const log = req.log\n  log.set({ order_dispatched: true })\n\n  log.fork!('process_order', async () => {\n    const childLog = useLogger()\n    childLog.set({ inventory_checked: true })\n    \u002F\u002F child emits automatically when this async function completes\n  })\n\n  res.json({ ok: true })\n})\n```\n\nUse optional chaining if `fork` might be absent: `log.fork?.('task', async () => { ... })`.\n\n### `initWorkersLogger(options?)`\n\nInitialize evlog for Cloudflare Workers (object logs + correct severity).\n\n```typescript\nimport { initWorkersLogger } from 'evlog\u002Fworkers'\n\ninitWorkersLogger({\n  env: { service: 'edge-api' },\n})\n```\n\n### `defineWorkerFetch(handler)`\n\nRecommended for Workers when using **`initWorkersLogger({ drain })`**. Wraps your handler so `createWorkersLogger` always receives `executionCtx` — you do not pass `ctx` into the factory yourself. Cloudflare does not expose `ExecutionContext` globally (only as `fetch`’s third argument), so this is the “automatic” option for plain Workers scripts.\n\n```typescript\nimport { defineWorkerFetch, initWorkersLogger } from 'evlog\u002Fworkers'\n\ninitWorkersLogger({ env: { service: 'edge-api' }, drain })\n\nexport default defineWorkerFetch(async (request, env, ctx, log) => {\n  log.emit({ status: 200 })\n  return new Response('ok')\n})\n```\n\n### `createWorkersLogger(request, options?)`\n\nCreate a request-scoped logger for Workers. Auto-extracts `cf-ray`, `request.cf`, method, and path.\n\n```typescript\nimport { createWorkersLogger } from 'evlog\u002Fworkers'\n\n\u002F\u002F ctx is the third argument to fetch(request, env, ctx)\nconst log = createWorkersLogger(request, {\n  requestId: 'custom-id',      \u002F\u002F Override cf-ray (default: cf-ray header)\n  headers: ['x-request-id'],   \u002F\u002F Headers to include (default: none)\n  executionCtx: ctx,           \u002F\u002F With initWorkersLogger({ drain }), registers async drain via waitUntil\n})\n\n\u002F\u002F Or pass waitUntil directly: waitUntil: ctx.waitUntil.bind(ctx)\n\nlog.set({ user: { id: '123' } })\nlog.emit({ status: 200 })\n```\n\n### `createError(options)`\n\nCreate a structured error with HTTP status support. Import from `evlog` directly to avoid conflicts with Nuxt\u002FNitro's `createError`.\n\n> **Note**: `createEvlogError` is also available as an auto-imported alias in Nuxt\u002FNitro to avoid conflicts.\n\n```typescript\nimport { createError } from 'evlog'\n\ncreateError({\n  message: string   \u002F\u002F What happened\n  status?: number   \u002F\u002F HTTP status code (default: 500)\n  why?: string      \u002F\u002F Why it happened\n  fix?: string      \u002F\u002F How to fix it\n  link?: string     \u002F\u002F Documentation URL\n  cause?: Error     \u002F\u002F Original error\n  internal?: Record\u003Cstring, unknown>  \u002F\u002F Backend-only; never in HTTP body or toJSON()\n})\n```\n\n**`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`.\n\n### `parseError(error)`\n\nParse a caught error into a flat structure with all evlog fields. Auto-imported in Nuxt.\n\n```typescript\nimport { parseError } from 'evlog'\n\ntry {\n  await $fetch('\u002Fapi\u002Fcheckout')\n} catch (err) {\n  const error = parseError(err)\n\n  \u002F\u002F Direct access to all fields\n  console.log(error.message)  \u002F\u002F \"Payment failed\"\n  console.log(error.status)   \u002F\u002F 402\n  console.log(error.why)      \u002F\u002F \"Card declined\"\n  console.log(error.fix)      \u002F\u002F \"Try another card\"\n  console.log(error.link)     \u002F\u002F \"https:\u002F\u002Fdocs.example.com\u002F...\"\n\n  \u002F\u002F Use with toast\n  toast.add({\n    title: error.message,\n    description: error.why,\n    color: 'error',\n  })\n}\n```\n\n## Framework Support\n\n| Framework | Integration |\n|-----------|-------------|\n| **Nuxt** | `modules: ['evlog\u002Fnuxt']` |\n| **Next.js** | `createEvlog()` factory with `import { createEvlog } from 'evlog\u002Fnext'` ([example](.\u002Fexamples\u002Fnextjs)) |\n| **SvelteKit** | `export const { handle, handleError } = createEvlogHooks()` with `import { createEvlogHooks } from 'evlog\u002Fsveltekit'` ([example](.\u002Fexamples\u002Fsveltekit)) |\n| **Nitro v3** | `modules: [evlog()]` with `import evlog from 'evlog\u002Fnitro\u002Fv3'` |\n| **Nitro v2** | `modules: [evlog()]` with `import evlog from 'evlog\u002Fnitro'` |\n| **TanStack Start** | Nitro v3 module setup ([example](.\u002Fexamples\u002Ftanstack-start)) |\n| **React Router** | `evlog()` middleware with `import { evlog } from 'evlog\u002Freact-router'` ([example](.\u002Fexamples\u002Freact-router)) |\n| **NestJS** | `EvlogModule.forRoot()` with `import { EvlogModule } from 'evlog\u002Fnestjs'` ([example](.\u002Fexamples\u002Fnestjs)) |\n| **Express** | `app.use(evlog())` with `import { evlog } from 'evlog\u002Fexpress'` ([example](.\u002Fexamples\u002Fexpress)) |\n| **Hono** | `app.use(evlog())` with `import { evlog } from 'evlog\u002Fhono'` ([example](.\u002Fexamples\u002Fhono)) |\n| **Fastify** | `app.register(evlog)` with `import { evlog } from 'evlog\u002Ffastify'` ([example](.\u002Fexamples\u002Ffastify)) |\n| **Elysia** | `.use(evlog())` with `import { evlog } from 'evlog\u002Felysia'` ([example](.\u002Fexamples\u002Felysia)) |\n| **Cloudflare Workers** | Manual setup with `import { initWorkersLogger, createWorkersLogger } from 'evlog\u002Fworkers'` ([example](.\u002Fexamples\u002Fworkers)) |\n| **Custom** | Build your own with `import { createMiddlewareLogger } from 'evlog\u002Ftoolkit'` ([guide](https:\u002F\u002Fevlog.dev\u002Fextend\u002Fcustom-framework)) |\n| **Analog** | Nitro v2 module setup |\n| **Vinxi** | Nitro v2 module setup |\n| **SolidStart** | Nitro v2 module setup ([example](.\u002Fexamples\u002Fsolidstart)) |\n\n## Agent Skills\n\nevlog provides [Agent Skills](https:\u002F\u002Fwww.evlog.dev\u002Freference\u002Fagent-skills) to help AI coding assistants understand and implement proper logging patterns in your codebase.\n\n### Installation\n\n```bash\nnpx skills add https:\u002F\u002Fwww.evlog.dev\n```\n\n### What it does\n\nOnce installed, your AI assistant will:\n- Review your logging code and suggest wide event patterns\n- Help refactor scattered `console.log` calls into structured events\n- Guide you to use `createError()` for self-documenting errors\n- Ensure proper use of `useLogger(event)` in Nuxt\u002FNitro routes\n\n### Examples\n\n```\nAdd logging to this endpoint\nReview my logging code\nHelp me set up logging for this service\n```\n\n## Philosophy\n\nInspired by [Logging Sucks](https:\u002F\u002Floggingsucks.com\u002F) by [Boris Tane](https:\u002F\u002Fx.com\u002Fboristane).\n\n1. **Wide Events**: One log per request with all context\n2. **Structured Errors**: Errors that explain themselves\n3. **Request Scoping**: Accumulate context, emit once\n4. **Pretty for Dev, JSON for Prod**: Human-readable locally, machine-parseable in production\n\n## License\n\n[MIT](.\u002FLICENSE)\n\nMade by [@HugoRCD](https:\u002F\u002Fgithub.com\u002FHugoRCD)\n","evlog 是一个用于提升应用可观测性的日志记录库。它通过为每个操作生成一个宽事件，包含所有上下文信息，从而简化了错误排查过程。该工具支持 TypeScript，并且能够自动生成结构化的错误信息，明确指出问题原因及解决建议。特别适合在复杂的生产环境中使用，尤其是在需要快速定位和解决问题时。此外，evlog 产生的日志格式对 AI 辅助开发友好，易于解析和理解，有助于提高调试效率。",2,"2026-06-11 03:51:49","high_star"]