Claude Code Wiki
首页 深入解析 开发实践

日志系统:诊断追踪与错误处理

中级 开发实践

Claude Code 的日志系统采用分层架构设计,通过队列缓冲、sink 抽象和多级日志通道实现高可用的诊断追踪能力。系统将错误记录、调试输出和诊断监控分离为独立通道,既保证了生产环境的性能要求,又为问题排查提供了完整的可观测性。

架构概览:三层日志管道

graph TB
    subgraph "应用层"
        A[业务代码] --> B[logError]
        A --> C[logForDebugging]
        A --> D[logForDiagnosticsNoPII]
        A --> E[logMCPError/logMCPDebug]
    end
    
    subgraph "缓冲层"
        B --> F[错误队列<br/>errorQueue]
        C --> G[缓冲写入器<br/>BufferedWriter]
        D --> H[同步文件追加]
        E --> I[MCP 日志写入器]
    end
    
    subgraph "Sink 层"
        F --> J[ErrorLogSink<br/>initializeErrorLogSink]
        G --> K[DebugLogSink<br/>getDebugWriter]
        I --> L[MCPLogSink<br/>getLogWriter]
    end
    
    subgraph "持久化层"
        J --> M[~/.claude/errors/*.jsonl]
        K --> N[~/.claude/debug/latest]
        H --> O[诊断文件<br/>CLAUDE_CODE_DIAGNOSTICS_FILE]
        L --> P[~/.claude/mcp-logs/*/*.jsonl]
        B --> Q[内存错误日志<br/>inMemoryErrorLog]
    end

核心设计原则:log.ts 保持零依赖以避免循环引用,所有重量级实现延迟到 errorLogSink.ts 通过 sink 接口注入。这种设计允许在应用启动早期就开始记录错误(队列缓冲),待文件系统和其他依赖就绪后再批量刷入磁盘。

Sources: log.ts, errorLogSink.ts, debug.ts

错误日志系统:多目标记录与队列缓冲

Sink 接口与队列机制

错误日志系统采用发布-订阅模式,通过 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
}

在 sink 附加之前,所有错误事件进入内存队列errorQueue)暂存,待 initializeErrorLogSink() 调用时一次性排空。这种设计确保即使在应用初始化阶段也不会丢失错误信息:

export function logError(error: unknown): void {
  const err = toError(error)
  // ... 检查隐私设置和云服务商配置
  
  // 立即添加到内存日志(无依赖)
  addToInMemoryErrorLog({ error: errorStr, timestamp: new Date().toISOString() })
  
  // 如果 sink 未附加,队列暂存
  if (errorLogSink === null) {
    errorQueue.push({ type: 'error', error: err })
    return
  }
  
  errorLogSink.logError(err)
}

三重记录目标:每个错误同时写入(1)调试日志(可通过 --debug 查看)、(2)内存日志(用于 bug 报告和会话内错误展示)、(3)持久化文件(仅限内部 ant 用户,存储在 ~/.claude/errors/)。

Sources: log.ts, log.ts

缓冲写入器:性能与可靠性的平衡

BufferedWriter 实现了智能批处理策略,在性能和数据完整性之间取得平衡:

export function createBufferedWriter({
  writeFn,
  flushIntervalMs = 1000,      // 定时刷盘间隔
  maxBufferSize = 100,         // 批次最大条目数
  maxBufferBytes = Infinity,   // 批次最大字节数
  immediateMode = false,       // 立即模式(同步写入)
}): BufferedWriter

两种写入模式

  • 缓冲模式(默认):日志累积到阈值或定时器触发时批量写入,适合高频低优先级日志
  • 立即模式:每条日志同步写入磁盘,适合 --debug 模式和关键错误路径

缓冲模式通过延迟刷盘(deferred flush)避免阻塞主线程:当缓冲区溢出时,当前批次通过 setImmediate 异步写入,调用方立即返回继续执行。这种设计确保即使在渲染或用户输入处理期间也不会卡顿。

function flushDeferred(): void {
  const detached = buffer
  buffer = []
  pendingOverflow = detached
  setImmediate(() => {
    const toWrite = pendingOverflow
    pendingOverflow = null
    if (toWrite) writeFn(toWrite.join(''))
  })
}

Sources: bufferedWriter.ts, errorLogSink.ts

错误日志文件结构与命名

