mcpField
R
review-logging-patterns

review-logging-patterns

github.com/evloghq开发1.9k752 次安装2 天前更新
代码仓库

关于 review-logging-patterns

用于审查和改进 TypeScript、JavaScript 项目日志的技能,围绕 evlog 提供结构化事件、错误上下文、覆盖率检查和 CI 回归门禁指导。

主要功能

  1. 01结构化宽事件日志
  2. 02带上下文的结构化错误
  3. 03evlog 配置与覆盖率 CLI
  4. 04CI 评分与基线门禁
  5. 05审计日志指导

适用场景

  1. 01替换零散的 console.log
  2. 02审查项目日志实践
  3. 03为错误处理补充上下文
  4. 04在 CI 中跟踪日志覆盖率

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/evloghq/evlog --skill review-logging-patterns

安装后在 Claude Code 中输入/review-logging-patterns确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
code-review.md
drain-pipeline.md
structured-errors.md
wide-events.md

详细介绍

审查日志记录模式

审查并改进 TypeScript/JavaScript 代码库中的日志记录模式。将零散的 console.logs 转换为结构化宽事件,并将通用错误转换为自我说明的结构化错误。

适用场景

  • 在新项目或现有项目(任何受支持的框架)中设置 evlog
  • 审查代码中的日志最佳实践
  • 将 console.log 语句转换为结构化日志记录
  • 通过更丰富的上下文改进错误处理
  • 配置日志排出、采样或丰富

快速参考

正在处理……资源
设置(CLI)evlog init:将 evlog 接入项目
项目约定(CLI)evlog agents:将 evlog 区块写入项目的 AGENTS.md
覆盖率映射(CLI)evlog map:为未记录日志的入口点评分
CI 门禁(CLI)evlog map --min-score / --baseline:阻止回归
宽事件模式references/wide-events.md
错误处理references/structured-errors.md
代码审查清单references/code-review.md
排出管道references/drain-pipeline.md
审计日志build-audit-logs 技能 + 文档

审计日志

对于与安全相关的操作(身份验证、计费、管理、数据导出),请使用 evlog 的审计层:在宽事件中使用类型化的 audit 字段,而不是另设并行日志记录器。有关端到端设置(log.audit、withAudit、拒绝操作、auditEnricher、auditOnly、signed、mockAudit),请参阅 build-audit-logs 技能。

log.audit({
  action: 'invoice.refund',
  actor: { type: 'user', id: user.id },
  target: { type: 'invoice', id: invoice.id },
  outcome: 'success',
})

文档:https://www.evlog.dev/use-cases/audit/overview

安装

npm install evlog

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

evlog 可执行文件随 evlog 包一起提供:pnpm evlog、npx evlog、bunx evlog。如果已安装,它会运行 @evlog/cli;否则会按需获取(不会向项目添加依赖),因此即使项目尚未配置任何内容也能使用。该功能尚处早期阶段,但值得一试。它会读取磁盘上的项目文件(不涉及流量,也无需配置)。对于七种受支持的框架,它覆盖整个流程:接入 evlog(init)、评估覆盖率(map)、在 CI 中锁定评分(--min-score、--baseline)。如果 CLI 不可用、框架尚无适配器,或用户拒绝,请继续使用下文的手动配置部分;此技能不依赖 CLI。安装任何内容前请先征得用户同意。

1. 设置:evlog init

对于尚未使用 evlog 的项目,优先使用 init,而不是手动编写设置,因为它会检测框架、读取项目现有内容,并一次性生成配置、排出器、丰富器和其他扩展。它支持 Agent 完全通过脚本操作:

# preview everything without writing (always start here)
npx evlog init --dry-run --yes

# then apply — flags instead of prompts
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:指定工作区包)、--no-install。应用前,请与用户一起审查 --dry-run 输出。文档:https://www.evlog.dev/cli/init

2. 评分:evlog map
npx evlog map --no-write
# agents: npx evlog map --json --no-write

你将获得:

  • 项目评分,以及仍未覆盖的入口点
  • 优先修复:最值得修复的三个位置
  • 进一步优化:不会影响评分的改进机会(目录、审计覆盖率、AI 日志记录、身份验证)
  • 逐文件检查:npx evlog map <file> --no-write 可显示处理程序可以采用的结构
  • 修复后重新运行,观察评分变化

