review-logging-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReview 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) | |
| Project conventions (CLI) | |
| Coverage map (CLI) | |
| CI gating (CLI) | |
| Wide events patterns | references/wide-events.md |
| Error handling | references/structured-errors.md |
| Code review checklist | references/code-review.md |
| Drain pipeline | references/drain-pipeline.md |
| Audit logs | build-audit-logs skill + docs |
| 正在处理... | 资源 |
|---|---|
| 设置(CLI) | |
| 项目约定(CLI) | |
| 覆盖率地图(CLI) | |
| CI gate(CLI) | |
| Wide event 模式 | references/wide-events.md |
| 错误处理 | references/structured-errors.md |
| 代码审查清单 | references/code-review.md |
| Drain pipeline | references/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 field on wide events, not a parallel logger. See the skill for end-to-end setup (, , denials, , , , ).
auditbuild-audit-logslog.auditwithAuditauditEnricherauditOnlysignedmockAudittypescript
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
})对于安全敏感操作(认证、计费、管理、数据导出),请使用 evlog 的 audit layer:在 wide event 中使用类型化的 字段,而不是建立并行 logger。有关端到端设置(、、拒绝事件、、、、),请参阅 skill。
auditlog.auditwithAuditauditEnricherauditOnlysignedmockAuditbuild-audit-logstypescript
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
})Installation
安装
bash
npm install evlogbash
npm install evlogUse the CLI (recommended on Nuxt, Nitro, Next.js, TanStack Start, Hono, Express, Fastify)
使用 CLI(推荐用于 Nuxt、Nitro、Next.js、TanStack Start、Hono、Express、Fastify)
The executable ships with the package: , , . It runs 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 (), score coverage (), lock the score in CI (, ). 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.
evlogevlogpnpm evlognpx evlogbunx evlog@evlog/cliinitmap--min-score--baselineevlogevlogpnpm evlognpx evlogbunx evlog@evlog/cliinitmap--min-score--baseline1. Setup: evlog init
evlog init1. 设置:evlog init
evlog initOn a project that doesn't use evlog yet, prefer 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:
initbash
undefined对于尚未使用 evlog 的项目,应优先使用 ,而不是手动编写设置,因为它可以检测框架、读取项目已有内容,并一次性生成配置、drain、enricher 和其他附加项。它针对 Agent 完全可脚本化:
initbash
undefinedpreview 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
--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/initnpx evlog init --yes
--service my-app
--drain fs
--prodDrain axiom
--extras enrichers,pipeline,sampling
--sampling medium
--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/init2. Score: evlog map
evlog map2. 评分:evlog map
evlog mapbash
npx evlog map --no-writebash
npx evlog map --no-writeagents: 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
--min-score--baseline3. 在 CI 中锁定:--min-score
和 --baseline
--min-score--baselineOn GitHub Actions, suggest the evlog action ( with ): 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 step.
uses: evloghq/action@v1min-scorerun:After fixing, propose making the score durable. This is where the CLI earns its keep:
bash
undefined在 GitHub Actions 中,建议使用 evlog action( 并设置 ):它会直接扫描 pull request 的 base,因此无需提交 map 文件,并会将回归标记到 diff 和一条评论中。下面的命令提供了从 步骤执行同一 gate 的方式。
uses: evloghq/action@v1min-scorerun:修复后,建议让评分具备持久性。这正是 CLI 的价值所在:
bash
undefinedin 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/**'],
},
})useLoggerlogparseErrorcreateErrorwhyfixlinkevlogtypescript
// 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 at the app root. The module loads it on the server, and the key in overrides it. The browser logger reads only the key.
evlog.config.tsevlognuxt.config.tsevlogtypescript
// 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: , , are auto-imported in components.
logsetIdentityclearIdentitytypescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
include: ['/api/**'],
},
})useLoggerlogparseErrorcreateErrorwhyfixlinkevlogtypescript
// 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 可以放在应用根目录的 中。模块会在服务端加载它,而 中的 配置项会覆盖它。浏览器 logger 只读取 配置项。
evlog.config.tsnuxt.config.tsevlogevlogtypescript
// 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
}客户端:组件中的 、 和 会自动导入。
logsetIdentityclearIdentityNext.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 wrapper:
withEvlog()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 + timing headers):
x-request-idtypescript
// 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 (SSR/RSC errors outside ). Use in root to gate Node + cache the import, or write / manually. Both are valid. For custom logic, wrap evlog’s / inside (compose with your own init or metrics), then re-export.
onRequestErrorwithEvlogdefineNodeInstrumentation(() => import('./lib/evlog'))instrumentation.tsregisteronRequestErrorregisteronRequestErrorlib/evlog.tsExport from alongside . See framework docs for coexistence with .
createInstrumentation()lib/evlog.tscreateEvlog()lockLoggerStep 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:使用 包装 route handler:
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 })
})步骤 3:Server Action。 使用相同的 wrapper:
withEvlog()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-idtypescript
// 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。 用于启动流程和全局 (处理 之外的 SSR/RSC 错误)。在根目录 中使用 ,以控制 Node 初始化并缓存 import;也可以手动编写 /。两种方式都有效。若使用自定义逻辑,请将 evlog 的 / 包装在 内(与自己的初始化或 metrics 组合),然后重新导出。
onRequestErrorwithEvloginstrumentation.tsdefineNodeInstrumentation(() => import('./lib/evlog'))registeronRequestErrorregisteronRequestErrorlib/evlog.ts在 中与 一起导出 。有关与 共存的信息,请参阅框架文档。
lib/evlog.tscreateEvlog()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 in route handlers or from anywhere in the call stack:
event.locals.loguseLogger()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 中通过 访问 logger,或在调用栈中的任意位置使用 :
event.locals.loguseLogger()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.tstypescript
// 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.tsxtypescript
// 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 from to access the logger:
useRequest()nitro/contexttypescript
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.tstypescript
// 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.tsxtypescript
// 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)],
},
})使用 中的 访问 logger:
nitro/contextuseRequest()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 from in routes. On Nitro v2 and v3, an at the app root is loaded by the module, and the options passed to override it.
useLoggerevlog/nitroevlog.config.tsevlog()typescript
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})在 route 中从 导入 。在 Nitro v2 和 v3 上,模块会加载应用根目录中的 ,传入 的选项会覆盖其中的配置。
evlog/nitrouseLoggerevlog.config.tsevlog()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()@nestjs/platform-express@nestjs/platform-fastifyreq.logevlog/fastifyuseLogger()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-fastifyreq.logevlog/fastifyuseLogger()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 to access the logger from anywhere in the call stack without passing :
useLogger()reqtypescript
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: [] })
})使用 可在调用栈中的任意位置访问 logger,而无需传递 :
useLogger()reqtypescript
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 in handlers. Use from in the layers underneath (services, repositories) where is not in hand. Both return the same logger:
c.get('log')useLogger()evlog/honoctypescript
import { useLogger } from 'evlog/hono'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}On Cloudflare Workers, needs the (or ) compatibility flag; works with or without it.
useLogger()nodejs_compatnodejs_alsc.get('log')Structured errors: throw , then in use and pass to (Hono types the status argument as , not ).
createError()app.onErrorparseError()parsed.status as ContentfulStatusCodec.json()ContentfulStatusCodenumbertypescript
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 中通过 访问 logger。在下层 layer(service、repository)中没有 时,使用 中的 。两者返回同一个 logger:
c.get('log')cevlog/honouseLogger()typescript
import { useLogger } from 'evlog/hono'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}在 Cloudflare Workers 上, 需要 (或 )compatibility flag; 则无论是否启用均可使用。
useLogger()nodejs_compatnodejs_alsc.get('log')Structured error:抛出 ,然后在 中使用 ,并将 传给 (Hono 会将 status 参数的类型定义为 ,而不是 )。
createError()app.onErrorparseError()parsed.status as ContentfulStatusCodec.json()ContentfulStatusCodenumbertypescript
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.logfastify.logUse to access the logger from anywhere in the call stack without passing :
useLogger()requesttypescript
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.logfastify.log使用 可在调用栈中的任意位置访问 logger,而无需传递 :
useLogger()requesttypescript
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 to access the logger from anywhere in the call stack:
useLogger()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)使用 可在调用栈中的任意位置访问 logger:
useLogger()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 Configtypescript
// 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 in loaders and actions:
context.get(loggerContext)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 to access the logger from anywhere in the call stack without passing context:
useLogger()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 Configtypescript
// 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 中通过 访问 logger:
context.get(loggerContext)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: [] }
}使用 可在调用栈中的任意位置访问 logger,而无需传递 context:
useLogger()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()os.use(evlog())context.logbaseoperation.Use to access the logger from utility modules:
useLogger()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()os.use(evlog())basecontext.log.operation使用 可在 utility module 中访问 logger:
useLogger()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 (, ). installs the middleware on every route and provides the service. calls when the layer builds and flushes a drain when its scope closes:
HttpRouterHttpApirouterLayerevlog()EvlogLoggerinitLoggerLayerinitLoggercreateDrainPipelinetypescript
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 and no . The event lives in the Effect context: in handlers and services (the requirement shows up in the effect's type), for typed fields, and in library code that should not require (a no-op outside a unit). It runs on Cloudflare Workers without .
AsyncLocalStoragelog.fork()yield* EvlogLoggeryield* useLogger<Fields>()yield* set(fields)EvlogLoggernodejs_compattypescript
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 (the primary failure as , the rest under , a pure interruption as ). Effect's router answers a failed with 500, so catch it in the handler with , call , and return built from . encodes declared errors as responses before the middleware: the event gets the status but no field.
Causeerrorerror.suppressedstatus: 499createError({ status: 402 })Effect.catchlog.error(error)HttpServerResponse.jsonUnsafe(body, { status: parsed.status })parseError(error)HttpApierrorNon-HTTP work (jobs, queue consumers, Durable Object alarms) uses . A Durable Object uses . On Workers, provide per request ( with ) so drains outlive the response. folds calls into the current wide event. Each unit continues an incoming , and and forward it on outgoing calls.
.pipe(withLogger('action-name', options))fetch.pipe(withRequestLogger(request, options))WaitUntilhandler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx)))HttpRouter.toWebHandlerloggerLayer()Effect.log*traceparentwithTracePropagation(client)traceHeadersFull 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(、)。 会在每个 route 上安装 middleware,并提供 service。 会在 layer 构建时调用 ,并在 scope 关闭时 flush drain:
HttpRouterHttpApirouterLayerevlog()EvlogLoggerinitLoggerLayerinitLoggercreateDrainPipelinetypescript
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))它没有 ,也没有 。Event 存在于 Effect context 中:在 handler 和 service 中使用 (该要求会反映在 effect 的类型中),使用 获取类型化字段,在不需要 的 library code 中使用 (在 unit 之外为 no-op)。它无需 即可在 Cloudflare Workers 上运行。
AsyncLocalStoragelog.fork()yield* EvlogLoggeryield* useLogger<Fields>()EvlogLoggeryield* set(fields)nodejs_compattypescript
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 从 中读取(主要 failure 为 ,其余位于 下,纯 interruption 对应 )。Effect router 会将失败的 响应为 500,因此应在 handler 中使用 捕获它,调用 ,并根据 构建 。 会在 middleware 之前将已声明的 error 编码为 response,因此 event 会获得 status,但不会包含 字段。
Causeerrorerror.suppressedstatus: 499createError({ status: 402 })Effect.catchlog.error(error)parseError(error)HttpServerResponse.jsonUnsafe(body, { status: parsed.status })HttpApierror非 HTTP 工作(job、queue consumer、Durable Object alarm)使用 。Durable Object 的 使用 。在 Workers 上,应为每个请求提供 ( 配合 ),以确保 drain 的生命周期超过 response。 会将 调用合并到当前 wide event 中。每个 unit 都会继续传入的 ,而 和 会在传出调用中转发它。
.pipe(withLogger('action-name', options))fetch.pipe(withRequestLogger(request, options))WaitUntilhandler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx)))HttpRouter.toWebHandlerloggerLayer()Effect.log*traceparentwithTracePropagation(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 })
})withEvloglog.emit()waitUntilrequestIdx-request-idcf-raymethodpathcf-raytraceparentrequest.cfdrainenrichkeepincludeexcluderoutescreateWorkersLogger(request)log.emit()useLogger()logtypescript
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 返回时, 会为每个请求产生一个 wide event,无需手动调用 。异步 drain 会通过 注册,因此其生命周期可以超过 response;streaming response 会将 emit 延迟到 body 完成。 来自 (回退到 );、、、 和 的安全子集会自动捕获。它接受与其他 integration 相同的选项(、、、、、)。如需手动控制(scheduled handler、queue),仍可使用 + 。Workers 上没有基于 ALS 的 ,因此需要显式传递 。
withEvloglog.emit()waitUntilrequestIdx-request-idcf-raymethodpathcf-raytraceparentrequest.cfdrainenrichkeepincludeexcluderoutescreateWorkersLogger(request)log.emit()useLogger()logAWS 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 in (). Pair with the Vite plugin (below) for auto-imports and build-time DX.
locals.logsrc/env.d.tsinterface Locals { log: RequestLogger }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
}
})在 中为 添加类型()。可结合下方 Vite plugin 使用,以获得自动导入和构建时 DX。
src/env.d.tslocals.loginterface Locals { log: RequestLogger }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., middleware for Hono/Express/SvelteKit). The Vite plugin handles build-time DX only.
evlog()对于任何基于 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 的 middleware)。Vite plugin 仅负责构建时 DX。
evlog()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 standalonetypescript
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 standaloneConfiguration Options
配置选项
All options work in Nuxt ( key), Nitro (passed to ), Next.js (), and standalone ().
evlogevlog()createEvlog()initLogger()They can also live in , written with . Nuxt and Nitro load the file on their own, with module options overriding it. Other frameworks import it and pass to and to the middleware. writes the file, prints what it resolves to, and shares a base config across apps (one level). Docs: https://www.evlog.dev/cli/config
evlog.config.tsdefineEvlog()toLoggerConfig(config)initLogger()toMiddlewareOptions(config)evlog initevlog configextends| Option | Type | Default | Description |
|---|---|---|---|
| | | Service name in logs |
| | | Global toggle (no-ops when false) |
| | | Pretty tree format vs JSON |
| | | Suppress console output. Events still go to drains |
| | All routes | Route glob patterns to log |
| | None | Route patterns to exclude (takes precedence) |
| | -- | Route-specific service names |
| | | Hard threshold for the global |
| | -- | Head sampling: |
| | -- | Tail sampling: |
| | -- | Drain callback (Next.js, standalone) |
| | -- | Enrich callback (Next.js) |
| | -- | Custom tail sampling callback (Next.js) |
| | | Enabled by default in production. |
所有选项均可用于 Nuxt( 配置项)、Nitro(传给 )、Next.js()和独立环境()。
evlogevlog()createEvlog()initLogger()它们也可以放在通过 编写的 中。Nuxt 和 Nitro 会自行加载该文件,module option 会覆盖其中的配置。其他框架需导入该文件,将 传给 ,并将 传给 middleware。 会写入该文件, 会打印最终解析出的配置, 可在多个应用之间共享基础配置(支持一级)。文档:https://www.evlog.dev/cli/config
defineEvlog()evlog.config.tstoLoggerConfig(config)initLogger()toMiddlewareOptions(config)evlog initevlog configextends| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| | | 日志中的 service 名称 |
| | | 全局开关(为 false 时不执行任何操作) |
| | 开发环境为 | 使用美化的 tree 格式或 JSON |
| | | 抑制 console 输出,event 仍会发送到 drain |
| | 所有 route | 要记录日志的 route glob 模式 |
| | 无 | 要排除的 route 模式(优先级更高) |
| | -- | route 专属 service 名称 |
| | | 全局 |
| | -- | Head sampling: |
| | -- | Tail sampling: |
| | -- | Drain callback(Next.js、独立环境) |
| | -- | Enrich callback(Next.js) |
| | -- | 自定义 tail sampling callback(Next.js) |
| | 生产环境为 | 默认在生产环境启用。可设为 |
Nitro Hooks (Nuxt, Nitro v2/v3)
Nitro Hook(Nuxt、Nitro v2/v3)
| Hook | When | Use |
|---|---|---|
| After enrichment | Send events to external services |
| After emit, before console output and drain | Add derived context |
| During emit | Custom tail sampling logic |
| Server shutdown | Flush drain pipeline buffers |
| Hook | 时机 | 用途 |
|---|---|---|
| Enrich 完成后 | 将 event 发送到外部服务 |
| Emit 后、console 输出和 drain 前 | 添加派生上下文 |
| Emit 期间 | 自定义 tail sampling 逻辑 |
| Server 关闭时 | Flush drain pipeline buffer |
Drain Adapters
Drain Adapter
| Adapter | Import | Env Vars |
|---|---|---|
| Axiom | | |
| OTLP | | |
| HyperDX | | |
| Sazabi | | |
| Grafana Loki | | |
| ClickHouse | | |
| PostHog | | |
| Sentry | | |
| Better Stack | | |
| Datadog | | |
| Grafana Loki | | |
| ClickHouse | | |
| File System | | None (local file system) |
| Memory | | None (in-process ring buffer; optional |
| NuxtHub | | None: stores wide events in the NuxtHub database with retention-based cleanup (set |
| HTTP (browser ingest) | | None (configure |
Use canonical env var names (e.g. , ), and the same names work in every framework.
AXIOM_API_KEYBETTER_STACK_API_KEYSetup 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.
| Adapter | Import | 环境变量 |
|---|---|---|
| Axiom | | |
| OTLP | | |
| HyperDX | | |
| Sazabi | | |
| Grafana Loki | | |
| ClickHouse | | |
| PostHog | | |
| Sentry | | |
| Better Stack | | |
| Datadog | | |
| Grafana Loki | | |
| ClickHouse | | |
| File System | | 无(本地文件系统) |
| Memory | | 无(进程内 ring buffer;可选 |
| NuxtHub | | 无:将 wide event 存储在 NuxtHub 数据库中,并根据保留期限清理(在 module option 中设置 |
| HTTP(browser ingest) | | 无(在代码中配置 |
请使用标准环境变量名称(例如 、),这些名称在所有框架中通用。
AXIOM_API_KEYBETTER_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: , , , , all from . Each accepts (default ). Use to compose all four in one call:
createUserAgentEnricher()createGeoEnricher()createRequestSizeEnricher()createTraceContextEnricher()evlog/enrichers{ overwrite?: boolean }falsecreateDefaultEnrichers()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 }falsecreateDefaultEnrichers()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 (), disabled in development. Uses smart partial masking, preserving enough context for debugging.
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],
}
}Built-in patterns with smart masking output:
| Pattern | Example Input | Masked Output |
|---|---|---|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
Opt-in credential patterns match the value under any key and inside longer strings. They are off by default, and runs only the names it lists, so add them next to the defaults:
builtins| Pattern | Example Input | Masked Output |
|---|---|---|
| | |
| | |
| | |
typescript
evlog: {
redact: {
builtins: ['creditCard', 'email', 'ipv4', 'phone', 'jwt', 'bearer', 'iban', 'secrets', 'apiKeys', 'privateKey', 'urlPassword'],
}
}apiKeyssecretsapiKeyauthorizationdsnWorks in all frameworks: Nuxt ( config), Nitro ( module options), Next.js (), standalone (), and all middleware integrations (Hono, Express, Fastify, Elysia, NestJS).
evlogevlog()createEvlog()initLogger()内置 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 | 输入示例 | 遮罩后的输出 |
|---|---|---|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
可选启用的 credential pattern 可匹配任意 key 下的值以及较长字符串内部的值。它们默认关闭;由于 仅运行列出的名称,因此应在默认项旁显式添加它们:
builtins| Pattern | 输入示例 | 遮罩后的输出 |
|---|---|---|
| | |
| | |
| | |
typescript
evlog: {
redact: {
builtins: ['creditCard', 'email', 'ipv4', 'phone', 'jwt', 'bearer', 'iban', 'secrets', 'apiKeys', 'privateKey', 'urlPassword'],
}
}apiKeyssecretsapiKeyauthorizationdsn适用于所有框架:Nuxt( 配置)、Nitro( module option)、Next.js()、独立环境(),以及所有 middleware integration(Hono、Express、Fastify、Elysia、NestJS)。
evlogevlog()createEvlog()initLogger()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 . Requires as a peer dependency.
evlog/aiai >=6.0.168 <8.0.0从 Vercel AI SDK 捕获 token 用量、tool call、模型信息、streaming metric、tool 执行耗时、成本估算和 embedding metadata,并写入 wide event。从 导入。需要 作为 peer dependency。
evlog/aiai >=6.0.168 <8.0.0Basic 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()generateTextstreamTextToolLoopAgenttypescript
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()generateTextstreamTextToolLoopAgentTelemetry 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 (per-tool ) and to the wide event.
ai.tools{ name, durationMs, success, error? }ai.totalDurationMs若要获取 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 添加 (每个 tool 包含 )和 。
ai.tools{ name, durationMs, success, error? }ai.totalDurationMsEmbeddings
Embedding
typescript
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage, model: 'text-embedding-3-small', dimensions: 1536 })For , pass the batch count:
embedManytypescript
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 })对于 ,传入 batch 数量:
embedManytypescript
ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })Cost estimation
成本估算
Pass a pricing map to get in the wide event:
ai.estimatedCosttypescript
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.estimatedCosttypescript
const ai = createAILogger(log, {
cost: {
'claude-sonnet-4.6': { input: 3, output: 15 },
'gpt-4o': { input: 2.5, output: 10 },
},
})Wide event ai
field
aiWide event 的 ai
字段
aiIncludes: , , , , , , , , , , , , , , , (via telemetry integration), (via telemetry integration), (via ), (via option).
callsmodelproviderinputTokensoutputTokenstotalTokenscacheReadTokensreasoningTokensfinishReasontoolCallsstepsmsToFirstChunkmsToFinishtokensPerSeconderrortoolstotalDurationMsembeddingcaptureEmbedestimatedCostcostAnti-patterns to detect:
| Anti-Pattern | Fix |
|---|---|
Manual token tracking in | |
| |
| No AI observability | Add |
| No tool execution timing | Add |
| Manual cost calculation | Use |
包含:、、、、、、、、、、、、、、、(通过 telemetry integration)、(通过 telemetry integration)、(通过 )、(通过 option)。
callsmodelproviderinputTokensoutputTokenstotalTokenscacheReadTokensreasoningTokensfinishReasontoolCallsstepsmsToFirstChunkmsToFinishtokensPerSeconderrortoolstotalDurationMsembeddingcaptureEmbedestimatedCostcost需要检测的反模式:
| 反模式 | 修复方式 |
|---|---|
在 | 使用 |
| 使用 |
| 缺少 AI 可观测性 | 添加 |
| 缺少 tool 执行耗时 | 将 |
| 手动计算成本 | 使用 |
Related Capabilities (pointers)
相关能力(指引)
Recommend these when the review surfaces the matching need. Each has full docs on evlog.dev:
| Need spotted | evlog answer | Docs |
|---|---|---|
| Auth'd user identity missing from events (Better Auth apps) | | https://www.evlog.dev/use-cases/better-auth/overview |
| Ad-hoc field names drifting across the codebase | Typed fields + error/audit catalogs ( | https://www.evlog.dev/learn/typed-fields · https://www.evlog.dev/learn/catalogs |
| Cross-cutting hooks (request start/finish, client logs, logger extension) | Plugins: | https://www.evlog.dev/extend/plugins |
| Tail logs live during dev / build a log viewer | | https://www.evlog.dev/extend/stream |
| Agents need to query logs over HTTP in dev | Memory adapter + | https://www.evlog.dev/integrate/adapters/self-hosted/memory |
当审查发现相应需求时,推荐以下能力。每个能力在 evlog.dev 上都有完整文档:
| 发现的需求 | evlog 方案 | 文档 |
|---|---|---|
| Event 中缺少已认证用户身份(Better Auth 应用) | | https://www.evlog.dev/use-cases/better-auth/overview |
| 临时 field 名称在整个代码库中不断变化 | 类型化 field + error/audit catalog( | https://www.evlog.dev/learn/typed-fields · https://www.evlog.dev/learn/catalogs |
| 需要跨切面 hook(request 开始/结束、客户端日志、logger 扩展) | Plugin: | https://www.evlog.dev/extend/plugins |
| 开发期间需要实时 tail 日志或构建 log viewer | | https://www.evlog.dev/extend/stream |
| Agent 需要在开发期间通过 HTTP 查询日志 | Memory adapter + 开发环境专用 endpoint 后面的 | 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 ( is never returned to clients):
parseError()internaltypescript
import { parseError } from 'evlog'
const error = parseError(err)
// error.message, error.status, error.why, error.fix, error.linkSee 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' },
})前端:使用 提取面向用户的 field( 永远不会返回给客户端):
parseError()internaltypescript
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-Pattern | Fix |
|---|---|
Multiple | Single wide event with |
| |
| |
| No logging in request handlers | Add |
Flat log data | Grouped objects: |
Logging sensitive data | Explicit fields: |
Putting support-only IDs in | Use |
See references/code-review.md for the full checklist.
| 反模式 | 修复方式 |
|---|---|
一个函数中存在多个 | 使用 |
| |
| |
| Request handler 中没有日志 | 添加 |
扁平日志数据 | 使用分组 object: |
记录敏感数据 | 显式指定 field: |
将仅供支持人员使用的 ID 放入 | 对非面向用户的诊断信息使用 |
完整清单请参阅 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