错误日志采用日期分片策略,每天一个 JSONL 文件:

export function getErrorsPath(): string {
  return join(CACHE_PATHS.errors(), DATE + '.jsonl')
}

const DATE = dateToFilename(new Date())  // "2025-01-15T12-30-45-123Z"

每条日志记录包含丰富的上下文信息:

{
  "timestamp": "2025-01-15T12:30:45.123Z",
  "error": "Error: Connection timeout\n    at NetworkClient.connect...",
  "cwd": "/Users/user/project",
  "userType": "ant",
  "sessionId": "abc123-def456",
  "version": "1.2.3"
}

MCP 服务器日志独立存储在 ~/.claude/mcp-logs/<server-name>/<date>.jsonl,实现不同服务器的日志隔离,便于针对性排查。

Sources: errorLogSink.ts, errorLogSink.ts

调试日志系统:动态过滤与多级输出

启用机制与模式检测

调试日志系统提供多种启用方式,适应不同调试场景:

export const isDebugMode = memoize((): boolean => {
  return (
    runtimeDebugEnabled ||              // 运行时启用(通过 /debug 命令)
    isEnvTruthy(process.env.DEBUG) ||   // 环境变量
    process.argv.includes('--debug') || // 命令行参数
    process.argv.includes('-d') ||
    isDebugToStdErr() ||                // stderr 输出模式
    process.argv.some(arg => arg.startsWith('--debug=')) || // 过滤模式
    getDebugFilePath() !== null         // 自定义文件路径
  )
})

运行时启用:非 ant 用户默认不记录调试日志,但可通过 /debug 命令在会话中途启用,无需重启。这通过 runtimeDebugEnabled 标志和缓存清除实现:

export function enableDebugLogging(): boolean {
  const wasActive = isDebugMode() || process.env.USER_TYPE === 'ant'
  runtimeDebugEnabled = true
  isDebugMode.cache.clear?.()  // 清除 memoize 缓存
  return wasActive
}

日志级别与过滤系统

系统定义五个日志级别,通过 CLAUDE_CODE_DEBUG_LOG_LEVEL 环境变量控制输出阈值:

export type DebugLogLevel = 'verbose' | 'debug' | 'info' | 'warn' | 'error'

const LEVEL_ORDER: Record<DebugLogLevel, number> = {
  verbose: 0,  // 高频诊断信息(shell 输出、状态更新)
  debug: 1,    // 默认级别
  info: 2,
  warn: 3,
  error: 4,
}

基于类别的过滤:通过 --debug=<pattern> 参数实现精准过滤,支持包含和排除模式:

claude --debug=api,hooks     # 仅显示 api 和 hooks 类别
claude --debug=!1p,!file     # 排除 1p 和 file 类别

过滤系统从日志消息中自动提取类别标识:

消息格式提取类别示例
category: message["category"]api: request sent["api"]
[CATEGORY] message["category"][ANT-ONLY] event logged["ant-only"]
MCP server "name": msg["mcp", "name"]MCP server "slack": connected["mcp", "slack"]
包含 1P event:添加 ["1p"][ANT-ONLY] 1P event: timer["ant-only", "1p"]

Sources: debug.ts, debug.ts, debugFilter.ts

符号链接与日志轮转

调试日志通过符号链接提供便捷访问路径:

async function updateLatestDebugLogSymlink(): Promise<void> {
  const latestPath = join(CACHE_PATHS.debug(), 'latest')
  const currentPath = getDebugLogPath()
  
  // 原子更新:先删除旧链接,再创建新链接
  await unlink(latestPath).catch(() => {})
  await symlink(currentPath, latestPath)
}

用户可通过 tail -f ~/.claude/debug/latest 实时查看最新日志,无需关注具体文件名。日志文件采用时间戳命名YYYY-MM-DDTHH-MM-SS-mmmZ.log),自然支持按时间排序和历史清理。

Sources: debug.ts

诊断日志系统:容器监控与性能追踪

无 PII 日志设计

logForDiagnosticsNoPII 专为容器环境监控设计,通过环境变量 CLAUDE_CODE_DIAGNOSTICS_FILE 指定输出路径。该函数强制要求不包含任何个人身份信息(PII),包括文件路径、项目名、代码片段等:

/**
 * 重要:此函数不得包含任何 PII,包括文件路径、项目名、仓库名、提示词等
 */