按顺序处理优先修复项,并尽量减少改动(useLogger()、log.set()、log.audit()、createError({ why, fix })),然后使用 --no-write 重新运行。只有在用户希望写入 evlog.map.json 时才省略 --no-write。

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

在 GitHub Actions 中,建议使用 evlog action(配合 uses: evloghq/action@v1 和 min-score):它会自行扫描拉取请求的基准分支,因此无需提交 map 文件,并会在差异中以及一条评论里显示回归情况。以下命令是在 run: 步骤中实现相同检查的方式。

修复完成后,建议让评分保持稳定。这正是 CLI 发挥作用的地方:

# 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 会按入口点和要求,将最新扫描结果与已提交的 evlog.map.json 进行比较,因此,即使总评分不变,只要某个重构为一个路由添加了埋点,却破坏了另一个路由,也会导致检查失败。通过评论停用一项已通过的检查,也会被视为回归。未添加埋点的新路由会列为 NEW AND DARK,但不会导致检查失败。工作流:提交一次 evlog.map.json,将 --baseline 命令添加到 CI,然后在接受有意的改动时,不带 --baseline 重新运行 map。文档:https://www.evlog.dev/cli/ci

如果项目有 evlog.config.ts,可以将检查配置在其中,而不是 CI 步骤里:map: { minScore: 80, baseline: true } 可以让不带参数的 evlog map 执行相同检查,且命令行标志仍具有优先权。同一个 map 配置块还可以为所有入口点关闭检查(rules: { 'error-catalog': 'off' }),并通过文件 glob 排除入口点(ignore)。CLI 不会运行该文件就读取这些值,因此它们必须是字面量。文档:https://www.evlog.dev/cli/config

目前仍处于早期阶段:适配器和规则仍在不断演进;不同版本之间的评分可能会变化。文档:https://www.evlog.dev/cli/map · 规则:https://www.evlog.dev/cli/rules


框架设置

Nuxt
// 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 导入它。

// 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、增强、采样和脱敏配置可以放在应用根目录的 evlog.config.ts 中。该模块会在服务端加载此配置,而 nuxt.config.ts 中的 evlog 键会覆盖它。浏览器日志记录器只读取 evlog 键。

// evlog.config.ts
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlog({
  service: 'my-app',
  drain: createAxiomDrain(),
})

服务器插件中的 Nitro hooks 仍可与该文件配合使用,并且一个事件会同时到达两个 drain:

// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})

客户端传输(自动配置的 Vue 插件):

// nuxt.config.ts
evlog: {
  transport: { enabled: true },  // logs sent to /api/_evlog/ingest
}

客户端:组件中会自动导入 log、setIdentity 和 clearIdentity。

Next.js

步骤 1:创建集中配置。 所有导出都来自此处:

// 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() 包装路由处理程序:

// 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:服务器操作。 使用相同的 withEvlog() 包装器:

// 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:中间件(可选,设置 x-request-id 和计时标头):

// proxy.ts
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = { matcher: ['/api/:path*'] }

步骤 5:客户端 Provider。 包装根布局:

// 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:客户端日志记录。 在任意客户端组件中:

'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'

setIdentity({ userId: 'usr_123' })
log.info({ action: 'checkout_click' })
clearIdentity()

步骤 7(可选):插桩。 启动时执行,并设置全局 onRequestError(处理 withEvlog 之外的 SSR/RSC 错误)。在根目录的 instrumentation.ts 中使用 defineNodeInstrumentation(() => import('./lib/evlog')),以限定为 Node 环境并缓存导入,或者手动编写 register/onRequestError。两种方式都有效。若需自定义逻辑,可在 lib/evlog.ts 中包装 evlog 的 register/onRequestError(与自有初始化或指标逻辑组合),然后重新导出。

在 lib/evlog.ts 中与 createEvlog() 一同导出 createInstrumentation()。如需了解如何与 lockLogger 共存,请参阅框架文档。

步骤 8:客户端采集端点。 接收客户端日志:

// 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
// src/hooks.server.ts
import { initLogger } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'

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

export const { handle, handleError } = createEvlogHooks()

在路由处理程序中通过 event.locals.log 访问日志记录器,或在调用栈的任意位置使用 useLogger():

// 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 })
}
import { useLogger } from 'evlog/sveltekit'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}

包含 drain、enrich 和尾部采样的完整管道:

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.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  modules: [evlog({ env: { service: 'my-api' } })],
})
// 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 使用 Nitro v3。安装 evlog 并添加 nitro.config.ts:

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

