review-logging-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Review logging patterns

审查日志模式

Review and improve logging patterns in TypeScript/JavaScript codebases. Transform scattered console.logs into structured wide events and convert generic errors into self-documenting structured errors.
审查并改进 TypeScript/JavaScript 代码库中的日志模式。将分散的 console.log 转换为结构化 wide event,并将通用错误转换为可自我说明的 structured error。

When to Use

使用场景

  • Setting up evlog in a new or existing project (any supported framework)
  • Reviewing code for logging best practices
  • Converting console.log statements to structured logging
  • Improving error handling with better context
  • Configuring log draining, sampling, or enrichment
  • 为新的或现有项目(任何受支持框架)设置 evlog
  • 审查代码是否符合日志最佳实践
  • 将 console.log 语句转换为结构化日志
  • 通过补充上下文改进错误处理
  • 配置日志 drain、sampling 或 enrichment

Quick Reference

快速参考

Working on...Resource
Setup (CLI)
evlog init
: wire evlog into the project
Project conventions (CLI)
evlog agents
: write the evlog block into the project's AGENTS.md
Coverage map (CLI)
evlog map
: score dark entry points
CI gating (CLI)
evlog map --min-score / --baseline
: gate regressions
Wide events patternsreferences/wide-events.md
Error handlingreferences/structured-errors.md
Code review checklistreferences/code-review.md
Drain pipelinereferences/drain-pipeline.md
Audit logsbuild-audit-logs skill + docs
正在处理...资源
设置(CLI)
evlog init
:将 evlog 接入项目
项目约定(CLI)
evlog agents
:将 evlog 配置块写入项目的 AGENTS.md
覆盖率地图(CLI)
evlog map
:评估未覆盖的 entry point
CI gate(CLI)
evlog map --min-score / --baseline
:阻止回归
Wide event 模式references/wide-events.md
错误处理references/structured-errors.md
代码审查清单references/code-review.md
Drain pipelinereferences/drain-pipeline.md
审计日志build-audit-logs skill + docs

Audit logs

审计日志

For security-sensitive actions (auth, billing, admin, data export), use evlog's audit layer: a typed
audit
field on wide events, not a parallel logger. See the
build-audit-logs
skill for end-to-end setup (
log.audit
,
withAudit
, denials,
auditEnricher
,
auditOnly
,
signed
,
mockAudit
).
typescript
log.audit({
  action: 'invoice.refund',
  actor: { type: 'user', id: user.id },
  target: { type: 'invoice', id: invoice.id },
  outcome: 'success',
})
对于安全敏感操作(认证、计费、管理、数据导出),请使用 evlog 的 audit layer:在 wide event 中使用类型化的
audit
字段,而不是建立并行 logger。有关端到端设置(
log.audit
、
withAudit
、拒绝事件、
auditEnricher
、
auditOnly
、
signed
、
mockAudit
),请参阅
build-audit-logs
skill。
typescript
log.audit({
  action: 'invoice.refund',
  actor: { type: 'user', id: user.id },
  target: { type: 'invoice', id: invoice.id },
  outcome: 'success',
})

Installation

安装

bash
npm install evlog
bash
npm install evlog

Use the CLI (recommended on Nuxt, Nitro, Next.js, TanStack Start, Hono, Express, Fastify)

使用 CLI(推荐用于 Nuxt、Nitro、Next.js、TanStack Start、Hono、Express、Fastify)

The
evlog
executable ships with the
evlog
package:
pnpm evlog
,
npx evlog
,
bunx evlog
. It runs
@evlog/cli
when installed and fetches it on demand otherwise (no dependency added to the project), so it works on a project with nothing yet. Early but worth trying. It reads the project on disk (no traffic, no config needed). On the seven supported frameworks it covers the whole loop: wire evlog in (
init
), score coverage (
map
), lock the score in CI (
--min-score
,
--baseline
). If the CLI is unavailable, the framework has no adapter yet, or the user declines, continue with the manual sections below; the skill does not depend on it. Ask before installing anything.
evlog
可执行文件随
evlog
包一同提供:可使用
pnpm evlog
、
npx evlog
或
bunx evlog
。安装后它会运行
@evlog/cli
,否则会按需获取该 CLI(不会向项目添加依赖),因此即使项目目前没有任何内容也可以使用。虽然仍处于早期阶段,但值得一试。它会读取磁盘上的项目(无需流量或配置)。对于七个受支持的框架,它覆盖完整流程:接入 evlog(
init
)、评估覆盖率(
map
)、在 CI 中锁定评分(
--min-score
、
--baseline
)。如果 CLI 不可用、框架尚无 adapter,或者用户拒绝使用,请继续采用下面的手动章节;该 skill 不依赖 CLI。安装任何内容前务必询问。

1. Setup:
evlog init

1. 设置:
evlog init

On a project that doesn't use evlog yet, prefer
init
over hand-writing the setup, since it detects the framework, reads what the project already has, and generates config, drains, enrichers, and extras in one pass. It is fully scriptable for agents:
bash
undefined
对于尚未使用 evlog 的项目,应优先使用
init
,而不是手动编写设置,因为它可以检测框架、读取项目已有内容,并一次性生成配置、drain、enricher 和其他附加项。它针对 Agent 完全可脚本化:
bash
undefined

preview everything without writing (always start here)

preview everything without writing (always start here)

npx evlog init --dry-run --yes
npx evlog init --dry-run --yes

then apply — flags instead of prompts

then apply — flags instead of prompts

npx evlog init --yes
--service my-app
--drain fs
--prodDrain axiom
--extras enrichers,pipeline,sampling
--sampling medium

Useful flags: `--framework` (override detection: `nuxt`, `nitro`, `next`, `tanstack-start`, `hono`, `express`, `fastify`), `--prodDrain` (comma-separated: `axiom`, `otlp`, `posthog`, `sentry`, `better-stack`, `datadog`, `hyperdx`, `sazabi`, `loki`, `clickhouse`), `--extras` (`enrichers`, `pipeline`, `sampling`, `vite`, `error-catalog`, `audit-catalog`, `ai`, `better-auth`), `--enrichers`, `--sampling` (traffic tier: `all`, `low`, `medium`, `high`, `very-high`), `--apps` (monorepo: which workspace packages), `--no-install`. Review the `--dry-run` output with the user before applying. Docs: https://www.evlog.dev/cli/init
npx evlog init --yes
--service my-app
--drain fs
--prodDrain axiom
--extras enrichers,pipeline,sampling
--sampling medium

常用参数:`--framework`(覆盖自动检测:`nuxt`、`nitro`、`next`、`tanstack-start`、`hono`、`express`、`fastify`)、`--prodDrain`(以逗号分隔:`axiom`、`otlp`、`posthog`、`sentry`、`better-stack`、`datadog`、`hyperdx`、`sazabi`、`loki`、`clickhouse`)、`--extras`(`enrichers`、`pipeline`、`sampling`、`vite`、`error-catalog`、`audit-catalog`、`ai`、`better-auth`)、`--enrichers`、`--sampling`(流量级别:`all`、`low`、`medium`、`high`、`very-high`)、`--apps`(monorepo:要选择的 workspace package)、`--no-install`。应用前应与用户一起审查 `--dry-run` 输出。文档:https://www.evlog.dev/cli/init

2. Score:
evlog map

2. 评分:
evlog map

bash
npx evlog map --no-write
bash
npx evlog map --no-write

agents: npx evlog map --json --no-write

agents: npx evlog map --json --no-write


What you get:

- A project score and which entry points are still dark
- **FIX FIRST**: the three most valuable places to fix
- **GOING FURTHER**: opportunities (catalogs, audit coverage, AI logging, auth identity) that never cost points
- Per-file inspect: `npx evlog map <file> --no-write` shows the shape the handler could take
- Re-run after fixes and watch the score move

Work FIX FIRST in order, keep changes minimal (`useLogger()`, `log.set()`, `log.audit()`, `createError({ why, fix })`), then re-run with `--no-write`. Omit `--no-write` only when the user wants `evlog.map.json` written.

你可以获得:

- 项目评分,以及仍未覆盖的 entry point
- **优先修复**:最值得修复的三个位置
- **进一步改进**:不会影响评分的机会(catalog、audit 覆盖率、AI 日志、认证身份)
- 按文件检查:`npx evlog map <file> --no-write` 会展示 handler 可以采用的结构
- 修复后重新运行并观察评分变化

请按顺序处理“优先修复”项,并将改动保持在最小范围(`useLogger()`、`log.set()`、`log.audit()`、`createError({ why, fix })`),然后使用 `--no-write` 重新运行。只有当用户希望写入 `evlog.map.json` 时,才省略 `--no-write`。

3. Lock it in CI:
--min-score
and
--baseline

3. 在 CI 中锁定:
--min-score
和
--baseline

On GitHub Actions, suggest the evlog action (
uses: evloghq/action@v1
with
min-score
): it scans the pull request base itself, so no map file has to be committed, and puts regressions on the diff and in one comment. The commands below are the same gate from a
run:
step.
After fixing, propose making the score durable. This is where the CLI earns its keep:
bash
undefined
在 GitHub Actions 中,建议使用 evlog action(
uses: evloghq/action@v1
并设置
min-score
):它会直接扫描 pull request 的 base,因此无需提交 map 文件,并会将回归标记到 diff 和一条评论中。下面的命令提供了从
run:
步骤执行同一 gate 的方式。
修复后,建议让评分具备持久性。这正是 CLI 的价值所在:
bash
undefined

in CI, after pnpm add -D @evlog/cli (pinned by the lockfile; ask first)

in CI, after pnpm add -D @evlog/cli (pinned by the lockfile; ask first)

pnpm exec evlog map --min-score 80 # absolute gate: exits 1 below the threshold pnpm exec evlog map --baseline # ratchet: exits 1 if this PR made things worse

