遥测、诊断与日志

📑 目录

在构建一个面向全球开发者的高频交互式 CLI 工具时,可观测性(Observability) 是支撑产品迭代与问题排查的核心基础设施。Claude Code 作为 Anthropic 推出的 AI 编程助手,其内部建立了一套完整且严谨的遥测、诊断与日志体系。本文将从源码层面深入解析这套体系的架构设计与实现细节,涵盖 services/analytics/ 下的多路事件上报、utils/diagnosticTracking.ts 的 IDE 诊断跟踪、utils/doctorDiagnostic.ts 的安装诊断、utils/log.ts 的日志系统、SentryErrorBoundary.ts 的错误边界,以及 services/internalLogging.ts 的内部调试日志。

一、遥测系统:分层上报与隐私优先

Claude Code 的遥测系统位于 src/services/analytics/ 目录,采用多 Sink 路由架构,将事件同时或选择性地上报到 Datadog 和 Anthropic 自有的一方(1P)服务端。整个模块的核心设计原则是:零依赖(避免循环导入)、事件队列化(Startup 前事件不丢失)、隐私优先(类型级防泄漏)

1.1 目录结构与模块职责

src/services/analytics/
├── index.ts              # 公共 API:logEvent / attachAnalyticsSink
├── sink.ts               # Sink 实现:路由到 Datadog 与 1P
├── config.ts             # 全局遥测开关(测试环境、Bedrock/Vertex、隐私级别)
├── datadog.ts            # Datadog 日志批量上报
├── firstPartyEventLogger.ts    # 1P OpenTelemetry 日志导出
├── firstPartyEventLoggingExporter.ts   # 1P HTTP 导出器(含失败重试与磁盘持久化)
├── metadata.ts           # 事件元数据 enricher(含 PII 脱敏)
├── sinkKillswitch.ts     # 按 Sink 的动态熔断开关
└── growthbook.ts         # 动态配置(采样率、批次参数)

1.2 核心 API:无依赖的事件队列

index.ts(约 140 行)是整个遥测系统的唯一公共入口。它刻意不引入任何其他内部模块,以彻底杜绝循环依赖。事件在 attachAnalyticsSink() 被调用前,会暂存于内存队列 eventQueue 中,待 Sink 就绪后通过 queueMicrotask 异步排空(src/services/analytics/index.ts,第 46–80 行):

// Module-local state — not exposed globally
let sink: AnalyticsSink | null = null
const eventQueue: QueuedEvent[] = []

export function attachAnalyticsSink(newSink: AnalyticsSink): void {
  if (sink !== null) return
  sink = newSink

  if (eventQueue.length > 0) {
    const queuedEvents = [...eventQueue]
    eventQueue.length = 0
    queueMicrotask(() => {
      for (const event of queuedEvents) {
        if (event.async) {
          void sink!.logEventAsync(event.eventName, event.metadata)
        } else {
          sink!.logEvent(event.eventName, event.metadata)
        }
      }
    })
  }
}

这种**延迟绑定(Late Binding)**模式在大型 TypeScript 项目中非常典型:子命令和默认命令可能从不同的启动路径调用 logEvent,但只要最终 attachAnalyticsSink 只执行一次,所有先前事件都不会丢失。

1.3 Sink 路由:Datadog + 1P 双通道

sink.ts(约 120 行)实现了真正的路由逻辑。事件首先经过采样决策shouldSampleEvent),然后被分发到两个后端:

  • Datadog:用于实时监控与告警,仅接收白名单事件(DATADOG_ALLOWED_EVENTS,约 40 余种),且会剥离 _PROTO_* 前缀的 PII 字段;
  • 1P Event Logging:基于 OpenTelemetry SDK,批量导出到 /api/event_logging/batch,保留完整的 PII-tagged 字段(src/services/analytics/sink.ts,第 60–85 行)。
flowchart LR
    A[业务代码调用 logEvent] --> B{Sink 是否已 attach?}
    B -->|否| C[eventQueue 内存队列]
    B -->|是| D[sink.ts 路由层]
    D --> E{shouldSampleEvent}
    E -->|sample_rate=0| F[丢弃]
    E -->|通过| G[stripProtoFields]
    G --> H[Datadog 白名单过滤]
    H --> I[HTTP 批量上报]
    D --> J[1P OpenTelemetry Exporter]
    J --> K[磁盘持久化 + 指数退避重试]

1.4 Datadog 上报:批量、降维、隐私脱敏

