evlog map 会显示哪些受支持的入口点缺少 evlog 日志模式。它会检查你的源代码中是否存在宽事件、上下文、结构化错误以及其他日志规则。
选定的框架适配器会发现它能够识别的入口点,例如 API 处理器、获取数据的页面和中间件。报告会为这些检查评分,并列出三个优先检查项。在源代码、CLI 版本和适配器相同的情况下,检查结果也会相同。
扫描不会执行处理器或检查流量。高分并不能证明日志能够到达你的日志接收端,也不能证明日志包含足够的上下文来诊断故障。请阅读可观测性评分指南,了解受支持的框架以及结果的局限性。
evlog map
提升我的 evlog map 评分
将它添加到已经投入生产的应用中
evlog map 的任何设计都不假设项目是全新的。在受支持的框架中,它读取的是源代码,而不是流量,因此一个三年前发布的应用与脚手架项目的评分方式相同:识别入口点、识别模式、执行相同的静态检查。你会在下一次事故发生之前得到问题列表,而不是在事故发生期间才得到它。不受支持的 Node.js 应用,以及适配器无法识别的包装器,不属于该评分范围。请参见可观测性评分。
无论代码库存在多久,工作都分为相同的四个步骤:
- 运行
npx @evlog/cli map。无需安装、无需部署,也无需让代理监视实时进程。 - 阅读 FIX FIRST 区块。它最多包含三个入口点,并根据它们涉及的内容进行排序:资金、认证和 PII 的优先级高于健康检查。
- 修复这三个入口点,并分别对它们运行
npx @evlog/cli map <file>,查看规则预期的结构。列表中的其他内容暂时无需处理。 - 提交
evlog.map.json,并使用--baseline以此设置门禁。从该提交开始,已识别的回退都需要经过审查。
大型代码库通常会在第一次运行时得到很低的评分,而这个数字并不是重点。门禁机制是 ratchet,而不是评分:一个从 34 分开始且从未回退的旧应用,比一个起初为 80 分、却在一年内逐渐下降的新应用更健康。
确定性的静态检查
evlog map 不是运行时基准测试,也不是对应用进行的不稳定测量。它是针对适配器能够识别的入口点,对约定日志规范执行的确定性静态检查:这是你可以据此限制拉取请求的检查类型。
确定性意味着,在任何机器上、每次运行中,相同的源代码、CLI 版本和适配器都会产生相同的评分。规则引擎使用 AST 读取源代码,并应用版本化规则集,因此任何结果都不依赖时序、流量或环境。由此会产生三点结论:
- 门禁不会不稳定。 由于结果不会在不同运行之间变化,
--min-score <n>的行为就像真正的测试:门禁通过意味着已识别的要求达到了你设置的最低标准,而不是运行器碰巧成功。确定性评分是你唯一应该用来阻止拉取请求的评分类型。 - 它会逐步提高要求。
--baseline会与已提交的evlog.map.json进行比较,并对现有入口点中已识别的要求或评分回退失败,其行为与快照测试完全相同。悄悄删除log.set的重构,无法绕过忽略差异的审查。请参见ratchet以及可观测性评分页面中的基线限制。 - 它适合审计。 map 是一种版本化、可复现的构建产物:修订版本相同,
evlog.map.json就相同,因此三周前的评分与今天表示的是相同的检查。你可以在审查中引用它,任何人也都可以重新生成它。
它还会在任何流量出现之前报告结果。与必须监视实时应用的代理不同,evlog map 会在代码写入的瞬间发现缺失的已识别模式,而这正是修复成本最低的时候。将它作为第一天就执行的规范检查,这样你就不会在事故处理中才发现这些缺口。
报告
这是一次针对 evlog playground 的真实运行,这是一个拥有 29 个入口点的 Nuxt 应用:
▀▀█ █▀▀ 分数 /100 evlog-playground · Nuxt
█ █▀█ ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ 已扫描 29 个入口点
▀ ▀▀▀ 良好 ▂▂▂▃▃▃▃▃▃▄▆▆▆▆▆███████████████
覆盖范围
● API 处理器 ▰▰▰▰▰▰▰▰▱▱ 79 28 个中有 13 个存在缺口
● 中间件与任务 ▰▰▰▰▰▱▱▱▱▱ 45 运行时没有任何日志记录
● 金钱与认证 ▰▰▰▰▰▰▰▱▱▱ 71 缺少审计轨迹
优先修复
1. ANY /api/auth/:all* A — 涉及认证但没有任何日志
server/api/auth/[...all].ts:1 · evlog.dev/learn/wide-events
2. GET /api/test/catalog/payment-declined $ — 涉及资金流转但没有审计轨迹
server/api/test/catalog/payment-declined.get.ts:11 · evlog.dev/use-cases/audit/overview
3. POST /api/auth/login A — 缺少 log.audit
server/api/auth/login.post.ts:1 · evlog.dev/use-cases/audit/overview
然后
· 为 /api/test/browser-ingest 添加 useLogger + log.set +4
· 为 /api/payment/process、/api/test/better-auth/whoami 添加 log.audit
· 为 /api/audit/deny 添加 log.set
· 为 /api/audit/with-audit 添加 log.set + createError({ why, fix })
· 为 /api/test/h3-error 添加 useLogger + log.set + createError({ why, fix })
· 为 /api/test/tail-sampling/error 添加 createError({ why, fix })
✓ 已经很稳:/api/audit/catalog/invoice-refund +14
▲ 76 → 86 通过修复上面的 3 项
────────────────────────────
evlog.map.json 已更新 · 这个分数如何计算 → evlog.dev/cli/scoring
▸ evlog map --all 每个入口点 · evlog map <file> 检查单个入口点
--min-score 80 CI 门槛
从上到下阅读,每个区块都会回答不同的问题。
分数
大号数字是全局评分,满分为 100:它是每个入口点的加权平均值,其中资金和认证路由的权重为双倍,页面的权重为一半。下方的单词是评分等级:excellent、good、needs work 或 at risk。
右侧的一排方块代表项目中的每个入口点,从左到右依次为最差到最好。右侧高方块成墙、左侧只有几个矮方块,说明应用整体健康,只存在少数盲点。一整排很低的方块说明应用根本没有任何埋点。在大型项目中,这一排会按终端宽度采样显示,每个桶里显示最差的入口点。
覆盖范围
分数的来源,会按照你理解应用的方式进行分组,而不是按规则分组。只有当项目中存在相应内容时,这些区域才会出现:一个没有 pages/ 的 Nuxt 应用不会显示 Pages 行。
| 区域 | 覆盖内容 |
|---|---|
| API 处理器 | 服务端路由处理器 |
| 页面 | 在服务端获取数据的页面 |
| 中间件与任务 | 中间件、定时任务、服务端动作 |
| 金钱与认证 | 敏感性分类器标记到的每个入口点,无论位于何处 |
金钱与认证这一行会刻意与其他行重叠。它是缺失事件代价最高的分组,因此它单独成行,并且当其得分低于整个应用时会带有 ⚠ 标记。
优先修复
最有收益的三个入口点,先敏感项,再按最差分数排序。每个条目都会给出方法、路径、敏感性标记、说明实际问题的一句话,以及文件、行号和失败规则的文档链接。
$ 是资金,A 是认证,@ 是 PII。它们来自敏感性分类器,被标记的入口点会被要求额外满足一项要求:审计轨迹。
然后
其余存在缺口的内容,按修复方式批量列出,而不是每个入口点单独占一行。为 /api/test/browser-ingest 添加 useLogger + log.set +4 表示有 5 个入口点需要同样的两行代码,因此你可以一次性处理它们。
进一步提升
只有在项目已经使用了某个 evlog 功能、但某些入口点还没有受益时,才会出现的独立部分:
进一步提升
你已经在用这些——你的应用还能从中获得更多收益
+ 这些重复错误应该变成目录条目吗?——2 个入口点
server/api/orders/[id].get.ts:4 · evlog.dev/learn/catalogs
建议不会改变分数。
这些是建议,不是缺口。它们以项目中某处已经使用该功能作为前提,绝不会被计为失败,而且 --min-score 门槛也不可能因为它们而失败。请参见机会。
最后两行
✓ Already solid 会列出无需再修复的入口点,因此一个健康的应用也会得到明确肯定。▲ 76 → 86 by fixing the 3 above 表示只修复 FIX FIRST 下列出的内容时将得到的评分,这正是应该从那里开始,而不是从其他地方开始的原因。
三种视图
默认报告回答“我做得怎么样”。另外两个旗标回答你还会有的另外两个问题。
每个入口点
--all 会打印检查矩阵:每个入口点一行,按最差情况优先,并按目录分组。
evlog-playground · Nuxt · 76/100 · 29 个入口点,最差情况优先
log ctx err audit catch fetch
server/
├─ api/auth/[...all].ts ▰▰▱▱▱▱▱▱▱▱ 20 A ● ● · ● · ·
├─ …ment-declined.get.ts ▰▰▱▱▱▱▱▱▱▱ 20 $ ● ● ● ● · ·
├─ …test/h3-error.get.ts ▰▰▰▱▱▱▱▱▱▱ 25 ● ● ● · · ·
├─ …owser-ingest.post.ts ▰▰▰▰▰▱▱▱▱▱ 45 ● ● · · · ·
├─ …t/with-audit.post.ts ▰▰▰▰▰▰▰▱▱▱ 65 ● ● ● · ● ·
├─ …i/auth/login.post.ts ▰▰▰▰▰▰▰▰▱▱ 75 A ● ● · ● · ·
├─ …ampling/error.get.ts ▰▰▰▰▰▰▰▰▱▱ 80 ● ● ● · · ·
└─ …st/wide-event.get.ts ▰▰▰▰▰▰▰▰▰▰ 100 ● ● · · · ·
● 已覆盖 ● 缺口 · 不适用 $ 金额 A 认证 @ 个人身份信息
每一列检查什么 → evlog.dev/cli/rules
每列对应一条规则,顺序按照它们所占的分数排列。圆点表示该规则不适用于此处:一个不会抛出任何内容的处理器,不需要检查其错误是否携带 why 和 fix,这会显示为圆点,而不是免费通过。空心圆 ○ 表示你已通过评论禁用的检查。每一列的说明都位于规则中。
单个入口点
传入路由或文件路径,即可获得单个入口点的完整说明:
evlog map server/api/auth/login.post.ts
# 或按路由
evlog map /api/auth/login
POST /api/auth/login A ▰▰▰▰▰▰▰▰▱▱ 75/100
server/api/auth/login.post.ts · Nuxt
扫描此文件的原因
▍ POST /api/auth/login — 服务端处理器
被标记为敏感的原因
▍ auth:路径包含“auth”
检查项
✓ useLogger 每个请求都会发出宽事件
✓ log.set 已通过 log.set() 附加上下文
✗ log.audit 敏感操作但没有审计轨迹 evlog.dev/use-cases/audit/overview
建议结构 — Nuxt
│ export default defineEventHandler(async (event) => {
│ log.audit({
│ action: 'auth.login',
│ actor: { type: 'user', id: user.id },
│ })
│ })
▲ 修复此入口点:75 → 100
有四点值得注意:
- 为什么扫描此文件说明 CLI 认为这个文件是什么。如果判断错误,下面的所有内容都会错误,而你可以在这里看到原因。
- 被标记为敏感的原因显示确切原因,因此发现误报只需要查看一行,而不必面对一个谜团。
- 建议结构根据实际失败的规则生成,并采用你的框架惯用的写法。审计操作会从路由中读取。
/api/auth/login会建议使用auth.login,而不是占位符。 - 当所有要求都通过但仍有建议时,裁决会一分为二:没有需要修复的内容,以及仍有 n 项可以提升。
什么算作入口点
检测按框架进行,依据文件布局。--framework 在检测猜错时会覆盖它。
server/api/** → API 处理程序,路径以前缀 /api 开头,方法来自文件名后缀
server/routes/** → API 处理程序,路径按原样写入
app/pages/**/*.vue → 页面(Nuxt 4 默认;也包括 pages/ 和 src/pages/)
server/middleware/** → 中间件
server/tasks/** → 定时任务
routes/** → API 处理程序,路径按原样写入
api/** → API 处理程序,路径以前缀 /api 开头
middleware/** → 中间件
app/**/route.ts → 每个导出的 HTTP 方法对应一个 API 处理程序(GET、POST,…)
app/**/page.tsx → 页面
middleware.ts → 中间件
"use server" file → 每个导出的函数对应一个服务器操作
src/routes/** → 当路由声明了 server handlers 或 createServerFn 时为 API 处理程序,
否则为页面。会跳过 __root 文件。
Next.js 同时支持 app/ 和 src/app/,以及 src/middleware.ts 与 middleware.ts。没有导出任何 HTTP 方法的 route.ts 会被列出一次,方法为 ANY。
类型在报告中显示为 ANY、PAGE、MID、ACT(服务器操作)、CRON 和 WS,或者在入口点具有 HTTP 方法时显示为该 HTTP 方法。
n/a,而不是失败。当裁决有误时
你不同意的检查应该只让你多写一条评论,而不该卡住你的 CI 门禁:
// evlog-map-disable-next-line wide-event, context -- 存活探针,特意静默
export default defineEventHandler(() => ({ ok: true }))
这项检查会变为 n/a,并附带你的理由,因此它不再扣分;报告还会说明项目禁用了多少项检查,所以高分永远不会掩盖一个完全不记录日志的应用。完整语法请参见规则。
标志
| 标志 | 默认值 | 作用 |
|---|---|---|
<entry> | — | 按路由路径或文件路径检查单个入口点 |
--all | off | 将每个入口点作为检查矩阵显示 |
--min-score <n> | off | 当全局评分低于 n 时以代码 1 退出 |
--baseline [ref] | off | 相对于已提交的 map 发生回退时以代码 1 退出(路径,或 git:<ref>) |
--framework <name> | detected | 强制使用 nuxt、nitro、next、tanstack-start 或 hono |
--no-write | writes | 跳过写入 evlog.map.json |
--verbose | off | 显示逐文件解析警告 |
--cwd <dir> | current | 扫描其他目录 |
--json | off | 将完整 map 以 JSON 格式输出到 stdout |
evlog.map.json
每次运行都会将 evlog.map.json 写入项目根目录:包括分数、框架、生成该文件的 CLI 版本和规则集版本,以及每个入口点、其检查项、建议、敏感性和自身分数。这与 --json 输出的数据相同。
如果不希望生成该文件,可使用 --no-write,适用于 CI 运行或快速查看他人的项目。是否提交它取决于你的门禁方式:
- 不对 CI 设置门禁,或仅使用
--min-score:该文件是构建产物,因此可以将其加入 gitignore。它与--json包含相同的数据,因此不会丢失任何内容。 - 使用
--baseline设置门禁:像 lockfile 一样跟踪它。ratchet 会与已提交的副本进行比较,因此git:<ref>只能读取已提交的文件。使用evlog map重新生成,在差异中审查它,永远不要手动编辑它。
它不会告诉你什么
静态分析有其局限性,了解这些局限性,决定了你是信任这个评分,还是被它惹恼。
- 它读取代码,而不是流量。 一个入口点可以得到 100 分,但如果它附加的上下文有误,运行时仍然会发出无用事件。评分说明的是结构存在。
- 它会跟踪一层导入。 通过本地模块重新导出的辅助函数可以被解析,
import { useLogger } from '@/lib/evlog'也会被计入,但通过你自己的三层抽象传递的 logger 可能无法被识别。 - 五个框架。 Nuxt、Nitro、Next.js App Router、TanStack Start 和 Hono 拥有适配器。evlog 集成的其他框架目前还不会被扫描。
- Hono 路由从字面量注册中读取。 带有内联函数或命名函数的
app.get('/path', handler)会被计入。计算得到的路径、控制器风格的处理器(app.get('/users', users.list)),以及子应用挂载时使用的前缀(app.route('/api', users)会报告不含/api的子路径)无法被解析。 - 敏感性是一种启发式判断。 它会读取导入和路由路径。
evlog map <file>始终会显示原因,因此错误判断是可见的,而不是悄无声息地发生。