Skip to content

OpenClaw 源码解读(二十一)日志与可观测性

一、导读

日志系统是 OpenClaw 的可观测性基石。当消息处理出问题、Agent 运行卡住、工具调用泄漏敏感信息时,正是日志系统帮助定位和诊断。

OpenClaw 的日志系统不只是简单的 console.log——它是一套分层、双通道、安全感知的完整可观测性方案:

业务代码 → SubsystemLogger
              ├── File Transport(JSON 行格式,滚动日志)
              ├── Console Transport(彩色/紧凑/JSON 三种样式)
              └── External Transport(Web UI 实时日志面板)

诊断系统 → diagnostic.ts
              ├── Webhook 统计
              ├── Session 状态追踪
              ├── 卡住会话检测
              └── 诊断心跳(30s 间隔)
              
敏感信息 → redact.ts
              ├── 15+ 内置脱敏正则
              └── 自定义脱敏模式

源码位于 src/logging/,约 30 个文件,~2500 行代码。

![日志与可观测性系统全局架构概览](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/01-infographic-logging-system-overview-1775150659804.png)


二、日志级别

typescript
const ALLOWED_LOG_LEVELS = ["silent", "fatal", "error", "warn", "info", "debug", "trace"] as const;

7 个级别,映射到 tslog 的数值系统:

级别数值用途
fatal0致命错误,进程即将退出
error1可恢复错误
warn2警告(卡住会话、工具循环)
info3正常运行信息(默认级别)
debug4调试详情
trace5最详细的追踪
silent完全静默

级别解析支持三级覆盖:环境变量 OPENCLAW_LOG_LEVEL > 配置文件 logging.level > 默认值 info

![日志级别层次与三级覆盖优先级](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/02-infographic-log-levels-1775150660489.png)


三、Logger 核心 —— logger.ts

3.1 底层引擎

日志系统基于 tslog(TypeScript Logger),但做了大量定制:

typescript
const logger = new TsLogger<LogObj>({
  name: "openclaw",
  minLevel: levelToMinLevel(settings.level),
  type: "hidden",  // 不使用 tslog 的 ANSI 格式化
});

type: "hidden" 是关键——tslog 的内置格式化被完全禁用,所有格式化由 OpenClaw 自己的 Transport 层处理。

3.2 滚动日志

typescript
const LOG_PREFIX = "openclaw";
const LOG_SUFFIX = ".log";
const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000;  // 24 小时
const DEFAULT_MAX_LOG_FILE_BYTES = 500 * 1024 * 1024;  // 500 MB

