Elysia
evlog/elysia 插件会自动创建一个请求作用域的日志记录器,通过 log 在路由上下文和 useLogger() 中访问,当响应完成时发出一个广泛事件。
在我的 Elysia 应用中设置 evlog
快速开始
1. 安装
pnpm add evlog elysia
bun add evlog elysia
yarn add evlog elysia
npm install evlog elysia
2. 初始化并注册插件
import { Elysia } from 'elysia'
import { initLogger } from 'evlog'
import { evlog } from 'evlog/elysia'
initLogger({
env: { service: 'my-api' },
})
const app = new Elysia()
.use(evlog())
.get('/health', ({ log }) => {
log.set({ route: 'health' })
return { ok: true }
})
.listen(3000)
log 属性在所有路由处理程序中通过 Elysia 的 derive 自动可用。
广泛事件
通过在处理程序中逐步构建上下文。一个请求 = 一个广泛事件:
app.get('/users/:id', async ({ log, params }) => {
const userId = params.id
log.set({ user: { id: userId } })
const user = await db.findUser(userId)
log.set({ user: { name: user.name, plan: user.plan } })
const orders = await db.findOrders(userId)
log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })
return { user, orders }
})
所有字段在请求完成时合并为一个单一广泛事件并发出:
14:58:15 INFO [my-api] GET /users/usr_123 200 in 12ms
├─ orders: count=2 totalRevenue=6298
├─ user: id=usr_123 name=Alice plan=pro
└─ requestId: 4a8ff3a8-...
useLogger()
使用 useLogger() 从调用堆栈的任何位置访问请求作用域日志记录器,而无需将上下文传递到服务层:
import { useLogger } from 'evlog/elysia'
export async function findUser(id: string) {
const log = useLogger()
log.set({ user: { id } })
const user = await db.findUser(id)
log.set({ user: { name: user.name, plan: user.plan } })
return user
}
import { findUser } from './services/user'
app.get('/users/:id', async ({ params }) => {
const user = await findUser(params.id)
return user
})
log 在上下文和 useLogger() 中都返回相同的日志记录器实例。useLogger() 使用 AsyncLocalStorage 在异步边界之间传播日志记录器。
后台工作 (log.fork)
从路由上下文中使用 log.fork(label, fn) 创建一个子广泛事件。参见 广泛事件 — 发送后。
import { evlog, useLogger } from 'evlog/elysia'
app
.use(evlog())
.post('/orders', ({ log }) => {
log.fork!('ship', async () => {
const l = useLogger()
l.set({ shipped: true })
})
return { ok: true }
})
错误处理
使用 createError 创建带有 why、fix 和 link 字段的结构化错误。Elysia 通过 onError 捕获抛出的错误:
import { createError, parseError } from 'evlog'
app
.use(evlog())
.get('/checkout', ({ log }) => {
log.set({ cart: { items: 3, total: 9999 } })
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer',
fix: 'Try a different payment method',
link: 'https://docs.example.com/payments/declined',
})
})
.onError(({ error, set }) => {
const parsed = parseError(error)
set.status = parsed.status
return {
message: parsed.message,
why: parsed.why,
fix: parsed.fix,
link: parsed.link,
}
})
错误被捕获并记录,同时包含自定义上下文和结构化错误字段:
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
├─ error: name=EvlogError message=Payment failed status=402
├─ cart: items=3 total=9999
└─ requestId: 880a50ac-...
配置
有关所有可用选项(initLogger、中间件选项、抽样、静默模式等),请参见 配置参考。
排水和增强器
在插件选项中直接配置排水适配器和增强器:
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
const userAgent = createUserAgentEnricher()
app.use(evlog({
drain: createAxiomDrain(),
enrich: (ctx) => {
userAgent(ctx)
ctx.event.region = process.env.FLY_REGION
},
}))
管道(批处理和重试)
对于生产环境,使用 createDrainPipeline 包装您的适配器以批处理事件并在失败时重试:
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({
batch: { size: 50, intervalMs: 5000 },
retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())
app.use(evlog({ drain }))
drain.flush(),以确保所有缓冲的事件都被发送。有关所有选项,请参见 管道文档。抽样
使用 keep 强制保留特定事件,无论头部抽样如何:
app.use(evlog({
drain: createAxiomDrain(),
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))
路由过滤
使用 include 和 exclude 模式控制哪些路由被记录:
app.use(evlog({
include: ['/api/**'],
exclude: ['/_internal/**', '/health'],
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
},
}))
客户端日志记录
使用 evlog/http 从任何前端向 Elysia 服务器发送结构化日志。这适用于任何客户端框架(React、Vue、Svelte、vanilla JS)。
浏览器设置
import { initLogger, log } from 'evlog'
import { createHttpLogDrain } from 'evlog/http'
const drain = createHttpLogDrain({
drain: { endpoint: '/v1/ingest' },
})
initLogger({ drain })
log.info({ action: 'page_view', path: location.pathname })
摄入端点
添加一个 POST 路由以接收来自浏览器的批处理 DrainContext[]:
import type { DrainContext } from 'evlog'
app.post('/v1/ingest', async ({ body }) => {
const batch = body as DrainContext[]
for (const ctx of batch) {
console.log('[浏览器]', JSON.stringify(ctx.event))
}
return new Response(null, { status: 204 })
})
本地运行
git clone https://github.com/hugorcd/evlog.git
cd evlog
pnpm install
pnpm run example:elysia
打开 http://localhost:3000 以探索交互式测试 UI。
下一步
- Wide Events: 通过上下文分层设计全面的事件
- Adapters: 将日志发送到 Axiom、Sentry、PostHog 等
- Sampling: 使用头部和尾部抽样控制日志量
- Structured Errors: 抛出带有
why、fix和link字段的错误