---
title: "遥测设置"
description: "将 @evlog/telemetry 接入 citty CLI、独立脚本和 GitHub Actions——投递配置、自动捕获标志、telemetry.set() 和调试模式。"
canonical_url: "https://evlog.zhcndoc.com/use-cases/telemetry/setup"
---
# 遥测设置

> 将 @evlog/telemetry 接入 citty CLI、独立脚本和 GitHub Actions——投递配置、自动捕获标志、telemetry.set() 和调试模式。

## 配置投递

发布 CLI 时，将其指向你的接收 URL，将其内置到二进制文件中，并允许按环境覆盖：

```ts [src/index.ts]
withTelemetry(command, {
  name: TOOL,
  version: VERSION,
  endpoint: 'https://telemetry.my-tool.dev/api/telemetry/ingest',
})
```

<table>
<thead>
  <tr>
    <th>
      覆盖项
    </th>
    
    <th>
      作用
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        EVLOG_TELEMETRY_ENDPOINT
      </code>
    </td>
    
    <td>
      替换内置的 URL（预发布、自托管）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        EVLOG_TELEMETRY=0
      </code>
      
       / <code>
        DO_NOT_TRACK=1
      </code>
    </td>
    
    <td>
      不记录，不发送
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        EVLOG_TELEMETRY_DEBUG=1
      </code>
    </td>
    
    <td>
      在不发送的情况下将负载打印到 stderr
    </td>
  </tr>
</tbody>
</table>

内置的 HTTP drain 会使用 `POST` 请求发送 `Content-Type: application/json`，请求体为 `{ events: RunEvent[] }`，并设置 400ms 超时。成功时（`res.ok`），已投递的键会从 outbox 中移除。`flush()` 最长 500ms，且绝不会抛出异常。

## 使用 citty 进行设置

使用 `withTelemetry()` 包裹根命令，通常位于调用 `runMain()` 的 `src/index.ts` 文件中。子命令可以位于同一文件中，也可以位于 `src/commands/*.ts` 中；只有入口点需要使用此包装器。

```ts [src/index.ts]
import { defineCommand, runMain } from 'citty'
import { withTelemetry, defineTelemetryCommands } from '@evlog/telemetry'
import { doctorCommand } from './commands/doctor'
import { syncCommand } from './commands/sync'

const TOOL = 'my-tool'
const VERSION = '1.0.0'

export const main = withTelemetry(
  defineCommand({
    meta: { name: 'my-tool', description: '…', version: VERSION },
    subCommands: {
      doctor: doctorCommand,
      sync: syncCommand,
      telemetry: defineTelemetryCommands({ name: TOOL }),
    },
  }),
  {
    name: TOOL,
    version: VERSION,
    endpoint: 'https://telemetry.my-tool.dev/api/telemetry/ingest',
    collect: {
      flags: { format: ['json', 'csv'] },
      fields: { framework: ['nuxt', 'next'] },
    },
  },
)

runMain(main)
```

### 会自动记录什么

`withTelemetry` 会遍历你的 citty 树。每个 `run` 处理器都会生成一个事件，无需为每个命令编写遥测样板代码。

<table>
<thead>
  <tr>
    <th>
      调用方式
    </th>
    
    <th>
      <code>
        event.command
      </code>
    </th>
    
    <th>
      <code>
        flags
      </code>
      
       说明
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        my-tool doctor
      </code>
    </td>
    
    <td>
      <code>
        doctor
      </code>
    </td>
    
    <td>
      <code>
        {}
      </code>
      
       — 未传入任何内容，默认值也不属于可选项
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        my-tool doctor --json
      </code>
    </td>
    
    <td>
      <code>
        doctor
      </code>
    </td>
    
    <td>
      <code>
        { json: true }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        my-tool sync --dry-run --output ./out
      </code>
    </td>
    
    <td>
      <code>
        sync
      </code>
    </td>
    
    <td>
      <code>
        { dryRun: true, output: "<set>" }
      </code>
      
       — 路径本身永远不会发送
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        my-tool sync --format json
      </code>
    </td>
    
    <td>
      <code>
        sync
      </code>
    </td>
    
    <td>
      只有在 <code>
        collect.flags
      </code>
      
       中列入允许项时才会记录 <code>
        { format: "json" }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        my-tool telemetry status
      </code>
    </td>
    
    <td>
      <code>
        telemetry status
      </code>
    </td>
    
    <td>
      嵌套子命令之间用空格连接
    </td>
  </tr>
