每个 signal 都是针对一个事件的一个问题。输入的形状决定答案的形状,答案的形状决定你可以如何使用这列。
| 目标 | 写法 | 得到的结果 | 用法 |
|---|---|---|---|
| 标记事件 | ask | value: boolean、confidence | WHERE signals.x.value = true,或使用 keep 保留事件 |
| 分类事件 | ask + choice | value 是一个选项名称;模型返回分布时还会有 confidence | GROUP BY signals.x.value |
| 排序事件 | ask + score | value 是级别名称,score 位于级别之间;模型返回分布时还会有 confidence | ORDER BY signals.x.score DESC |
标记:带概率的是否问题
最简形式。模型返回 P(yes),列的内容为 value: true, confidence: 0.9。criteria 是可选的,用于告诉模型每个答案的含义,从而提高边界事件的置信度:
import { defineSignal } from '@evlog/signals'
export const retryable = defineSignal({
name: 'retryable',
when: e => (e.status ?? 0) >= 500,
ask: 'Would the same request most likely succeed if retried in a few seconds?',
criteria: {
true: 'Timeout, connection reset, rate limit, transient upstream error',
false: 'Bug, bad data, missing resource',
},
})
"retryable": { "value": true, "confidence": 0.9 }
这是 keep 适用的形状:重试策略、“客户是否得到了想要的结果”、“这是否值得人工审核”。判定结果是布尔值,因此 keep: v => v.value && v.confidence > 0.8 的含义一目了然。
分类:从列表中选择一个选项
为选项命名,并为每个选项提供模型匹配的描述。列的 value 是其中一个键,因此当问题是“属于哪一类”时,应使用这种形状:
export const fault = defineSignal({
name: 'fault',
when: e => (e.status ?? 0) >= 400,
ask: 'Who is responsible for this failure?',
choice: {
client: 'Bad input, expired session, missing permission, client mistake',
app: 'A bug, a misconfiguration or a validation error in our own code',
upstream: 'A third-party dependency failed, timed out or rate-limited us',
},
})
"fault": { "value": "upstream", "confidence": 0.93 }
在 keep 中,v.value 会自动补全为选项名称,因此 v.value === 'app' 会由编译器检查。当事件可能不包含答案时,请添加 unknown 选项;否则模型会从其他选项中选择最不错误的一个,你也会失去事件缺少字段这一信号。
排序:评分表中的位置
按顺序排列级别,从最低级开始。列会将最可能的级别写入 value,将按概率加权的位置写入 score,因此 severity.score > 1.5 可理解为“更接近 page,而不是 watch”:
export const severity = defineSignal({
name: 'severity',
when: e => (e.status ?? 0) >= 500,
ask: 'How urgent is this failure for the on-call engineer?',
score: ['noise', 'watch', 'page'],
})
"severity": { "value": "watch", "score": 1.2, "confidence": 0.6 }
使用 score 排序,使用 value 分桶。score signal 的置信度是最高级别的概率,因此这里的 0.6 表示模型在 watch 和 page 之间犹豫。未返回分布的模型不会在 choice 和 score 列中提供 confidence;但 value 和 score 仍然存在。
始终使用 when 限定范围
when 是普通谓词,会在其他操作之前运行。当它返回 false 时不会调用模型,这正是 signal 保持低成本的原因:上面的 fault 在 99% 的成功请求上不会产生费用。任何带有 keep 的 signal 都必须设置它。
signal 在 enrich 中看到的是宽事件,在 keep 中看到的是请求上下文以及 status、path、method 和 durationMs。evlog 设置的字段具有类型;处理程序通过 log.set() 记录的字段也会存在,但类型为 unknown:
when: e => e.status === 200 && (e.payment as { provider?: string } | undefined)?.provider === 'stripe'
编写模型能够回答的问题
模型只读取事件和问题。发布前检查三点:
- 答案位于事件中。 “重试会成功吗”之所以可行,是因为事件中有错误名称和状态。“这个用户即将流失吗”则不行。
- 答案是一个值。 “它很慢吗,而且是我们的责任吗”其实是两个 signal。
- 规则无法完成这件事。 如果仅靠
when就能回答问题,就不需要模型。status >= 500是规则,“临时性还是永久性”则不是。
配方提供了十二个可行的问题,以及一些不可行的例子。