export function logForDiagnosticsNoPII(
  level: DiagnosticLogLevel,
  event: string,
  data?: Record<string, unknown>,
): void

日志条目采用结构化 JSON 格式,便于日志聚合系统解析:

{
  "timestamp": "2025-01-15T12:30:45.123Z",
  "level": "info",
  "event": "mcp_connected",
  "data": { "server_count": 3 }
}

性能计时包装器

withDiagnosticsTiming 提供自动性能追踪,在函数执行前后记录耗时:

export async function withDiagnosticsTiming<T>(
  event: string,
  fn: () => Promise<T>,
  getData?: (result: T) => Record<string, unknown>,
): Promise<T> {
  const startTime = Date.now()
  logForDiagnosticsNoPII('info', `${event}_started`)
  
  try {
    const result = await fn()
    const additionalData = getData ? getData(result) : {}
    logForDiagnosticsNoPII('info', `${event}_completed`, {
      duration_ms: Date.now() - startTime,
      ...additionalData,
    })
    return result
  } catch (error) {
    logForDiagnosticsNoPII('error', `${event}_failed`, {
      duration_ms: Date.now() - startTime,
    })
    throw error
  }
}

使用示例:

const status = await withDiagnosticsTiming(
  'git_status',
  () => gitStatus(),
  (result) => ({ file_count: result.files.length })
)

生成的日志序列:

{"level":"info","event":"git_status_started","data":{}}
{"level":"info","event":"git_status_completed","data":{"duration_ms":45,"file_count":12}}

Sources: diagLogs.ts

错误处理体系:类型系统与错误规范化

自定义错误类层次

Claude Code 定义了丰富的错误类型体系,通过语义化错误类提供精准的错误处理:

classDiagram
    Error <|-- ClaudeError
    Error <|-- AbortError
    Error <|-- ConfigParseError
    Error <|-- ShellError
    Error <|-- TeleportOperationError
    Error <|-- TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
    
    class ClaudeError {
        +name: string
        +message: string
    }
    
    class AbortError {
        +name: "AbortError"
    }
    
    class ConfigParseError {
        +filePath: string
        +defaultConfig: unknown
    }
    
    class ShellError {
        +stdout: string
        +stderr: string
        +code: number
        +interrupted: boolean
    }
    
    class TelemetrySafeError {
        +telemetryMessage: string
    }

关键设计点

  • ConfigParseError:携带文件路径和默认配置,便于降级处理
  • ShellError:保存完整的命令输出,支持用户查看详细错误
  • TelemetrySafeError:分离用户可见消息和遥测安全消息,避免泄露敏感信息

中止错误识别

系统中有三种中止错误来源,通过 isAbortError 统一识别:

export function isAbortError(e: unknown): boolean {
  return (
    e instanceof AbortError ||                    // 自定义 AbortError
    e instanceof APIUserAbortError ||             // Anthropic SDK 中止
    (e instanceof Error && e.name === 'AbortError') // DOMException
  )
}

使用 instanceof 而非字符串匹配是因为生产构建会混淆类名constructor.name 变成 'nJT'),但 instanceof 仍然可靠。

错误规范化工具

toErrorerrorMessage 提供统一的错误转换接口:

// 规范化为 Error 实例
export function toError(e: unknown): Error {
  return e instanceof Error ? e : new Error(String(e))
}

// 仅提取消息(用于日志/展示)
export function errorMessage(e: unknown): string {
  return e instanceof Error ? e.message : String(e)
}

文件系统错误识别:通过 getErrnoCodeisENOENTisFsInaccessible 提供类型安全的错误码检查:

export function isFsInaccessible(e: unknown): e is NodeJS.ErrnoException {
  const code = getErrnoCode(e)
  return (
    code === 'ENOENT' ||  // 路径不存在
    code === 'EACCES' ||  // 权限拒绝
    code === 'EPERM' ||   // 操作不允许
    code === 'ENOTDIR' || // 路径组件不是目录
    code === 'ELOOP'      // 符号链接循环
  )
}

堆栈跟踪压缩

shortErrorStack 为模型上下文优化堆栈输出,默认保留前 5 帧:

export function shortErrorStack(e: unknown, maxFrames = 5): string {
  if (!(e instanceof Error)) return String(e)
  if (!e.stack) return e.message
  
  const lines = e.stack.split('\n')
  const header = lines[0] ?? e.message
  const frames = lines.slice(1).filter(l => l.trim().startsWith('at '))
  
  if (frames.length <= maxFrames) return e.stack
  return [header, ...frames.slice(0, maxFrames)].join('\n')
}

完整堆栈(500-2000 字符)保留在调试日志中,工具结果使用压缩版本节省 token。

Sources: errors.ts, errors.ts

MCP 日志系统:服务器隔离与独立追踪

独立日志通道

每个 MCP 服务器拥有独立的日志文件,存储在 ~/.claude/mcp-logs/<server-name>/<date>.jsonl

export function getMCPLogsPath(serverName: string): string {
  return join(CACHE_PATHS.mcpLogs(serverName), DATE + '.jsonl')
}

这种设计避免了不同服务器日志混合,便于针对性排查特定服务的问题。

双通道记录

MCP 日志提供错误调试两个通道:

// 错误通道:记录异常和失败
export function logMCPError(serverName: string, error: unknown): void {
  if (errorLogSink === null) {
    errorQueue.push({ type: 'mcpError', serverName, error })
    return
  }
  errorLogSink.logMCPError(serverName, error)
}

// 调试通道:记录状态变化和通信
export function logMCPDebug(serverName: string, message: string): void {
  if (errorLogSink === null) {
    errorQueue.push({ type: 'mcpDebug', serverName, message })
    return
  }
  errorLogSink.logMCPDebug(serverName, message)
}

错误日志包含完整的堆栈跟踪,调试日志记录服务器生命周期事件(连接、断开、工具调用等)。

Axios 错误增强

MCP 错误实现智能错误增强,自动提取 HTTP 请求上下文:

function logErrorImpl(error: Error): void {
  let context = ''
  if (axios.isAxiosError(error) && error.config?.url) {
    const parts = [`url=${error.config.url}`]
    if (error.response?.status !== undefined) {
      parts.push(`status=${error.response.status}`)
    }
    const serverMessage = extractServerMessage(error.response?.data)
    if (serverMessage) {
      parts.push(`body=${serverMessage}`)
    }
    context = `[${parts.join(',')}] `
  }
  
  logForDebugging(`${error.name}: ${context}${errorStr}`, { level: 'error' })
}

转换示例:

Error: Request failed
→ Error: [url=https://api.example.com,status=503,body=Service Unavailable] Request failed

Sources: log.ts, errorLogSink.ts

系统初始化与 Sink 附加流程

初始化顺序

日志系统通过 initSinks() 函数在应用启动时初始化:

export function initSinks(): void {
  initializeErrorLogSink()     // 1. 附加错误日志 sink
  initializeAnalyticsSink()     // 2. 附加分析 sink
}

幂等性保证initializeErrorLogSink 检查 sink 是否已附加,避免重复初始化:

export function attachErrorLogSink(newSink: ErrorLogSink): void {
  if (errorLogSink !== null) {
    return  // 已附加,跳过
  }
  errorLogSink = newSink
  
  // 立即排空队列
  if (errorQueue.length > 0) {
    const queuedEvents = [...errorQueue]
    errorQueue.length = 0
    
    for (const event of queuedEvents) {
      switch (event.type) {
        case 'error':
          errorLogSink.logError(event.error)
          break
        case 'mcpError':
          errorLogSink.logMCPError(event.serverName, event.error)
          break
        case 'mcpDebug':
          errorLogSink.logMCPDebug(event.serverName, event.message)
          break
      }
    }
  }
}

调用时机

不同入口点的初始化策略:

入口类型调用位置说明
默认命令setup()initSinks()完整初始化流程
子命令直接调用 initSinks()跳过 setup 避免循环依赖
Daemon直接调用 initSinks()后台服务启动
Bridge直接调用 initSinks()IDE 集成启动

Sources: sinks.ts, log.ts, errorLogSink.ts

内部日志服务:容器环境追踪

Kubernetes 上下文提取

内部日志服务为容器化部署提供环境上下文追踪:

// 提取 Kubernetes 命名空间
const getKubernetesNamespace = memoize(async (): Promise<string | null> => {
  if (process.env.USER_TYPE !== 'ant') return null
  
  const namespacePath = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'
  const content = await readFile(namespacePath, { encoding: 'utf8' })
  return content.trim()
})

// 提取容器 ID
export const getContainerId = memoize(async (): Promise<string | null> => {
  if (process.env.USER_TYPE !== 'ant') return null
  
  const mountinfo = await readFile('/proc/self/mountinfo', { encoding: 'utf8' })
  const containerIdPattern = /(?:\/docker\/containers\/|\/sandboxes\/)([0-9a-f]{64})/
  
  for (const line of mountinfo.split('\n')) {
    const match = line.match(containerIdPattern)
    if (match && match[1]) return match[1]
  }
  return null
})

支持 Docker 和 containerd/CRI-O 两种容器运行时的 ID 提取。

权限上下文记录

logPermissionContextForAnts 记录工具权限配置快照,用于安全审计和问题排查:

export async function logPermissionContextForAnts(
  toolPermissionContext: ToolPermissionContext | null,
  moment: 'summary' | 'initialization',
): Promise<void> {
  if (process.env.USER_TYPE !== 'ant') return
  
  void logEvent('tengu_internal_record_permission_context', {
    moment,
    namespace: await getKubernetesNamespace(),
    toolPermissionContext: jsonStringify(toolPermissionContext),
    containerId: await getContainerId(),
  })
}

这些信息帮助定位特定容器或命名空间中的权限配置问题。

Sources: internalLogging.ts

配置选项与环境变量

调试日志配置

环境变量作用示例值
DEBUG启用调试模式true
DEBUG_SDK启用 SDK 调试true
CLAUDE_CODE_DEBUG_LOG_LEVEL最低日志级别verbose, debug, info, warn, error
CLAUDE_CODE_DIAGNOSTICS_FILE诊断日志文件路径/var/log/claude/diagnostics.jsonl
DISABLE_ERROR_REPORTING禁用错误报告true
USER_TYPE用户类型(控制日志行为)ant(内部用户)

命令行参数

参数作用说明
--debug, -d启用调试模式写入 ~/.claude/debug/latest
--debug=<pattern>启用带过滤的调试仅显示匹配类别的日志
--debug-to-stderr, -d2e调试输出到 stderr适合 CI/CD 环境
--debug-file=<path>自定义调试日志路径指定输出文件
--hard-fail硬失败模式(测试用)遇错即崩溃

隐私控制

日志系统尊重隐私级别设置,当 isEssentialTrafficOnly() 返回 true 时禁用错误报告。云服务商部署(Bedrock/Vertex/Foundry)自动禁用遥测功能:

if (
  isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
  isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
  isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
  process.env.DISABLE_ERROR_REPORTING ||
  isEssentialTrafficOnly()
) {
  return  // 跳过错误记录
}

Sources: log.ts, debug.ts

最佳实践与故障排查

日志查看命令

# 实时查看调试日志
tail -f ~/.claude/debug/latest

# 查看最近的错误日志
cat ~/.claude/errors/$(ls -t ~/.claude/errors/ | head -1)

# 查看 MCP 服务器日志
cat ~/.claude/mcp-logs/slack/$(ls -t ~/.claude/mcp-logs/slack/ | head -1)

# 过滤特定类别的调试日志
claude --debug=api,hooks

# 排除敏感类别
claude --debug=!1p,!file

错误处理模式

推荐:使用语义化错误类和类型守卫

// ✅ 正确:使用自定义错误类
throw new ConfigParseError(
  'Invalid settings.json',
  filePath,
  defaultSettings
)

// ✅ 正确:使用类型守卫识别文件系统错误
try {
  await fs.readFile(path)
} catch (e) {
  if (isENOENT(e)) {
    // 文件不存在,降级处理
  } else if (isFsInaccessible(e)) {
    // 权限问题,提示用户
  } else {
    throw e  // 重新抛出未知错误
  }
}

// ❌ 避免:直接类型转换
const code = (e as NodeJS.ErrnoException).code  // 不安全

性能优化建议

  1. 高频日志使用 verbose 级别:避免在默认调试模式下淹没关键信息
  2. 避免在日志中包含大对象:使用 jsonStringify 前考虑截断或采样
  3. MCP 调试日志适度使用:仅记录关键状态变化,避免记录每次消息传递
  4. 诊断日志保持无 PII:确保容器监控日志不泄露用户数据

Sources: errors.ts

相关主题