AI工具

Codex logs_2.sqlite 后续(五):RUST_LOG 为什么拦不住 TRACE,最后又用回 Trigger

发布于 2026-07-20 #Codex#Procmon#SQLite#TRACE日志#RUST_LOG#Trigger

概要:

前几次排查已经确认:Codex 会持续向 ~/.codex/logs_2.sqlite 写入诊断日志,即使数据库保留的行数没有增加,反复执行的插入、淘汰、WAL 写入和 checkpoint 仍然会产生实际磁盘 I/O。

这次使用自制监控器查看新增日志来源时,发现:

1
2
codex_app_server::outgoing_message
355 行 / 514 行,占 69.1%,全部为 TRACE

第一反应是用 Rust 常见的 RUST_LOG 定向关闭这个 target。实际测试后却发现,RUST_LOG=info 并不能阻止这些 TRACE/DEBUG 写入 logs_2.sqlite

继续查源码才发现,Codex 的普通日志和 SQLite 持久化日志使用了两套过滤器。绕了一圈,最终又回到了最早使用过的 SQLite Trigger。



监控发现 outgoing_message 占比最高

第一轮监控持续约 469 秒,结果如下:

指标 结果
日志 ID 增量 514
Procmon 捕获的数据库写入 6.763 MiB
折算写入速度 51.91 MiB/h
WriteFile 次数 3,092

新增日志 Top 5:

target 新增行 占比 level
codex_app_server::outgoing_message 355 69.07% TRACE 355
codex_api::sse::responses 35 6.81% TRACE 35
codex_core::stream_events_utils 30 5.84% DEBUG 26 / INFO 4
feedback_tags 17 3.31% INFO 17
opentelemetry-otlp 10 1.95% DEBUG 10

outgoing_message 不仅条数最多,而且全部是 TRACE,看上去非常适合优先过滤。

尝试使用 RUST_LOG

Codex 官方文档说明,CLI 和 app-server 支持通过 RUST_LOG 控制 Rust 日志级别,也支持按 target 设置过滤规则。例如:

1
codex_core=debug,codex_tui=debug

参考:Codex 环境变量文档

最初考虑过同时设置全局级别和 target 级别,例如让大部分日志保留 INFO,同时为部分模块保留 DEBUG/TRACE,再单独关闭 outgoing_message

但这种写法有一个容易忽略的问题:

1
codex_app_server=trace

并不是“保留当前已有的 TRACE”,而是允许整个 codex_app_server 命名空间产生 TRACE。它可能打开原本没有启用的其他低级别日志。把 RUST_LOG 设置为 Windows 用户级环境变量,还可能影响其他主动读取该变量的 Rust 程序。

于是改为更简单的测试:

1
setx RUST_LOG "info"

彻底退出 Codex 后重新打开,再运行一轮监控。按通常的 Rust 日志过滤语义,info 应当保留 INFO、WARN、ERROR,并关闭 TRACE、DEBUG。

实测 RUST_LOG 对 logs_2.sqlite 无效

第二轮监控持续约 626 秒logs_2.sqlite 中仍然出现:

target 新增行 level
codex_app_server::message_processor 58 TRACE 56 / INFO 2
codex_config::loader::layer_io 56 DEBUG 56
codex_app_server::outgoing_message 31 TRACE 31

只要数据库中仍然有 TRACE 和 DEBUG,就足以说明:这条 SQLite 写入链路没有被 RUST_LOG=info 关闭。

两轮数据对比如下:

指标 设置前 设置后 变化
新增日志/分钟 65.76 33.82 -48.6%
outgoing_message/分钟 45.42 2.97 -93.5%
数据库写入 51.91 MiB/h 26.08 MiB/h -49.8%

第二轮写入确实更低,但不能据此认定是 RUST_LOG 生效:

  • 两轮执行的工作内容不同;
  • outgoing_message 数量会随界面通知、工具调用和输出过程变化;
  • 测试期间 Codex 还可能发生版本更新;
  • 最关键的是,第二轮数据库里依然明确存在 TRACE 和 DEBUG。

OpenAI 仓库中已有同类问题:即使 app-server 进程带有 RUST_LOG=warnlogs_2.sqlite 仍会保存 TRACE。参考:Issue #17320

为什么会有两套日志过滤

一条 Rust tracing 事件可以同时交给多个接收层:

1
2
3
4
5
tracing::trace!(...)

├─ 终端/文本日志层 ── RUST_LOG
├─ SQLite 日志层 ───── log_db::default_filter()
└─ OpenTelemetry 层 ── OTEL 配置

这里的“持久化层”,指的是把日志真正保存到磁盘、使它在进程退出后仍然存在的接收层。logs_2.sqlite 就是本地持久化日志数据库。

当前官方源码中的 SQLite 默认过滤器大致如下:

1
2
3
4
5
6
7
pub fn default_filter() -> Targets {
Targets::new()
.with_default(LevelFilter::TRACE)
.with_target("log", LevelFilter::OFF)
.with_target("codex_otel.log_only", LevelFilter::OFF)
.with_target("codex_otel.trace_safe", LevelFilter::OFF)
}

也就是说:

  • SQLite 层默认接收全部 target 的 TRACE;
  • 只有源码里明确列出的 target 会被关闭;
  • 这个过滤器目前没有公开的 config.toml 或环境变量入口;
  • RUST_LOG 控制另一层,不能直接改写这份 Targets 规则。

