---
title: "oRPC"
description: "`withEvlog(handler)` 会包装 oRPC handler，并且 procedure 中间件会将 logger 放在 `context.log` 上，为每个 procedure 添加其 `operation` 标记。"
canonical_url: "https://evlog.zhcndoc.com/integrate/frameworks/orpc"
---
# oRPC

> `withEvlog(handler)` 会包装 oRPC handler，并且 procedure 中间件会将 logger 放在 `context.log` 上，为每个 procedure 添加其 `operation` 标记。

`evlog/orpc` 提供两个原语：`withEvlog(handler)` 会包装任意 oRPC handler（`RPCHandler`、`OpenAPIHandler`），使每个请求都成为一个宽事件；而 `evlog()` 是一个 procedure 中间件，它会暴露 `context.log`，并将该宽事件以 procedure 路径作为 `operation` 进行标记。

<callout color="info" icon="i-lucide-info">

**oRPC v2：** oRPC 维护者已[宣布在 v2 中提供一方 evlog 支持](https://github.com/middleapi/orpc/discussions/1293#discussioncomment-17032656)（支持 edge）。在 v2 发布之前，`evlog/orpc` 是 oRPC v1 的集成路径；无论 oRPC 之后如何接入，它仍然是 evlog 完整流水线（drain、enricher、尾部采样、结构化错误）的入口。

</callout>

<prompt :actions="["copy","cursor","claude"]" description="在我的 oRPC 应用中设置 evlog" icon="i-lucide-network">

在我的 oRPC 应用中设置 evlog。

- 安装 evlog：pnpm add evlog
- 创建 evlog.config.ts，其中包含 export default defineEvlog({ service: 'my-rpc' })
- 在启动时导入 evlog.config.ts，并调用 initLogger(toLoggerConfig(config))
- 使用来自 'evlog/orpc' 的 withEvlog() 包装你的 RPCHandler / OpenAPIHandler
- 在基础 procedure 上添加 os.use(evlog())，以获得类型化的 context.log + 每个 procedure 的 operation
- 在基础 context 上声明 EvlogOrpcContext，以为 context.log 提供类型
- 直接在 procedure 中抛出 evlog 错误（createError 或 defineErrorCatalog）。evlog/orpc 会将它们桥接为 ORPCError
- 将 drain、enrich、include 和 keep 放入 evlog.config.ts；将 toMiddlewareOptions(config) 传递给 withEvlog()

文档：[https://www.evlog.dev/integrate/frameworks/orpc](https://www.evlog.dev/integrate/frameworks/orpc)
适配器：[https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

::。

## 快速开始

### 1. 安装

<code-group>

```bash [pnpm]
pnpm add evlog @orpc/server
```

```bash [bun]
bun add evlog @orpc/server
```

```bash [yarn]
yarn add evlog @orpc/server
```

```bash [npm]
npm install evlog @orpc/server
```

</code-group>
</prompt>

### 2. 创建 evlog.config.ts

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-rpc',
})
```

所有选项都位于此文件中：采样、drain、`enrich` 和路由过滤器。`npx evlog init` 会为你写入它，以及下方的接入代码。请参阅[配置](https://evlog.zhcndoc.com/reference/configuration)。

### 3. 初始化并连接包装器

```typescript [server/orpc.ts]
import { os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog, withEvlog, type EvlogOrpcContext } from 'evlog/orpc'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

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), toMiddlewareOptions(config))

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

<callout color="info" icon="i-custom-vite">

**使用 Vite？** [`evlog/vite` 插件](https://evlog.zhcndoc.com/reference/vite-plugin) 会从生产构建中移除 `log.debug()`，并注入源代码位置。它不会读取 `evlog.config.ts`，因此保留 `initLogger(toLoggerConfig(config))` 调用：该调用会在插件自动初始化后运行，并替代它。

</callout>

`EvlogOrpcContext` 在 procedure context 上声明了 `log: RequestLogger`，因此从 `base` 派生的每个 procedure 中，`context.log` 都具有完整类型。

## 宽事件

通过你的 handler 逐步构建 context。一次请求 = 一个宽事件：

```typescript [server/orpc.ts]
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'`）：

```bash [Terminal output]
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 透传到服务层：

```typescript [server/services/user.ts]
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
}
```

```typescript [server/orpc.ts]
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。

## 后台工作（`log.fork`）

`useLogger()` 返回的 logger 还支持用于子宽事件的 `fork`，因此后台工作会在其独立的宽事件下运行。请参阅宽事件中的[发出之后](https://evlog.zhcndoc.com/learn/wide-events#after-emit-sealing-and-background-work)。

```typescript [server/services/user.ts]
import { useLogger } from 'evlog/orpc'

export async function findUser(id: string) {
  const log = useLogger()

  log.fork!('background', async () => {
    const child = useLogger()
    child.set({ step: 'complete' })
  })

  const user = await db.findUser(id)
  log.set({ user: { name: user.name, plan: user.plan } })

  return user
}
```

## 错误处理

使用 `createError` 创建带有 `why`、`fix` 和 `link` 字段的结构化错误。`evlog()` 中间件会捕获抛出的错误，将其记录到宽事件中，并将其桥接为 `ORPCError`，从而让网络响应携带你的 `code`、`status`、`message` 以及面向用户的指导字段：

```typescript [server/orpc.ts]
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 和结构化错误字段一起记录：

```bash [Terminal output]
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-...
```

返回给客户端的网络响应：

```json [HTTP 402]
{
  "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"
  }
}
```

<callout color="info" icon="i-lucide-info">

oRPC 的错误封装格式为 `{ defined, code, status, message, data }`。客户端通过 `@orpc/client` 中的 `safe()` 将错误反序列化为类型化联合类型。evlog 遵循该协议，因此 `why`／`fix`／`link` 位于 `data` 下，而不是响应根部。错误编写 API（`createError`／[`defineErrorCatalog`](https://evlog.zhcndoc.com/learn/structured-errors#error-catalogs)）与 evlog 的其他部分相同。

</callout>

## 配置

请参阅[配置参考](https://evlog.zhcndoc.com/reference/configuration)了解所有可用选项（`initLogger`、中间件选项、采样、静默模式等）。所有选项都位于 `evlog.config.ts` 中。

## Drain 与 Enrichers

在 `evlog.config.ts` 中设置 `drain` 和 `enrich`，它们会通过 `toMiddlewareOptions(config)` 到达应用。也可以直接传入 `withEvlog()` 选项；内联键会替换文件中的同名键，而不是与其合并：

```typescript [server/orpc.ts]
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` 包装你的适配器，以批量发送事件并在失败时重试：

```typescript [server/orpc.ts]
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 })
```

<callout color="info" icon="i-lucide-info">

在服务器关闭时调用 `drain.flush()`，以确保所有缓冲的事件都被发送。有关全部选项，请参阅[流水线文档](https://evlog.zhcndoc.com/extend/drain-pipeline)。
:::

## 尾部采样

使用 `keep` 强制保留特定事件，而不受头部采样影响：

```typescript [server/orpc.ts]
const handler = withEvlog(new RPCHandler(router), {
  drain: createAxiomDrain(),
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
```

## 路由过滤

`include` / `exclude` 匹配的是 HTTP 路径（请求 URL），而不是 procedure 名称：

```typescript [server/orpc.ts]
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。

## 本地运行

```bash [Terminal]
git clone https://github.com/evloghq/evlog.git
cd evlog
pnpm install
pnpm example orpc
```

打开 [http://localhost:3000](http://localhost:3000) 来探索交互式测试界面。

<card-group>
<card icon="i-simple-icons-github" title="源代码" to="https://github.com/evloghq/evlog/tree/main/examples/orpc">

在 GitHub 上浏览完整的 oRPC 示例源码。

</card>

:::

## 下一步

进一步深化你的 **oRPC** 集成：

- [宽事件](https://evlog.zhcndoc.com/learn/wide-events)：通过上下文分层设计全面的事件
- [适配器](https://evlog.zhcndoc.com/integrate/adapters/overview)：将日志发送到 Axiom、Sentry、PostHog 等
- [采样](https://evlog.zhcndoc.com/learn/sampling)：通过头部和尾部采样控制日志量
- [结构化错误](https://evlog.zhcndoc.com/learn/structured-errors)：抛出包含 `why`、`fix` 和 `link` 字段的错误。

</card-group>
</callout>

---

- [源代码](https://github.com/evloghq/evlog/tree/main/examples/orpc)


## Sitemap

See the full [sitemap](https://evlog.zhcndoc.com/sitemap.md) for all pages.
