evlog map 会告诉你哪些入口点是黑暗的。evlog init 就是在此之前运行的命令。它会在你当前所在的项目中,执行框架指南所描述的设置。
npx @evlog/cli init
在终端中,它会读取你的项目,询问那些它无法推断的信息,并在采取任何操作前向你展示计划:
┌ evlog init checkout
│
◇ 检测到 Nuxt
│
◇ 每个广域事件上的服务名称
│ checkout
│
◇ 在开发环境中,事件应该发送到哪里?
│ 本地文件
│
◇ 生产环境中呢?
│ Axiom, Sentry
│
◇ 还有别的吗?
│ Context
│ ◻ Request enrichers
│ Delivery
│ ◻ Batching and retry
│ ◻ Sampling
│ Catalogs
│ ◼ Error catalog · 发现 3 个重复错误
│ ◻ Audit actions · 2 条敏感路由没有审计轨迹
│
◇ 计划 ───────────────────────────────────╮
│ │
│ run pnpm add evlog │
│ update nuxt.config.ts │
│ create server/plugins/evlog-drain.ts │
│ create server/plugins/evlog-enrich.ts │
│ │
├──────────────────────────────────────────╯
│
◇ 应用?
│ 是
│
◇ 4 ok · 1 warn · 0 fail
│
◇ 在接收到任何内容之前先设置这些 ─────────────╮
│ │
│ AXIOM_DATASET 要写入的数据集 │
│ AXIOM_API_KEY 具有 ingest 权限的 API token │
│ │
├─────────────────────────────────────────────────────╯
│
│ verify evlog doctor
│ score evlog map
│
└ Nuxt 已接入 · evlog.dev/integrate/frameworks/nuxt
生产目标选择器是模糊搜索(输入 ax 可查找 Axiom),并且可以选择多个目标:同一个事件会分发到每个目标。附加项会分组显示,并且只列出你的项目实际可以使用的选项,同时说明使它们出现的依据。
它的作用
- 读取你的项目,使用与
evlog map相同的分析:框架、已安装的软件包、重复出现的错误、没有审计轨迹的入口点。这些就是生成选项的依据。 - 安装
evlog,使用锁文件所表明的软件包管理器,除非它已经存在。--no-install会打印命令而不是运行它。 - 注册集成:Nuxt 模块、Nitro 模块,或 Next.js instrumentation 文件。
- 接入你的目标,在同一位置区分开发和生产环境;如果你选择了批处理、enrichers 和采样,也会一并接入。
- 教会你的 AI agents,如果你允许它这样做。将 evlog 约定作为代码块写入
AGENTS.md,创建一个指向它的CLAUDE.md,并通过npx skills add添加 agent skills。这与evlog agents执行的操作相同;这些写入会与接入操作一起规划,因此你只需确认一次。已经安装的 Skills 会留给npx skills update处理。--no-agents会跳过此步骤。 - 运行
evlog doctor,然后再结束,这样运行结果会直接回答“是否成功”,而不是告诉你自行检查。
它写入的每一项都会逐行报告,包括那些已经就绪的部分。
它只提供能够支持的内容
不适用的选项不会显示,显示出来的选项会说明原因:
| 提供项 | 出现条件 |
|---|---|
| 错误目录 | 扫描发现同一个 createError 出现在多个文件中 |
| 审计操作 | map 标记了敏感入口点,但没有 log.audit() |
| AI SDK 日志记录 | ai 是一个依赖项 |
| 认证身份 | better-auth 是一个依赖项 |
| 批处理与重试 | 选择了生产目标——否则没有任何内容可批处理 |
| Vite 插件 | 该框架基于 Vite |
将这些中的某一项作为标志传给一个无法使用它的项目时,会将其丢弃并给出说明,而不是连接任何无效内容。
非交互式
只要有任何迹象表明没有人在关注,提示就会被跳过,每个答案都来自标志和默认值:
| 条件 | 原因 |
|---|---|
--yes | 这是你说的 |
--json | 负载就是契约;前面再套一层半吊子的 TUI 帮不上忙 |
| stdin 或 stdout 不是 TTY | 被管道传递、重定向,或由工具启动 |
CI 被设置为除 false / 0 之外的任何值 | 工作流运行器没有键盘 |
evlog init --yes --prod-drain axiom,sentry --extras pipeline,enrichers
evlog init --yes --extras sampling --sampling high
evlog init --json --drain none # 将计划和结果以 JSON 输出,无需阅读
--drain 或 --extras 值会终止运行,而不是回退到默认值,因为悄悄接入错误的目标比命令失败更糟糕。它不会覆盖你的代码
init 只会追加;它绝不会重写。具体来说:
- 配置文件会在它要添加到的节点的精确偏移位置进行修改。你的注释、引号样式和格式都会保留。不会从 AST 重新打印任何内容。
- 已经存在的文件会保持不变,并报告为已经存在。没有
--force。 - 任何无法安全执行的操作都会变成一个手动步骤,其中包含要粘贴的代码片段以及它停止的原因。由变量构建的
modules键是常见情况:CLI 无法看见数组中的内容,因此不存在将字符串插入其中的正确位置。
这使得它可以安全地运行两次。第二次运行会报告哪些内容已经接入,并且不会写入任何东西:
· nuxt.config.ts already registers evlog/nuxt
· nuxt.config.ts already has an evlog block
· server/plugins/evlog-drain.ts already exists
如果你更愿意先查看计划再对任何内容进行修改,请先使用 --dry-run。
按框架
Nuxt
将 'evlog/nuxt' 添加到 modules,并添加包含服务名称的 evlog 配置块。useLogger 和 parseError 会从那里自动导入,createError 则从 evlog 导入。
export default defineNuxtConfig({
modules: ['@nuxt/ui', 'evlog/nuxt'],
evlog: {
env: { service: 'checkout' },
},
})
Nitro
添加 import 和模块调用,并选择与主版本匹配的子路径:Nitro 3 使用 evlog/nitro/v3,nitropack 使用 evlog/nitro。如果没有 nitro.config.ts,则创建它。
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
modules: [
evlog({
env: { service: 'api' },
}),
],
})
Next.js
创建 instrumentation.ts 和 lib/evlog.ts;如果你的应用位于 src/ 下,则会在该目录下创建,因为 Next 只会加载与之匹配的文件。
包装处理器仍然由你负责:Next 没有环境级的请求日志器,因此每个路由都通过 withEvlog() 显式启用。init 会打印代码片段。
import { createEvlog } from 'evlog/next'
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'web',
})
TanStack Start
写入启用 experimental.asyncContext 的 Nitro v3 配置。如果没有它,useRequest() 会返回空值,你得到的会是一个看起来完整、却不记录任何业务上下文的安装结果。
根路由上的 evlogErrorHandler 中间件是一个手动步骤:__root.tsx 是组件文件,直接往里插入内容只能靠猜。
Hono
创建 src/evlog.ts(如果没有 src/,则创建 evlog.ts),其中包含 initLogger 和配置好的中间件导出,因为 Hono 没有可供追加的配置文件。drains 和 enrichers 会写入中间件选项,采样则写入 initLogger。
注册由你负责:你的应用入口属于应用代码,因此 init 会打印出需要粘贴的两行代码。
import { initLogger } from 'evlog'
import { evlog } from 'evlog/hono'
initLogger({
env: { service: 'api' },
})
/** Register once, before your routes: `app.use(evlogMiddleware)`. */
export const evlogMiddleware = evlog({
// drains and enrichers, from your answers
})
目标
这是两个问题,而不是一张列表:没人会把本地开发流量发送到 Axiom,也没人会从服务器的文件系统中读取生产日志。--drain 设置开发环境的 drain(fs 或 none);--prod-drain 接受一个或多个托管目标。
| Id | 目标 | 所需内容 |
|---|---|---|
fs | .evlog/logs 下的本地文件(开发默认) | 无 |
axiom | Axiom | AXIOM_DATASET, AXIOM_API_KEY |
otlp | 任何 OpenTelemetry 收集器 | OTEL_EXPORTER_OTLP_ENDPOINT |
posthog | PostHog | POSTHOG_API_KEY |
sentry | Sentry | SENTRY_DSN |
better-stack | Better Stack | BETTER_STACK_API_KEY |
datadog | Datadog | DATADOG_API_KEY, DATADOG_SITE |
hyperdx | HyperDX | HYPERDX_API_KEY |
none | 仅输出漂亮的控制台内容 | 无 |
选择多个生产目标会把同一事件分发到每一个目标。生成的插件只在一个地方基于环境分支,因此“哪个 drain 在哪里运行”从来不需要事后推断:
const drains = import.meta.dev
? [createFsDrain()]
: [pipeline(createAxiomDrain()), pipeline(createSentryDrain())]
批处理只包装网络发送,因为缓冲本地文件写入会给你希望事件立即显示在屏幕上的那个循环增加延迟。
init 从不索要密钥。 它会打印出适配器会读取哪些环境变量,然后把它们留给你自己处理。一个会提示你输入 API token 的设置命令,实际上是在把凭据写入它自己选择的文件,而这个答案在途中还会进入你的 shell 历史记录。
::。附加项
| Id | 你将获得什么 |
|---|---|
enrichers | 从用户代理、地理位置、请求大小、追踪上下文中选择(--enrichers) |
pipeline | 批处理和重试,而不是每个请求一次 HTTP 调用 |
sampling | 一个速率配置文件,见下文(--sampling) |
error-catalog | 一个带类型的目录,预先填充你已经反复出现的错误 |
audit-catalog | 为没有审计轨迹的敏感路由提供带类型的审计动作 |
ai | AI SDK 生成中的令牌使用量、工具调用和成本 |
better-auth | 每个事件中的已登录用户 |
vite | 从生产构建中剥离 log.debug() 的 Vite 插件 |
--extras vite,enrichers 也会正确执行。采样预设
根据应用承受的流量命名。移动的是 Info:它占据大部分流量,也占据大部分账单;Warnings 只在顶部让步,即使将其减半,整体形状仍然保留。| Id | Info | Warnings | 适用于 |
|---|---|---|---|
all | 全部 | 全部 | 在流量或成本说不之前,这是正确答案 |
low | 50% | 100% | 一个开始不断重复自身的小型应用 |
medium | 25% | 100% | 稳定流量,账单值得关注 |
high | 10% | 100% | Info 是你付费的主要部分 |
very-high | 1% | 50% | 看趋势,而不是单个请求 |
debug 从不被采样,也不会出现在配置中。 未指定的级别会被完整保留,而 debug 事件之所以存在,只是因为有人为了追查某些问题而把它们打开了:对你为排查问题而开启的日志做 5% 采样,意味着只有 5% 的概率看到你真正需要的那一行。生产构建反正也会移除 log.debug(),所以那里通常没有多少量值得采样。evlog: {
env: { service: 'shop' },
sampling: {
rates: { info: 10, warn: 100, error: 100 },
},
}
目录是预先填充的,不是脚手架生成的
error-catalog 不会写出一个把名称填进去的模板。它会写出你代码里已经反复出现的错误,以及你已经写好的说明文字:export const shopErrors = defineErrorCatalog('shop', {
/** 目前内联写在 server/api/checkout.post.ts、server/api/refund.post.ts 中 */
CARD_DECLINED: {
status: 402,
message: '银行卡被拒绝',
why: '发卡机构拒绝了它',
fix: '待办:他们应该如何处理它',
},
})
audit-catalog 的工作方式相同,为每个被标记为敏感且未跟踪的入口点 map 命名一个动作。两者都会以待办清单的形式落地,并且类型已经写好,因此迁移更像是查找和替换,而不是设计工作。单仓库
从工作区根目录运行时,init 会设置各个应用,而不是设置不提供流量的根软件包。它会列出能够检测到框架的工作区软件包。共享的 utils 软件包没有可插桩的入口点,并会询问要接入哪些内容:evlog init # 从列表中选择
evlog init --yes --apps apps/web,apps/api
package.json。本地 drain
对于基于 Nitro 的应用,init 会写入一个 drain 插件;对于 Next.js,它会将 drain 添加到 lib/evlog.ts。文件系统 drain,且只有它,会限制为开发环境:import { createFsDrain } from 'evlog/fs'
const drain = createFsDrain()
export default defineNitroPlugin((nitroApp) => {
// 本地文件是开发时的便利——绝不能作为生产环境的 sink。
if (!import.meta.dev) return
nitroApp.hooks.hook('evlog:drain', drain)
})
.evlog/logs,而且 evlog 会在首次写入时把它加入 gitignore。evlog doctor 也会查找这个目录。标志
| 标志 | 作用 |
|---|---|
--yes, -y | 跳过所有问题并采用默认值 |
--framework <name> | 覆盖检测结果(nuxt、nitro、next、tanstack-start、hono) |
--service <name> | 每个广域事件上的服务名称(默认:你的软件包名称,不含作用域) |
--drain <id> | 开发环境 drain:fs(默认)或 none |
--prod-drain <a,b> | 生产环境目标——见上表 |
--extras <a,b> | 逗号分隔,见“附加项” |
--enrichers <a,b> | user-agent、geo、request-size、trace-context(默认:全部) |
--sampling <id> | all、low、medium(默认)、high、very-high |
--apps <a,b> | 要设置的工作区软件包(仅限单仓库根目录) |
--dry-run | 打印计划,不写入任何内容 |
--no-install | 不运行软件包管理器,而是打印命令 |
--no-agents | 全部跳过——不写入 AGENTS.md 代码块、不创建 CLAUDE.md、不添加 agent skills |
--cwd <dir> | 在工作区中设置其他应用 |
--json | 将计划和结果以 JSON 输出到 stdout(隐含非交互式) |
下一步
evlog agents:单独重新运行 agent 指南,无论何时 Skills 有所更新evlog doctor:确认接入配置能够解析,并且日志正在写入evlog map:既然 evlog 已经就位,就评估仍然黑暗的部分- 快速开始:接入背后的概念