将错误处理中间件添加到 __root.tsx:

// 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() 访问日志记录器:

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.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'

export default defineNitroConfig({
  modules: [evlog({ env: { service: 'my-api' } })],
})

在路由中从 evlog/nitro 导入 useLogger。在 Nitro v2 和 v3 中,模块会加载应用根目录下的 evlog.config.ts,而传递给 evlog() 的选项会覆盖该配置。

NestJS
// 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。在这两个平台上,控制器中的 req.log 都是 evlog 日志记录器。不要在 Nest Fastify 应用中同时注册 evlog/fastify,否则每个请求都会发出两个宽事件。使用 useLogger() 可在任意控制器或服务中访问请求作用域的日志记录器:

import { useLogger } from 'evlog/nestjs'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}

包含 drain、enrich 和尾部采样的完整管道:

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():

EvlogModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config) => ({
    drain: createAxiomDrain({ apiKey: config.get('AXIOM_API_KEY') }),
  }),
})
Express
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() 可在调用栈的任意位置访问日志记录器,无需传递 req:

import { useLogger } from 'evlog/express'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}

包含 drain、enrich 和尾部采样的完整管道:

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
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: [] })
})

在处理程序中通过 c.get('log') 访问日志记录器。在下层(服务、存储库)无法直接使用 c 时,可使用 evlog/hono 中的 useLogger()。两者返回的是同一个日志记录器:

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)兼容性标志;无论是否启用该标志,c.get('log') 都可正常工作。

结构化错误:抛出 createError(),然后在 app.onError 中使用 parseError(),并将 parsed.status as ContentfulStatusCode 传递给 c.json()(Hono 将状态参数的类型定义为 ContentfulStatusCode,而非 number)。

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 和尾部采样的完整管道:

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
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 宽事件日志记录器(会覆盖请求上的 Fastify 内置 pino 日志记录器)。仍可通过 fastify.log 访问 Fastify 的 pino 日志记录器。

使用 useLogger() 可在调用栈的任意位置访问日志记录器,无需传递 request:

import { useLogger } from 'evlog/fastify'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}

包含 drain、enrich 和尾部采样的完整管道:

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
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() 可在调用栈的任意位置访问日志记录器:

import { useLogger } from 'evlog/elysia'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}

包含 drain、enrich 和尾部采样的完整管道:

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.config.ts
import type { Config } from '@react-router/dev/config'

export default {
  future: {
    v8_middleware: true,
  },
} satisfies Config
// 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:

// 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(),无需传递 context,即可在调用栈中的任意位置访问 logger:

import { useLogger } from 'evlog/react-router'

async function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}

包含 drain、enrich 和 tail sampling 的完整管道:

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
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()) 会在所有从 base 派生的过程上提供 context.log,并使用 operation(以 . 连接的过程路径)为宽事件添加标签。

使用 useLogger() 从工具模块访问 logger:

import { useLogger } from 'evlog/orpc'

async function chargeCard(amount: number) {
  const log = useLogger()
  log.set({ payment: { amount } })
}

包含 drain、enrich 和 tail sampling 的完整管道:

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 4(HttpRouter、HttpApi)。routerLayer 会在每条路由上安装 evlog() 中间件,并提供 EvlogLogger 服务。initLoggerLayer 会在 layer 构建时调用 initLogger,并在其作用域关闭时刷新 createDrainPipeline drain:

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()。事件存储在 Effect context 中:在处理程序和服务中使用 yield* EvlogLogger(此要求会体现在 effect 的类型中),使用 yield* useLogger<Fields>() 获取类型化字段,并在不应要求 EvlogLogger 的库代码中使用 yield* set(fields)(在 unit 外部为空操作)。它可在无需 nodejs_compat 的 Cloudflare Workers 上运行。

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 } })
})

失败信息从 Cause 中读取(主要失败作为 error,其余内容放在 error.suppressed 下,纯中断则为 status: 499)。Effect 的 router 会将失败的 createError({ status: 402 }) 返回为 500,因此要在处理程序中使用 Effect.catch 捕获它,调用 log.error(error),并返回由 parseError(error) 构建的 HttpServerResponse.jsonUnsafe(body, { status: parsed.status })。HttpApi 会在中间件运行前将声明的错误编码为响应:事件会获得状态码,但没有 error 字段。