datadog.ts(约 260 行)通过 axios 将日志批量发送到 Datadog HTTP Intake Endpoint。为了控制成本与查询性能,它执行了多层**降维(Cardinality Reduction)**策略:

  1. 用户分桶:通过 SHA-256 哈希将用户 ID 映射到 30 个固定桶之一,用于估算受影响用户数,而不暴露原始 ID(src/services/analytics/datadog.ts,第 230–240 行);
  2. 模型名规范化:外部用户将模型名映射为 MODEL_COSTS 中的标准名称,非标准型号统一归类为 other
  3. 开发版本截断:去掉时间戳与 Git SHA,仅保留 2.0.53-dev.20251124
  4. MCP 工具名归一化:所有 mcp__* 前缀的工具名统一替换为 mcp

1.5 1P 导出器:可靠性与断网自愈

firstPartyEventLoggingExporter.ts(约 400 行)是整个遥测系统中工程细节最丰富的模块。它实现了基于磁盘的失败事件持久化指数退避重试

  • 导出失败时,事件被追加写入 ~/.claude/telemetry/1p_failed_events.{sessionId}.{batchUuid}.json
  • 后台任务会定期重试历史批次,采用二次退避(500ms → 30s 上限),最多 8 次尝试;
  • 当某次导出成功时,立即触发对所有历史失败文件的“机会性重试”;
  • 支持 GrowthBook 动态配置变更后的热重建:先 forceFlush 旧处理器、再切换新实例,旧实例在后台优雅关闭(src/services/analytics/firstPartyEventLogger.ts,第 260–310 行)。

1.6 用户隐私保护:三层防御

Claude Code 的隐私控制并非简单的“开关”,而是一个分层递进的体系,定义在 src/utils/privacyLevel.ts(约 50 行):

级别环境变量影响范围
default全部启用
no-telemetryDISABLE_TELEMETRY禁用 Datadog、1P 事件、反馈问卷
essential-trafficCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC额外禁用自动更新、模型能力探测、Release Notes 等非必要网络流量

isAnalyticsDisabled()config.ts 中综合判断:测试环境、第三方云提供商(Bedrock/Vertex/Foundry)、以及上述任意隐私级别都会彻底关闭遥测(src/services/analytics/config.ts,第 14–24 行)。

1.7 数据脱敏:类型级强制审查

为了防止开发者在 logEvent 中误传代码片段或文件路径,Claude Code 引入了一个极具 TypeScript 特色的安全机制:AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS。这个类型被定义为 never,意味着它不能被直接赋值,必须通过显式的 as 类型断言才能使用(src/services/analytics/index.ts,第 12–16 行):

export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never

metadata.ts 中,所有可能暴露用户敏感信息的字符串字段(如工具名、MCP 服务器名)都必须经过脱敏函数处理,并以该类型标记。例如,sanitizeToolNameForAnalytics 会将用户自定义的 MCP 工具名替换为无害的 mcp_toolsrc/services/analytics/metadata.ts,第 50–60 行)。

此外,对于确实需要记录的用户标识类数据,系统使用 _PROTO_* 前缀键 将其标记为 PII,这些字段在发往 Datadog 前会被 stripProtoFields 移除,但会保留在 1P 导出路径中,并写入具有更严格访问控制的 BQ Proto 列(src/services/analytics/index.ts,第 25–40 行)。


二、诊断跟踪:IDE 语言服务与安装自检

Claude Code 的诊断跟踪分为两类:运行时 IDE 诊断(与 Language Server Protocol 交互)和 安装环境诊断claude doctor 命令)。

2.1 IDE 诊断跟踪:增量 Diff 与基线管理

src/services/diagnosticTracking.ts(约 400 行)实现了对 IDE 语言服务器返回的 Diagnostic[] 的增量跟踪。其核心目标是:在 Claude 编辑文件后,只向模型反馈“新增”的诊断错误,而非全部错误,从而减少上下文噪音

该服务以单例模式运行,维护以下状态:

  • baseline: Map<normalizedPath, Diagnostic[]> —— 编辑前的诊断基线;
  • rightFileDiagnosticsState: Map<normalizedPath, Diagnostic[]> —— 右侧 diff 视图的诊断状态;
  • lastProcessedTimestamps: 防止对未变更文件的重复处理。

工作流程如下:

sequenceDiagram
    participant Agent as Claude Agent
    participant DTS as DiagnosticTrackingService
    participant IDE as IDE LSP Server

    Agent->>DTS: beforeFileEdited(path)
    DTS->>IDE: getDiagnostics(file://path)
    IDE-->>DTS: baseline diagnostics
    DTS->>DTS: baseline.set(path, diagnostics)
    Agent->>DTS: getNewDiagnostics()
    DTS->>IDE: getDiagnostics({})
    IDE-->>DTS: all workspace diagnostics
    DTS->>DTS: diff with baseline
    DTS-->>Agent: new diagnostics only
    DTS->>DTS: baseline.update(current)

一个有趣的细节是对 _claude_fs_right: 协议前缀的处理。当 IDE 支持右侧 diff 视图时,系统会优先比对右侧视图的诊断结果,因为只有右侧才真正代表“编辑后的状态”(src/services/diagnosticTracking.ts,第 180–220 行)。诊断的比对采用深度字段匹配(message、severity、source、code、range),而非简单的对象引用比较。

2.2 安装诊断:claude doctor 的实现

src/utils/doctorDiagnostic.ts(约 550 行)是 claude doctor 命令的后端实现。它收集的信息覆盖安装类型、权限、配置一致性、ripgrep 状态、多安装冲突等维度。

DiagnosticInfo 类型定义了完整的诊断数据结构(src/utils/doctorDiagnostic.ts,第 40–60 行):

export type DiagnosticInfo = {
  installationType: InstallationType
  version: string
  installationPath: string
  invokedBinary: string
  configInstallMethod: InstallMethod | 'not set'
  autoUpdates: string
  hasUpdatePermissions: boolean | null
  multipleInstallations: Array<{ type: string; path: string }>
  warnings: Array<{ issue: string; fix: string }>
  recommendation?: string
  packageManager?: string
  ripgrepStatus: {
    working: boolean
    mode: 'system' | 'builtin' | 'embedded'
    systemPath: string | null
  }
}

getCurrentInstallationType() 函数通过多层级启发式规则判断安装方式:首先检查 NODE_ENV === 'development';其次检测 bundled 模式并分辨包管理器(Homebrew、Winget、Pacman 等);再检测 npm 本地/全局安装;最后回退到 unknownsrc/utils/doctorDiagnostic.ts,第 70–120 行)。

detectConfigurationIssues() 则是配置健康检查的核心,它会验证:

  • ~/.local/bin 是否在 PATH 中(针对 native 安装);
  • installMethod 配置项与实际安装类型是否一致;
  • 是否存在多个冲突安装(如同时存在 native 与 npm-global);
  • Linux 沙箱 glob 规则是否被支持;
  • 自动更新权限是否充足。

三、日志系统:多目标、多级别、Sink 模式

src/utils/log.ts(约 280 行)是 Claude Code 的通用日志与错误记录中心。它的设计同样采用了** Sink 解耦模式**,将日志的消费端(文件系统、内存、调试输出)与生产端分离。

3.1 ErrorLogSink 接口与队列化

export type ErrorLogSink = {
  logError: (error: Error) => void
  logMCPError: (serverName: string, error: unknown) => void
  logMCPDebug: (serverName: string, message: string) => void
  getErrorsPath: () => string
  getMCPLogsPath: (serverName: string) => string
}

与遥测系统类似,logErrorattachErrorLogSink() 被调用前,会将错误事件暂存于 errorQueuesrc/utils/log.ts,第 55–90 行)。这种设计保证了即使在应用启动的最早期(如模块顶层脚本执行阶段)发生的错误,也能被后续持久化到磁盘。

3.2 错误上报的多级隐私检查

logError 并非无条件上报。它会在每次调用时检查一系列隐私与环境条件(src/utils/log.ts,第 125–150 行):

  • 使用 Bedrock、Vertex 或 Foundry 时直接返回(第三方云环境禁用内部错误上报);
  • DISABLE_ERROR_REPORTING 环境变量为真时返回;
  • isEssentialTrafficOnly() 为真时返回;
  • 以上均通过后,才写入内存日志并调用 Sink。

3.3 内存错误日志与持久化

内存中的错误日志以循环队列形式维护,上限为 100 条(MAX_IN_MEMORY_ERRORS)。这对于在崩溃后向用户展示最近的错误摘要非常有用。getInMemoryErrors() 返回的副本可用于 bug 报告或用户界面展示(src/utils/log.ts,第 45–55 行)。

除内存外,错误日志还会被持久化到 ~/.claude/errors/ 目录,以时间戳命名文件。loadErrorLogs()getErrorLogByIndex() 提供了按时间排序的日志加载能力,支持 claude --debugtail -f ~/.claude/debug/latest 查看。

3.4 API 请求捕获与信息裁剪

captureAPIRequest() 是一个精心设计的辅助函数,用于在 bug 报告中包含最后一次 API 请求参数。它做了两处关键裁剪:

  1. 对所有用户:从请求参数中移除 messages 数组,避免在内存中长期保留完整对话历史(消息已保存在对话记录文件中);
  2. 仅对内部 ant 用户:保留 messages 引用,以便 /share 命令导出精确的 post-compaction、CLAUDE.md-injected 请求体(src/utils/log.ts,第 270–285 行)。

四、错误处理:边界捕获与用户感知

