主题
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 行代码。
日志与可观测性/01-infographic-logging-system-overview-1775150659804.png)
二、日志级别
typescript
const ALLOWED_LOG_LEVELS = ["silent", "fatal", "error", "warn", "info", "debug", "trace"] as const;7 个级别,映射到 tslog 的数值系统:
| 级别 | 数值 | 用途 |
|---|---|---|
fatal | 0 | 致命错误,进程即将退出 |
error | 1 | 可恢复错误 |
warn | 2 | 警告(卡住会话、工具循环) |
info | 3 | 正常运行信息(默认级别) |
debug | 4 | 调试详情 |
trace | 5 | 最详细的追踪 |
silent | ∞ | 完全静默 |
级别解析支持三级覆盖:环境变量 OPENCLAW_LOG_LEVEL > 配置文件 logging.level > 默认值 info。
日志与可观测性/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;
}测试环境下文件日志默认静默,且跳过所有配置加载和文件系统操作——这是一个性能优化,避免测试启动时的重配置开销。
日志与可观测性/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。
日志与可观测性/04-infographic-subsystem-dual-channel-1775150662188.png)
4.3 Console 样式
三种控制台输出样式:
pretty(默认):
14:30:25 [gateway] client connectedcompact:
[gateway] client connectedjson:
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/telegram | telegram |
gateway/providers/openai | openai |
agents/tools/web-search | tools/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 允许为控制台和文件提供不同的消息——文件记录详细信息,控制台显示精简版。
日志与可观测性/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, AIza | OpenAI/GitHub/Slack/Google key |
| npm token | npm_abc... | npm 认证令牌 |
| Telegram Bot | bot123456: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 风险,拒绝可能导致灾难性回溯的正则。
日志与可观测性/06-infographic-redaction-system-1775150663666.png)
六、诊断系统 —— diagnostic.ts
诊断系统在日志之上提供了更高层次的可观测性:
6.1 Webhook 统计
typescript
const webhookStats = {
received: 0,
processed: 0,
errors: 0,
lastReceived: 0,
};三个计数器追踪 Webhook 处理的全生命周期。每个 logWebhook* 调用同时:
- 递增计数器
- 写入子系统日志
- 发送诊断事件(
emitDiagnosticEvent) - 更新活跃时间戳
日志与可观测性/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() 标记——不阻止进程退出。
日志与可观测性/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 也能被日志系统捕获和格式化。
日志与可观测性/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_COLOR 和 FORCE_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 Channel | SubsystemLogger | 文件 + 控制台独立级别控制 |
| Rolling File | logger.ts | 按日滚动 + 大小上限保护 |
| Structured Logging | JSON 行格式 | 可机器解析的日志 |
| Subsystem Tagging | createSubsystemLogger | 模块化日志分类 |
| Sensitive Redaction | redact.ts | 15+ 模式自动脱敏 |
| Health Heartbeat | diagnostic.ts | 30s 间隔健康检查 |
| Stuck Detection | logSessionStuck | 2 分钟超时告警 |
| Circuit Breaker | logToolLoopAction | 4 种工具循环检测器 |
| Console Capture | captureConsole | 第三方库日志统一 |
| Test Fast Path | Vitest 检测 | 测试环境跳过文件 I/O |
日志与可观测性/10-infographic-design-patterns-1775150669686.png)
十二、推荐阅读顺序
src/logging/levels.ts— 日志级别定义src/logging/timestamps.ts— 时间戳格式化src/logging/state.ts— 全局日志状态src/logging/config.ts— 日志配置加载src/logging/env-log-level.ts— 环境变量覆盖src/logging/logger.ts— Logger 核心(滚动日志/大小保护)src/logging/console.ts— 控制台配置src/logging/subsystem.ts— SubsystemLogger(双通道/颜色/缩写)src/logging/redact.ts— 敏感信息脱敏src/logging/redact-bounded.ts— 有界正则替换src/logging/diagnostic.ts— 诊断系统(Webhook 统计/卡住检测/心跳)src/logging/diagnostic-session-state.ts— Session 状态追踪src/logging/parse-log-line.ts— 日志行反向解析
十三、思考题
双通道(文件+控制台)独立级别控制的使用场景是什么? 什么时候需要文件记录 debug 但控制台只显示 warn?
500MB 文件大小上限是否足够? 在高流量场景下(比如处理数千条消息/小时),日志可能多快达到上限?
脱敏系统对
sk-等前缀的硬编码检测会过时吗? 如果 OpenAI 更换了 Key 前缀格式怎么办?卡住会话检测的 2 分钟阈值是否合理? 有些 Agent 运行可能需要更长时间(比如处理大型文件),这会产生误报吗?
为什么使用 tslog 而不是更流行的 pino 或 winston? 考虑 OpenClaw 的使用场景(菜单栏应用 + CLI + 网关),tslog 有什么独特优势?