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的描述来自教程章节本身,没有另行验证。