非 HTTP 工作(任务、队列消费者、Durable Object 闹钟)使用 .pipe(withLogger('action-name', options))。Durable Object 的 fetch 使用 .pipe(withRequestLogger(request, options))。在 Workers 上,应为每个请求提供 WaitUntil(使用 HttpRouter.toWebHandler 时调用 handler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx)))),使 drain 在响应返回后仍能继续执行。loggerLayer() 会将 Effect.log* 调用合并到当前宽事件中。每个 unit 都会延续传入的 traceparent,而 withTracePropagation(client) 和 traceHeaders 会在传出调用时转发该值。

包含 drain、enrich 和 tail sampling 的完整管道:

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
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 会为每个请求发出一个宽事件,无需手动调用 log.emit()。异步 drain 会通过 waitUntil 注册,因此在响应返回后仍能继续执行;流式响应则会延迟发出事件,直到请求体处理完成。requestId 取自 x-request-id(若无则回退到 cf-ray);method、path、cf-ray、traceparent 和 request.cf 中安全的子集都会自动采集。它与其他集成接受相同的选项(drain、enrich、keep、include、exclude、routes)。如需手动控制(定时处理程序、队列),仍可使用 createWorkersLogger(request) + log.emit()。Workers 上没有基于 ALS 的 useLogger(),因此请显式传递 log。

AWS Lambda

Lambda 没有 HTTP 中间件生命周期,因此 evlog 的行为与独立 TypeScript 一样,但有一条关键规则:每次调用创建一个 logger,绝不能使用共享的模块级 logger(Lambda 会复用执行环境,因此共享实例会导致不同调用之间的字段泄漏)。

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
// 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 插件(见下文)使用,可获得自动导入和构建时开发体验。

Vite 插件(适用于任何基于 Vite 的框架)

对于任何基于 Vite 的项目(SvelteKit、Astro、SolidStart、React+Vite 等),使用 Vite 插件实现自动初始化、自动导入和构建时功能:

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

服务端中间件(drain、enrich、keep、routes)仍在框架集成中配置(例如,为 Hono/Express/SvelteKit 配置 evlog() 中间件)。Vite 插件仅处理构建时的开发体验功能。

独立 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

配置选项

所有选项均适用于 Nuxt(evlog 键)、Nitro(传入 evlog())、Next.js(createEvlog())和独立模式(initLogger())。

这些选项也可以写入 evlog.config.ts,并使用 defineEvlog() 编写。Nuxt 和 Nitro 会自行加载该文件,模块选项优先级更高。其他框架需导入该文件,并将 toLoggerConfig(config) 传给 initLogger(),将 toMiddlewareOptions(config) 传给中间件。evlog init 会生成该文件,evlog config 会打印解析后的配置,而 extends 可在多个应用间共享基础配置(仅支持一层继承)。文档:https://www.evlog.dev/cli/config

选项类型默认值描述
env.service / servicestring'app'日志中的服务名称
enabledbooleantrue全局开关(为 false 时不执行任何操作)
prettyboolean开发环境中为 true美化的树状格式或 JSON
silentbooleanfalse禁止控制台输出。事件仍会发送到 drains
includestring[]所有路由要记录的路由 glob 模式
excludestring[]无要排除的路由模式(优先级更高)
routesRecord<string, { service }>--路由专属的服务名称
minLevel'debug' | 'info' | 'warn' | 'error''debug'全局 log API 和客户端 log 的硬性阈值(不适用于请求范围内的事件)。使用 sampling.rates 按概率控制请求量
sampling.ratesobject--头部采样:{ info: 10, warn: 50 }(0-100%)
sampling.keeparray--尾部采样:[{ status: 400 }, { duration: 1000 }]
drain(ctx) => void--Drain 回调(Next.js、独立模式)
enrich(ctx) => void--丰富上下文的回调(Next.js)
keep(ctx) => void--自定义尾部采样回调(Next.js)
redactboolean | RedactConfig生产环境中为 true在生产环境中默认启用。设为 false 可禁用。传入对象可进行精细控制
Nitro Hooks(Nuxt、Nitro v2/v3)
Hook触发时机用途
evlog:drain丰富上下文后将事件发送到外部服务
evlog:enrichemit 后、控制台输出和 drain 前添加派生上下文
evlog:emit:keepemit 期间自定义尾部采样逻辑
close服务端关闭时刷新 drain 管道缓冲区

日志出口适配器

