关于 review-logging-patterns
用于审查和改进 TypeScript、JavaScript 项目日志的技能,围绕 evlog 提供结构化事件、错误上下文、覆盖率检查和 CI 回归门禁指导。
主要功能
- 01结构化宽事件日志
- 02带上下文的结构化错误
- 03evlog 配置与覆盖率 CLI
- 04CI 评分与基线门禁
- 05审计日志指导
适用场景
- 01替换零散的 console.log
- 02审查项目日志实践
- 03为错误处理补充上下文
- 04在 CI 中跟踪日志覆盖率
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ evloghq/ evlog --skill review-logging-patterns安装后在 Claude Code 中输入/review-logging-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/code-review.mddrain-pipeline.mdstructured-errors.mdwide-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 / service | string | 'app' | 日志中的服务名称 |
enabled | boolean | true | 全局开关(为 false 时不执行任何操作) |
pretty | boolean | 开发环境中为 true | 美化的树状格式或 JSON |
silent | boolean | false | 禁止控制台输出。事件仍会发送到 drains |
include | string[] | 所有路由 | 要记录的路由 glob 模式 |
exclude | string[] | 无 | 要排除的路由模式(优先级更高) |
routes | Record<string, { service }> | -- | 路由专属的服务名称 |
minLevel | 'debug' | 'info' | 'warn' | 'error' | 'debug' | 全局 log API 和客户端 log 的硬性阈值(不适用于请求范围内的事件)。使用 sampling.rates 按概率控制请求量 |
sampling.rates | object | -- | 头部采样:{ info: 10, warn: 50 }(0-100%) |
sampling.keep | array | -- | 尾部采样:[{ status: 400 }, { duration: 1000 }] |
drain | (ctx) => void | -- | Drain 回调(Next.js、独立模式) |
enrich | (ctx) => void | -- | 丰富上下文的回调(Next.js) |
keep | (ctx) => void | -- | 自定义尾部采样回调(Next.js) |
redact | boolean | RedactConfig | 生产环境中为 true | 在生产环境中默认启用。设为 false 可禁用。传入对象可进行精细控制 |
Nitro Hooks(Nuxt、Nitro v2/v3)
| Hook | 触发时机 | 用途 |
|---|---|---|
evlog:drain | 丰富上下文后 | 将事件发送到外部服务 |
evlog:enrich | emit 后、控制台输出和 drain 前 | 添加派生上下文 |
evlog:emit:keep | emit 期间 | 自定义尾部采样逻辑 |
close | 服务端关闭时 | 刷新 drain 管道缓冲区 |
日志出口适配器
| 适配器 | 导入 | 环境变量 |
|---|---|---|
| Axiom | evlog/axiom | AXIOM_API_KEY、AXIOM_DATASET |
| OTLP | evlog/otlp | OTLP_ENDPOINT(或标准变量 OTEL_EXPORTER_OTLP_[LOGS_]ENDPOINT、_HEADERS、_COMPRESSION、_PROTOCOL、OTEL_RESOURCE_ATTRIBUTES)。protocol: 'http/protobuf' 会发送二进制 protobuf。semanticConventions: true 会添加 OTel 属性名称(http.*、exception.*、gen_ai.*) |
| HyperDX | evlog/hyperdx | HYPERDX_API_KEY(可选的 HYPERDX_OTLP_ENDPOINT;默认为 https://in-otel.hyperdx.io) |
| Sazabi | evlog/sazabi | SAZABI_INTAKE_URL(日志流的接收 URL;其密钥包含在 URL 中),可选的 SAZABI_SERVICE_NAME |
| Grafana Loki | evlog/loki | LOKI_ENDPOINT,可选的 LOKI_API_KEY、LOKI_USER |
| ClickHouse | evlog/clickhouse | CLICKHOUSE_ENDPOINT(必须存在 evlog_events 表),可选的 CLICKHOUSE_USER、CLICKHOUSE_PASSWORD、CLICKHOUSE_DATABASE、CLICKHOUSE_TABLE |
| PostHog | evlog/posthog | POSTHOG_API_KEY、POSTHOG_HOST |
| Sentry | evlog/sentry | SENTRY_DSN |
| Better Stack | evlog/better-stack | BETTER_STACK_API_KEY |
| Datadog | evlog/datadog | DD_API_KEY 或 DATADOG_API_KEY,可选的 DD_SITE / DATADOG_LOGS_URL |
| Grafana Loki | evlog/loki | LOKI_ENDPOINT,可选的 LOKI_API_KEY + LOKI_USER(Grafana Cloud)或 LOKI_TENANT_ID(多租户) |
| ClickHouse | evlog/clickhouse | CLICKHOUSE_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],
}
}
内置模式及其智能掩码输出:
| 模式 | 输入示例 | 掩码输出 |
|---|---|---|
creditCard | 4111111111111111 | ****1111 |
email | [email protected] | a***@***.com |
ipv4 | 192.168.1.100 | ***.***.***.100 |
phone | +33 6 12 34 56 78 | +33 ****5678 |
jwt | eyJhbGciOi... | eyJ***.*** |
bearer | Bearer sk_live_abc... | Bearer *** |
iban | FR76 3000 6000 ...189 | FR76****189 |
适用于所有框架: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 |
| 横切钩子(请求开始/结束、客户端日志、日志记录器扩展) | 插件:definePlugin | https://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 e | log.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。
加载参考文件
根据当前工作加载相应文件,不要一次性全部加载:
- 设计宽事件 → references/wide-events.md
- 改进错误处理 → references/structured-errors.md
- 全面代码审查 → references/code-review.md
- 设置 Drain 管道 → references/drain-pipeline.md