review-logging-patterns by hugorcd/evlog
npx skills add https://github.com/hugorcd/evlog --skill review-logging-patterns审查并改进 TypeScript/JavaScript 代码库中的日志记录模式。将分散的 console.log 转换为结构化的宽事件,并将通用错误转换为自描述的结构化错误。
| 正在处理... | 资源 |
|---|---|
| 宽事件模式 | references/wide-events.md |
| 错误处理 | references/structured-errors.md |
| 代码审查清单 | references/code-review.md |
| 输出管道 | references/drain-pipeline.md |
npm install evlog
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
include: ['/api/**'],
},
})
所有 evlog 函数(useLogger、createError、parseError、log)都是自动导入的——无需导入语句。
// server/api/checkout.post.ts — 无需导入
export default defineEventHandler(async (event) => {
const log = useLogger(event)
log.set({ user: { id: user.id, plan: user.plan } })
return { success: true }
})
输出、增强和尾部采样在服务器插件中使用 Nitro 钩子:
// 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 }, // 日志发送到 /api/_evlog/ingest
}
客户端:log、setIdentity、clearIdentity 在组件中自动导入。
步骤 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', token: process.env.AXIOM_TOKEN! }))
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() // 零参数——使用 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:客户端提供者——包装根布局:
// 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:客户端接收端点——接收客户端日志:
// 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: '无效来源' }, { status: 403 })
}
const body = await request.json()
if (!body?.timestamp || !body?.level || !VALID_LEVELS.includes(body.level)) {
return Response.json({ error: '无效负载' }, { status: 400 })
}
const { service: _, ...sanitized } = body
console.log('[客户端日志]', JSON.stringify({ ...sanitized, service: 'my-app', source: 'client' }))
return new Response(null, { status: 204 })
}
// 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' } })
}
完整的输出、增强和尾部采样管道:
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.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 使用 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.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})
在路由中从 evlog/nitro 导入 useLogger。
// src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
@Module({
imports: [EvlogModule.forRoot()],
})
export class AppModule {}
EvlogModule.forRoot() 注册全局中间件。使用 useLogger() 从任何控制器或服务访问请求作用域的记录器:
import { useLogger } from 'evlog/nestjs'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
完整的输出、增强和尾部采样管道:
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({ token: config.get('AXIOM_TOKEN') }),
}),
})
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' } })
}
完整的输出、增强和尾部采样管道:
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
},
}))
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') 访问记录器。没有 useLogger()——使用 c.get('log') 并显式传递它,或者如果需要跨异步边界使用 useLogger(),请使用 Express/Fastify/Elysia。
完整的输出、增强和尾部采样管道:
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
},
}))
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 的 pino 记录器仍可通过 fastify.log 访问。
使用 useLogger() 从调用堆栈中的任何位置访问记录器,而无需传递 request:
import { useLogger } from 'evlog/fastify'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
完整的输出、增强和尾部采样管道:
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
},
})
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' } })
}
完整的输出、增强和尾部采样管道:
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.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(),
]
在加载器和操作中通过 context.get(loggerContext) 访问记录器:
// 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() 从调用堆栈中的任何位置访问记录器,而无需传递上下文:
import { useLogger } from 'evlog/react-router'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
完整的输出、增强和尾部采样管道:
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
},
}),
]
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
initWorkersLogger({ env: { service: 'edge-api' } })
export default {
async fetch(request: Request) {
const log = createWorkersLogger(request)
try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
},
}
对于任何基于 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, // 自动导入 log、createEvlogError、parseError
strip: ['debug'], // 在生产环境中移除 log.debug()
sourceLocation: true, // 在开发和生产环境中注入 file:line
client: { // 客户端日志记录
transport: { endpoint: '/api/logs' },
},
}),
],
})
服务器端中间件(输出、增强、保留、路由)仍在框架集成中配置(例如,Hono/Express/SvelteKit 的 evlog() 中间件)。Vite 插件仅处理构建时开发体验。
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() // 在独立使用中需要手动触发
所有选项在 Nuxt(evlog 键)、Nitro(传递给 evlog())、Next.js(createEvlog())和独立使用(initLogger())中都有效。
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
env.service / service | string | 'app' | 日志中的服务名称 |
enabled | boolean | true | 全局开关(为 false 时无操作) |
pretty | boolean | 开发环境中为 true | 漂亮的树格式 vs JSON |
silent | boolean | false | 抑制控制台输出。事件仍会发送到输出器 |
include | string[] | 所有路由 | 要记录的路由通配符模式 |
exclude | string[] | 无 | 要排除的路由模式(优先级更高) |
routes | Record<string, { service }> | -- | 路由特定的服务名称 |
sampling.rates | object | -- | 头部采样:{ info: 10, warn: 50 } (0-100%) |
sampling.keep | array | -- | 尾部采样:[{ status: 400 }, { duration: 1000 }] |
drain | (ctx) => void | -- | 输出回调(Next.js,独立使用) |
enrich | (ctx) => void | -- | 增强回调(Next.js) |
keep | (ctx) => void | -- | 自定义尾部采样回调(Next.js) |
| 钩子 | 时机 | 用途 |
|---|---|---|
evlog:drain | 增强后 | 将事件发送到外部服务 |
evlog:enrich | 触发后,输出前 | 添加派生上下文 |
evlog:emit:keep | 触发期间 | 自定义尾部采样逻辑 |
close | 服务器关闭时 | 刷新输出管道缓冲区 |
| 适配器 | 导入 | 环境变量 |
|---|---|---|
| Axiom | evlog/axiom | AXIOM_TOKEN, AXIOM_DATASET |
| OTLP | evlog/otlp | OTLP_ENDPOINT(或 OTEL_EXPORTER_OTLP_ENDPOINT) |
| HyperDX | evlog/hyperdx | HYPERDX_API_KEY(可选 HYPERDX_OTLP_ENDPOINT;默认为 https://in-otel.hyperdx.io) |
| PostHog | evlog/posthog | POSTHOG_API_KEY, POSTHOG_HOST |
| Sentry | evlog/sentry | SENTRY_DSN |
| Better Stack | evlog/better-stack | BETTER_STACK_SOURCE_TOKEN |
| 文件系统 | evlog/fs | 无(本地文件系统) |
在 Nuxt/Nitro 中,使用 NUXT_ 前缀(例如 NUXT_AXIOM_TOKEN),以便值可通过 useRuntimeConfig() 获得。所有适配器也读取未加前缀的变量作为后备。
每个框架的设置模式:
// 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: 在中间件选项中传递 drain
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({ drain: createAxiomDrain() }))
// Fastify: 在插件选项中传递 drain
import { createAxiomDrain } from 'evlog/axiom'
await app.register(evlog, { drain: createAxiomDrain() })
// NestJS: 在模块选项中传递 drain
import { createAxiomDrain } from 'evlog/axiom'
EvlogModule.forRoot({ drain: createAxiomDrain() })
// Next.js: 将 drain 传递给 createEvlog()
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50 } })
const drain = pipeline(createAxiomDrain())
// 然后:createEvlog({ ..., drain })
// 独立使用:将 drain 传递给 initLogger()
initLogger({ env: { service: 'my-app' }, drain: createAxiomDrain() })
有关批处理、重试和缓冲区溢出配置,请参阅 references/drain-pipeline.md。
内置:createUserAgentEnricher()、createGeoEnricher()、createRequestSizeEnricher()、createTraceContextEnricher()——全部来自 evlog/enrichers。
// 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: 在 lib/evlog.ts 中
createEvlog({
enrich: (ctx) => {
for (const enricher of enrichers) enricher(ctx)
ctx.event.region = process.env.VERCEL_REGION
},
})
从 Vercel AI SDK 捕获令牌使用量、工具调用、模型信息和流式指标到宽事件中。从 evlog/ai 导入。需要 ai >= 6.0.0 作为对等依赖。
import { createAILogger } from 'evlog/ai'
const log = useLogger(event) // 或任何 RequestLogger
const ai = createAILogger(log)
const result = streamText({
model: ai.wrap('anthropic/claude-sonnet-4.6'), // 接受字符串或模型对象
messages,
onFinish: ({ text }) => {
// 用户回调保持自由——无冲突
},
})
ai.wrap() 使用模型中间件透明地捕获所有 LLM 调用。适用于 generateText、streamText、generateObject、streamObject 和 ToolLoopAgent。
对于嵌入(不同的模型类型):
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage })
宽事件 ai 字段包括:calls、model、provider、inputTokens、outputTokens、totalTokens、cacheReadTokens、reasoningTokens、finishReason、toolCalls、steps、msToFirstChunk、msToFinish、tokensPerSecond、error。
要检测的反模式:
| 反模式 | 修复 |
|---|---|
在 onFinish 中手动跟踪令牌 | ai.wrap()——中间件自动捕获 |
console.log('tokens:', result.usage) | ai.wrap()——宽事件中的结构化 ai.* 字段 |
| 无 AI 可观测性 | 添加 createAILogger(log) + ai.wrap() |
import { createError } from 'evlog' // 或在 Nuxt 中自动导入
// 最小化
throw createError({ message: '数据库连接失败', status: 500 })
// 标准
throw createError({ message: '支付失败', status: 402, why: '发卡行拒绝该卡' })
// 完整
throw createError({
message: '支付失败',
status: 402,
why: '发卡行拒绝该卡 - 资金不足',
fix: '请使用其他支付方式或联系您的银行',
link: 'https://docs.example.com/payments/declined',
cause: originalError,
})
前端——使用 parseError() 提取所有字段:
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 } } |
有关完整清单,请参阅 references/code-review.md。
根据您正在处理的内容加载——不要一次加载所有文件:
每周安装次数
560
仓库
GitHub 星标数
962
首次出现
2026年1月26日
安全审计
安装于
opencode476
codex469
github-copilot467
gemini-cli464
amp445
kimi-cli444
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.
| Working on... | Resource |
|---|---|
| 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 |
npm install evlog
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
include: ['/api/**'],
},
})
All evlog functions (useLogger, createError, parseError, log) are auto-imported — no import statements needed.
// 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, and tail sampling use Nitro hooks in server plugins:
// 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):
// nuxt.config.ts
evlog: {
transport: { enabled: true }, // logs sent to /api/_evlog/ingest
}
Client-side: log, setIdentity, clearIdentity are auto-imported in components.
Step 1: Create central config — all exports come from here:
// 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', token: process.env.AXIOM_TOKEN! }))
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():
// app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'
export const POST = withEvlog(async (request: Request) => {
const log = useLogger() // Zero arguments — uses AsyncLocalStorage
log.set({ user: { id: 'user_123', plan: 'enterprise' } })
log.set({ cart: { items: 3, total: 14999 } })
return Response.json({ success: true })
})
Step 3: Server Actions — same withEvlog() wrapper:
// app/actions.ts
'use server'
import { withEvlog, useLogger } from '@/lib/evlog'
export const checkout = withEvlog(async (formData: FormData) => {
const log = useLogger()
log.set({ action: 'checkout', source: 'server-action' })
return { success: true }
})
Step 4: Middleware (optional — sets x-request-id + timing headers):
// proxy.ts
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = { matcher: ['/api/:path*'] }
Step 5: Client Provider — wrap root layout:
// 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:
'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'
setIdentity({ userId: 'usr_123' })
log.info({ action: 'checkout_click' })
clearIdentity()
Step 7: Client ingest endpoint — receives client logs:
// 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 })
}
// src/hooks.server.ts
import { initLogger } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'
initLogger({ env: { service: 'my-app' } })
export const { handle, handleError } = createEvlogHooks()
Access the logger via event.locals.log in route handlers or useLogger() from anywhere in the call stack:
// 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' } })
}
Full pipeline with drain, enrich, and tail sampling:
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.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 uses Nitro v3. Install evlog and add a 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' } })],
})
Add the error handling middleware to __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)],
},
})
Use useRequest() from nitro/context to access the logger:
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.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})
Import useLogger from evlog/nitro in routes.
// src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
@Module({
imports: [EvlogModule.forRoot()],
})
export class AppModule {}
EvlogModule.forRoot() registers a global middleware. Use useLogger() to access the request-scoped logger from any controller or service:
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:
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():
EvlogModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config) => ({
drain: createAxiomDrain({ token: config.get('AXIOM_TOKEN') }),
}),
})
import express from 'express'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/express'
initLogger({ env: { service: 'my-api' } })
const app = express()
app.use(evlog())
app.get('/api/users', (req, res) => {
req.log.set({ users: { count: 42 } })
res.json({ users: [] })
})
Use useLogger() to access the logger from anywhere in the call stack without passing req:
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:
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
},
}))
import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
initLogger({ env: { service: 'my-api' } })
const app = new Hono<EvlogVariables>()
app.use(evlog())
app.get('/api/users', (c) => {
const log = c.get('log')
log.set({ users: { count: 42 } })
return c.json({ users: [] })
})
Access the logger via c.get('log') in handlers. No useLogger() — use c.get('log') and pass it down explicitly, or use Express/Fastify/Elysia if you need useLogger() across async boundaries.
Full pipeline with drain, enrich, and tail sampling:
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
},
}))
import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/fastify'
initLogger({ env: { service: 'my-api' } })
const app = Fastify({ logger: false })
await app.register(evlog)
app.get('/api/users', async (request) => {
request.log.set({ users: { count: 42 } })
return { users: [] }
})
request.log is the evlog wide-event logger (shadows Fastify's built-in pino logger on the request). Fastify's pino logger remains accessible via fastify.log.
Use useLogger() to access the logger from anywhere in the call stack without passing request:
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:
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
},
})
import { Elysia } from 'elysia'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/elysia'
initLogger({ env: { service: 'my-api' } })
const app = new Elysia()
.use(evlog())
.get('/api/users', ({ log }) => {
log.set({ users: { count: 42 } })
return { users: [] }
})
.listen(3000)
Use useLogger() to access the logger from anywhere in the call stack:
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:
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.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(),
]
Access the logger via context.get(loggerContext) in loaders and actions:
// app/routes/api.users.$id.tsx
import { loggerContext } from 'evlog/react-router'
export async function loader({ params, context }: Route.LoaderArgs) {
const log = context.get(loggerContext)
log.set({ user: { id: params.id } })
return { users: [] }
}
Use useLogger() to access the logger from anywhere in the call stack without passing context:
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:
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
},
}),
]
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
initWorkersLogger({ env: { service: 'edge-api' } })
export default {
async fetch(request: Request) {
const log = createWorkersLogger(request)
try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
},
}
For any Vite-based project (SvelteKit, Astro, SolidStart, React+Vite, etc.), use the Vite plugin for auto-init, auto-imports, and build-time features:
// vite.config.ts
import evlog from 'evlog/vite'
export default defineConfig({
plugins: [
evlog({
service: 'my-app',
autoImports: true, // auto-import log, createEvlogError, parseError
strip: ['debug'], // remove log.debug() in production
sourceLocation: true, // inject file:line in dev + prod
client: { // client-side logging
transport: { endpoint: '/api/logs' },
},
}),
],
})
Server-side middleware (drain, enrich, keep, routes) is still configured in the framework integration (e.g., evlog() middleware for Hono/Express/SvelteKit). The Vite plugin handles build-time DX only.
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
All options work in Nuxt (evlog key), Nitro (passed to evlog()), Next.js (createEvlog()), and standalone (initLogger()).
| Option | Type | Default | Description |
|---|---|---|---|
env.service / service | string | 'app' | Service name in logs |
enabled | boolean | true | Global toggle (no-ops when false) |
| Hook | When | Use |
|---|---|---|
evlog:drain | After enrichment | Send events to external services |
evlog:enrich | After emit, before drain | Add derived context |
evlog:emit:keep | During emit | Custom tail sampling logic |
close | Server shutdown | Flush drain pipeline buffers |
| Adapter | Import | Env Vars |
|---|---|---|
| Axiom | evlog/axiom | AXIOM_TOKEN, AXIOM_DATASET |
| OTLP | evlog/otlp | OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_ENDPOINT) |
| HyperDX | evlog/hyperdx | (optional ; defaults to ) |
In Nuxt/Nitro, use the NUXT_ prefix (e.g., NUXT_AXIOM_TOKEN) so values are available via useRuntimeConfig(). All adapters also read unprefixed variables as fallback.
Setup pattern per framework:
// 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.
Built-in: createUserAgentEnricher(), createGeoEnricher(), createRequestSizeEnricher(), createTraceContextEnricher() — all from evlog/enrichers.
// 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
},
})
Capture token usage, tool calls, model info, and streaming metrics from the Vercel AI SDK into wide events. Import from evlog/ai. Requires ai >= 6.0.0 as a peer dependency.
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,
onFinish: ({ text }) => {
// User callbacks remain free — no conflict
},
})
ai.wrap() uses model middleware to transparently capture all LLM calls. Works with generateText, streamText, generateObject, streamObject, and ToolLoopAgent.
For embeddings (different model type):
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage })
Wide event ai field includes: calls, model, provider, inputTokens, outputTokens, totalTokens, cacheReadTokens, reasoningTokens, finishReason, toolCalls, steps, , , , .
Anti-patterns to detect:
| Anti-Pattern | Fix |
|---|---|
Manual token tracking in onFinish | ai.wrap() — middleware captures automatically |
console.log('tokens:', result.usage) | ai.wrap() — structured ai.* fields in wide event |
| No AI observability | Add createAILogger(log) + ai.wrap() |
import { createError } from 'evlog' // or auto-imported in Nuxt
// 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,
})
Frontend — extract all fields with parseError():
import { parseError } from 'evlog'
const error = parseError(err)
// error.message, error.status, error.why, error.fix, error.link
See references/structured-errors.md for common patterns and templates.
| Anti-Pattern | Fix |
|---|---|
Multiple console.log in one function | Single wide event with log.set() |
throw new Error('...') | throw createError({ message, status, why, fix }) |
console.error(e); throw e | log.error(e); throw createError(...) |
| No logging in request handlers | Add useLogger(event) / / |
See references/code-review.md for the full checklist.
Load based on what you're working on — do not load all at once :
Weekly Installs
560
Repository
GitHub Stars
962
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode476
codex469
github-copilot467
gemini-cli464
amp445
kimi-cli444
Next.js 16+ 缓存组件指南:实现部分预渲染(PPR)与混合内容缓存
12,700 周安装
OpenAPI 转 TypeScript 工具 - 自动生成 API 接口与类型守卫
563 周安装
数据库模式设计器 - 内置最佳实践,自动生成生产级SQL/NoSQL数据库架构
564 周安装
Rust Unsafe代码检查器 - 安全使用Unsafe Rust的完整指南与最佳实践
564 周安装
.NET并发编程模式指南:async/await、Channels、Akka.NET选择决策树
565 周安装
韩语语法检查器 - 基于国立国语院标准的拼写、空格、语法、标点错误检测与纠正
565 周安装
技能安全扫描器 - 检测Claude技能安全漏洞,防范提示注入与恶意代码
565 周安装
pretty |
boolean |
true in dev |
| Pretty tree format vs JSON |
silent | boolean | false | Suppress console output. Events still go to drains |
include | string[] | All routes | Route glob patterns to log |
exclude | string[] | None | Route patterns to exclude (takes precedence) |
routes | Record<string, { service }> | -- | Route-specific service names |
sampling.rates | object | -- | Head sampling: { info: 10, warn: 50 } (0-100%) |
sampling.keep | array | -- | Tail sampling: [{ status: 400 }, { duration: 1000 }] |
drain | (ctx) => void | -- | Drain callback (Next.js, standalone) |
enrich | (ctx) => void | -- | Enrich callback (Next.js) |
keep | (ctx) => void | -- | Custom tail sampling callback (Next.js) |
HYPERDX_API_KEYHYPERDX_OTLP_ENDPOINThttps://in-otel.hyperdx.io| PostHog | evlog/posthog | POSTHOG_API_KEY, POSTHOG_HOST |
| Sentry | evlog/sentry | SENTRY_DSN |
| Better Stack | evlog/better-stack | BETTER_STACK_SOURCE_TOKEN |
| File System | evlog/fs | None (local file system) |
msToFirstChunkmsToFinishtokensPerSeconderroruseLogger()createRequestLogger()Flat log data { uid, n, t } | Grouped objects: { user: {...}, cart: {...} } |
Logging sensitive data log.set({ user: body }) | Explicit fields: { user: { id: body.id, plan: body.plan } } |