适配器导入环境变量
Axiomevlog/axiomAXIOM_API_KEY、AXIOM_DATASET
OTLPevlog/otlpOTLP_ENDPOINT(或标准变量 OTEL_EXPORTER_OTLP_[LOGS_]ENDPOINT、_HEADERS、_COMPRESSION、_PROTOCOL、OTEL_RESOURCE_ATTRIBUTES)。protocol: 'http/protobuf' 会发送二进制 protobuf。semanticConventions: true 会添加 OTel 属性名称(http.*、exception.*、gen_ai.*)
HyperDXevlog/hyperdxHYPERDX_API_KEY(可选的 HYPERDX_OTLP_ENDPOINT;默认为 https://in-otel.hyperdx.io)
Sazabievlog/sazabiSAZABI_INTAKE_URL(日志流的接收 URL;其密钥包含在 URL 中),可选的 SAZABI_SERVICE_NAME
Grafana Lokievlog/lokiLOKI_ENDPOINT,可选的 LOKI_API_KEY、LOKI_USER
ClickHouseevlog/clickhouseCLICKHOUSE_ENDPOINT(必须存在 evlog_events 表),可选的 CLICKHOUSE_USER、CLICKHOUSE_PASSWORD、CLICKHOUSE_DATABASE、CLICKHOUSE_TABLE
PostHogevlog/posthogPOSTHOG_API_KEY、POSTHOG_HOST
Sentryevlog/sentrySENTRY_DSN
Better Stackevlog/better-stackBETTER_STACK_API_KEY
Datadogevlog/datadogDD_API_KEY 或 DATADOG_API_KEY,可选的 DD_SITE / DATADOG_LOGS_URL
Grafana Lokievlog/lokiLOKI_ENDPOINT,可选的 LOKI_API_KEY + LOKI_USER(Grafana Cloud)或 LOKI_TENANT_ID(多租户)
ClickHouseevlog/clickhouseCLICKHOUSE_ENDPOINT,可选的 CLICKHOUSE_USER / CLICKHOUSE_PASSWORD / CLICKHOUSE_DATABASE / CLICKHOUSE_TABLE
文件系统evlog/fs无(本地文件系统)
内存evlog/memory无(进程内环形缓冲区;可选的 EVLOG_MEMORY_STORE、EVLOG_MEMORY_MAX_EVENTS)。使用 readMemoryLogs() 读取日志,适用于仅供开发环境使用、可供 Agent 查询的日志端点
NuxtHub@evlog/nuxthub(独立软件包,Nuxt 模块)无:将宽事件存储在 NuxtHub 数据库中,并根据保留策略进行清理(在模块选项中设置 evlog.retention: '7d';支持 d/h/m)
HTTP(浏览器摄取)evlog/http无(在代码中配置 endpoint)。evlog/browser 已弃用;API 相同,将在下一个主版本中移除

请使用规范的环境变量名称(例如 AXIOM_API_KEY、BETTER_STACK_API_KEY),这些名称在所有框架中都通用。

各框架的设置模式:

// 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() })

有关批处理、重试和缓冲区溢出配置,请参阅 references/drain-pipeline.md。


丰富器

内置丰富器:createUserAgentEnricher()、createGeoEnricher()、createRequestSizeEnricher()、createTraceContextEnricher(),均来自 evlog/enrichers。每个函数都接受 { overwrite?: boolean }(默认为 false)。使用 createDefaultEnrichers() 一次调用即可组合全部四个丰富器:

import { createDefaultEnrichers } from 'evlog/enrichers'
app.use(evlog({ enrich: createDefaultEnrichers() }))
// 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
  },
})

自动脱敏(PII 保护)

内置脱敏功能会在控制台输出之前以及任何日志出口接收数据之前,从宽事件中清除敏感数据。在生产环境中默认启用(NODE_ENV === 'production'),在开发环境中禁用。采用智能部分掩码,保留足够的上下文以便调试。

// 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],
  }
}

内置模式及其智能掩码输出:

模式输入示例掩码输出
creditCard4111111111111111****1111
email[email protected]a***@***.com
ipv4192.168.1.100***.***.***.100
phone+33 6 12 34 56 78+33 ****5678
jwteyJhbGciOi...eyJ***.***
bearerBearer sk_live_abc...Bearer ***
ibanFR76 3000 6000 ...189FR76****189