`--baseline` compares the fresh scan against the committed `evlog.map.json`, **per entry point and per requirement**, so a refactor that instruments one route and breaks another fails even if the total score is unchanged. Disabling a passing check with a comment counts as a regression too. New uninstrumented routes are listed as `NEW AND DARK` without failing. Workflow: commit `evlog.map.json` once, add the `--baseline` run to CI, then re-run `map` without `--baseline` to accept an intentional change. Docs: https://www.evlog.dev/cli/ci

When the project has an `evlog.config.ts`, the gate can live there instead of in the CI step: `map: { minScore: 80, baseline: true }` makes a bare `evlog map` gate the same way, and a flag still wins. The same `map` block turns checks off for every entry point (`rules: { 'error-catalog': 'off' }`) and leaves entry points out by file glob (`ignore`). The CLI reads these values without running the file, so they must be literals. Docs: https://www.evlog.dev/cli/config

Early days: adapters and rules are still evolving; expect scores to move between releases. Docs: https://www.evlog.dev/cli/map · Rules: https://www.evlog.dev/cli/rules

---
pnpm exec evlog map --min-score 80 # absolute gate: exits 1 below the threshold pnpm exec evlog map --baseline # ratchet: exits 1 if this PR made things worse

`--baseline` 会将最新扫描结果与已提交的 `evlog.map.json` 进行比较,且比较粒度为**每个 entry point 和每项要求**,因此即使总分未发生变化,只要重构接入了一个 route 却破坏了另一个 route,检查也会失败。通过评论禁用一项原本通过的检查,同样会被计为回归。新增但未接入日志的 route 会列为 `NEW AND DARK`,但不会导致失败。流程:先提交一次 `evlog.map.json`,再将带 `--baseline` 的运行添加到 CI,然后在不带 `--baseline` 的情况下重新运行 `map`,以接受有意进行的变更。文档:https://www.evlog.dev/cli/ci

当项目包含 `evlog.config.ts` 时,也可以将 gate 放在该文件中,而不是 CI 步骤中:`map: { minScore: 80, baseline: true }` 会让普通的 `evlog map` 使用相同方式执行 gate,而命令行参数仍具有更高优先级。同一个 `map` 配置块还可以为所有 entry point 关闭检查(`rules: { 'error-catalog': 'off' }`),并通过 file glob 排除 entry point(`ignore`)。CLI 会在不运行文件的情况下读取这些值,因此它们必须是字面量。文档:https://www.evlog.dev/cli/config

目前仍处于早期阶段:adapter 和规则还在演进,因此不同版本之间的评分可能发生变化。文档:https://www.evlog.dev/cli/map · 规则:https://www.evlog.dev/cli/rules

---

Framework Setup

框架设置

Nuxt

Nuxt

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    env: { service: 'my-app' },
    include: ['/api/**'],
  },
})
useLogger
,
log
, and
parseError
are auto-imported.
createError
is not: a bare one resolves to h3's, which drops
why
,
fix
, and
link
. Import it from
evlog
.
typescript
// server/api/checkout.post.ts — no imports needed
export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  log.set({ user: { id: user.id, plan: user.plan } })
  return { success: true }
})
Drain, enrich, sampling and redaction can live in
evlog.config.ts
at the app root. The module loads it on the server, and the
evlog
key in
nuxt.config.ts
overrides it. The browser logger reads only the
evlog
key.
typescript
// evlog.config.ts
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlog({
  service: 'my-app',
  drain: createAxiomDrain(),
})
Nitro hooks in server plugins keep working next to the file, and an event reaches both drains:
typescript
// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
Client transport (auto-configured Vue plugin):
typescript
// nuxt.config.ts
evlog: {
  transport: { enabled: true },  // logs sent to /api/_evlog/ingest
}
Client-side:
log
,
setIdentity
,
clearIdentity
are auto-imported in components.
typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    env: { service: 'my-app' },
    include: ['/api/**'],
  },
})
useLogger
、
log
和
parseError
会自动导入。
createError
不会自动导入:直接使用会解析为 h3 的版本,从而丢失
why
、
fix
和
link
。应从
evlog
导入它。
typescript
// server/api/checkout.post.ts — no imports needed
export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  log.set({ user: { id: user.id, plan: user.plan } })
  return { success: true }
})
Drain、enrich、sampling 和 redaction 可以放在应用根目录的
evlog.config.ts
中。模块会在服务端加载它,而
nuxt.config.ts
中的
evlog
配置项会覆盖它。浏览器 logger 只读取
evlog
配置项。
typescript
// evlog.config.ts
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlog({
  service: 'my-app',
  drain: createAxiomDrain(),
})
Nitro hook 仍然可以在 server plugin 中与该文件配合使用,同一个事件会同时进入两个 drain:
typescript
// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
客户端 transport(由 Vue plugin 自动配置):
typescript
// nuxt.config.ts
evlog: {
  transport: { enabled: true },  // logs sent to /api/_evlog/ingest
}
客户端:组件中的
log
、
setIdentity
和
clearIdentity
会自动导入。

Next.js

Next.js

Step 1: Create central config. All exports come from here:
typescript
// lib/evlog.ts
import type { DrainContext } from 'evlog'
import { createEvlog } from 'evlog/next'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'
import { createDrainPipeline } from 'evlog/pipeline'

