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/)。
缓冲写入器:性能与可靠性的平衡
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 仍然可靠。
错误规范化工具
toError 和 errorMessage 提供统一的错误转换接口:
// 规范化为 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)
}
文件系统错误识别:通过 getErrnoCode、isENOENT、isFsInaccessible 提供类型安全的错误码检查:
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。
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 // 跳过错误记录
}
最佳实践与故障排查
日志查看命令
# 实时查看调试日志
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 // 不安全
性能优化建议
- 高频日志使用 verbose 级别:避免在默认调试模式下淹没关键信息
- 避免在日志中包含大对象:使用
jsonStringify前考虑截断或采样 - MCP 调试日志适度使用:仅记录关键状态变化,避免记录每次消息传递
- 诊断日志保持无 PII:确保容器监控日志不泄露用户数据
Sources: errors.ts
相关主题
- 配置管理:settings、环境变量与迁移系统 - 了解日志相关配置项
- 性能优化:启动性能与运行时优化技巧 - 缓冲写入器的性能影响
- MCP 服务:服务器连接、资源管理与协议实现 - MCP 日志的应用场景