日志文件采用按日滚动策略:

  • 文件名格式:openclaw-2026-03-25.log
  • 每天一个新文件
  • 超过 24 小时的旧文件自动清理(pruneOldRollingLogs
  • 单文件最大 500MB,超限后停止写入并发出警告

3.3 文件大小保护

typescript
if (nextBytes > settings.maxFileBytes) {
  if (!warnedAboutSizeCap) {
    warnedAboutSizeCap = true;
    // 写入一条警告到日志文件
    // 同时输出到 stderr
  }
  return;  // 停止写入
}

500MB 上限保护磁盘不被打满。warnedAboutSizeCap 标志确保警告只触发一次——避免写入大量重复的"已停止写入"消息。

3.4 External Transport

typescript
const externalTransports = new Set<LogTransport>();

支持注册外部日志传输——Web UI 的实时日志面板就是通过这个机制接收日志数据的。

3.5 测试快速路径

typescript
function canUseSilentVitestFileLogFastPath(envLevel) {
  return process.env.VITEST === "true"
    && process.env.OPENCLAW_TEST_FILE_LOG !== "1"
    && !envLevel
    && !loggingState.overrideSettings;
}

测试环境下文件日志默认静默,且跳过所有配置加载和文件系统操作——这是一个性能优化,避免测试启动时的重配置开销。

![Logger 核心——滚动日志与大小保护](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/03-infographic-logger-core-1775150661278.png)


四、Subsystem Logger —— 结构化日志

subsystem.ts 中的 SubsystemLogger 是整个项目最常用的日志 API:

4.1 创建方式

typescript
const log = createSubsystemLogger("gateway/auth");
log.info("client connected", { clientId: "abc123" });

4.2 双通道输出

每条日志同时输出到两个通道:

通道格式配置
文件JSON 行logging.level(默认 info)
控制台彩色格式化logging.console.level

两个通道的级别独立控制——可以文件记录 debug 但控制台只显示 warn。

![SubsystemLogger 双通道输出架构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/04-infographic-subsystem-dual-channel-1775150662188.png)

4.3 Console 样式

三种控制台输出样式:

pretty(默认):

14:30:25 [gateway] client connected

compact

[gateway] client connected

json

json
{"time":"2026-03-25T14:30:25.123Z","level":"info","subsystem":"gateway","message":"client connected"}

4.4 Subsystem 颜色系统

6 种颜色按子系统名哈希分配:

typescript
const SUBSYSTEM_COLORS = ["cyan", "green", "yellow", "blue", "magenta", "red"];

function pickSubsystemColor(color, subsystem) {
  let hash = 0;
  for (let i = 0; i < subsystem.length; i++) {
    hash = (hash * 31 + subsystem.charCodeAt(i)) | 0;
  }
  return color[SUBSYSTEM_COLORS[Math.abs(hash) % 6]];
}

同名子系统总是同色——便于在终端中视觉区分不同模块的日志。

4.5 Subsystem 名缩写

控制台输出时自动缩写子系统名:

原始名缩写后
gateway/channels/telegramtelegram
gateway/providers/openaiopenai
agents/tools/web-searchtools/web-search

规则:去掉 gateway/channels/providers 等冗余前缀,渠道名直接用渠道标识符,最多保留 2 段。

4.6 去重前缀剥离

typescript
// "[discord] discord: client connected" → "client connected"
stripRedundantSubsystemPrefixForConsole(message, displaySubsystem)

当消息文本手动包含了子系统标签时([discord] discord: ...),自动去重——避免 [discord] discord: ... 的重复显示。

4.7 consoleMessage 覆盖

typescript
log.info("detailed internal message", {
  consoleMessage: "brief user-facing message",
  requestId: "abc123",
});

meta.consoleMessage 允许为控制台和文件提供不同的消息——文件记录详细信息,控制台显示精简版。

![控制台增强——颜色系统、名称缩写与去重剥离](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/05-infographic-console-enhancements-1775150662895.png)


五、敏感信息脱敏 —— redact.ts

5.1 内置脱敏模式

15+ 个内置正则模式,覆盖常见的敏感信息格式:

类别模式示例匹配目标
环境变量赋值API_KEY=sk-abc...*_KEY, *_TOKEN, *_SECRET
JSON 字段"apiKey": "..."apiKey, token, secret, password
CLI 标志--api-key sk-abc...--api-key, --token, --secret
Authorization 头Bearer eyJ...Bearer token
PEM 密钥-----BEGIN PRIVATE KEY-----私钥块
API Key 前缀sk-, ghp_, xox, AIzaOpenAI/GitHub/Slack/Google key
npm tokennpm_abc...npm 认证令牌
Telegram Botbot123456:ABC...Bot API token

5.2 掩码策略

typescript
function maskToken(token: string): string {
  if (token.length < 18) return "***";
  const start = token.slice(0, 6);
  const end = token.slice(-4);
  return `${start}…${end}`;
}
// "sk-abc123def456ghi789" → "sk-abc…i789"

保留前 6 位和后 4 位——足够识别 token 类型,但不足以还原完整密钥。长度不足 18 位的直接全部掩码。

5.3 PEM 块特殊处理

typescript
function redactPemBlock(block: string): string {
  return `${lines[0]}\n…redacted…\n${lines[lines.length - 1]}`;
}
// -----BEGIN PRIVATE KEY-----
// …redacted…
// -----END PRIVATE KEY-----

保留头尾标记,中间内容完全替换。

5.4 两种脱敏模式

typescript
type RedactSensitiveMode = "off" | "tools";
  • off:不脱敏(开发环境)
  • tools:仅对工具调用的输出做脱敏(默认),保护 Agent 工具返回的敏感信息

5.5 安全正则编译

所有脱敏正则都通过 compileSafeRegex() 编译——内部使用 safe-regex2 库检测 ReDoS 风险,拒绝可能导致灾难性回溯的正则。

![敏感信息脱敏——检测模式、掩码策略与安全编译](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/06-infographic-redaction-system-1775150663666.png)


六、诊断系统 —— diagnostic.ts

诊断系统在日志之上提供了更高层次的可观测性:

6.1 Webhook 统计

typescript
const webhookStats = {
  received: 0,
  processed: 0,
  errors: 0,
  lastReceived: 0,
};

三个计数器追踪 Webhook 处理的全生命周期。每个 logWebhook* 调用同时:

  1. 递增计数器
  2. 写入子系统日志
  3. 发送诊断事件(emitDiagnosticEvent
  4. 更新活跃时间戳

![诊断系统——Webhook 统计与 Session 状态追踪](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/07-infographic-diagnostic-webhook-session-1775150664530.png)

6.2 Session 状态追踪

typescript
type SessionStateValue = "idle" | "waiting" | "processing";

function logSessionStateChange(params: {
  sessionId, sessionKey, state, reason
})

每个活跃会话的状态变化都被追踪:idle → waiting → processing → idle。这是卡住会话检测的基础。

6.3 卡住会话检测

typescript
const DEFAULT_STUCK_SESSION_WARN_MS = 120_000;  // 2 分钟

for (const state of diagnosticSessionStates) {
  if (state.state === "processing" && ageMs > stuckSessionWarnMs) {
    logSessionStuck({ sessionId, state, ageMs });
  }
}

当一个会话在 processing 状态停留超过 2 分钟(可配置),触发 session.stuck 警告。

6.4 工具循环检测

typescript
function logToolLoopAction(params: {
  toolName, level, action,
  detector: "generic_repeat" | "known_poll_no_progress" | "global_circuit_breaker" | "ping_pong",
  count, message
})

4 种工具循环检测器:

  • generic_repeat:同一工具连续调用超过阈值
  • known_poll_no_progress:已知的轮询模式无进展
  • global_circuit_breaker:全局调用次数熔断
  • ping_pong:两个工具交替调用(乒乓模式)

6.5 诊断心跳

typescript
startDiagnosticHeartbeat();
// 每 30 秒:
// 1. 清理过期的 session 状态
// 2. 统计活跃/等待/排队数
// 3. 检测卡住会话
// 4. 清理过期的命令轮询退避

心跳定时器使用 .unref() 标记——不阻止进程退出。

![工具循环检测与诊断心跳](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/08-infographic-tool-loop-heartbeat-1775150665358.png)


七、时间戳格式化

typescript
function formatLocalIsoWithOffset(date: Date): string {
  // "2026-03-25T14:30:25.123+08:00"
}

所有日志时间戳使用 本地 ISO 8601 + 时区偏移 格式——比纯 UTC 更易于本地调试,时区偏移保证了全球一致性。


八、日志行解析

parse-log-line.ts 支持从日志文件反向解析日志条目:

typescript
function parseLogLine(line: string): ParsedLogLine | null {
  // 支持两种格式:
  // 1. JSON 行(主格式):{"time":"...","level":"info",...}
  // 2. tslog 原生格式(兼容旧日志)
}

Web UI 的日志面板就是用这个解析器来显示历史日志。


九、Console Capture

日志系统支持捕获并替换原生 console.* 方法:

typescript
function captureConsole(logger: SubsystemLogger) {
  const original = { log: console.log, warn: console.warn, error: console.error };
  console.log = (...args) => logger.info(formatArgs(args));
  console.warn = (...args) => logger.warn(formatArgs(args));
  console.error = (...args) => logger.error(formatArgs(args));
  return () => Object.assign(console, original);  // 恢复原始
}

这确保了第三方库的 console.log 也能被日志系统捕获和格式化。

![辅助能力——时间戳、日志解析与 Console 捕获](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/09-infographic-auxiliary-capabilities-1775150668918.png)


十、环境感知

10.1 颜色检测

typescript
function isRichConsoleEnv(): boolean {
  const term = (process.env.TERM ?? "").toLowerCase();
  if (process.env.COLORTERM || process.env.TERM_PROGRAM) return true;
  return term.length > 0 && term !== "dumb";
}

根据终端环境自适应颜色输出。尊重 NO_COLORFORCE_COLOR 标准。

10.2 Windows CI 安全

typescript
const sanitized =
  process.platform === "win32" && process.env.GITHUB_ACTIONS === "true"
    ? line.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "?").replace(/[\uD800-\uDFFF]/g, "?")
    : line;

Windows + GitHub Actions 环境下替换代理对字符——防止 CI 输出乱码。

10.3 Progress Line 兼容

typescript
clearActiveProgressLine();

每条日志输出前清除活跃的进度条行——避免日志和进度条的输出交错。


十一、设计模式总结

模式应用位置效果
Dual ChannelSubsystemLogger文件 + 控制台独立级别控制
Rolling Filelogger.ts按日滚动 + 大小上限保护
Structured LoggingJSON 行格式可机器解析的日志
Subsystem TaggingcreateSubsystemLogger模块化日志分类
Sensitive Redactionredact.ts15+ 模式自动脱敏
Health Heartbeatdiagnostic.ts30s 间隔健康检查
Stuck DetectionlogSessionStuck2 分钟超时告警
Circuit BreakerlogToolLoopAction4 种工具循环检测器
Console CapturecaptureConsole第三方库日志统一
Test Fast PathVitest 检测测试环境跳过文件 I/O

![设计模式总结——日志系统 10 大设计模式](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十一)日志与可观测性/10-infographic-design-patterns-1775150669686.png)


