CLI

地图评分

规则CI
evlog map 如何将规则结果转换为分数:每个入口点的权重、项目平均分、等级阈值,以及路由如何被标记。

分数用于与自身进行比较。你可以在修复问题时观察它的变化,也可以据此设置 pull request 的门禁,而不是将其作为与其他项目比较的基准。

一个入口点

每个入口点从 100 分开始,按其未通过的每一项 要求 扣除相应权重,最低降到 0 分。

要求权重
wide-event40
audit25
structured-errors20
page-error-handling20
context15
error-handling15

通过的规则不会扣分。报告 n/a 的规则也不会扣分,因为它从未适用,所以不会算在入口点头上。机会完全没有权重,也不会出现在这里。

通过注释禁用的检查也会标记为 n/a,因此不会扣分;报告会统计禁用了多少项,因为部分得分来自已禁用检查时,必须明确说明这一点。

以 playground 里的登录处理器为例:

evlog map server/api/auth/login.post.ts
CHECKS
✓ useLogger  每个请求都发出了 wide event
✓ log.set    使用 log.set() 关联了 context
✗ log.audit  敏感操作但没有审计轨迹

structured-errorserror-handlingn/a:该处理器既不抛出异常,也不捕获异常。一个要求未通过,因此得分为 100 − 25 = 75

权重之和故意超过 100。一个未通过所有规则的处理器会降到 0 分,而优先修复顺序依然有意义:wide-event 的 40 分高于任何两个较小规则的权重之和。

这样,无论从哪个方向看,算术都清晰易懂。下面是 playground 中最差的入口点,它从 20 逐步升到 100,每一行修复都恰好等于其满足的规则权重:

api/auth/[...all].ts·20/100
export default defineEventHandler(async (event) => {
const log = useLogger()
log.set({ provider, flow: 'oauth' })
const session = await auth.handler(event)
log.audit('auth.login', { userId: session.user.id })
return session
})
20/100at risk
log useLogger()−40
ctx log.set()−15
audit log.audit()−25
err createError()n/a
catch catch loggingn/a
fetch fetch handlingn/a
every rule costs exactly its weight ▲ 20 → 100 in three lines

项目分数如何计算

全局分数是每个入口点的平均值,并根据盲区会给你造成多大损失来加权:

入口点权重
标记为资金或认证相关×2
页面×0.5
其他所有×1

页面的权重更低,因为吞掉 fetch 错误的页面会带来比可观测性缺口更差的用户体验,而且它们通常也是应用中数量最多的文件。敏感处理器的权重加倍,因为那正是你需要事件的地方。

三个入口点:一个得分为 75 的敏感处理器、一个得分为 100 的普通处理器,以及一个得分为 50 的页面:

(75 × 2) + (100 × 1) + (50 × 0.5)   275
───────────────────────────────── = ───── = 79
        2 + 1 + 0.5                  3.5

没有可评分入口点的项目目前得分为 100。这是一个空结果,并不代表埋点已完整完成。在解读分数之前,请检查检测到的框架和入口点数量。

如何分配等级

分数等级
90–100优秀
70–89良好
50–69需要改进
0–49有风险

如何对文件进行分类

除了分数之外,每个入口点还会被分类,这决定了 JSON 中 summary 块的统计方式,也决定了报告中的覆盖率行如何构建。

类别何时
instrumented同时传递了 wide-eventcontext 的处理程序,或者能够处理其 fetch 错误的页面
partial只传递了两者之一的处理程序
dark两者都未传递的处理程序,或者吞掉 fetch 错误的页面
exempt没有需要埋点的内容——evlog 自身的基础设施,或者没有发起任何 fetch 的页面。它与你自己的入口点分开统计,而不是作为一个缺口来统计

dark 表示分析器未识别出所需的 evlog 埋点或页面错误处理。其他日志记录器或不受支持的抽象仍可能产生有用的证据。在将其分类视为没有日志之前,请检查代码。

标记涉及资金、认证或 PII 的内容

敏感度决定了一个入口点是否提升为双倍权重,并添加 audit 要求,所以准确了解它是如何判定的很重要。

资金:$

  • 导入 stripe@stripe/stripe-jspaddle-sdk@lemonsqueezy/lemonsqueezy.js
  • 路径 包含 checkoutpaymentbillinginvoicerefundsubscriptionchargepayout

认证:A

  • 导入 better-authnext-authlucia@auth/core@auth/nextjs
  • 路径 包含 authoauthloginlogoutsigninsignupregisterpasswordtokensessionmfaotp

PII:@

字段匹配 emailphoneaddressssniban,并且在同一个处理程序中存在写入调用(createupdateinsertupsert)。

金融和认证属于 high 敏感度:双倍权重,并且需要审计轨迹。PII 属于 medium:没有额外要求,但会在报告中标记。

匹配是如何工作的

有两个决定避免了噪音。

导入是从 AST 中读取的。 只有当某个包确实被导入时才计数,所以 // TODO: drop stripe 注释不会让某条路由处理金融相关内容。

路径术语匹配完整单词,同时允许复数形式。/api/authors 不是认证路由,而 /api/refunds 是资金路由。这一点比看起来更重要:被错误标记的路由会被分配一个没有理由满足的 25 分要求,并在平均分中按双倍计算,这是让整个分数失去可信度的最快方式。

当你检查某个入口点时,每个原因都会完整显示,所以错误调用只需一行就能发现:

evlog map server/api/auth/login.post.ts
FLAGGED SENSITIVE BECAUSE
▍ auth: path says "auth"

诚实地解读分数

  • 高分并不等于良好的可观测性。 它只说明结构已经具备:每个入口点都有事件,附带上下文,错误可以解释,敏感操作有审计。你附加的上下文是否正是你将来需要的上下文,这是静态分析器无法判断的。最佳实践 →
  • 比较的是运行结果,而不是项目。 一个有 200 条路由、得分 70 的应用,状态比一个只有 6 条路由、得分同样为 70 的应用更好。
  • 重点关注 dark 和 `资金与认证,而不是总分。这两个地方正是下一次事故最可能潜伏的地方。

接下来

  • CI:将分数转换为通过/失败检查
  • 规则:每个要求实际检查的内容。