Effect 里怎么给函数加 span?

你想知道一次请求慢在哪一步,于是在每一步前后手写 Date.now() 相减,结果这些计时代码散得到处都是,异常路径上还漏了几处。

在 Effect 里不用手写:用 Effect.withSpan("名字") 包住一段 Effect,或者用 Effect.fn("名字") 定义函数,函数就自带一个同名 span。 span 会记录开始、结束和耗时,里面如果还有 span,会自动成为它的子 span。

要注意的不是怎么加,而是名字怎么起、加在哪。这两件事决定了 span 数据能不能用。如果你还不熟悉三种信号的分工,先看 Effect 里日志、span 和指标各自该观测什么;span 的父子关系跟着 fiber 的父子结构走,所以并发里的子任务也能正确挂在父 span 下。

withSpan 和 Effect.fn 有什么区别?

Effect.withSpan("名字") 把已有的一段 Effect 包成 span。Effect.fn 是定义 effectful 函数的写法,给它一个名字,函数就自带一个同名 span,更常用:

import { Effect } from "effect"

declare const loadOrder: (id: string) => Effect.Effect<{ readonly customerId: string }>
declare const charge: (customerId: string) => Effect.Effect<void>

export const processOrder = Effect.fn("order.process")(function* (orderId: string) {
  const order = yield* loadOrder(orderId)
  yield* Effect.annotateCurrentSpan("order.id", orderId)
  yield* charge(order.customerId)
})

Effect.fn 的更多选项见 Effect.fn 进阶。

span 的名字该怎么起?

名字要稳定、低基数,比如 order.process,而不是 order.process.o_12345。会变化的东西,比如订单号,放进 attribute:用 Effect.annotateCurrentSpan,或者 withSpan 的 attributes 选项。上面的例子里 Effect.annotateCurrentSpan("order.id", orderId) 就是这个做法。

原因很直接:名字是拿去分组的,变化的名字等于无法分组。

span 应该打在哪里?

打在有意义的边界上:service 的方法、请求入口、一次外部调用。我的建议是不要给每个小函数都加 span,否则调用树变得巨大,却没有信息量。

Effect.fnUntraced 是什么,能当默认写法吗?

Effect.fnUntraced 是去掉 span 的变体,适合热路径或内部小函数。不要拿它做公共边界函数的默认写法,否则这些边界上的可观测性就没了。

span 里能放什么?

别把整个请求体、密钥、令牌当 attribute 放进去。结构化的写法让「顺手打印」更容易,也更容易泄漏。

版本:Effect v4(教程章节没有标注具体的 effect 版本号)。代码是示意代码,用 declare 省略了业务函数,我没有在本文中逐段运行;关于 Effect.fn、withSpan、annotateCurrentSpan 和 fnUntraced 的描述来自教程章节本身,没有另行验证。