十二、推荐阅读顺序

  1. src/logging/levels.ts — 日志级别定义
  2. src/logging/timestamps.ts — 时间戳格式化
  3. src/logging/state.ts — 全局日志状态
  4. src/logging/config.ts — 日志配置加载
  5. src/logging/env-log-level.ts — 环境变量覆盖
  6. src/logging/logger.ts — Logger 核心(滚动日志/大小保护)
  7. src/logging/console.ts — 控制台配置
  8. src/logging/subsystem.ts — SubsystemLogger(双通道/颜色/缩写)
  9. src/logging/redact.ts — 敏感信息脱敏
  10. src/logging/redact-bounded.ts — 有界正则替换
  11. src/logging/diagnostic.ts — 诊断系统(Webhook 统计/卡住检测/心跳)
  12. src/logging/diagnostic-session-state.ts — Session 状态追踪
  13. src/logging/parse-log-line.ts — 日志行反向解析

十三、思考题

  1. 双通道(文件+控制台)独立级别控制的使用场景是什么? 什么时候需要文件记录 debug 但控制台只显示 warn?

  2. 500MB 文件大小上限是否足够? 在高流量场景下(比如处理数千条消息/小时),日志可能多快达到上限?

  3. 脱敏系统对 sk- 等前缀的硬编码检测会过时吗? 如果 OpenAI 更换了 Key 前缀格式怎么办?

  4. 卡住会话检测的 2 分钟阈值是否合理? 有些 Agent 运行可能需要更长时间(比如处理大型文件),这会产生误报吗?

  5. 为什么使用 tslog 而不是更流行的 pino 或 winston? 考虑 OpenClaw 的使用场景(菜单栏应用 + CLI + 网关),tslog 有什么独特优势?

读文档、看源码、写代码,理解 AI Agent 本质 🤖