4.1 SentryErrorBoundary:React 层的兜底

src/components/SentryErrorBoundary.ts(约 25 行)是一个极简的 React Class Component,实现了 getDerivedStateFromError 生命周期方法。当 Ink 渲染层(Claude Code 的 TUI 基于 React + Ink)抛出未捕获异常时,该边界会捕获错误并将组件状态置为 hasError: true,渲染时返回 null,从而避免整个界面崩溃(src/components/SentryErrorBoundary.ts,第 12–20 行):

export class SentryErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(): State {
    return { hasError: true }
  }

  render(): React.ReactNode {
    if (this.state.hasError) {
      return null
    }
    return this.props.children
  }
}

注意,这里的类名虽然包含 "Sentry",但当前源码中并未直接调用 @sentry/react 的 SDK 方法;它更像是一个语义化占位,未来可无缝接入 Sentry 的 componentDidCatch 上报逻辑。

4.2 全局错误捕获与上报

log.ts 中,logError 函数与 Node.js 的 uncaughtException / unhandledRejection 事件挂钩(具体注册点在 bootstrapmain.tsx 中)。当发生未捕获异常时,系统会:

  1. 调用 logError 写入内存与持久化日志;
  2. 对于内部用户,触发 --hard-fail 模式直接 process.exit(1),以便 CI 快速失败;
  3. 通过 tengu_uncaught_exception / tengu_unhandled_rejection 事件名上报到遥测系统。

五、内部日志:面向开发者的深度调试

src/services/internalLogging.ts(约 60 行)是 Claude Code 中权限最敏感、受众最窄的日志模块。它仅服务于内部员工(USER_TYPE === 'ant'),用于记录容器环境、工具权限上下文等高度敏感但排错必需的信息。

5.1 Kubernetes 与容器环境探测

该模块通过读取 /var/run/secrets/kubernetes.io/serviceaccount/namespace 获取当前 Kubernetes 命名空间,并通过解析 /proc/self/mountinfo 中的 Docker/Containerd 路径正则,提取 64 位十六进制容器 ID(src/services/internalLogging.ts,第 15–45 行):

const containerIdPattern =
  /(?:\/docker\/containers\/|\/sandboxes\/)([0-9a-f]{64})/

这些函数都被 memoize 包装,确保每个进程生命周期内只执行一次 I/O。

5.2 工具权限上下文的审计日志

logPermissionContextForAnts() 将当前工具权限上下文(ToolPermissionContext)序列化为 JSON,与命名空间、容器 ID 一并通过 logEvent 上报到内部事件系统。这对于排查内部测试环境中的权限策略问题至关重要(src/services/internalLogging.ts,第 50–60 行)。


六、体系联动:可观测性的全景图

将上述模块串联起来,可以看到 Claude Code 的可观测性体系呈现出清晰的分层结构

flowchart TB
    subgraph 应用层
        A1[业务代码]
        A2[React Ink UI]
        A3[MCP Client]
    end

    subgraph 日志与诊断层
        B1[utils/log.ts
错误/调试日志] B2[services/diagnosticTracking.ts
IDE 诊断增量] B3[utils/doctorDiagnostic.ts
安装自检] B4[services/internalLogging.ts
内部 ant 日志] end subgraph 遥测层 C1[services/analytics/index.ts
事件队列] C2[sink.ts
采样+路由] C3[Datadog
实时监控] C4[1P Exporter
磁盘重试+HTTP批量] end subgraph 错误边界 D1[SentryErrorBoundary.ts
React 兜底] D2[uncaughtException
Node.js 兜底] end A1 --> B1 A1 --> C1 A2 --> D1 A3 --> B2 B1 --> C1 C1 --> C2 C2 --> C3 C2 --> C4 B3 --> A1 D1 --> B1 D2 --> B1

这套体系的核心哲学可以总结为三点:

  1. Fail-Silent but Not Fail-Blind:所有日志和遥测调用都被 try/catch 包裹,确保不会阻塞主业务流程;同时通过内存队列、磁盘持久化保证信息不丢失。
  2. Privacy by Design:从类型系统(never 标记类型)、环境变量(三层隐私级别)、到字段级过滤(stripProtoFields),隐私保护贯穿数据全生命周期。
  3. Dynamic Configuration:通过 GrowthBook 动态配置,采样率、批次大小、Sink 开关都可在服务端实时调整,无需发版即可应对突发流量或故障熔断。

对于正在构建 AI Agent 或 CLI 工具的开发者来说,Claude Code 的可观测性架构提供了一个优秀的参考范本:在性能、隐私、可靠性之间取得了精妙的平衡,同时通过 TypeScript 的类型系统,将“不泄露用户代码”从规范变成了一种编译期约束。