参考:PR #29457 的源码改动

如果官方要在 SQLite 层关闭本次发现的日志,只需要增加类似规则:

1
2
3
4
.with_target(
"codex_app_server::outgoing_message",
LevelFilter::OFF,
)

但普通用户无法直接修改桌面版内置的 codex.exe。自己编译虽然可行,却还要解决桌面版如何使用自编译 app-server、版本升级后如何维护等问题,不适合作为日常处理方案。

官方之前是怎么过滤日志的

上一篇提到的官方修复,本质上也是修改 Rust 源码,而不是让用户设置 `RUST_LOG`:
  1. PR #29432 直接移除了成功 Responses WebSocket 事件的逐条 TRACE 和重复 OpenTelemetry 事件,只保留计数、耗时和错误等信息。
  2. PR #29457 修改 SQLite 持久化层的 default_filter(),排除 target=logcodex_otel.log_onlycodex_otel.trace_safe
  3. 后续修复又在桥接日志完成转换后补了一次拦截,避免 target=log 绕过前面的判断。相关汇总见 Issue #28224

因此,设置 RUST_LOG=info 不会把官方已经排除的两个 INFO target 重新写进 SQLite;它们是在 SQLite 自己的过滤器中按 target 关闭的。

清除全局 RUST_LOG

由于使用的是 setxRUST_LOG=info 被保存为 Windows 用户级环境变量。它不仅会被之后启动的 Codex 继承,也可能影响其他主动读取 RUST_LOG 的 Rust 程序。

这里的影响不一定都是“减少日志”。如果某个程序原本只记录 WARN/ERROR,全局设置成 INFO 反而可能让它输出更多内容。

删除用户级变量:

1
[Environment]::SetEnvironmentVariable('RUST_LOG', $null, 'User')

清除当前 PowerShell 会话中可能残留的值:

1
Remove-Item Env:RUST_LOG -ErrorAction SilentlyContinue

验证用户级变量已经删除:

1
[Environment]::GetEnvironmentVariable('RUST_LOG', 'User')

最后一条没有输出即表示删除成功。已经运行的程序不会动态更新环境变量,需要彻底退出 Codex 后重新打开;如果新进程仍然继承旧值,可以注销并重新登录 Windows。

最终重新启用 Trigger

绕了一圈之后,最终又回到了第一次排查时使用的 SQLite Trigger。

逐个 target 拦截最精确,但需要持续追踪新出现的日志来源。既然本地正常使用并不依赖 TRACE/DEBUG,直接阻止这两个级别入库更简单:

执行前必须彻底退出 Codex,并确认没有其他 codex.exe 正在使用 logs_2.sqlite

1
2
3
4
5
6
CREATE TRIGGER IF NOT EXISTS block_trace_debug
BEFORE INSERT ON logs
WHEN UPPER(NEW.level) IN ('TRACE', 'DEBUG')
BEGIN
SELECT RAISE(IGNORE);
END;

这样会:

  • 保留 INFO、WARN、ERROR;
  • 阻止 TRACE、DEBUG 真正写入表和索引;
  • 减少后续日志淘汰、WAL 写入和 checkpoint 压力;
  • 不影响其他 Rust 应用;
  • 不需要维护不断变化的 target 列表。

它仍然不是源码级关闭:事件已经产生,也已经到达 SQLite 写入器,Trigger 只是在 INSERT 的最后一步返回 RAISE(IGNORE)。因此还会保留少量事件格式化、队列和 Trigger 判断开销,但能避免这些日志真正成为数据库行。

官方问题中的测试也显示,忽略 INSERT 可以消除大部分数据库与 CPU 开销,但完全关闭 SQLite 日志层仍会更轻。参考:Issue #17320 的 Trigger 与完全禁用对比

需要恢复完整日志时:

1
DROP TRIGGER IF EXISTS block_trace_debug;

检查 Trigger 是否仍然存在:

1
2
3
4
SELECT name, sql
FROM sqlite_master
WHERE type = 'trigger'
ORDER BY name;

Codex 更新、数据库迁移或重建后,Trigger 可能消失,需要重新检查。启用 RAISE(IGNORE) 后,sqlite_sequence 的 ID 高水位也不再适合作为绝对精确的成功插入数;分析效果时应优先查看 Procmon 捕获到的目标文件实际写入量。

结论

这次排查最后回到了原点,但中间确认了几个重要边界:

  • outgoing_message 是当前最主要的低级别日志来源之一;
  • RUST_LOG 能控制 Codex 的普通 Rust 日志,但当前管不到 SQLite 持久化层;
  • 官方之前的降噪也是修改 Rust 源码中的事件产生点和 SQLite default_filter()
  • 当前桌面版没有开放 SQLite 日志级别配置;
  • 不自编译 Codex 的前提下,Trigger 仍然是最直接、可撤销、能够明显减少磁盘写入的本地方案。

真正理想的修复,是官方为 SQLite 日志层增加可配置级别,或者直接把 codex_app_server::outgoing_message 加入默认排除列表。在此之前,重新启用只拦截 TRACE/DEBUG 的 Trigger,至少比全局设置 RUST_LOG 更精确,也不会影响其他程序。

评论
分享

评论