const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50, intervalMs: 5000 } })
const drain = pipeline(createAxiomDrain({ dataset: 'logs', apiKey: process.env.AXIOM_API_KEY! }))

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  sampling: {
    rates: { info: 10 },
    keep: [{ status: 400 }, { duration: 1000 }],
  },
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/checkout/**': { service: 'checkout-service' },
  },
  keep: (ctx) => {
    const user = ctx.context.user as { premium?: boolean } | undefined
    if (user?.premium) ctx.shouldKeep = true
  },
  enrich: (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  },
  drain,
})
Step 2: Wrap route handlers with
withEvlog()
:
typescript
// app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'

export const POST = withEvlog(async (request: Request) => {
  const log = useLogger()  // Zero arguments — uses AsyncLocalStorage
  log.set({ user: { id: 'user_123', plan: 'enterprise' } })
  log.set({ cart: { items: 3, total: 14999 } })
  return Response.json({ success: true })
})
Step 3: Server Actions. Same
withEvlog()
wrapper:
typescript
// app/actions.ts
'use server'
import { withEvlog, useLogger } from '@/lib/evlog'

export const checkout = withEvlog(async (formData: FormData) => {
  const log = useLogger()
  log.set({ action: 'checkout', source: 'server-action' })
  return { success: true }
})
Step 4: Middleware (optional, sets
x-request-id
+ timing headers):
typescript
// proxy.ts
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = { matcher: ['/api/:path*'] }
Step 5: Client Provider. Wrap the root layout:
tsx
// app/layout.tsx
import { EvlogProvider } from 'evlog/next/client'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <EvlogProvider service="my-app" transport={{ enabled: true, endpoint: '/api/evlog/ingest' }}>
          {children}
        </EvlogProvider>
      </body>
    </html>
  )
}
Step 6: Client logging. In any client component:
tsx
'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'

setIdentity({ userId: 'usr_123' })
log.info({ action: 'checkout_click' })
clearIdentity()
Step 7 (optional): Instrumentation. Startup plus global
onRequestError
(SSR/RSC errors outside
withEvlog
). Use
defineNodeInstrumentation(() => import('./lib/evlog'))
in root
instrumentation.ts
to gate Node + cache the import, or write
register
/
onRequestError
manually. Both are valid. For custom logic, wrap evlog’s
register
/
onRequestError
inside
lib/evlog.ts
(compose with your own init or metrics), then re-export.
Export
createInstrumentation()
from
lib/evlog.ts
alongside
createEvlog()
. See framework docs for coexistence with
lockLogger
.
Step 8: Client ingest endpoint. Receives client logs:
typescript
// app/api/evlog/ingest/route.ts
import { NextRequest } from 'next/server'

const VALID_LEVELS = ['info', 'error', 'warn', 'debug'] as const

export async function POST(request: NextRequest) {
  const origin = request.headers.get('origin')
  const host = request.headers.get('host')
  if (origin && new URL(origin).host !== host) {
    return Response.json({ error: 'Invalid origin' }, { status: 403 })
  }
  const body = await request.json()
  if (!body?.timestamp || !body?.level || !VALID_LEVELS.includes(body.level)) {
    return Response.json({ error: 'Invalid payload' }, { status: 400 })
  }
  const { service: _, ...sanitized } = body
  console.log('[CLIENT LOG]', JSON.stringify({ ...sanitized, service: 'my-app', source: 'client' }))
  return new Response(null, { status: 204 })
}
步骤 1:创建集中配置。 所有导出都来自这里:
typescript
// lib/evlog.ts
import type { DrainContext } from 'evlog'
import { createEvlog } from 'evlog/next'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'
import { createDrainPipeline } from 'evlog/pipeline'

const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50, intervalMs: 5000 } })
const drain = pipeline(createAxiomDrain({ dataset: 'logs', apiKey: process.env.AXIOM_API_KEY! }))

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  sampling: {
    rates: { info: 10 },
    keep: [{ status: 400 }, { duration: 1000 }],
  },
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/checkout/**': { service: 'checkout-service' },
  },
  keep: (ctx) => {
    const user = ctx.context.user as { premium?: boolean } | undefined
    if (user?.premium) ctx.shouldKeep = true
  },
  enrich: (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  },
  drain,
})
步骤 2:使用
withEvlog()
包装 route handler:
typescript
// app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'

export const POST = withEvlog(async (request: Request) => {
  const log = useLogger()  // Zero arguments — uses AsyncLocalStorage
  log.set({ user: { id: 'user_123', plan: 'enterprise' } })
  log.set({ cart: { items: 3, total: 14999 } })
  return Response.json({ success: true })
})
步骤 3:Server Action。 使用相同的
withEvlog()
wrapper:
typescript
// app/actions.ts
'use server'
import { withEvlog, useLogger } from '@/lib/evlog'

export const checkout = withEvlog(async (formData: FormData) => {
  const log = useLogger()
  log.set({ action: 'checkout', source: 'server-action' })
  return { success: true }
})
步骤 4:Middleware(可选,设置
x-request-id
和计时响应头):
typescript
// proxy.ts
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = { matcher: ['/api/:path*'] }
步骤 5:Client Provider。 包装根 layout:
tsx
// app/layout.tsx
import { EvlogProvider } from 'evlog/next/client'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <EvlogProvider service="my-app" transport={{ enabled: true, endpoint: '/api/evlog/ingest' }}>
          {children}
        </EvlogProvider>
      </body>
    </html>
  )
}
步骤 6:客户端日志。 在任意 client component 中:
tsx
'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'

setIdentity({ userId: 'usr_123' })
log.info({ action: 'checkout_click' })
clearIdentity()
步骤 7(可选):Instrumentation。 用于启动流程和全局
onRequestError
(处理
withEvlog
之外的 SSR/RSC 错误)。在根目录
instrumentation.ts
中使用
defineNodeInstrumentation(() => import('./lib/evlog'))
,以控制 Node 初始化并缓存 import;也可以手动编写
register
/
onRequestError
。两种方式都有效。若使用自定义逻辑,请将 evlog 的
register
/
onRequestError
包装在
lib/evlog.ts
内(与自己的初始化或 metrics 组合),然后重新导出。
在
lib/evlog.ts
中与
createEvlog()
一起导出
createInstrumentation()
。有关与
lockLogger
共存的信息,请参阅框架文档。
步骤 8:客户端 ingest endpoint。 用于接收客户端日志:
typescript
// app/api/evlog/ingest/route.ts
import { NextRequest } from 'next/server'

const VALID_LEVELS = ['info', 'error', 'warn', 'debug'] as const

export async function POST(request: NextRequest) {
  const origin = request.headers.get('origin')
  const host = request.headers.get('host')
  if (origin && new URL(origin).host !== host) {
    return Response.json({ error: 'Invalid origin' }, { status: 403 })
  }
  const body = await request.json()
  if (!body?.timestamp || !body?.level || !VALID_LEVELS.includes(body.level)) {
    return Response.json({ error: 'Invalid payload' }, { status: 400 })
  }
  const { service: _, ...sanitized } = body
  console.log('[CLIENT LOG]', JSON.stringify({ ...sanitized, service: 'my-app', source: 'client' }))
  return new Response(null, { status: 204 })
}

SvelteKit

SvelteKit

typescript
// src/hooks.server.ts
import { initLogger } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'

initLogger({ env: { service: 'my-app' } })

export const { handle, handleError } = createEvlogHooks()
Access the logger via
event.locals.log
in route handlers or
useLogger()
from anywhere in the call stack:
typescript
// src/routes/api/users/[id]/+server.ts
import { json } from '@sveltejs/kit'

export const GET = ({ locals, params }) => {
  locals.log.set({ user: { id: params.id } })
  return json({ id: params.id })
}
typescript
import { useLogger } from 'evlog/sveltekit'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

export const { handle, handleError } = createEvlogHooks({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
typescript
// src/hooks.server.ts
import { initLogger } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'

initLogger({ env: { service: 'my-app' } })

export const { handle, handleError } = createEvlogHooks()
可在 route handler 中通过
event.locals.log
访问 logger,或在调用栈中的任意位置使用
useLogger()
:
typescript
// src/routes/api/users/[id]/+server.ts
import { json } from '@sveltejs/kit'

export const GET = ({ locals, params }) => {
  locals.log.set({ user: { id: params.id } })
  return json({ id: params.id })
}
typescript
import { useLogger } from 'evlog/sveltekit'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

export const { handle, handleError } = createEvlogHooks({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})

Nitro v3

Nitro v3

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

export default defineConfig({
  modules: [evlog({ env: { service: 'my-api' } })],
})
typescript
// routes/api/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler(async (event) => {
  const log = useLogger(event)
  log.set({ action: 'checkout' })
  return { ok: true }
})
typescript
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  modules: [evlog({ env: { service: 'my-api' } })],
})
typescript
// routes/api/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler(async (event) => {
  const log = useLogger(event)
  log.set({ action: 'checkout' })
  return { ok: true }
})

TanStack Start

TanStack Start

TanStack Start uses Nitro v3. Install evlog and add a
nitro.config.ts
:
typescript
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  experimental: { asyncContext: true },
  modules: [evlog({ env: { service: 'my-app' } })],
})
Add the error handling middleware to
__root.tsx
:
typescript
// src/routes/__root.tsx
import { createMiddleware } from '@tanstack/react-start'
import { evlogErrorHandler } from 'evlog/nitro/v3'

export const Route = createRootRoute({
  server: {
    middleware: [createMiddleware().server(evlogErrorHandler)],
  },
})
Use
useRequest()
from
nitro/context
to access the logger:
typescript
import { useRequest } from 'nitro/context'
import type { RequestLogger } from 'evlog'

const req = useRequest()
const log = req.context.log as RequestLogger
log.set({ user: { id: 'user_123' } })
TanStack Start 使用 Nitro v3。安装 evlog 并添加
nitro.config.ts
:
typescript
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  experimental: { asyncContext: true },
  modules: [evlog({ env: { service: 'my-app' } })],
})
将错误处理 middleware 添加到
__root.tsx
:
typescript
// src/routes/__root.tsx
import { createMiddleware } from '@tanstack/react-start'
import { evlogErrorHandler } from 'evlog/nitro/v3'

export const Route = createRootRoute({
  server: {
    middleware: [createMiddleware().server(evlogErrorHandler)],
  },
})
使用
nitro/context
中的
useRequest()
访问 logger:
typescript
import { useRequest } from 'nitro/context'
import type { RequestLogger } from 'evlog'

const req = useRequest()
const log = req.context.log as RequestLogger
log.set({ user: { id: 'user_123' } })

Nitro v2

Nitro v2

typescript
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'

export default defineNitroConfig({
  modules: [evlog({ env: { service: 'my-api' } })],
})
Import
useLogger
from
evlog/nitro
in routes. On Nitro v2 and v3, an
evlog.config.ts
at the app root is loaded by the module, and the options passed to
evlog()
override it.
typescript
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'

export default defineNitroConfig({
  modules: [evlog({ env: { service: 'my-api' } })],
})
在 route 中从
evlog/nitro
导入
useLogger
。在 Nitro v2 和 v3 上,模块会加载应用根目录中的
evlog.config.ts
,传入
evlog()
的选项会覆盖其中的配置。

NestJS

NestJS

typescript
// src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'

@Module({
  imports: [EvlogModule.forRoot()],
})
export class AppModule {}
EvlogModule.forRoot()
registers a global middleware, on both
@nestjs/platform-express
and
@nestjs/platform-fastify
.
req.log
in controllers is the evlog logger on both platforms. Do not also register
evlog/fastify
on a Nest Fastify app, or every request emits two wide events. Use
useLogger()
to access the request-scoped logger from any controller or service:
typescript
import { useLogger } from 'evlog/nestjs'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

EvlogModule.forRoot({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
For async configuration with NestJS DI, use
forRootAsync()
:
typescript
EvlogModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config) => ({
    drain: createAxiomDrain({ apiKey: config.get('AXIOM_API_KEY') }),
  }),
})
typescript
// src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'

@Module({
  imports: [EvlogModule.forRoot()],
})
export class AppModule {}
EvlogModule.forRoot()
会在
@nestjs/platform-express
和
@nestjs/platform-fastify
上注册全局 middleware。在这两个平台中,controller 里的
req.log
都是 evlog logger。不要在 Nest Fastify 应用中再次注册
evlog/fastify
,否则每个请求都会产生两个 wide event。使用
useLogger()
可从任意 controller 或 service 访问 request-scoped logger:
typescript
import { useLogger } from 'evlog/nestjs'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

EvlogModule.forRoot({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
如需在 NestJS DI 中使用异步配置,请使用
forRootAsync()
:
typescript
EvlogModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config) => ({
    drain: createAxiomDrain({ apiKey: config.get('AXIOM_API_KEY') }),
  }),
})

Express

Express

typescript
import express from 'express'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/express'

initLogger({ env: { service: 'my-api' } })

const app = express()
app.use(evlog())

app.get('/api/users', (req, res) => {
  req.log.set({ users: { count: 42 } })
  res.json({ users: [] })
})
Use
useLogger()
to access the logger from anywhere in the call stack without passing
req
:
typescript
import { useLogger } from 'evlog/express'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
}))
typescript
import express from 'express'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/express'

initLogger({ env: { service: 'my-api' } })

const app = express()
app.use(evlog())

app.get('/api/users', (req, res) => {
  req.log.set({ users: { count: 42 } })
  res.json({ users: [] })
})
使用
useLogger()
可在调用栈中的任意位置访问 logger,而无需传递
req
:
typescript
import { useLogger } from 'evlog/express'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
}))

Hono

Hono

typescript
import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'

initLogger({ env: { service: 'my-api' } })

const app = new Hono<EvlogVariables>()
app.use(evlog())

app.get('/api/users', (c) => {
  const log = c.get('log')
  log.set({ users: { count: 42 } })
  return c.json({ users: [] })
})
Access the logger via
c.get('log')
in handlers. Use
useLogger()
from
evlog/hono
in the layers underneath (services, repositories) where
c
is not in hand. Both return the same logger:
typescript
import { useLogger } from 'evlog/hono'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
On Cloudflare Workers,
useLogger()
needs the
nodejs_compat
(or
nodejs_als
) compatibility flag;
c.get('log')
works with or without it.
Structured errors: throw
createError()
, then in
app.onError
use
parseError()
and pass
parsed.status as ContentfulStatusCode
to
c.json()
(Hono types the status argument as
ContentfulStatusCode
, not
number
).
typescript
import { createError, parseError } from 'evlog'
import type { ContentfulStatusCode } from 'hono/utils/http-status'

app.onError((error, c) => {
  c.get('log').error(error)
  const parsed = parseError(error)
  return c.json(
    { message: parsed.message, why: parsed.why, fix: parsed.fix, link: parsed.link },
    parsed.status as ContentfulStatusCode,
  )
})
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
}))
typescript
import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'

initLogger({ env: { service: 'my-api' } })

const app = new Hono<EvlogVariables>()
app.use(evlog())

app.get('/api/users', (c) => {
  const log = c.get('log')
  log.set({ users: { count: 42 } })
  return c.json({ users: [] })
})
在 handler 中通过
c.get('log')
访问 logger。在下层 layer(service、repository)中没有
c
时,使用
evlog/hono
中的
useLogger()
。两者返回同一个 logger:
typescript
import { useLogger } from 'evlog/hono'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
在 Cloudflare Workers 上,
useLogger()
需要
nodejs_compat
(或
nodejs_als
)compatibility flag;
c.get('log')
则无论是否启用均可使用。
Structured error:抛出
createError()
,然后在
app.onError
中使用
parseError()
,并将
parsed.status as ContentfulStatusCode
传给
c.json()
(Hono 会将 status 参数的类型定义为
ContentfulStatusCode
,而不是
number
)。
typescript
import { createError, parseError } from 'evlog'
import type { ContentfulStatusCode } from 'hono/utils/http-status'

app.onError((error, c) => {
  c.get('log').error(error)
  const parsed = parseError(error)
  return c.json(
    { message: parsed.message, why: parsed.why, fix: parsed.fix, link: parsed.link },
    parsed.status as ContentfulStatusCode,
  )
})
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
}))

Fastify

Fastify

typescript
import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/fastify'

initLogger({ env: { service: 'my-api' } })

const app = Fastify({ logger: false })
await app.register(evlog)

app.get('/api/users', async (request) => {
  request.log.set({ users: { count: 42 } })
  return { users: [] }
})
request.log
is the evlog wide-event logger (shadows Fastify's built-in pino logger on the request). Fastify's pino logger remains accessible via
fastify.log
.
Use
useLogger()
to access the logger from anywhere in the call stack without passing
request
:
typescript
import { useLogger } from 'evlog/fastify'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

await app.register(evlog, {
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
typescript
import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/fastify'

initLogger({ env: { service: 'my-api' } })

const app = Fastify({ logger: false })
await app.register(evlog)

app.get('/api/users', async (request) => {
  request.log.set({ users: { count: 42 } })
  return { users: [] }
})
request.log
是 evlog 的 wide-event logger(会覆盖 request 上 Fastify 内置的 pino logger)。仍可通过
fastify.log
访问 Fastify 的 pino logger。
使用
useLogger()
可在调用栈中的任意位置访问 logger,而无需传递
request
:
typescript
import { useLogger } from 'evlog/fastify'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

await app.register(evlog, {
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})

Elysia

Elysia

typescript
import { Elysia } from 'elysia'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/elysia'

initLogger({ env: { service: 'my-api' } })

const app = new Elysia()
  .use(evlog())
  .get('/api/users', ({ log }) => {
    log.set({ users: { count: 42 } })
    return { users: [] }
  })
  .listen(3000)
Use
useLogger()
to access the logger from anywhere in the call stack:
typescript
import { useLogger } from 'evlog/elysia'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
}))
typescript
import { Elysia } from 'elysia'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/elysia'

initLogger({ env: { service: 'my-api' } })

const app = new Elysia()
  .use(evlog())
  .get('/api/users', ({ log }) => {
    log.set({ users: { count: 42 } })
    return { users: [] }
  })
  .listen(3000)
使用
useLogger()
可在调用栈中的任意位置访问 logger:
typescript
import { useLogger } from 'evlog/elysia'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
}))

React Router

React Router

typescript
// react-router.config.ts
import type { Config } from '@react-router/dev/config'

export default {
  future: {
    v8_middleware: true,
  },
} satisfies Config
typescript
// app/root.tsx
import { initLogger } from 'evlog'
import { evlog } from 'evlog/react-router'

initLogger({ env: { service: 'my-api' } })

export const middleware: Route.MiddlewareFunction[] = [
  evlog(),
]
Access the logger via
context.get(loggerContext)
in loaders and actions:
typescript
// app/routes/api.users.$id.tsx
import { loggerContext } from 'evlog/react-router'

export async function loader({ params, context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ user: { id: params.id } })
  return { users: [] }
}
Use
useLogger()
to access the logger from anywhere in the call stack without passing context:
typescript
import { useLogger } from 'evlog/react-router'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    include: ['/api/**'],
    drain: createAxiomDrain(),
    enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
    keep: (ctx) => {
      if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
    },
  }),
]
typescript
// react-router.config.ts
import type { Config } from '@react-router/dev/config'

export default {
  future: {
    v8_middleware: true,
  },
} satisfies Config
typescript
// app/root.tsx
import { initLogger } from 'evlog'
import { evlog } from 'evlog/react-router'

initLogger({ env: { service: 'my-api' } })

export const middleware: Route.MiddlewareFunction[] = [
  evlog(),
]
在 loader 和 action 中通过
context.get(loggerContext)
访问 logger:
typescript
// app/routes/api.users.$id.tsx
import { loggerContext } from 'evlog/react-router'

export async function loader({ params, context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ user: { id: params.id } })
  return { users: [] }
}
使用
useLogger()
可在调用栈中的任意位置访问 logger,而无需传递 context:
typescript
import { useLogger } from 'evlog/react-router'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    include: ['/api/**'],
    drain: createAxiomDrain(),
    enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
    keep: (ctx) => {
      if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
    },
  }),
]

oRPC

oRPC

typescript
import { os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { initLogger } from 'evlog'
import { evlog, withEvlog, type EvlogOrpcContext } from 'evlog/orpc'

initLogger({ env: { service: 'my-rpc' } })

const base = os.$context<EvlogOrpcContext>().use(evlog())

const router = {
  ping: base.handler(({ context }) => {
    context.log.set({ pinged: true })
    return { ok: true }
  }),
}

const handler = withEvlog(new RPCHandler(router))

export default async function fetch(request: Request) {
  const { matched, response } = await handler.handle(request, { prefix: '/rpc' })
  return matched ? response : new Response('Not Found', { status: 404 })
}
withEvlog()
wraps the handler so each matched request emits one wide event;
os.use(evlog())
exposes
context.log
on every procedure that descends from
base
and tags the wide event with
operation
(the procedure path joined with
.
).
Use
useLogger()
to access the logger from utility modules:
typescript
import { useLogger } from 'evlog/orpc'

async function chargeCard(amount: number) {
  const log = useLogger()
  log.set({ payment: { amount } })
}
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

const handler = withEvlog(new RPCHandler(router), {
  include: ['/rpc/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
typescript
import { os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { initLogger } from 'evlog'
import { evlog, withEvlog, type EvlogOrpcContext } from 'evlog/orpc'

initLogger({ env: { service: 'my-rpc' } })

const base = os.$context<EvlogOrpcContext>().use(evlog())

const router = {
  ping: base.handler(({ context }) => {
    context.log.set({ pinged: true })
    return { ok: true }
  }),
}

const handler = withEvlog(new RPCHandler(router))

export default async function fetch(request: Request) {
  const { matched, response } = await handler.handle(request, { prefix: '/rpc' })
  return matched ? response : new Response('Not Found', { status: 404 })
}
withEvlog()
包装 handler,使每个匹配的请求产生一个 wide event;
os.use(evlog())
会向所有从
base
派生的 procedure 暴露
context.log
,并使用以
.
连接的 procedure path 为 wide event 标记
operation
。
使用
useLogger()
可在 utility module 中访问 logger:
typescript
import { useLogger } from 'evlog/orpc'

async function chargeCard(amount: number) {
  const log = useLogger()
  log.set({ payment: { amount } })
}
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

const handler = withEvlog(new RPCHandler(router), {
  include: ['/rpc/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})

Effect

Effect

Effect 4 (
HttpRouter
,
HttpApi
).
routerLayer
installs the
evlog()
middleware on every route and provides the
EvlogLogger
service.
initLoggerLayer
calls
initLogger
when the layer builds and flushes a
createDrainPipeline
drain when its scope closes:
typescript
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', '/api/users', Effect.gen(function* () {
  const log = yield* EvlogLogger
  log.set({ users: { count: 42 } })
  return HttpServerResponse.jsonUnsafe({ users: [] })
}))

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))
There is no
AsyncLocalStorage
and no
log.fork()
. The event lives in the Effect context:
yield* EvlogLogger
in handlers and services (the requirement shows up in the effect's type),
yield* useLogger<Fields>()
for typed fields, and
yield* set(fields)
in library code that should not require
EvlogLogger
(a no-op outside a unit). It runs on Cloudflare Workers without
nodejs_compat
.
typescript
import { Effect } from 'effect'
import { useLogger } from 'evlog/effect'

const chargeCard = (amount: number) => Effect.gen(function* () {
  const log = yield* useLogger<{ payment: { amount: number } }>()
  log.set({ payment: { amount } })
})
Failures are read from the
Cause
(the primary failure as
error
, the rest under
error.suppressed
, a pure interruption as
status: 499
). Effect's router answers a failed
createError({ status: 402 })
with 500, so catch it in the handler with
Effect.catch
, call
log.error(error)
, and return
HttpServerResponse.jsonUnsafe(body, { status: parsed.status })
built from
parseError(error)
.
HttpApi
encodes declared errors as responses before the middleware: the event gets the status but no
error
field.
Non-HTTP work (jobs, queue consumers, Durable Object alarms) uses
.pipe(withLogger('action-name', options))
. A Durable Object
fetch
uses
.pipe(withRequestLogger(request, options))
. On Workers, provide
WaitUntil
per request (
handler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx)))
with
HttpRouter.toWebHandler
) so drains outlive the response.
loggerLayer()
folds
Effect.log*
calls into the current wide event. Each unit continues an incoming
traceparent
, and
withTracePropagation(client)
and
traceHeaders
forward it on outgoing calls.
Full pipeline with drain, enrich, and tail sampling:
typescript
import { createAxiomDrain } from 'evlog/axiom'

routerLayer({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
Effect 4(
HttpRouter
、
HttpApi
)。
routerLayer
会在每个 route 上安装
evlog()
middleware,并提供
EvlogLogger
service。
initLoggerLayer
会在 layer 构建时调用
initLogger
,并在 scope 关闭时 flush
createDrainPipeline
drain:
typescript
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', '/api/users', Effect.gen(function* () {
  const log = yield* EvlogLogger
  log.set({ users: { count: 42 } })
  return HttpServerResponse.jsonUnsafe({ users: [] })
}))

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))
它没有
AsyncLocalStorage
,也没有
log.fork()
。Event 存在于 Effect context 中:在 handler 和 service 中使用
yield* EvlogLogger
(该要求会反映在 effect 的类型中),使用
yield* useLogger<Fields>()
获取类型化字段,在不需要
EvlogLogger
的 library code 中使用
yield* set(fields)
(在 unit 之外为 no-op)。它无需
nodejs_compat
即可在 Cloudflare Workers 上运行。
typescript
import { Effect } from 'effect'
import { useLogger } from 'evlog/effect'

const chargeCard = (amount: number) => Effect.gen(function* () {
  const log = yield* useLogger<{ payment: { amount: number } }>()
  log.set({ payment: { amount } })
})
Failure 从
Cause
中读取(主要 failure 为
error
,其余位于
error.suppressed
下,纯 interruption 对应
status: 499
)。Effect router 会将失败的
createError({ status: 402 })
响应为 500,因此应在 handler 中使用
Effect.catch
捕获它,调用
log.error(error)
,并根据
parseError(error)
构建
HttpServerResponse.jsonUnsafe(body, { status: parsed.status })
。
HttpApi
会在 middleware 之前将已声明的 error 编码为 response,因此 event 会获得 status,但不会包含
error
字段。
非 HTTP 工作(job、queue consumer、Durable Object alarm)使用
.pipe(withLogger('action-name', options))
。Durable Object 的
fetch
使用
.pipe(withRequestLogger(request, options))
。在 Workers 上,应为每个请求提供
WaitUntil
(
handler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx)))
配合
HttpRouter.toWebHandler
),以确保 drain 的生命周期超过 response。
loggerLayer()
会将
Effect.log*
调用合并到当前 wide event 中。每个 unit 都会继续传入的
traceparent
,而
withTracePropagation(client)
和
traceHeaders
会在传出调用中转发它。
包含 drain、enrich 和 tail sampling 的完整 pipeline:
typescript
import { createAxiomDrain } from 'evlog/axiom'

routerLayer({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})

Cloudflare Workers

Cloudflare Workers

typescript
import { initWorkersLogger, withEvlog } from 'evlog/workers'

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

export default withEvlog(async (request, _env, _ctx, log) => {
  log.set({ action: 'handle_request' })
  return Response.json({ ok: true })
})
withEvlog
emits one wide event per request when the handler returns, with no manual
log.emit()
. Async drains are registered with
waitUntil
so they survive the response; streaming responses defer the emit until the body completes.
requestId
comes from
x-request-id
(fallback
cf-ray
);
method
,
path
,
cf-ray
,
traceparent
, and the safe subset of
request.cf
are captured automatically. It accepts the same options (
drain
,
enrich
,
keep
,
include
,
exclude
,
routes
) as every other integration. For manual control (scheduled handlers, queues),
createWorkersLogger(request)
+
log.emit()
remains available. No ALS-based
useLogger()
on Workers, so pass
log
explicitly.
typescript
import { initWorkersLogger, withEvlog } from 'evlog/workers'

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

export default withEvlog(async (request, _env, _ctx, log) => {
  log.set({ action: 'handle_request' })
  return Response.json({ ok: true })
})
当 handler 返回时,
withEvlog
会为每个请求产生一个 wide event,无需手动调用
log.emit()
。异步 drain 会通过
waitUntil
注册,因此其生命周期可以超过 response;streaming response 会将 emit 延迟到 body 完成。
requestId
来自
x-request-id
(回退到
cf-ray
);
method
、
path
、
cf-ray
、
traceparent
和
request.cf
的安全子集会自动捕获。它接受与其他 integration 相同的选项(
drain
、
enrich
、
keep
、
include
、
exclude
、
routes
)。如需手动控制(scheduled handler、queue),仍可使用
createWorkersLogger(request)
+
log.emit()
。Workers 上没有基于 ALS 的
useLogger()
,因此需要显式传递
log
。

AWS Lambda

AWS Lambda

Lambda has no HTTP middleware lifecycle, so evlog behaves like standalone TypeScript, with one critical rule: one logger per invocation, never a shared module-level logger (Lambda reuses execution environments, so a shared instance leaks fields between invocations).
typescript
import { initLogger, createLogger } from 'evlog'

initLogger({ env: { service: 'my-fn' } })  // once at module load (cold start)

export async function handler(event: SQSEvent) {
  for (const record of event.Records) {
    const log = createLogger({ messageId: record.messageId })
    try {
      log.set({ queue: { source: record.eventSourceARN } })
      await processMessage(record)
    } catch (error) {
      log.error(error as Error)
      throw error
    } finally {
      log.emit()
    }
  }
}
Lambda 没有 HTTP middleware 生命周期,因此 evlog 的行为与独立 TypeScript 类似,但有一条关键规则:每次 invocation 使用一个 logger,绝不能使用共享的 module-level logger(Lambda 会复用 execution environment,共享实例会在不同 invocation 之间泄漏字段)。
typescript
import { initLogger, createLogger } from 'evlog'

initLogger({ env: { service: 'my-fn' } })  // once at module load (cold start)

export async function handler(event: SQSEvent) {
  for (const record of event.Records) {
    const log = createLogger({ messageId: record.messageId })
    try {
      log.set({ queue: { source: record.eventSourceARN } })
      await processMessage(record)
    } catch (error) {
      log.error(error as Error)
      throw error
    } finally {
      log.emit()
    }
  }
}

Astro

Astro

typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
import { initLogger, createRequestLogger } from 'evlog'

initLogger({ env: { service: 'my-astro-app' } })

export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
  const url = new URL(request.url)
  const log = createRequestLogger({ method: request.method, path: url.pathname })
  locals.log = log

  try {
    const response = await next()
    log.emit()
    return response
  } catch (error) {
    log.error(error instanceof Error ? error : new Error(String(error)))
    log.emit()
    throw error
  }
})
Type
locals.log
in
src/env.d.ts
(
interface Locals { log: RequestLogger }
). Pair with the Vite plugin (below) for auto-imports and build-time DX.
typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
import { initLogger, createRequestLogger } from 'evlog'

initLogger({ env: { service: 'my-astro-app' } })

export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
  const url = new URL(request.url)
  const log = createRequestLogger({ method: request.method, path: url.pathname })
  locals.log = log

  try {
    const response = await next()
    log.emit()
    return response
  } catch (error) {
    log.error(error instanceof Error ? error : new Error(String(error)))
    log.emit()
    throw error
  }
})
在
src/env.d.ts
中为
locals.log
添加类型(
interface Locals { log: RequestLogger }
)。可结合下方 Vite plugin 使用,以获得自动导入和构建时 DX。

Vite Plugin (any Vite-based framework)

Vite Plugin(任何基于 Vite 的框架)

For any Vite-based project (SvelteKit, Astro, SolidStart, React+Vite, etc.), use the Vite plugin for auto-init, auto-imports, and build-time features:
typescript
// vite.config.ts
import evlog from 'evlog/vite'

export default defineConfig({
  plugins: [
    evlog({
      service: 'my-app',
      autoImports: true,           // auto-import log, createEvlogError, parseError
      strip: ['debug'],            // remove log.debug() in production
      sourceLocation: true,        // inject file:line in dev + prod
      client: {                    // client-side logging
        transport: { endpoint: '/api/logs' },
      },
    }),
  ],
})
Server-side middleware (drain, enrich, keep, routes) is still configured in the framework integration (e.g.,
evlog()
middleware for Hono/Express/SvelteKit). The Vite plugin handles build-time DX only.
对于任何基于 Vite 的项目(SvelteKit、Astro、SolidStart、React+Vite 等),可使用 Vite plugin 实现自动初始化、自动导入和构建时功能:
typescript
// vite.config.ts
import evlog from 'evlog/vite'

export default defineConfig({
  plugins: [
    evlog({
      service: 'my-app',
      autoImports: true,           // auto-import log, createEvlogError, parseError
      strip: ['debug'],            // remove log.debug() in production
      sourceLocation: true,        // inject file:line in dev + prod
      client: {                    // client-side logging
        transport: { endpoint: '/api/logs' },
      },
    }),
  ],
})
服务端 middleware(drain、enrich、keep、routes)仍需在框架 integration 中配置(例如 Hono/Express/SvelteKit 的
evlog()
middleware)。Vite plugin 仅负责构建时 DX。

Standalone TypeScript

独立 TypeScript

typescript
import { initLogger, createRequestLogger } from 'evlog'

initLogger({ env: { service: 'my-worker', environment: 'production' } })

const log = createRequestLogger({ jobId: job.id })
log.set({ source: job.source, recordsSynced: 150 })
log.emit()  // Manual emit required in standalone

typescript
import { initLogger, createRequestLogger } from 'evlog'

initLogger({ env: { service: 'my-worker', environment: 'production' } })

const log = createRequestLogger({ jobId: job.id })
log.set({ source: job.source, recordsSynced: 150 })
log.emit()  // Manual emit required in standalone

Configuration Options

配置选项

All options work in Nuxt (
evlog
key), Nitro (passed to
evlog()
), Next.js (
createEvlog()
), and standalone (
initLogger()
).
They can also live in
evlog.config.ts
, written with
defineEvlog()
. Nuxt and Nitro load the file on their own, with module options overriding it. Other frameworks import it and pass
toLoggerConfig(config)
to
initLogger()
and
toMiddlewareOptions(config)
to the middleware.
evlog init
writes the file,
evlog config
prints what it resolves to, and
extends
shares a base config across apps (one level). Docs: https://www.evlog.dev/cli/config
OptionTypeDefaultDescription
env.service
/
service
string
'app'
Service name in logs
enabled
boolean
true
Global toggle (no-ops when false)
pretty
boolean
true
in dev
Pretty tree format vs JSON
silent
boolean
false
Suppress console output. Events still go to drains
include
string[]
All routesRoute glob patterns to log
exclude
string[]
NoneRoute patterns to exclude (takes precedence)
routes
Record<string, { service }>
--Route-specific service names
minLevel
'debug' | 'info' | 'warn' | 'error'
'debug'
Hard threshold for the global
log
API and client
log
(not request wide events). Use
sampling.rates
for probabilistic volume on requests
sampling.rates
object
--Head sampling:
{ info: 10, warn: 50 }
(0-100%)
sampling.keep
array
--Tail sampling:
[{ status: 400 }, { duration: 1000 }]
drain
(ctx) => void
--Drain callback (Next.js, standalone)
enrich
(ctx) => void
--Enrich callback (Next.js)
keep
(ctx) => void
--Custom tail sampling callback (Next.js)
redact
boolean | RedactConfig
true
in production
Enabled by default in production.
false
to disable. Object for fine-grained control
所有选项均可用于 Nuxt(
evlog
配置项)、Nitro(传给
evlog()
)、Next.js(
createEvlog()
)和独立环境(
initLogger()
)。
它们也可以放在通过
defineEvlog()
编写的
evlog.config.ts
中。Nuxt 和 Nitro 会自行加载该文件,module option 会覆盖其中的配置。其他框架需导入该文件,将
toLoggerConfig(config)
传给
initLogger()
,并将
toMiddlewareOptions(config)
传给 middleware。
evlog init
会写入该文件,
evlog config
会打印最终解析出的配置,
extends
可在多个应用之间共享基础配置(支持一级)。文档:https://www.evlog.dev/cli/config
选项类型默认值说明
env.service
/
service
string
'app'
日志中的 service 名称
enabled
boolean
true
全局开关(为 false 时不执行任何操作)
pretty
boolean
开发环境为
true
使用美化的 tree 格式或 JSON
silent
boolean
false
抑制 console 输出,event 仍会发送到 drain
include
string[]
所有 route要记录日志的 route glob 模式
exclude
string[]
无要排除的 route 模式(优先级更高)
routes
Record<string, { service }>
--route 专属 service 名称
minLevel
'debug' | 'info' | 'warn' | 'error'
'debug'
全局
log
API 和客户端
log
的硬阈值(不适用于 request wide event)。若要按概率控制 request 流量,请使用
sampling.rates
sampling.rates
object
--Head sampling:
{ info: 10, warn: 50 }
(0–100%)
sampling.keep
array
--Tail sampling:
[{ status: 400 }, { duration: 1000 }]
drain
(ctx) => void
--Drain callback(Next.js、独立环境)
enrich
(ctx) => void
--Enrich callback(Next.js)
keep
(ctx) => void
--自定义 tail sampling callback(Next.js)
redact
boolean | RedactConfig
生产环境为
true
默认在生产环境启用。可设为
false
禁用,也可通过 object 进行细粒度控制

Nitro Hooks (Nuxt, Nitro v2/v3)

Nitro Hook(Nuxt、Nitro v2/v3)

HookWhenUse
evlog:drain
After enrichmentSend events to external services
evlog:enrich
After emit, before console output and drainAdd derived context
evlog:emit:keep
During emitCustom tail sampling logic
close
Server shutdownFlush drain pipeline buffers

Hook时机用途
evlog:drain
Enrich 完成后将 event 发送到外部服务
evlog:enrich
Emit 后、console 输出和 drain 前添加派生上下文
evlog:emit:keep
Emit 期间自定义 tail sampling 逻辑
close
Server 关闭时Flush drain pipeline buffer

Drain Adapters

Drain Adapter

AdapterImportEnv Vars
Axiom
evlog/axiom
AXIOM_API_KEY
,
AXIOM_DATASET
OTLP
evlog/otlp
OTLP_ENDPOINT
(or the standard
OTEL_EXPORTER_OTLP_[LOGS_]ENDPOINT
,
_HEADERS
,
_COMPRESSION
,
_PROTOCOL
,
OTEL_RESOURCE_ATTRIBUTES
).
protocol: 'http/protobuf'
sends binary protobuf.
semanticConventions: true
adds OTel attribute names (
http.*
,
exception.*
,
gen_ai.*
)
HyperDX
evlog/hyperdx
HYPERDX_API_KEY
(optional
HYPERDX_OTLP_ENDPOINT
; defaults to
https://in-otel.hyperdx.io
)
Sazabi
evlog/sazabi
SAZABI_INTAKE_URL
(the log stream's intake URL; its key is part of the URL), optional
SAZABI_SERVICE_NAME
Grafana Loki
evlog/loki
LOKI_ENDPOINT
, optional
LOKI_API_KEY
,
LOKI_USER
ClickHouse
evlog/clickhouse
CLICKHOUSE_ENDPOINT
(the
evlog_events
table must exist), optional
CLICKHOUSE_USER
,
CLICKHOUSE_PASSWORD
,
CLICKHOUSE_DATABASE
,
CLICKHOUSE_TABLE
PostHog
evlog/posthog
POSTHOG_API_KEY
,
POSTHOG_HOST
Sentry
evlog/sentry
SENTRY_DSN
Better Stack
evlog/better-stack
BETTER_STACK_API_KEY
Datadog
evlog/datadog
DD_API_KEY
or
DATADOG_API_KEY
, optional
DD_SITE
/
DATADOG_LOGS_URL
Grafana Loki
evlog/loki
LOKI_ENDPOINT
, optional
LOKI_API_KEY
+
LOKI_USER
(Grafana Cloud) or
LOKI_TENANT_ID
(multi-tenant)
ClickHouse
evlog/clickhouse
CLICKHOUSE_ENDPOINT
, optional
CLICKHOUSE_USER
/
CLICKHOUSE_PASSWORD
/
CLICKHOUSE_DATABASE
/
CLICKHOUSE_TABLE
File System
evlog/fs
None (local file system)
Memory
evlog/memory
None (in-process ring buffer; optional
EVLOG_MEMORY_STORE
,
EVLOG_MEMORY_MAX_EVENTS
). Read back with
readMemoryLogs()
, ideal for dev-only log endpoints agents can query
NuxtHub
@evlog/nuxthub
(separate package, Nuxt module)
None: stores wide events in the NuxtHub database with retention-based cleanup (set
evlog.retention: '7d'
in the module options; accepts
d
/
h
/
m
)
HTTP (browser ingest)
evlog/http
None (configure
endpoint
in code).
evlog/browser
is deprecated; same API, removed next major
Use canonical env var names (e.g.
AXIOM_API_KEY
,
BETTER_STACK_API_KEY
), and the same names work in every framework.
Setup pattern per framework:
typescript
// Nuxt/Nitro: server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})

// Hono / Express / Elysia: pass drain in middleware options
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({ drain: createAxiomDrain() }))

// Fastify: pass drain in plugin options
import { createAxiomDrain } from 'evlog/axiom'
await app.register(evlog, { drain: createAxiomDrain() })

// NestJS: pass drain in module options
import { createAxiomDrain } from 'evlog/axiom'
EvlogModule.forRoot({ drain: createAxiomDrain() })

// Next.js: pass drain to createEvlog()
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50 } })
const drain = pipeline(createAxiomDrain())
// then: createEvlog({ ..., drain })

// Standalone: pass drain to initLogger()
initLogger({ env: { service: 'my-app' }, drain: createAxiomDrain() })
See references/drain-pipeline.md for batching, retry, and buffer overflow config.

AdapterImport环境变量
Axiom
evlog/axiom
AXIOM_API_KEY
、
AXIOM_DATASET
OTLP
evlog/otlp
OTLP_ENDPOINT
(或标准
OTEL_EXPORTER_OTLP_[LOGS_]ENDPOINT
、
_HEADERS
、
_COMPRESSION
、
_PROTOCOL
、
OTEL_RESOURCE_ATTRIBUTES
)。
protocol: 'http/protobuf'
会发送 binary protobuf。
semanticConventions: true
会添加 OTel attribute 名称(
http.*
、
exception.*
、
gen_ai.*
)
HyperDX
evlog/hyperdx
HYPERDX_API_KEY
(可选
HYPERDX_OTLP_ENDPOINT
;默认为
https://in-otel.hyperdx.io
)
Sazabi
evlog/sazabi
SAZABI_INTAKE_URL
(log stream 的 intake URL;key 是 URL 的一部分),可选
SAZABI_SERVICE_NAME
Grafana Loki
evlog/loki
LOKI_ENDPOINT
,可选
LOKI_API_KEY
、
LOKI_USER
ClickHouse
evlog/clickhouse
CLICKHOUSE_ENDPOINT
(必须存在
evlog_events
表),可选
CLICKHOUSE_USER
、
CLICKHOUSE_PASSWORD
、
CLICKHOUSE_DATABASE
、
CLICKHOUSE_TABLE
PostHog
evlog/posthog
POSTHOG_API_KEY
、
POSTHOG_HOST
Sentry
evlog/sentry
SENTRY_DSN
Better Stack
evlog/better-stack
BETTER_STACK_API_KEY
Datadog
evlog/datadog
DD_API_KEY
或
DATADOG_API_KEY
,可选
DD_SITE
/
DATADOG_LOGS_URL
Grafana Loki
evlog/loki
LOKI_ENDPOINT
,可选
LOKI_API_KEY
+
LOKI_USER
(Grafana Cloud)或
LOKI_TENANT_ID
(multi-tenant)
ClickHouse
evlog/clickhouse
CLICKHOUSE_ENDPOINT
,可选
CLICKHOUSE_USER
/
CLICKHOUSE_PASSWORD
/
CLICKHOUSE_DATABASE
/
CLICKHOUSE_TABLE
File System
evlog/fs
无(本地文件系统)
Memory
evlog/memory
无(进程内 ring buffer;可选
EVLOG_MEMORY_STORE
、
EVLOG_MEMORY_MAX_EVENTS
)。使用
readMemoryLogs()
读取,适合仅用于开发环境、可供 Agent 查询的 log endpoint
NuxtHub
@evlog/nuxthub
(独立 package、Nuxt module)
无:将 wide event 存储在 NuxtHub 数据库中,并根据保留期限清理(在 module option 中设置
evlog.retention: '7d'
;接受
d
/
h
/
m
)
HTTP(browser ingest)
evlog/http
无(在代码中配置
endpoint
)。
evlog/browser
已弃用;API 相同,并将在下一个主版本中移除
请使用标准环境变量名称(例如
AXIOM_API_KEY
、
BETTER_STACK_API_KEY
),这些名称在所有框架中通用。
各框架的设置模式:
typescript
// Nuxt/Nitro: server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})

// Hono / Express / Elysia: pass drain in middleware options
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({ drain: createAxiomDrain() }))

// Fastify: pass drain in plugin options
import { createAxiomDrain } from 'evlog/axiom'
await app.register(evlog, { drain: createAxiomDrain() })

// NestJS: pass drain in module options
import { createAxiomDrain } from 'evlog/axiom'
EvlogModule.forRoot({ drain: createAxiomDrain() })

// Next.js: pass drain to createEvlog()
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50 } })
const drain = pipeline(createAxiomDrain())
// then: createEvlog({ ..., drain })

// Standalone: pass drain to initLogger()
initLogger({ env: { service: 'my-app' }, drain: createAxiomDrain() })
有关 batching、retry 和 buffer overflow 配置,请参阅 references/drain-pipeline.md。

Enrichers

Enricher

Built-in:
createUserAgentEnricher()
,
createGeoEnricher()
,
createRequestSizeEnricher()
,
createTraceContextEnricher()
, all from
evlog/enrichers
. Each accepts
{ overwrite?: boolean }
(default
false
). Use
createDefaultEnrichers()
to compose all four in one call:
typescript
import { createDefaultEnrichers } from 'evlog/enrichers'
app.use(evlog({ enrich: createDefaultEnrichers() }))
typescript
// Nuxt/Nitro: server/plugins/evlog-enrich.ts
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'
export default defineNitroPlugin((nitroApp) => {
  const enrichers = [createUserAgentEnricher(), createGeoEnricher()]
  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  })
})

// Next.js: in lib/evlog.ts
createEvlog({
  enrich: (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
    ctx.event.region = process.env.VERCEL_REGION
  },
})

内置选项:
createUserAgentEnricher()
、
createGeoEnricher()
、
createRequestSizeEnricher()
、
createTraceContextEnricher()
,均可从
evlog/enrichers
导入。每个选项都接受
{ overwrite?: boolean }
(默认
false
)。使用
createDefaultEnrichers()
可在一次调用中组合全部四项:
typescript
import { createDefaultEnrichers } from 'evlog/enrichers'
app.use(evlog({ enrich: createDefaultEnrichers() }))
typescript
// Nuxt/Nitro: server/plugins/evlog-enrich.ts
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'
export default defineNitroPlugin((nitroApp) => {
  const enrichers = [createUserAgentEnricher(), createGeoEnricher()]
  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  })
})

// Next.js: in lib/evlog.ts
createEvlog({
  enrich: (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
    ctx.event.region = process.env.VERCEL_REGION
  },
})

Auto-Redaction (PII Protection)

自动脱敏(PII 保护)

Built-in redaction scrubs sensitive data from wide events before console output and before any drain sees the data. Enabled by default in production (
NODE_ENV === 'production'
), disabled in development. Uses smart partial masking, preserving enough context for debugging.
typescript
// Disable in production (opt-out)
evlog: { redact: false }

// Add custom paths on top of built-ins
evlog: {
  redact: {
    paths: ['user.password', 'headers.authorization'],
  }
}

// Only specific built-ins
evlog: {
  redact: {
    builtins: ['email', 'creditCard'],
  }
}

// No built-ins, only custom (uses flat [REDACTED] replacement)
evlog: {
  redact: {
    builtins: false,
    paths: ['user.ssn'],
    patterns: [/SECRET_\w+/g],
  }
}
Built-in patterns with smart masking output:
PatternExample InputMasked Output
creditCard
4111111111111111
****1111
email
alice@example.com
a***@***.com
ipv4
192.168.1.100
***.***.***.100
phone
+33 6 12 34 56 78
+33 ****5678
jwt
eyJhbGciOi...
eyJ***.***
bearer
Bearer sk_live_abc...
Bearer ***
iban
FR76 3000 6000 ...189
FR76****189
Opt-in credential patterns match the value under any key and inside longer strings. They are off by default, and
builtins
runs only the names it lists, so add them next to the defaults:
PatternExample InputMasked Output
apiKeys
ghp_
followed by 36 characters
ghp_***
privateKey
-----BEGIN RSA PRIVATE KEY-----
block
-----BEGIN RSA PRIVATE KEY-----***
urlPassword
postgres://app:hunter2@db
postgres://app:***@db
typescript
evlog: {
  redact: {
    builtins: ['creditCard', 'email', 'ipv4', 'phone', 'jwt', 'bearer', 'iban', 'secrets', 'apiKeys', 'privateKey', 'urlPassword'],
  }
}
apiKeys
covers GitHub, npm, Anthropic, OpenAI, Stripe, Slack and AWS key IDs by their fixed prefix. Suggest them when the code logs provider tokens, private keys or connection strings, even under keys that
secrets
does not cover (
apiKey
,
authorization
,
dsn
).
Works in all frameworks: Nuxt (
evlog
config), Nitro (
evlog()
module options), Next.js (
createEvlog()
), standalone (
initLogger()
), and all middleware integrations (Hono, Express, Fastify, Elysia, NestJS).

内置 redaction 会在 console 输出之前以及任何 drain 看到数据之前清除 wide event 中的敏感数据。生产环境默认启用(
NODE_ENV === 'production'
),开发环境中禁用。它使用智能部分遮罩,在保留足够调试上下文的同时隐藏敏感信息。
typescript
// Disable in production (opt-out)
evlog: { redact: false }

// Add custom paths on top of built-ins
evlog: {
  redact: {
    paths: ['user.password', 'headers.authorization'],
  }
}

// Only specific built-ins
evlog: {
  redact: {
    builtins: ['email', 'creditCard'],
  }
}

// No built-ins, only custom (uses flat [REDACTED] replacement)
evlog: {
  redact: {
    builtins: false,
    paths: ['user.ssn'],
    patterns: [/SECRET_\w+/g],
  }
}
内置模式及智能遮罩输出:
Pattern输入示例遮罩后的输出
creditCard
4111111111111111
****1111
email
alice@example.com
a***@***.com
ipv4
192.168.1.100
***.***.***.100
phone
+33 6 12 34 56 78
+33 ****5678
jwt
eyJhbGciOi...
eyJ***.***
bearer
Bearer sk_live_abc...
Bearer ***
iban
FR76 3000 6000 ...189
FR76****189
可选启用的 credential pattern 可匹配任意 key 下的值以及较长字符串内部的值。它们默认关闭;由于
builtins
仅运行列出的名称,因此应在默认项旁显式添加它们:
Pattern输入示例遮罩后的输出
apiKeys
ghp_
后跟 36 个字符
ghp_***
privateKey
-----BEGIN RSA PRIVATE KEY-----
block
-----BEGIN RSA PRIVATE KEY-----***
urlPassword
postgres://app:hunter2@db
postgres://app:***@db
typescript
evlog: {
  redact: {
    builtins: ['creditCard', 'email', 'ipv4', 'phone', 'jwt', 'bearer', 'iban', 'secrets', 'apiKeys', 'privateKey', 'urlPassword'],
  }
}
apiKeys
根据固定前缀覆盖 GitHub、npm、Anthropic、OpenAI、Stripe、Slack 和 AWS key ID。当代码记录 provider token、private key 或 connection string 时,即使其 key 不在
secrets
的覆盖范围内(例如
apiKey
、
authorization
、
dsn
),也应建议启用它。
适用于所有框架:Nuxt(
evlog
配置)、Nitro(
evlog()
module option)、Next.js(
createEvlog()
)、独立环境(
initLogger()
),以及所有 middleware integration(Hono、Express、Fastify、Elysia、NestJS)。

AI SDK Integration

AI SDK Integration

Capture token usage, tool calls, model info, streaming metrics, tool execution timing, cost estimation, and embedding metadata from the Vercel AI SDK into wide events. Import from
evlog/ai
. Requires
ai >=6.0.168 <8.0.0
as a peer dependency.
从 Vercel AI SDK 捕获 token 用量、tool call、模型信息、streaming metric、tool 执行耗时、成本估算和 embedding metadata,并写入 wide event。从
evlog/ai
导入。需要
ai >=6.0.168 <8.0.0
作为 peer dependency。

Basic setup (middleware)

基础设置(middleware)

typescript
import { createAILogger } from 'evlog/ai'

const log = useLogger(event) // or any RequestLogger
const ai = createAILogger(log)

const result = streamText({
  model: ai.wrap('anthropic/claude-sonnet-4.6'),  // accepts string or model object
  messages,
})
ai.wrap()
uses model middleware to transparently capture all LLM calls. Works with
generateText
,
streamText
, and
ToolLoopAgent
.
typescript
import { createAILogger } from 'evlog/ai'

const log = useLogger(event) // or any RequestLogger
const ai = createAILogger(log)

const result = streamText({
  model: ai.wrap('anthropic/claude-sonnet-4.6'),  // accepts string or model object
  messages,
})
ai.wrap()
使用 model middleware,以透明方式捕获所有 LLM call。可配合
generateText
、
streamText
和
ToolLoopAgent
使用。

Telemetry integration (deeper observability)

Telemetry integration(更深层可观测性)

For tool execution timing, success/failure tracking, and total generation wall time, add
createEvlogIntegration()
:
typescript
import { createAILogger, createEvlogIntegration } from 'evlog/ai'

const ai = createAILogger(log)

const agent = new ToolLoopAgent({
  model: ai.wrap('anthropic/claude-sonnet-4.6'),
  tools: { searchWeb, queryDatabase },
  stopWhen: stepCountIs(5),
  telemetry: {
    integrations: [createEvlogIntegration(ai)],
  },
})
This adds
ai.tools
(per-tool
{ name, durationMs, success, error? }
) and
ai.totalDurationMs
to the wide event.
若要获取 tool 执行耗时、成功/失败跟踪和总生成 wall time,请添加
createEvlogIntegration()
:
typescript
import { createAILogger, createEvlogIntegration } from 'evlog/ai'

const ai = createAILogger(log)

const agent = new ToolLoopAgent({
  model: ai.wrap('anthropic/claude-sonnet-4.6'),
  tools: { searchWeb, queryDatabase },
  stopWhen: stepCountIs(5),
  telemetry: {
    integrations: [createEvlogIntegration(ai)],
  },
})
这会向 wide event 添加
ai.tools
(每个 tool 包含
{ name, durationMs, success, error? }
)和
ai.totalDurationMs
。

Embeddings

Embedding

typescript
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage, model: 'text-embedding-3-small', dimensions: 1536 })
For
embedMany
, pass the batch count:
typescript
ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })
typescript
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage, model: 'text-embedding-3-small', dimensions: 1536 })
对于
embedMany
,传入 batch 数量:
typescript
ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })

Cost estimation

成本估算

Pass a pricing map to get
ai.estimatedCost
in the wide event:
typescript
const ai = createAILogger(log, {
  cost: {
    'claude-sonnet-4.6': { input: 3, output: 15 },
    'gpt-4o': { input: 2.5, output: 10 },
  },
})
传入 pricing map,以在 wide event 中获得
ai.estimatedCost
:
typescript
const ai = createAILogger(log, {
  cost: {
    'claude-sonnet-4.6': { input: 3, output: 15 },
    'gpt-4o': { input: 2.5, output: 10 },
  },
})

Wide event
ai
field

Wide event 的
ai
字段

Includes:
calls
,
model
,
provider
,
inputTokens
,
outputTokens
,
totalTokens
,
cacheReadTokens
,
reasoningTokens
,
finishReason
,
toolCalls
,
steps
,
msToFirstChunk
,
msToFinish
,
tokensPerSecond
,
error
,
tools
(via telemetry integration),
totalDurationMs
(via telemetry integration),
embedding
(via
captureEmbed
),
estimatedCost
(via
cost
option).
Anti-patterns to detect:
Anti-PatternFix
Manual token tracking in
onFinish
ai.wrap()
: middleware captures automatically
console.log('tokens:', result.usage)
ai.wrap()
: structured
ai.*
fields in wide event
No AI observabilityAdd
createAILogger(log)
+
ai.wrap()
No tool execution timingAdd
createEvlogIntegration(ai)
to
telemetry.integrations
Manual cost calculationUse
cost
option in
createAILogger()

包含:
calls
、
model
、
provider
、
inputTokens
、
outputTokens
、
totalTokens
、
cacheReadTokens
、
reasoningTokens
、
finishReason
、
toolCalls
、
steps
、
msToFirstChunk
、
msToFinish
、
tokensPerSecond
、
error
、
tools
(通过 telemetry integration)、
totalDurationMs
(通过 telemetry integration)、
embedding
(通过
captureEmbed
)、
estimatedCost
(通过
cost
option)。
需要检测的反模式:
反模式修复方式
在
onFinish
中手动跟踪 token
使用
ai.wrap()
:由 middleware 自动捕获
console.log('tokens:', result.usage)
使用
ai.wrap()
:在 wide event 中提供结构化
ai.*
字段
缺少 AI 可观测性添加
createAILogger(log)
+
ai.wrap()
缺少 tool 执行耗时将
createEvlogIntegration(ai)
添加到
telemetry.integrations
手动计算成本使用
createAILogger()
的
cost
option

Related Capabilities (pointers)

相关能力(指引)

Recommend these when the review surfaces the matching need. Each has full docs on evlog.dev:
Need spottedevlog answerDocs
Auth'd user identity missing from events (Better Auth apps)
evlog/better-auth
:
identifyUser()
,
createAuthMiddleware()
, client identity sync
https://www.evlog.dev/use-cases/better-auth/overview
Ad-hoc field names drifting across the codebaseTyped fields + error/audit catalogs (
evlog/catalog
)
https://www.evlog.dev/learn/typed-fields · https://www.evlog.dev/learn/catalogs
Cross-cutting hooks (request start/finish, client logs, logger extension)Plugins:
definePlugin
https://www.evlog.dev/extend/plugins
Tail logs live during dev / build a log viewer
createStreamDrain
(
evlog/stream
, SSE) +
readFsLogs
/
tailFsLogs
(
evlog/fs
)
https://www.evlog.dev/extend/stream
Agents need to query logs over HTTP in devMemory adapter +
readMemoryLogs()
behind a dev-only endpoint
https://www.evlog.dev/integrate/adapters/self-hosted/memory

当审查发现相应需求时,推荐以下能力。每个能力在 evlog.dev 上都有完整文档:
发现的需求evlog 方案文档
Event 中缺少已认证用户身份(Better Auth 应用)
evlog/better-auth
:
identifyUser()
、
createAuthMiddleware()
、客户端身份同步
https://www.evlog.dev/use-cases/better-auth/overview
临时 field 名称在整个代码库中不断变化类型化 field + error/audit catalog(
evlog/catalog
)
https://www.evlog.dev/learn/typed-fields · https://www.evlog.dev/learn/catalogs
需要跨切面 hook(request 开始/结束、客户端日志、logger 扩展)Plugin:
definePlugin
https://www.evlog.dev/extend/plugins
开发期间需要实时 tail 日志或构建 log viewer
createStreamDrain
(
evlog/stream
、SSE)+
readFsLogs
/
tailFsLogs
(
evlog/fs
)
https://www.evlog.dev/extend/stream
Agent 需要在开发期间通过 HTTP 查询日志Memory adapter + 开发环境专用 endpoint 后面的
readMemoryLogs()
https://www.evlog.dev/integrate/adapters/self-hosted/memory

Structured Errors

Structured Error

typescript
import { createError } from 'evlog'  // required in Nuxt too: a bare createError is h3's

// Minimal
throw createError({ message: 'Database connection failed', status: 500 })

// Standard
throw createError({ message: 'Payment failed', status: 402, why: 'Card declined by issuer' })

// Complete
throw createError({
  message: 'Payment failed',
  status: 402,
  why: 'Card declined by issuer - insufficient funds',
  fix: 'Please use a different payment method or contact your bank',
  link: 'https://docs.example.com/payments/declined',
  cause: originalError,
})

// Backend-only context (wide events / drains — never HTTP body or parseError())
throw createError({
  message: 'Not allowed',
  status: 403,
  why: 'Insufficient permissions',
  internal: { correlationId: 'req_abc', resourceId: 'proj_123' },
})
Frontend: extract user-facing fields with
parseError()
(
internal
is never returned to clients):
typescript
import { parseError } from 'evlog'

const error = parseError(err)
// error.message, error.status, error.why, error.fix, error.link
See references/structured-errors.md for common patterns and templates.

typescript
import { createError } from 'evlog'  // required in Nuxt too: a bare createError is h3's

// Minimal
throw createError({ message: 'Database connection failed', status: 500 })

// Standard
throw createError({ message: 'Payment failed', status: 402, why: 'Card declined by issuer' })

// Complete
throw createError({
  message: 'Payment failed',
  status: 402,
  why: 'Card declined by issuer - insufficient funds',
  fix: 'Please use a different payment method or contact your bank',
  link: 'https://docs.example.com/payments/declined',
  cause: originalError,
})

// Backend-only context (wide events / drains — never HTTP body or parseError())
throw createError({
  message: 'Not allowed',
  status: 403,
  why: 'Insufficient permissions',
  internal: { correlationId: 'req_abc', resourceId: 'proj_123' },
})
前端:使用
parseError()
提取面向用户的 field(
internal
永远不会返回给客户端):
typescript
import { parseError } from 'evlog'

const error = parseError(err)
// error.message, error.status, error.why, error.fix, error.link
有关常见模式和模板,请参阅 references/structured-errors.md。

Anti-Patterns to Detect

需要检测的反模式

Anti-PatternFix
Multiple
console.log
in one function
Single wide event with
log.set()
throw new Error('...')
throw createError({ message, status, why, fix })
console.error(e); throw e
log.error(e); throw createError(...)
No logging in request handlersAdd
useLogger(event)
/
useLogger()
/
createRequestLogger()
Flat log data
{ uid, n, t }
Grouped objects:
{ user: {...}, cart: {...} }
Logging sensitive data
log.set({ user: body })
Explicit fields:
{ user: { id: body.id, plan: body.plan } }
+ enable
redact: true
Putting support-only IDs in
why
/
message
Use
createError({ ..., internal: { ... } })
for non-user-facing diagnostics
See references/code-review.md for the full checklist.

反模式修复方式
一个函数中存在多个
console.log
使用
log.set()
生成单个 wide event
throw new Error('...')
throw createError({ message, status, why, fix })
console.error(e); throw e
log.error(e); throw createError(...)
Request handler 中没有日志添加
useLogger(event)
/
useLogger()
/
createRequestLogger()
扁平日志数据
{ uid, n, t }
使用分组 object:
{ user: {...}, cart: {...} }
记录敏感数据
log.set({ user: body })
显式指定 field:
{ user: { id: body.id, plan: body.plan } }
并启用
redact: true
将仅供支持人员使用的 ID 放入
why
/
message
对非面向用户的诊断信息使用
createError({ ..., internal: { ... } })
完整清单请参阅 references/code-review.md。

Loading Reference Files

加载参考文件

Load based on what you're working on, and do not load all at once:
  • Designing wide events → references/wide-events.md
  • Improving errors → references/structured-errors.md
  • Full code review → references/code-review.md
  • Drain pipeline setup → references/drain-pipeline.md
根据当前处理内容按需加载,不要一次加载全部文件:
  • 设计 wide event → references/wide-events.md
  • 改进 error → references/structured-errors.md
  • 完整代码审查 → references/code-review.md
  • 设置 drain pipeline → references/drain-pipeline.md