适用于所有框架:Nuxt(evlog 配置)、Nitro(evlog() 模块选项)、Next.js(createEvlog())、独立环境(initLogger()),以及所有中间件集成(Hono、Express、Fastify、Elysia、NestJS)。


AI SDK 集成

将 Vercel AI SDK 的令牌用量、工具调用、模型信息、流式传输指标、工具执行耗时、成本估算和嵌入元数据捕获到宽事件中。从 evlog/ai 导入。需要将 ai >=6.0.168 <8.0.0 作为对等依赖。

基本设置(中间件)
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() 使用模型中间件,透明地捕获所有 LLM 调用。适用于 generateText、streamText 和 ToolLoopAgent。

遥测集成(更深入的可观测性)

若要跟踪工具执行耗时、成功/失败状态和生成总耗时,请添加 createEvlogIntegration():

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

这会向宽事件中添加 ai.tools(每个工具对应 { name, durationMs, success, error? })和 ai.totalDurationMs。

嵌入
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage, model: 'text-embedding-3-small', dimensions: 1536 })

对于 embedMany,请传入批次数量:

ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })
成本估算

传入定价映射,即可在宽事件中获取 ai.estimatedCost:

const ai = createAILogger(log, {
  cost: {
    'claude-sonnet-4.6': { input: 3, output: 15 },
    'gpt-4o': { input: 2.5, output: 10 },
  },
})
宽事件的 ai 字段

包括:calls、model、provider、inputTokens、outputTokens、totalTokens、cacheReadTokens、reasoningTokens、finishReason、toolCalls、steps、msToFirstChunk、msToFinish、tokensPerSecond、error、tools(通过遥测集成)、totalDurationMs(通过遥测集成)、embedding(通过 captureEmbed)、estimatedCost(通过 cost 选项)。

需要检测的反模式:

反模式修复方法
在 onFinish 中手动跟踪令牌用量ai.wrap():中间件会自动捕获
console.log('tokens:', result.usage)ai.wrap():宽事件中提供结构化的 ai.* 字段
缺少 AI 可观测性添加 createAILogger(log) + ai.wrap()
缺少工具执行耗时将 createEvlogIntegration(ai) 添加到 telemetry.integrations
手动计算成本在 createAILogger() 中使用 cost 选项

相关功能(参考)

当审查中发现相应需求时,请推荐以下功能。每项功能的完整文档见 evlog.dev:

发现的需求evlog 解决方案文档
事件中缺少已认证用户身份(Better Auth 应用)evlog/better-auth:identifyUser()、createAuthMiddleware()、客户端身份同步https://www.evlog.dev/use-cases/better-auth/overview
代码库中临时使用的字段名称不一致类型化字段 + 错误/审计目录(evlog/catalog)https://www.evlog.dev/learn/typed-fields · https://www.evlog.dev/learn/catalogs
横切钩子(请求开始/结束、客户端日志、日志记录器扩展)插件:definePluginhttps://www.evlog.dev/extend/plugins
开发期间实时查看日志 / 构建日志查看器createStreamDrain(evlog/stream、SSE)+ readFsLogs / tailFsLogs(evlog/fs)https://www.evlog.dev/extend/stream
Agent 需要在开发期间通过 HTTP 查询日志内存适配器 + 由仅开发环境端点提供的 readMemoryLogs()https://www.evlog.dev/integrate/adapters/self-hosted/memory

结构化错误

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() 提取面向用户的字段(internal 永远不会返回给客户端):

import { parseError } from 'evlog'

const error = parseError(err)
// error.message, error.status, error.why, error.fix, error.link

常见模式和模板请参阅 references/structured-errors.md。


要检测的反模式

反模式修复方法
在一个函数中多次调用 console.log使用 log.set() 记录一个宽事件
throw new Error('...')throw createError({ message, status, why, fix })
console.error(e); throw elog.error(e); throw createError(...)
请求处理程序中没有日志记录添加 useLogger(event) / useLogger() / createRequestLogger()
扁平的日志数据 { uid, n, t }使用分组对象:{ user: {...}, cart: {...} }
记录敏感数据 log.set({ user: body })明确指定字段:{ user: { id: body.id, plan: body.plan } } + 启用 redact: true
将仅供支持人员使用的 ID 放入 why / message对于不面向用户的诊断信息,使用 createError({ ..., internal: { ... } })

完整检查清单请参阅 references/code-review.md。


加载参考文件

根据当前工作加载相应文件,不要一次性全部加载:

评论

支持 Markdown