oRPC
evlog/orpc 提供两个原语:withEvlog(handler) 会包装任意 oRPC handler(RPCHandler、OpenAPIHandler),使每个请求都成为一个宽事件;而 evlog() 是一个 procedure 中间件,它会暴露 context.log,并将该宽事件以 procedure 路径作为 operation 进行标记。
evlog/orpc 是 oRPC v1 的集成路径——无论未来 oRPC 如何接入它,它仍然是 evlog 完整流水线(drain、enricher、尾部采样、结构化错误)的入口。在我的 oRPC 应用中设置 evlog
快速开始
1. 安装
pnpm add evlog @orpc/server
bun add evlog @orpc/server
yarn add evlog @orpc/server
npm install evlog @orpc/server
2. 初始化并连接包装器
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 = {
health: base.handler(({ context }) => {
context.log.set({ route: 'health' })
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 })
}
EvlogOrpcContext 在 procedure context 上声明了 log: RequestLogger,因此从 base 派生的每个 procedure 中,context.log 都具有完整类型。
宽事件
通过你的 handler 逐步构建 context。一次请求 = 一个宽事件:
const getUser = base
.input(z.object({ id: z.string() }))
.handler(async ({ input, context }) => {
context.log.set({ user: { id: input.id } })
const user = await db.findUser(input.id)
context.log.set({ user: { name: user.name, plan: user.plan } })
const orders = await db.findOrders(input.id)
context.log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })
return { user, orders }
})
所有字段都会合并到同一个宽事件中,并在请求完成时发出。operation 字段会根据 procedure 路径自动填充(像 users.profile.get 这样的嵌套路由会显示为 operation: 'users.profile.get'):
14:58:15 INFO [my-rpc] POST /rpc/getUser 200 in 12ms
├─ operation: getUser
├─ orders: count=2 totalRevenue=6298
├─ user: id=usr_123 name=Alice plan=pro
└─ requestId: 4a8ff3a8-...
useLogger()
使用 useLogger() 可以在调用栈中的任何位置访问请求作用域的 logger,而无需把 context 透传到服务层:
import { useLogger } from 'evlog/orpc'
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'
const getUser = base
.input(z.object({ id: z.string() }))
.handler(async ({ input }) => findUser(input.id))
context.log 和 useLogger() 都返回同一个 logger 实例。useLogger() 使用 AsyncLocalStorage 在异步边界之间传递 logger。
错误处理
使用 createError 创建带有 why、fix 和 link 字段的结构化错误。evlog() 中间件会捕获抛出的错误,将其记录到宽事件中,并将其桥接为 ORPCError,从而让网络响应携带你的 code、status、message 以及面向用户的指导字段:
import { createError } from 'evlog'
const checkout = base
.handler(({ context }) => {
context.log.set({ cart: { items: 3, total: 9999 } })
throw createError({
message: 'Payment failed',
code: 'PAYMENT_DECLINED',
status: 402,
why: 'Card declined by issuer',
fix: 'Try a different payment method',
link: 'https://docs.example.com/payments/declined',
})
})
错误会被捕获,并连同自定义 context 和结构化错误字段一起记录:
14:58:20 ERROR [my-rpc] POST /rpc/checkout 402 in 3ms
├─ operation: checkout
├─ error: name=EvlogError code=PAYMENT_DECLINED status=402 message=Payment failed
├─ cart: items=3 total=9999
└─ requestId: 880a50ac-...
返回给客户端的网络响应:
{
"defined": false,
"code": "PAYMENT_DECLINED",
"status": 402,
"message": "Payment failed",
"data": {
"why": "Card declined by issuer",
"fix": "Try a different payment method",
"link": "https://docs.example.com/payments/declined"
}
}
{ defined, code, status, message, data } ——客户端通过 @orpc/client 的 safe() 将错误反序列化为一个有类型的联合类型。evlog 遵循该协议,因此 why/fix/link 位于 data 下,而不是响应根部。编写 API(createError / defineErrorCatalog)与 evlog 其余部分完全一致。配置
请参阅配置参考了解所有可用选项(initLogger、中间件选项、采样、静默模式等)。
Drain 与 Enrichers
直接在 withEvlog() 选项中配置 drain 适配器和 enricher:
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
const userAgent = createUserAgentEnricher()
const handler = withEvlog(new RPCHandler(router), {
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())
const handler = withEvlog(new RPCHandler(router), { drain })
drain.flush(),以确保所有缓冲的事件都被发送。有关全部选项,请参阅流水线文档。尾部采样
使用 keep 强制保留特定事件,而不受头部采样影响:
const handler = withEvlog(new RPCHandler(router), {
drain: createAxiomDrain(),
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
路由过滤
include / exclude 匹配的是 HTTP 路径(请求 URL),而不是 procedure 名称:
const handler = withEvlog(new RPCHandler(router), {
include: ['/rpc/**'],
exclude: ['/rpc/_internal/**', '/health'],
routes: {
'/rpc/auth/**': { service: 'auth-service' },
'/rpc/payment/**': { service: 'payment-service' },
},
})
当某条路由被过滤掉时,包装器仍然会注入一个无操作的 context.log,因此 procedure 永远不会因为缺少字段而崩溃——宽事件只是不会被发出,drain/enrich 也不会被调用。
本地运行
git clone https://github.com/hugorcd/evlog.git
cd evlog
pnpm install
pnpm run example:orpc
打开 http://localhost:3000 来探索交互式测试界面。
下一步
进一步深化你的 oRPC 集成: