---
title: "Effect"
description: "`routerLayer` 为每个 HttpRouter 或 HttpApi 请求生成一个宽事件；任意 effect 都可通过 `EvlogLogger` 读取它，作业和定时任务则使用 `withLogger`。"
canonical_url: "https://evlog.zhcndoc.com/integrate/frameworks/effect"
---
# Effect

> `routerLayer` 为每个 HttpRouter 或 HttpApi 请求生成一个宽事件；任意 effect 都可通过 `EvlogLogger` 读取它，作业和定时任务则使用 `withLogger`。

`evlog/effect` 会将 Effect 的 `HttpRouter` 或 `HttpApi` 处理的每个请求记录为一个宽事件；其他 effect（作业、队列消息、Durable Object 定时任务）则可通过 `withLogger` 记录为一个事件。事件会作为 `EvlogLogger` 服务存在于 Effect context 中，因此处理器可通过 `yield* EvlogLogger` 读取它，事件也会记录导致 effect 失败的 `Cause`。此集成需要 `effect` 4。

<callout color="warning" icon="i-lucide-flask-conical">

**Beta 版。** `evlog/effect` 是新集成，Effect 仍将 `effect/http` 标记为不稳定。它的导出和写入字段可能会在 evlog 的次要版本中变化，包括不兼容变更。请固定 evlog 版本，并在升级时阅读变更日志。欢迎通过 [GitHub](https://github.com/evloghq/evlog/issues)反馈。

</callout>

<prompt :actions="["copy","cursor","claude"]" description="在我的 Effect 应用中配置 evlog" icon="i-simple-icons-effect">

在我的 Effect HTTP 应用中配置 evlog。

- 安装 evlog：pnpm add evlog（effect 4 是 peer dependency）
- 创建 evlog.config.ts，并写入 export default defineEvlog({ service: 'my-api' })
- 将来自 'evlog/effect' 的 initLoggerLayer(toLoggerConfig(config)) 提供给服务器 Layer
- 将来自 'evlog/effect' 的 routerLayer(toMiddlewareOptions(config)) 提供给 HttpRouter 路由，并向 HttpRouter.serve 或 HttpRouter.toWebHandler 传入 disableLogger: true
- 使用 yield* EvlogLogger 读取事件；需要类型化字段时使用 yield* useLogger<fields>

()，并调用 log.set() 累积上下文

</fields>
- 使用 withLogger('action-name') 包装作业、队列消费者和 Durable Object 定时任务
- 提供 loggerLayer()，让 Effect.log* 调用写入当前宽事件

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

</prompt>

## 快速开始

### 1. 安装

<code-group>

```bash [pnpm]
pnpm add evlog effect @effect/platform-node
```

```bash [bun]
bun add evlog effect @effect/platform-node
```

```bash [yarn]
yarn add evlog effect @effect/platform-node
```

```bash [npm]
npm install evlog effect @effect/platform-node
```

</code-group>

`@effect/platform-node` 会在 Node 上提供路由服务。在 Cloudflare Workers 中无需安装它，可使用 `HttpRouter.toWebHandler` 提供路由服务。参见 [Cloudflare Workers 和 Durable Objects](#cloudflare-workers-and-durable-objects)。

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

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

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

采样、drain、`enrich` 和路由过滤等选项都在此文件中配置。参见[配置](https://evlog.zhcndoc.com/reference/configuration)。

### 3. 提供 Layer

`routerLayer` 会在路由器的每条路由上安装 evlog 中间件，并向其处理器提供 `EvlogLogger`。Layer 构建时，`initLoggerLayer` 会调用 `initLogger`：

```typescript [src/index.ts]
import { createServer } from 'node:http'
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node'
import { Effect, Layer } from 'effect'
import { HttpRouter, HttpServerResponse } from 'effect/http'
import { toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { EvlogLogger, initLoggerLayer, routerLayer } from 'evlog/effect'
import config from '../evlog.config'

const Routes = HttpRouter.add('GET', '/health', Effect.gen(function* () {
  const log = yield* EvlogLogger
  log.set({ route: 'health' })
  return HttpServerResponse.jsonUnsafe({ ok: true })
}))

const Server = HttpRouter.serve(
  Routes.pipe(Layer.provide(routerLayer(toMiddlewareOptions(config)))),
  { disableLogger: true },
).pipe(
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
  Layer.provide(initLoggerLayer(toLoggerConfig(config))),
)

NodeRuntime.runMain(Layer.launch(Server))
```

`disableLogger: true` 会关闭路由器自带的 `Sent HTTP response` 日志行，由宽事件取代。相同的 Layer 也适用于 `HttpRouter.toWebHandler`；对于 `HttpApi`，可与 `HttpApiBuilder.layer(Api)` 配合使用。

不使用路由器时，`evlog()` 就是原始的 `effect/http` 中间件：`HttpServer.serve(app, evlog(options))`。`routerLayer(options)` 则会将该中间件全局注册到路由器。

## 宽事件

在处理器执行过程中逐步累积上下文。每个请求对应一个宽事件：

```typescript [src/routes/users.ts]
import { Effect } from 'effect'
import { HttpRouter, HttpServerResponse } from 'effect/http'
import { EvlogLogger } from 'evlog/effect'

export const UserRoute = HttpRouter.add('GET', '/users/:id', Effect.gen(function* () {
  const log = yield* EvlogLogger
  const { id } = yield* HttpRouter.params

  log.set({ user: { id } })

  const user = yield* Users.find(id)
  log.set({ user: { name: user.name, plan: user.plan } })

  const orders = yield* Orders.forUser(id)
  log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })

  return HttpServerResponse.jsonUnsafe({ user, orders })
}))
```

所有字段都会合并到一个事件中，并在响应就绪时发出。请求运行在 Effect span 中，因此事件会携带 `traceId` 和 `spanId`：

```bash [Terminal output]
16:03:30.441 INFO [my-api] GET /users/42 200 in 3ms
  ├─ requestId: 11362fc9-...
  ├─ traceId: 370aa48d6f233739a22e1297da7cabb5
  ├─ spanId: 6cf0065dae1b90e3
  ├─ user: id=42 name=Alice plan=pro
  └─ orders: count=2 totalRevenue=6298
```

## 从任意 effect 中获取事件

其他集成通过 `AsyncLocalStorage` 将 logger 提供给下层代码。`evlog/effect` 则使用 Effect context：`EvlogLogger` 是一项服务，读取它的函数会在类型中声明此依赖。

```typescript [src/services/payment.ts]
import { Effect } from 'effect'
import { EvlogLogger } from 'evlog/effect'

export const chargeCard = (amount: number) => Effect.gen(function* () {
  const log = yield* EvlogLogger
  log.set({ payment: { amount } })
})
```

`chargeCard` 的类型是 `Effect<void, never, EvlogLogger>`，因此在工作单元之外运行它会导致类型检查失败。不应携带此依赖的库代码可以改用 `set`：它会将字段合并到当前事件中，在事件之外则不执行任何操作，类似于在 span 之外调用 `Effect.annotateCurrentSpan`：

```typescript [src/services/cache.ts]
import { Effect } from 'effect'
import { set } from 'evlog/effect'

export const recordCacheHit = Effect.gen(function* () {
  yield* set({ cache: { hit: true } })
})
```

入口点完全不使用 `AsyncLocalStorage`，因此在 Cloudflare Workers 上运行时无需设置 `nodejs_compat` 标志。这里也没有 `log.fork()`：请通过 [`withLogger`](#jobs-queues-and-alarms) 运行后台工作，它会在同一个 trace 中发出自己的事件。

### 类型化字段

`useLogger<T>()` 会返回同一个事件，并按你的字段定义类型，因此错误的结构无法通过编译：

```typescript [src/routes/checkout.ts]
import { Effect } from 'effect'
import { useLogger } from 'evlog/effect'

interface CheckoutFields {
  cart: { items: number, total: number }
  user: { id: string }
}

const checkout = Effect.gen(function* () {
  const log = yield* useLogger<CheckoutFields>()
  log.set({ cart: { items: 3, total: 9999 } })
  log.set({ cart: { items: 'three' } }) // type error
})
```

## 错误处理

处理器失败时，evlog 会读取 `Cause`。Effect 的 `Cause.squash` 会优先选择预期失败而非 defect，排在首位的错误会成为事件的 `error`。其他失败和 defect 会以 `{ name, message }` 的形式放在 `error.suppressed` 中。仅被中断的工作单元会记录 `status: 499`，这是 evlog 表示请求已取消的状态，且不会记录 `error`。失败值不是 `Error` 时，会记录其 `_tag`（或 `UnknownError`），不会复制其他属性，因此包含 prompt 或 payload 的失败值不会泄漏到事件中。

事件的 `status` 是客户端实际收到的状态码。Effect 路由器不会从错误中读取 `status`，因此传入 `createError({ status: 402 })` 最终会返回 `500`。若要使用错误自身的状态码响应，请在处理器中捕获并记录错误，然后用 `parseError()` 构造响应：

```typescript [src/routes/checkout.ts]
import { Effect } from 'effect'
import { HttpRouter, HttpServerResponse } from 'effect/http'
import { createError, parseError } from 'evlog'
import { EvlogLogger } from 'evlog/effect'

const respondWithError = (error: unknown) => Effect.gen(function* () {
  const log = yield* EvlogLogger
  log.error(error instanceof Error ? error : String(error))
  const parsed = parseError(error)
  return HttpServerResponse.jsonUnsafe(
    { message: parsed.message, why: parsed.why, fix: parsed.fix, link: parsed.link },
    { status: parsed.status },
  )
})

export const CheckoutRoute = HttpRouter.add('POST', '/checkout', Effect.gen(function* () {
  const log = yield* EvlogLogger
  log.set({ cart: { items: 3, total: 9999 } })

  return yield* Effect.fail(createError({
    message: 'Payment failed',
    status: 402,
    why: 'Card declined by issuer',
    fix: 'Try a different payment method',
    link: 'https://docs.example.com/payments/declined',
  }))
}).pipe(Effect.catch(respondWithError)))
```

```bash [Terminal output]
16:03:30.452 ERROR [my-api] POST /checkout 402 in 2ms
  ├─ error: Payment failed
  │  Why: Card declined by issuer
  │  Fix: Try a different payment method
  │  More: https://docs.example.com/payments/declined
  ├─ requestId: ecb06410-...
  ├─ traceId: 74b88b043b5093c642d81d4aa6c54ab8
  └─ cart: items=3 total=9999
```

A route that does not exist is recorded as `404`, with the router's `HttpServerError` as the error.

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

**HttpApi** 会在路由器中间件看到错误前，将端点声明的错误编码为响应。事件会记录该响应状态码（声明为 `httpApiStatus: 404` 的 `UserNotFound` 会记录为 `404`），但不会包含 `error` 字段。端点未声明的 defect 和失败会记录为 `error`。声明的错误需要携带详细信息时，请在处理器中调用 `log.error()`。

</callout>

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

**流式响应**会在处理器返回 `HttpServerResponse` 时发出事件，而不是等流式正文结束，因此事件不会记录流的持续时间或流传输过程中的错误。

</callout>

## 作业、队列和定时任务 [](undefined)

非 HTTP 请求的工作可通过 `withLogger` 记录为独立宽事件。它会提供 `EvlogLogger`，在以 action 命名的 span 中运行 effect，并在 effect 成功、失败或被中断时发出事件：

```typescript [src/jobs/digest.ts]
import { Effect } from 'effect'
import { EvlogLogger, withLogger } from 'evlog/effect'

export const sendDigest = Effect.gen(function* () {
  const log = yield* EvlogLogger
  const users = yield* Users.subscribed()
  log.set({ digest: { recipients: users.length } })
  yield* Mailer.sendDigest(users)
}).pipe(withLogger('send-digest', { fields: { job: { queue: 'mail' } } }))
```

事件使用 `action` 取代 `method` 和 `path`。如果作业在请求中运行，其事件会与请求共用 `traceId`，并将请求 span 记录为 `parentSpanId`：

```bash [Terminal output]
16:03:30.461 INFO [my-api] in 4ms
  ├─ action: send-digest
  ├─ traceId: 23e560634c887a4a598c8ab52a044397
  ├─ spanId: bdbe7c5b55e6b542
  ├─ parentSpanId: 8858cf5e94ca2ad9
  ├─ job: queue=mail
  └─ digest: recipients=128
```

`withLogger(action, options)` takes `drain`, `enrich`, `keep`, `redact`, `plugins` and `waitUntil` as `routerLayer` does, plus:

<table>
<thead>
  <tr>
    <th>
      选项
    </th>
    
    <th>
      类型
    </th>
    
    <th>
      对事件的影响
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        fields
      </code>
    </td>
    
    <td>
      <code>
        Record<string, unknown>
      </code>
    </td>
    
    <td>
      事件的初始字段
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        traceparent
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      延续此 W3C trace，而不是当前 span。格式错误的值会启动新 trace
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tracestate
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      随 <code>
        traceparent
      </code>
      
       一起转发到外发请求
    </td>
  </tr>
</tbody>
</table>

传入队列消息携带的 `traceparent`，即可延续将该消息入队的请求 trace。

## Cloudflare Workers 和 Durable Objects [](undefined)

`HttpRouter.toWebHandler` 会将路由转换为 Fetch handler。为了让 drain 的工作在响应返回后继续运行，需要使用请求的 `waitUntil`，因此请通过 handler 的 context 参数按请求提供它：

```typescript [src/worker.ts]
import { Context, Layer } from 'effect'
import { HttpRouter } from 'effect/http'
import { WaitUntil, routerLayer } from 'evlog/effect'
import { createAxiomDrain } from 'evlog/axiom'
import { initWorkersLogger } from 'evlog/workers'

initWorkersLogger({ env: { service: 'my-worker' } })

const { handler } = HttpRouter.toWebHandler(
  Routes.pipe(Layer.provide(routerLayer({ drain: createAxiomDrain() }))),
  { disableLogger: true },
)

export default {
  fetch(request: Request, _env: Env, ctx: ExecutionContext) {
    return handler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx)))
  },
}
```

在路由器之外，可通过 `Effect.provideService(WaitUntil, fn)` 为任意 effect 设置该值。没有 `WaitUntil` 时，工作单元会在结束前等待 drains 完成。

Durable Object 的 `fetch` 会收到原始 `Request`。`withRequestLogger` 会包装生成 `Response` 的 effect，并像路由请求一样记录 method、path、status；失败时从错误的 `status` / `statusCode` 读取状态码，否则使用 `500`。定时任务没有请求，因此使用 `withLogger`；由于这里没有提供 `WaitUntil`，定时任务会在返回前等待 drains 完成：

```typescript [src/room.ts]
import { DurableObject } from 'cloudflare:workers'
import { Effect } from 'effect'
import { withRequestLogger, withLogger } from 'evlog/effect'

export class Room extends DurableObject {
  fetch(request: Request) {
    return Effect.runPromise(join(request).pipe(withRequestLogger(request)))
  }

  alarm() {
    return Effect.runPromise(sweepIdleMembers.pipe(withLogger('room-alarm')))
  }
}
```

## 跨服务跟踪同一操作

每个工作单元都会延续传入的 W3C `traceparent` 并保留 `tracestate`。因此，请求经过 gateway、一个 Durable Object 和两个服务时，每一跳都会生成一个带有相同 `traceId` 的事件；每个事件的 `parentSpanId` 都指向发起调用的 span。

Effect 的 `HttpClient` 已会为当前 span 发送 `traceparent`。`withTracePropagation` 会将收到的 `tracestate` 加到该 client 发出的每个请求中：

```typescript [src/services/search.ts]
import { Effect } from 'effect'
import { HttpClient } from 'effect/http'
import { withTracePropagation } from 'evlog/effect'

export const search = (query: string) => Effect.gen(function* () {
  const client = withTracePropagation(yield* HttpClient.HttpClient)
  return yield* client.get(`https://search.internal/q?${new URLSearchParams({ query })}`)
})
```

不通过 `HttpClient` 发出的调用（例如 Durable Object stub 的 `fetch`）可以从 `traceHeaders` 获取标头：

```typescript [src/routes/messages.ts]
import { Effect } from 'effect'
import { traceHeaders } from 'evlog/effect'

export const postMessage = (stub: DurableObjectStub, body: string) => Effect.gen(function* () {
  const headers = yield* traceHeaders
  return yield* Effect.promise(() => stub.fetch('https://room/messages', { method: 'POST', headers, body }))
})
```

## 将 Effect.log 合并到事件中

默认情况下，`Effect.logInfo` 等方法每次调用都会输出一行。`loggerLayer()` 会改为将这些调用写入当前宽事件，并使用 evlog 中相同严重性的日志方法（`fatal`、`error`、`warn`、`info`、`debug`、`trace`），因此 `Effect.logError` 会提升事件级别。消息使用 evlog 的 printf 风格格式（`%s`、`%d`、`%j`），`Effect.annotateLogs` 注解会作为字段合并到事件中。工作单元之外的调用会交给 `loggerLayer()` 之前安装的 logger，因此现有 logger 仍可继续使用。

```typescript [src/index.ts]
import { loggerLayer } from 'evlog/effect'

const Server = HttpRouter.serve(App, { disableLogger: true }).pipe(
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
  Layer.provide(loggerLayer()),
)
```

```bash [Terminal output]
16:03:30.461 INFO [my-api] in 4ms
  ├─ action: send-digest
  ├─ digest: recipients=128
  └─ requestLogs: [{"level":"info","message":"digest rendered","timestamp":"..."}]
```

## Drain 与丰富化器

`routerLayer` 接受与其他 evlog 集成相同的选项。可在 `evlog.config.ts` 中设置 `drain` 和 `enrich`，由 `toMiddlewareOptions(config)` 传入，也可以直接内联配置。内联字段会覆盖文件中的同名字段：

```typescript [src/index.ts]
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
import { routerLayer } from 'evlog/effect'

const userAgent = createUserAgentEnricher()

const EvlogLive = routerLayer({
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    userAgent(ctx)
    ctx.event.region = process.env.FLY_REGION
  },
})
```

`withLogger` 和 `withRequestLogger` 可以各自设置 `drain` 和 `enrich`。工作单元没有指定 `drain` 时，会使用传给 `initLogger` 的 drain。

### 管道（批处理与重试）

`createDrainPipeline` 创建的 drain 会缓冲事件，因此进程退出前必须刷新。将它传给 `initLoggerLayer`：当作用域关闭时，该 Layer 会刷新 drain；`NodeRuntime.runMain` 会在关闭时完成此操作。`routerLayer` 和 `withLogger` 自身没有 `drain` 时会回退到它：

```typescript [src/index.ts]
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { initLoggerLayer } from 'evlog/effect'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})

const EvlogLive = initLoggerLayer({
  env: { service: 'my-api' },
  drain: pipeline(createAxiomDrain()),
})
```

完整选项见[管道文档](https://evlog.zhcndoc.com/extend/drain-pipeline)。

## 尾部采样

使用 `keep` 可在头部采样之外强制保留事件。它会在工作单元结束后运行，此时可读取持续时间、状态和字段：

```typescript [src/index.ts]
routerLayer({
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
```

`withLogger` 也接受相同的 `keep`，因此慢作业和慢请求都会被保留。参见[采样](https://evlog.zhcndoc.com/learn/sampling)。

## 路由过滤

使用 `include` 和 `exclude` 控制哪些请求会生成事件，并通过 `routes` 为每条路由指定 service。被排除的路由仍会运行：其处理器会获得一个空操作的 `EvlogLogger`，不会发出事件。

```typescript [src/index.ts]
routerLayer({
  include: ['/api/**'],
  exclude: ['/_internal/**', '/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})
```

## 脱敏

`redact` 会在事件到达 drain 前遮蔽所有事件中的 PII 和秘密信息，包括在 Effect 代码中设置的字段。传入 `redact: true` 可启用内置模式，也可以列出需要删除内容的路径，例如 prompt 和消息正文：

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

export default defineEvlog({
  service: 'my-api',
  redact: { paths: ['prompt', '**.content'] },
})
```

模式和路径语法见[脱敏](https://evlog.zhcndoc.com/learn/redaction)。

## 本地运行

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

打开 [http://localhost:3000](http://localhost:3000) 查看交互式测试界面。

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

在 GitHub 上查看完整的 Effect 示例源码。

</card>
</card-group>

## 后续步骤

- [宽事件](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` 字段的错误

---

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


## Sitemap

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