</tbody>
</table>

当根命令只是委托给 `subCommands` 时，根 `meta.name` 不会作为前缀添加。

### 为一次运行补充信息

可以在命令处理器内部的任意位置调用 `telemetry.set()`，也可以在同一异步调用栈中运行的辅助函数中调用（运行上下文通过 AsyncLocalStorage 保留）。

<code-collapse>

```ts [src/commands/doctor.ts]
import { telemetry } from '@evlog/telemetry'
import { existsSync } from 'node:fs'
import { resolveConfigPath } from '../lib/config'

async function runHealthChecks() {
  let checksFailed = 0
  let checksWarn = 0

  const configPath = resolveConfigPath()
  if (!existsSync(configPath)) {
    checksFailed++
  }

  try {
    await fetch('https://registry.npmjs.org/my-tool')
  } catch {
    checksWarn++
  }

  // 计数器会进入本次运行的宽事件中的 event.custom
  telemetry.set({ checksFailed, checksWarn })
  return checksFailed === 0
}

export const doctorCommand = {
  meta: { name: 'doctor', description: '检查环境' },
  args: {
    json: { type: 'boolean', alias: 'j' },
  },
  async run({ args }: { args: { json?: boolean } }) {
    const ok = await runHealthChecks()
    if (!ok) {
      throw Object.assign(new Error('Health checks failed'), { code: 'DOCTOR_FAILED' })
    }
    if (!args.json) process.stdout.write('ok\n')
  },
}
```

</code-collapse>

抛出带有 `code` 属性的错误时，会自动记录 `outcome: "error"` 和 `errorCode`：

```ts [src/commands/doctor.ts]
throw Object.assign(new Error('Config missing'), { code: 'CONFIG_NOT_FOUND' })
```

下面是一个用于对比的最简 sync 处理器。标志会自动捕获，你只需使用 `set()` 记录业务计数器：

```ts [src/commands/sync.ts]
import { telemetry } from '@evlog/telemetry'

export const syncCommand = {
  meta: { name: 'sync', description: '拉取远程状态' },
  args: {
    dryRun: { type: 'boolean' },
    output: { type: 'string', description: '输出路径' },
  },
  async run() {
    const itemsSynced = await pullRemoteState()
    telemetry.set({ itemsSynced })
  },
}
```

## 无 citty 的设置

对于脚本、迁移器或自定义 CLI，请使用 `createTelemetry()`，并用 `t.run()` 包裹每次逻辑运行：

```ts [scripts/migrate.ts]
import { createTelemetry, telemetry } from '@evlog/telemetry'

const t = createTelemetry({ name: 'my-migrator', version: '2.0.0' })

await t.run('migrate', async () => {
  const rows = await migrateBatch()
  telemetry.set({ rowsMigrated: rows.length, batchSize: 500 })
})

await t.flush() // 可选 — 也会在每次 t.run() 结束时执行
```

GitHub Actions：在同一文件中将 `createTelemetry` 替换为 `createGitHubActionsTelemetry()`，它仅从 `GITHUB_ACTION` / `GITHUB_EVENT_NAME` 中读取并将 `ghaAction` 和 `ghaEvent` 添加到 `custom`，不会读取仓库内容。

```ts [scripts/ci-report.ts]
import { createGitHubActionsTelemetry, telemetry } from '@evlog/telemetry'

const t = createGitHubActionsTelemetry({ name: 'my-action', version: '1.0.0' })

await t.run('report', async () => {
  const artifacts = await collectArtifacts()
  telemetry.set({ artifactCount: artifacts.length })
})
```

## 调试

```bash
EVLOG_TELEMETRY_DEBUG=1 my-tool doctor
EVLOG_TELEMETRY=0 my-tool sync
```

调试模式会将原本将要发送的载荷打印到 stderr。除非已配置端点且投递成功，否则不会发送任何内容。

---

- [概述](https://evlog.zhcndoc.com/use-cases/telemetry/overview)
- [接收](https://evlog.zhcndoc.com/use-cases/telemetry/ingest)
- [参考](https://evlog.zhcndoc.com/use-cases/telemetry/reference)


## Sitemap

See the full [sitemap](https://evlog.zhcndoc.com/sitemap.md) for all pages.
