Codex 系统手册

反馈捕获、调试产物与日志持久化

stage-20.510 个文件

这一阶段是幕后支撑,不直接干主业务,而是在出问题时留下线索。反馈模块把日志、标签和网络诊断打包,反馈医生再附一份报告,但不阻塞提交。报错提取器只捞安全的上下文,脱敏器把密钥、口令换成占位符。代理请求、分析事件、终端会话会按需落成本地记录。最后日志数据库负责稳稳写入、按条件查询,并定期清掉过旧内容,方便事后复盘。

本阶段的文件10

反馈组装

这些文件构建反馈捕获管线,使用可选诊断信息进行丰富,并在上传打包前添加 doctor-report 附件和标签。

feedback/src/lib.rs源码 ↗
domain_logiccross-cutting;程序运行时持续收集日志和标签,用户提交反馈时生成快照并上传

这个文件解决的是“用户说出问题了,但开发者不知道当时发生了什么”的问题。它会把日志写进一个有上限的环形缓冲区(像一个只保留最近内容的小本子,写满了就擦掉最旧的),同时收集一些反馈标签,比如请求端点、认证方式、错误码等。CodexFeedback 是主要入口:它能生成日志层,让 tracing(Rust 里的结构化日志系统)把日志写进内存;也能生成元数据层,专门抓取反馈用的标签。用户提交反馈时,FeedbackSnapshot 会冻结当时的日志、标签和环境诊断信息,然后可以保存到临时文件,或作为附件上传到 Sentry(一个收集错误和反馈的服务)。文件里还特别注意了安全和稳定:上传标签时保留系统字段不被外部覆盖;附件读不到就跳过;日志容量满了只丢旧内容。

函数细节38
FeedbackRequestSnapshot::from_tags77–102 ↗
fn from_tags(tags: &'a FeedbackRequestTags<'a>) -> Self

作用:把一组可能有空值的反馈请求标签整理成一份稳定快照。这样后面写日志时不用反复判断哪些字段有没有值。

数据流:输入是一份 FeedbackRequestTags,里面有端点、认证信息、错误信息等。它把缺失的文本字段变成空字符串,把布尔值或状态值转成字符串,最后输出 FeedbackRequestSnapshot。

调用关系:emit_feedback_request_tags 和 emit_feedback_request_tags_with_auth_env 在真正写日志前都会先调用它,相当于先把散装信息整理成一张表。

调用图:被 2 处调用(emit_feedback_request_tags, emit_feedback_request_tags_with_auth_env)。

emit_feedback_request_tags105–124 ↗
fn emit_feedback_request_tags(tags: &FeedbackRequestTags<'_>)

作用:把一次反馈请求相关的标签写进 tracing 日志系统,供后面的反馈元数据层捕获。它不上传东西,只是在本地留下结构化记录。

数据流:输入是请求标签。它先调用 FeedbackRequestSnapshot::from_tags 做标准化,然后把端点、认证头、认证模式、错误码等字段作为日志字段发出去。

调用关系:它把数据交给 tracing::info!;如果 CodexFeedback::metadata_layer 正在工作,FeedbackMetadataLayer::on_event 会在之后接住这些字段并保存。

调用图:调用 1 个内部函数(from_tags);外部调用 1 个(info!)。

emit_feedback_request_tags_with_auth_env126–161 ↗
fn emit_feedback_request_tags_with_auth_env(
    tags: &FeedbackRequestTags<'_>,
    auth_env: &AuthEnvTelemetry,
)

作用:和 emit_feedback_request_tags 类似,但会额外记录认证相关的环境变量状态。这样排查“为什么认证失败”时能看到 API key 是否存在、是否启用等线索。

数据流:输入是一份请求标签和 AuthEnvTelemetry。它先把请求标签整理成快照,再把请求字段和环境字段一起写进 tracing 日志。

调用关系:它同样依赖 FeedbackRequestSnapshot::from_tags,并把结果交给 tracing;后续由元数据层从特定日志目标里提取这些信息。

调用图:调用 1 个内部函数(from_tags);外部调用 1 个(info!)。

CodexFeedback::default169–171 ↗
fn default() -> Self

作用:提供一个默认的反馈收集器。别人只想“给我一个可用的反馈系统”时,可以直接用默认值。

数据流:没有输入。它调用 CodexFeedback::new,输出一个使用默认日志容量的 CodexFeedback。

调用关系:这是 Rust 的默认构造入口,实际创建工作交给 CodexFeedback::new。

调用图:外部调用 1 个(new)。

CodexFeedback::new175–177 ↗
fn new() -> Self

作用:创建一个普通大小的反馈收集器。大多数地方都用它来开始收集日志和反馈标签。

数据流:没有输入。它使用 DEFAULT_MAX_BYTES 作为容量,调用 CodexFeedback::with_capacity,输出 CodexFeedback。

调用关系:很多运行时和测试代码会调用它;它把具体容量设置交给 CodexFeedback::with_capacity。

调用图:被 30 处调用(runtime_start_args_forward_environment_manager, runtime_start_args_use_remote_thread_config_loader_when_configured, start_test_client_with_capacity, start_test_client_with_capacity, build_test_processor, run_main_with_transport_options, get_conversation_summary_by_thread_id_reads_pathless_store_thread, mcp_resource_read_returns_error_for_unknown_thread, start_in_process_client, thread_list_includes_store_thread_without_rollout_path (+15 more));外部调用 1 个(with_capacity)。

CodexFeedback::with_capacity179–183 ↗
fn with_capacity(max_bytes: usize) -> Self

作用:创建一个指定容量的反馈收集器。主要用于测试或需要控制内存占用的场景。

数据流:输入是最大字节数 max_bytes。它创建 FeedbackInner,把日志环形缓冲区和标签表包进 Arc(可共享引用)里,输出 CodexFeedback。

调用关系:CodexFeedback::new 会调用它;测试 ring_buffer_drops_front_when_full 也用它来验证容量满时会丢旧日志。

调用图:调用 1 个内部函数(new);被 1 处调用(ring_buffer_drops_front_when_full);外部调用 1 个(new)。

CodexFeedback::make_writer185–189 ↗
fn make_writer(&self) -> FeedbackMakeWriter

作用:做出一个日志写入器工厂,让日志系统知道“日志要写到反馈收集器里”。

数据流:输入是当前 CodexFeedback。它复制一份内部共享引用,输出 FeedbackMakeWriter。

调用关系:CodexFeedback::logger_layer 会调用它,把这个写入器交给 tracing_subscriber 的格式化日志层。

调用图:被 1 处调用(logger_layer)。

CodexFeedback::logger_layer196–208 ↗
fn logger_layer(&self) -> impl Layer<S> + Send + Sync + 'static

作用:生成一个日志层,用来捕获程序里的所有 tracing 日志,并写进反馈缓冲区。这样用户反馈时能带上完整的近期日志。

数据流:输入是 CodexFeedback 自身。它创建一个格式化日志层,设置写入器、时间戳、关闭彩色字符,并把过滤级别设到 TRACE,输出可安装到 tracing 的 Layer。

调用关系:它会调用 CodexFeedback::make_writer。外部启动日志系统时安装这个层,之后每条日志会通过 FeedbackMakeWriter 和 FeedbackWriter 写入内存。

调用图:调用 1 个内部函数(make_writer);外部调用 2 个(new, layer)。

CodexFeedback::metadata_layer214–222 ↗
fn metadata_layer(&self) -> impl Layer<S> + Send + Sync + 'static

作用:生成一个专门收集反馈标签的日志层。它只关心发到反馈标签目标的事件,不会把所有普通日志都当标签。

数据流:输入是 CodexFeedback 自身。它创建 FeedbackMetadataLayer,共享内部标签表,并加上只接收 FEEDBACK_TAGS_TARGET 的过滤条件,输出 Layer。

调用关系:emit_feedback_request_tags 发出的事件会被这个层的 FeedbackMetadataLayer::on_event 接住,再转成快照里的 tags。

调用图:外部调用 1 个(new)。

CodexFeedback::snapshot224–243 ↗
fn snapshot(&self, session_id: Option<ThreadId>) -> FeedbackSnapshot

作用:把当前收集到的日志、标签和诊断信息冻结成一份反馈快照。用户真正提交反馈或打开反馈确认页时会用它。

数据流:输入是可选的会话线程 ID。它从日志缓冲区复制字节,从标签表复制标签,再收集环境诊断信息;如果没有线程 ID,就生成一个 no-active-thread 开头的临时 ID,最后输出 FeedbackSnapshot。

调用关系:upload_feedback_response 和 open_feedback_consent 会调用它。它读取 FeedbackInner 里的两个互斥锁(一把锁,防止多个任务同时改同一份数据)。

调用图:调用 2 个内部函数(collect_from_env, new);被 2 处调用(upload_feedback_response, open_feedback_consent)。

FeedbackInner::new252–257 ↗
fn new(max_bytes: usize) -> Self

作用:创建反馈收集器真正存数据的内部对象。外层 CodexFeedback 只是共享它、包装它。

数据流:输入是日志最大容量。它创建 RingBuffer 保存日志字节,创建 BTreeMap 保存标签,并分别放进 Mutex(互斥锁)里,输出 FeedbackInner。

调用关系:CodexFeedback::with_capacity 会调用它。之后日志写入器和元数据层都通过这个内部对象共享数据。

调用图:调用 1 个内部函数(new);被 1 处调用(with_capacity);外部调用 2 个(new, new)。

FeedbackMakeWriter::make_writer268–272 ↗
fn make_writer(&'a self) -> Self::Writer

作用:按 tracing 的要求,真正生产一个可以写日志的 FeedbackWriter。每次日志系统需要写入时,就能拿到一个写入器。

数据流:输入是 FeedbackMakeWriter 自身。它复制内部共享引用,输出 FeedbackWriter。

调用关系:它服务于 CodexFeedback::logger_layer 创建的日志层;后续日志内容会交给 FeedbackWriter::write。

FeedbackWriter::write280–284 ↗
fn write(&mut self, buf: &[u8]) -> io::Result<usize>

作用:把一段日志字节写进内存里的环形缓冲区。它是日志真正进入反馈系统的地方。

数据流:输入是一段字节 buf。它先锁住内部 RingBuffer,再调用 push_bytes 追加内容;成功后返回写入的字节数,如果锁坏了就返回 IO 错误。

调用关系:由 tracing 日志层间接调用。它把实际存储工作交给 RingBuffer::push_bytes。

FeedbackWriter::flush286–288 ↗
fn flush(&mut self) -> io::Result<()>

作用:满足写入器接口要求的“刷新”动作。因为这里写的是内存,不需要真的刷到磁盘,所以它直接成功。

数据流:没有额外输入,也不改动数据。调用后返回 Ok,表示刷新完成。

调用关系:日志系统可能会按标准写入流程调用它;在这个实现里它只是占位,保证接口完整。

RingBuffer::new297–302 ↗
fn new(capacity: usize) -> Self

作用:创建一个固定容量的环形缓冲区,用来保存最近的日志字节。容量限制能防止反馈日志无限占内存。

数据流:输入是容量 capacity。它创建 VecDeque(两头都能进出的队列)作为底层存储,输出 RingBuffer。

调用关系:FeedbackInner::new 会调用它,后续写日志时由 RingBuffer::push_bytes 维护容量。

调用图:被 1 处调用(new);外部调用 1 个(with_capacity)。

RingBuffer::len304–306 ↗
fn len(&self) -> usize

作用:返回当前缓冲区里有多少字节。它帮助判断再写入新日志会不会超容量。

数据流:输入是 RingBuffer 自身。它读取内部队列长度,输出数字。

调用关系:RingBuffer::push_bytes 会调用它来计算需要丢掉多少旧字节。

调用图:被 1 处调用(push_bytes);外部调用 1 个(len)。

RingBuffer::push_bytes308–331 ↗
fn push_bytes(&mut self, data: &[u8])

作用:把新日志追加到缓冲区,并保证总大小不超过上限。满了就从最前面丢旧内容,保留最新内容。

数据流:输入是一段字节 data。空数据直接忽略;如果新数据本身比容量还大,只保留它最后那一段;否则先按需要弹掉旧字节,再把新字节追加进去。

调用关系:FeedbackWriter::write 会调用它。它内部用 RingBuffer::len 判断当前大小,并操作队列完成“旧的出去,新的进来”。

调用图:调用 1 个内部函数(len);外部调用 3 个(clear, extend, pop_front)。

RingBuffer::snapshot_bytes333–335 ↗
fn snapshot_bytes(&self) -> Vec<u8>

作用:复制当前缓冲区里的日志内容,做成一份稳定的字节列表。这样生成快照后,后续新日志不会影响这份结果。

数据流:输入是 RingBuffer 自身。它按当前顺序遍历缓冲区,复制每个字节,输出 Vec<u8>。

调用关系:CodexFeedback::snapshot 在冻结反馈日志时会调用它。

调用图:外部调用 1 个(iter)。

FeedbackSnapshot::as_bytes388–390 ↗
fn as_bytes(&self) -> &[u8]

作用:拿到快照里的原始日志字节。保存文件或测试检查日志内容时会用它。

数据流:输入是 FeedbackSnapshot 自身。它返回内部 bytes 的只读引用,不复制也不修改。

调用关系:FeedbackSnapshot::save_to_temp_file 会调用它把日志写到临时文件;测试也用它检查缓冲区行为。

调用图:被 1 处调用(save_to_temp_file)。

FeedbackSnapshot::feedback_diagnostics392–394 ↗
fn feedback_diagnostics(&self) -> &FeedbackDiagnostics

作用:读取快照里的反馈诊断信息。调用者可以查看当时收集到的环境或连接问题提示。

数据流:输入是 FeedbackSnapshot 自身。它返回 FeedbackDiagnostics 的只读引用,不改动快照。

调用关系:它是快照对外暴露诊断信息的小窗口,不负责生成或上传。

FeedbackSnapshot::with_feedback_diagnostics396–399 ↗
fn with_feedback_diagnostics(mut self, feedback_diagnostics: FeedbackDiagnostics) -> Self

作用:替换快照里的诊断信息,并返回修改后的快照。测试或特殊流程可以用它塞入指定诊断结果。

数据流:输入是一个快照和新的 FeedbackDiagnostics。它把快照里的 feedback_diagnostics 字段换掉,输出这个快照本身。

调用关系:测试 feedback_attachments_gate_connectivity_diagnostics 用它来构造有诊断和无诊断两种情况。

FeedbackSnapshot::feedback_diagnostics_attachment_text401–407 ↗
fn feedback_diagnostics_attachment_text(&self, include_logs: bool) -> Option<String>

作用:决定是否把诊断信息做成文本附件。只有包含日志时,才会考虑附带这些诊断说明。

数据流:输入是 include_logs。为 false 时直接返回 None;为 true 时调用诊断对象生成附件文本,可能返回 Some 文本,也可能没有内容。

调用关系:FeedbackSnapshot::feedback_attachments 会调用它,决定是否追加诊断附件。

调用图:调用 1 个内部函数(attachment_text);被 1 处调用(feedback_attachments)。

FeedbackSnapshot::save_to_temp_file409–415 ↗
fn save_to_temp_file(&self) -> io::Result<PathBuf>

作用:把反馈日志保存到系统临时目录里的一个文件。这样可以让用户或其他流程拿到一份本地日志文件。

数据流:输入是快照。它找系统临时目录,用 thread_id 拼出 codex-feedback-xxx.log 文件名,然后把 as_bytes 返回的日志字节写进去,输出文件路径。

调用关系:它调用 FeedbackSnapshot::as_bytes 取日志内容,并通过文件系统写入磁盘。

调用图:调用 1 个内部函数(as_bytes);外部调用 3 个(format!, write, temp_dir)。

FeedbackSnapshot::upload_feedback418–487 ↗
fn upload_feedback(&self, options: FeedbackUploadOptions<'_>) -> Result<()>

作用:把这份反馈快照上传到 Sentry。它会创建事件、填好标签、加上日志和额外附件,然后发送出去。

数据流:输入是 FeedbackUploadOptions,包括反馈分类、原因、是否带日志、额外附件等。它创建 Sentry 客户端,调用 upload_tags 生成标签,根据分类选择错误级别或信息级别,组装事件和附件,发送 envelope,等待最多一段超时时间后返回结果。

调用关系:它是上传流程的总入口。标签由 FeedbackSnapshot::upload_tags 准备,附件由 FeedbackSnapshot::feedback_attachments 准备,标题里的分类显示文字由 display_classification 生成。

调用图:调用 2 个内部函数(feedback_attachments, upload_tags);外部调用 11 个(new, default, from_str, from_secs, new, Attachment, Event, from, from_config, format! (+1 more))。

FeedbackSnapshot::upload_tags489–536 ↗
fn upload_tags(
        &self,
        classification: &str,
        reason: Option<&str>,
        client_tags: Option<&BTreeMap<String, String>>,
        session_source: Option<&SessionSource>,
    )

作用:整理上传到 Sentry 的标签,并保护关键系统字段不被外部输入覆盖。比如 thread_id、分类、版本号必须以系统生成的为准。

数据流:输入是分类、可选原因、客户端传来的标签、可选会话来源。它先放入 thread_id、classification、cli_version 等保留字段;再加入非保留的客户端标签;最后加入快照自己收集的非保留标签,但不会覆盖已有键,输出标签表。

调用关系:FeedbackSnapshot::upload_feedback 会调用它。测试 upload_tags_include_client_tags_and_preserve_reserved_fields 专门验证保留字段不会被乱改。

调用图:被 1 处调用(upload_feedback);外部调用 3 个(from, from, env!)。

FeedbackSnapshot::feedback_attachments538–605 ↗
fn feedback_attachments(
        &self,
        include_logs: bool,
        extra_attachments: &[FeedbackAttachment],
        extra_attachment_paths: &[FeedbackAttachmentPath],
        logs_override:

作用:把要随反馈上传的附件列表组装好。附件可能包括日志、调用方给的内存附件、诊断文本,以及磁盘上的额外日志文件。

数据流:输入包括是否包含日志、额外附件、额外文件路径、可选日志替代内容。它按顺序加入日志附件、额外附件、诊断附件;再逐个读取路径里的文件,读失败就写警告并跳过,成功就作为文本附件加入,最后输出附件列表。

调用关系:FeedbackSnapshot::upload_feedback 会调用它。它内部会调用 feedback_diagnostics_attachment_text,并在读文件失败时通过 tracing::warn! 留下提醒。

调用图:调用 1 个内部函数(feedback_diagnostics_attachment_text);被 1 处调用(upload_feedback);外部调用 5 个(from, new, iter, read, warn!)。

display_classification608–616 ↗
fn display_classification(classification: &str) -> String

作用:把机器使用的反馈分类代码变成人能看懂的标题文字。比如 bug 变成 Bug。

数据流:输入是分类字符串。它匹配几个已知分类,输出对应的显示文字;不认识的分类统一输出 Other。

调用关系:FeedbackSnapshot::upload_feedback 用它生成 Sentry 事件标题,让后台看到的反馈更清楚。

FeedbackMetadataLayer::on_event627–648 ↗
fn on_event(&self, event: &Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>)

作用:在收到 tracing 事件时,从专门的反馈标签事件里提取字段并保存。它是“日志事件”变成“反馈标签”的关键步骤。

数据流:输入是一个 tracing 事件。它先检查事件目标是不是 FEEDBACK_TAGS_TARGET,不是就忽略;是的话用 FeedbackTagsVisitor 读取字段;如果读到标签,就锁住内部标签表,最多保存到 MAX_FEEDBACK_TAGS,已有键可以更新。

调用关系:CodexFeedback::metadata_layer 创建的层会在 tracing 事件发生时调用它。字段读取工作交给 FeedbackTagsVisitor 的各个 record_* 方法。

调用图:外部调用 3 个(default, metadata, record)。

FeedbackTagsVisitor::record_i64657–660 ↗
fn record_i64(&mut self, field: &tracing::field::Field, value: i64)

作用:记录一个有符号整数标签。比如某个字段值是 -1 或 42,就会转成文字保存。

数据流:输入是字段名和 i64 数值。它取出字段名,把数值转成字符串,写入 visitor 的标签表。

调用关系:FeedbackMetadataLayer::on_event 调用 event.record 时,tracing 会按字段类型回调这个方法。

调用图:外部调用 1 个(name)。

FeedbackTagsVisitor::record_u64662–665 ↗
fn record_u64(&mut self, field: &tracing::field::Field, value: u64)

作用:记录一个无符号整数标签。适合保存计数、状态码这类不会为负的数字。

数据流:输入是字段名和 u64 数值。它把字段名作为键,把数值转成字符串作为值,放进标签表。

调用关系:它由 tracing 在记录事件字段时自动调用,结果最后会被 FeedbackMetadataLayer::on_event 合并进全局标签表。

调用图:外部调用 1 个(name)。

FeedbackTagsVisitor::record_bool667–670 ↗
fn record_bool(&mut self, field: &tracing::field::Field, value: bool)

作用:记录一个真假值标签。比如 cached=true 这种信息会变成字符串保存。

数据流:输入是字段名和 bool 值。它把 true 或 false 转成文本,按字段名写入标签表。

调用关系:metadata_layer_records_tags_from_feedback_target 这个测试里 cached=true 就会经过这个方法记录。

调用图:外部调用 1 个(name)。

FeedbackTagsVisitor::record_f64672–675 ↗
fn record_f64(&mut self, field: &tracing::field::Field, value: f64)

作用:记录一个小数标签。需要保存耗时、比例等浮点数时会用到。

数据流:输入是字段名和 f64 小数。它把小数转成字符串,写入 visitor 的标签表。

调用关系:它是 FeedbackTagsVisitor 处理不同字段类型的一部分,由 tracing 的事件记录流程触发。

调用图:外部调用 1 个(name)。

FeedbackTagsVisitor::record_str677–680 ↗
fn record_str(&mut self, field: &tracing::field::Field, value: &str)

作用:记录一个字符串标签。模型名、错误码、请求 ID 这类文本字段会走这里。

数据流:输入是字段名和字符串值。它复制字段名和值,存进 visitor 的标签表。

调用关系:FeedbackMetadataLayer::on_event 通过 event.record 使用它;测试里的 model="gpt-5" 就会被它记录下来。

调用图:外部调用 1 个(name)。

FeedbackTagsVisitor::record_debug682–685 ↗
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug)

作用:记录一个只能用 Debug 形式展示的字段。Debug 可以理解成“给程序员看的打印格式”。

数据流:输入是字段名和一个可 Debug 打印的值。它用 {:?} 格式把值转成字符串,再放进标签表。

调用关系:emit_feedback_request_tags 里很多字段用 tracing::field::debug 发出,最终就可能由这个方法接收并转成文本标签。

调用图:外部调用 2 个(name, format!)。

tests::ring_buffer_drops_front_when_full700–710 ↗
fn ring_buffer_drops_front_when_full()

作用:测试日志缓冲区写满后会丢掉最旧内容,只保留最新内容。这能保证反馈日志不会无限变大。

数据流:它创建容量为 8 字节的 CodexFeedback,写入 10 个字节 abcdefghij,然后生成快照,检查最终只剩 cdefghij。

调用关系:它直接使用 CodexFeedback::with_capacity、make_writer、snapshot 和 as_bytes,验证 RingBuffer::push_bytes 的核心行为。

调用图:调用 1 个内部函数(with_capacity);外部调用 1 个(assert_eq!)。

tests::metadata_layer_records_tags_from_feedback_target713–724 ↗
fn metadata_layer_records_tags_from_feedback_target()

作用:测试元数据层确实能从指定目标的 tracing 事件里抓到标签。否则反馈上传时就会缺少模型名、缓存状态等线索。

数据流:它创建 CodexFeedback,安装 metadata_layer,发出一个目标为 FEEDBACK_TAGS_TARGET 的 info 事件,里面有 model 和 cached 字段;之后生成快照并检查标签存在。

调用关系:这个测试覆盖了 CodexFeedback::metadata_layer、FeedbackMetadataLayer::on_event 和 FeedbackTagsVisitor 的记录流程。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, info!, registry)。

tests::feedback_attachments_gate_connectivity_diagnostics727–795 ↗
fn feedback_attachments_gate_connectivity_diagnostics()

作用:测试附件组装规则,特别是诊断信息什么时候会作为附件出现。它还确认额外文件附件能被读取并带上正确文件名。

数据流:它先在临时目录写一个额外文件,再构造带诊断信息的快照,调用 feedback_attachments,检查附件顺序和内容;随后构造无诊断快照,确认只剩日志附件;最后删除临时文件。

调用关系:它主要验证 FeedbackSnapshot::feedback_attachments、with_feedback_diagnostics 和诊断附件生成逻辑。

调用图:调用 2 个内部函数(new, new);外部调用 8 个(assert_eq!, default, format!, remove_file, write, temp_dir, from_ref, vec!)。

tests::upload_tags_include_client_tags_and_preserve_reserved_fields798–871 ↗
fn upload_tags_include_client_tags_and_preserve_reserved_fields()

作用:测试上传标签会合并客户端标签,但不会让客户端覆盖系统保留字段。这样后台收到的 thread_id、分类、版本等关键信息才可信。

数据流:它构造一份带错误保留字段的快照标签和客户端标签,然后调用 upload_tags。最后检查保留字段使用真实系统值,普通字段则按规则合并。

调用关系:它直接验证 FeedbackSnapshot::upload_tags 的优先级规则:系统保留字段最高,客户端普通标签优先于快照里的同名普通标签。

调用图:外部调用 4 个(new, new, assert_eq!, default)。

feedback/src/feedback_diagnostics.rs源码 ↗
domain_logicfeedback collection / request handling

这个文件解决的是一个很现实的问题:用户说“连不上”,开发者光看错误信息可能猜不出原因,而电脑上的代理环境变量可能正好影响了网络连接。这里的做法很简单:检查一组常见代理变量,比如 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY;如果发现有设置,就生成一条诊断提示,说明“代理环境变量存在,可能影响连接”,并把具体变量和值列出来。FeedbackDiagnostics 像一个小文件夹,里面装多条诊断;FeedbackDiagnostic 像其中一张便签,包含一句标题和若干细节。最后 attachment_text 会把这些便签排版成一段纯文本,可作为反馈附件。一个重要点是:它会原样记录代理值,不会修剪空格,也不会判断格式对不对,更不会隐藏密码等敏感内容;这对排查很有用,但也意味着调用方需要清楚这是给反馈附件用的原始诊断信息。

函数细节10
FeedbackDiagnostics::new25–27 ↗
fn new(diagnostics: Vec<FeedbackDiagnostic>) -> Self

作用:用现成的一组诊断条目创建一个 FeedbackDiagnostics。别人已经准备好了诊断内容时,就用它把这些内容包装成统一格式。

数据流:输入是一串 FeedbackDiagnostic,每条里面有标题和细节;函数不修改、不筛选,直接把它们放进 FeedbackDiagnostics 里;输出就是这个新的诊断集合。

调用关系:它通常被反馈相关流程用来手动构造诊断信息,比如生成反馈附件、弹出上传确认窗口、或判断是否展示连接诊断细节时。它自己不再调用别的项目函数,只是一个简单的装盒步骤。

调用图:被 4 处调用(feedback_attachments_gate_connectivity_diagnostics, should_show_feedback_connectivity_details_only_for_non_good_result_with_diagnostics, feedback_good_result_consent_popup_includes_connectivity_diagnostics_filename, feedback_upload_consent_popup_snapshot)。

FeedbackDiagnostics::collect_from_env29–31 ↗
fn collect_from_env() -> Self

作用:从当前程序运行环境里自动收集诊断信息。也就是查看这台机器当前有没有设置会影响网络连接的代理变量。

数据流:它读取系统环境变量,也就是程序启动时能看到的一堆“名字=值”;然后把这些变量交给 collect_from_pairs 做统一检查;最后返回整理好的 FeedbackDiagnostics。

调用关系:它是实际运行时的入口之一,比如快照或反馈收集流程需要知道当前网络环境时会调用它。它把读取环境变量这一步做完后,把真正的筛选和整理工作交给 FeedbackDiagnostics::collect_from_pairs。

调用图:被 1 处调用(snapshot);外部调用 2 个(collect_from_pairs, vars)。

FeedbackDiagnostics::collect_from_pairs33–61 ↗
fn collect_from_pairs(pairs: I) -> Self

作用:从一批给定的“变量名和值”里找出代理设置,并生成诊断条目。测试也用它,因为测试可以喂进去假环境变量,不必真的改系统环境。

数据流:输入是一批键值对,例如 HTTP_PROXY 对应某个代理地址;它先把这些键值对转成方便查找的表,再按固定名单检查 HTTP_PROXY、http_proxy、HTTPS_PROXY 等代理变量;找到的每一项会变成“变量名 = 变量值”的细节;如果至少找到一项,就输出一个包含代理提醒的 FeedbackDiagnostics,否则输出空的诊断集合。

调用关系:它是这个文件的核心整理器。FeedbackDiagnostics::collect_from_env 会把真实环境变量交给它;几个测试函数会把模拟数据交给它,确认它会报告原始值、忽略不存在的变量、保留空格和空值含义、以及不检查代理格式。

调用图:被 4 处调用(collect_from_pairs_ignores_absent_values, collect_from_pairs_preserves_whitespace_and_empty_values, collect_from_pairs_reports_raw_values_and_attachment, collect_from_pairs_reports_values_verbatim);外部调用 2 个(into_iter, new)。

FeedbackDiagnostics::is_empty63–65 ↗
fn is_empty(&self) -> bool

作用:判断当前有没有任何诊断信息。调用方可以用它决定要不要显示额外说明,或者要不要附加诊断内容。

数据流:输入是已有的 FeedbackDiagnostics;它查看内部诊断列表是不是空的;输出 true 或 false,不改变任何内容。

调用关系:反馈上传确认参数和连接诊断展示逻辑会调用它。它像一个门卫:如果没有诊断,就让界面少显示一些无用内容;如果有诊断,后续流程再继续展示或处理。

调用图:被 2 处调用(feedback_upload_consent_params, should_show_feedback_connectivity_details)。

FeedbackDiagnostics::diagnostics67–69 ↗
fn diagnostics(&self) -> &[FeedbackDiagnostic]

作用:把内部保存的诊断条目拿出来给别人查看,但不允许别人直接改。适合界面或参数组装代码读取标题和细节。

数据流:输入是一个 FeedbackDiagnostics;它返回内部诊断列表的只读视图;调用方能看见里面的 FeedbackDiagnostic,但不会拿到所有权,也不会改动原数据。

调用关系:反馈上传确认参数生成流程会用它读取具体诊断内容。它不负责格式化成文本,只负责把结构化的数据安全地递出去。

调用图:被 1 处调用(feedback_upload_consent_params)。

FeedbackDiagnostics::attachment_text71–88 ↗
fn attachment_text(&self) -> Option<String>

作用:把诊断信息排版成一段可以放进反馈附件的纯文本。如果没有诊断,就明确返回“没有附件内容”。

数据流:输入是当前诊断集合;如果集合为空,输出 None,表示不用生成文本;如果不为空,它先写标题“Connectivity diagnostics”,再把每条诊断的标题和细节按项目符号排好,最后输出完整字符串。

调用关系:生成反馈诊断附件文本的流程会调用它。它使用前面收集好的结构化诊断,把机器更容易处理的数据变成人更容易阅读、也方便作为附件保存的文本。

调用图:被 1 处调用(feedback_diagnostics_attachment_text);外部调用 2 个(format!, vec!)。

tests::collect_from_pairs_reports_raw_values_and_attachment99–137 ↗
fn collect_from_pairs_reports_raw_values_and_attachment()

作用:这个测试确认:当传入多个代理变量时,代码会把它们原样写进诊断,并能生成预期的附件文本。

数据流:输入是几组模拟代理环境变量,其中一个还带用户名、密码和查询参数;测试调用 collect_from_pairs 得到诊断集合,再调用 attachment_text 得到附件文本;最后用断言检查结果是否和预期完全一致。

调用关系:它直接检验 FeedbackDiagnostics::collect_from_pairs 和附件文本格式是否配合正确。这个测试特别说明代码会报告原始值,而不是清洗、隐藏或改写这些值。

调用图:调用 1 个内部函数(collect_from_pairs);外部调用 1 个(assert_eq!)。

tests::collect_from_pairs_ignores_absent_values140–144 ↗
fn collect_from_pairs_ignores_absent_values()

作用:这个测试确认:没有代理变量时,不应该凭空生成诊断,也不应该生成附件文本。

数据流:输入是一个空的键值对列表;测试调用 collect_from_pairs;输出应当是默认的空 FeedbackDiagnostics,并且 attachment_text 应当返回 None。

调用关系:它保护的是“没问题就别打扰”的行为。collect_from_pairs 如果以后误报了代理诊断,这个测试会失败。

调用图:调用 1 个内部函数(collect_from_pairs);外部调用 2 个(new, assert_eq!)。

tests::collect_from_pairs_preserves_whitespace_and_empty_values147–161 ↗
fn collect_from_pairs_preserves_whitespace_and_empty_values()

作用:这个测试确认:代理值前后的空格会被原样保留下来。代码不会自作主张地修剪用户环境变量。

数据流:输入是一个带前后空格的 HTTP_PROXY 值;测试调用 collect_from_pairs;输出里的细节必须包含那些空格,和输入保持一致。

调用关系:它检验 FeedbackDiagnostics::collect_from_pairs 的“原样记录”原则。这样排查问题时,异常空格本身也能被看见。

调用图:调用 1 个内部函数(collect_from_pairs);外部调用 1 个(assert_eq!)。

tests::collect_from_pairs_reports_values_verbatim164–178 ↗
fn collect_from_pairs_reports_values_verbatim()

作用:这个测试确认:即使代理值看起来不像合法代理地址,也会被原样报告。这个文件只负责记录线索,不负责判断代理配置对不对。

数据流:输入是 HTTP_PROXY = “not a valid proxy”;测试调用 collect_from_pairs;输出的诊断细节必须原封不动包含这段文字。

调用关系:它约束 collect_from_pairs 不要加入格式校验。这样做能避免漏掉可疑配置,把判断权留给看反馈的人或后续排查工具。

调用图:调用 1 个内部函数(collect_from_pairs);外部调用 1 个(assert_eq!)。

app-server/src/request_processors/feedback_doctor_report.rs源码 ↗
domain_logicrequest handling

用户提交反馈时,开发者常常需要知道用户机器上的环境哪里不对。这个文件做的事,就像在寄投诉信时顺手附上一张“体检单”。它会启动当前配置里的 Codex 程序,运行 codex doctor --json,拿到一份 JSON 格式的诊断报告。JSON 可以理解成一种机器和人都比较容易读的结构化文本。为了安全和稳定,它设置了 25 秒超时;程序启动失败、超时、输出不是合法 JSON,都只会记一条警告,然后跳过附件,不影响反馈上传。拿到报告后,它会把报告整理成反馈附件,同时从里面提炼几个 Sentry 标签。Sentry 是用来收集错误和现场信息的服务,这些标签只放状态、数量和检查项名字,方便后台按问题分类查看。

函数细节5
doctor_feedback_report37–98 ↗
async fn doctor_feedback_report(config: &Config) -> Option<DoctorFeedbackReport>

作用:这个函数负责真的去生成反馈用的诊断报告。有人上传反馈时会调用它;如果一切顺利,它返回一个附件和一组简短标签;如果失败,它返回空,让反馈继续走。

数据流:进去的是配置 Config,里面可能写着应该运行哪个 Codex 可执行文件;如果没写,就尝试用当前正在运行的程序。它启动这个程序并传入 doctor --json,最多等 25 秒。然后它读取子程序输出,把第一个 { 之后的内容当作 JSON 来解析。解析成功后,它把 JSON 美化成更好读的字节内容,做成名为诊断报告的反馈附件,同时调用 doctor_report_tags 提炼标签。出来的是 DoctorFeedbackReport;如果启动失败、超时、没有 JSON 或 JSON 坏了,就只写警告并返回 None

调用关系:它是在反馈上传流程里被 upload_feedback_response 调用的一个“可选加料”步骤。它自己不分析每个检查项,而是把解析好的报告交给 doctor_report_tags 做摘要。它还依赖外部库来启动子进程、等待超时、解析 JSON 和记录警告。这样设计的重点是:诊断报告有最好,没有也不能耽误用户反馈。

调用图:调用 1 个内部函数(doctor_report_tags);被 1 处调用(upload_feedback_response);外部调用 6 个(from_utf8_lossy, new, from_str, to_vec_pretty, timeout, warn!)。

doctor_report_tags100–152 ↗
fn doctor_report_tags(report: &Value) -> BTreeMap<String, String>

作用:这个函数把一整份诊断报告压缩成几条适合搜索和分类的标签。它不保存完整细节,只数一数成功、警告、失败各有多少,并记下出问题的检查项。

数据流:进去的是一份 JSON 报告。它先看有没有 overallStatus,有的话放进 doctor_overall_status。然后它读取 checks,通过 check_values 兼容两种报告形状:一种是数组,一种是按名字分组的对象。对每个检查项,它看 statusokwarning 还是 fail,分别计数;警告和失败的检查项还会收集它们的 id。最后它把这些数字和名称放进一个有序的键值表里;太长的值会交给 truncate_tag_value 截短。

调用关系:它被 doctor_feedback_report 用来给反馈附件配套生成 Sentry 标签,也被测试函数 tests::doctor_report_tags_summarize_status_counts 单独验证。它在流程中像“摘要员”:完整报告仍然作为附件上传,但后台排查问题时先看它生成的短标签。

调用图:调用 2 个内部函数(check_values, truncate_tag_value);被 2 处调用(doctor_feedback_report, doctor_report_tags_summarize_status_counts);外部调用 3 个(new, get, new)。

check_values155–161 ↗
fn check_values(checks: &Value) -> Box<dyn Iterator<Item = &Value> + '_>

作用:这个函数负责从报告的 checks 字段里取出一个个检查项。它的作用是兼容新旧两种报告格式,避免格式稍有变化就读不出来。

数据流:进去的是 JSON 里的 checks 值。如果它是数组,就逐个返回数组里的元素;如果它是对象,就逐个返回对象里的值;如果既不是数组也不是对象,就返回一个空的迭代器。出来的是一串可被循环读取的检查项,它不修改原始报告。

调用关系:它只被 doctor_report_tags 调用。doctor_report_tags 不需要关心报告到底是“列表式”还是“字典式”,只管循环这些检查项并统计状态;这个函数就是中间的适配器。

调用图:被 1 处调用(doctor_report_tags);外部调用 2 个(new, empty)。

truncate_tag_value163–172 ↗
fn truncate_tag_value(value: &str) -> String

作用:这个函数把过长的标签值剪短。这样做是为了避免把太长的字符串塞进 Sentry 标签里,影响存储、搜索或显示。

数据流:进去的是一个字符串。如果它的字符数不超过限制,就原样返回。如果太长,它保留前面一部分,再在末尾加上 ...,表示后面被省略了。出来的是一个长度受控的新字符串,原字符串不会被改动。

调用关系:它被 doctor_report_tags 用在整体状态、失败检查项列表、警告检查项列表这些可能变长的值上。它像给标签加了一个“限高杆”,保证传给 Sentry 的内容不会无限变长。

调用图:被 1 处调用(doctor_report_tags);外部调用 1 个(format!)。

tests::doctor_report_tags_summarize_status_counts181–211 ↗
fn doctor_report_tags_summarize_status_counts()

作用:这是一个测试,专门确认 doctor_report_tags 能正确统计诊断报告里的状态。它用一份假的报告检查输出标签是不是符合预期。

数据流:进去的是测试里现场构造的一份 JSON:一个整体失败状态,三个检查项分别是成功、警告和失败。测试把这份报告传给 doctor_report_tags,得到实际标签。然后它构造一份期望标签表,并用断言比较两者。结果是:如果完全一样,测试通过;如果有任何统计或字段名不对,测试失败。

调用关系:它只在测试运行时活跃,不参与真实反馈上传。它直接调用 doctor_report_tags,用来保护核心摘要逻辑,防止以后改代码时不小心把计数、检查项名称或标签键弄错。

调用图:调用 1 个内部函数(doctor_report_tags);外部调用 3 个(from, assert_eq!, json!)。

安全调试提取

这些工具会清理敏感值并提取响应元数据,使后续调试产物和遥测既有用又不会泄露密钥。

response-debug-context/src/lib.rs源码 ↗
utilcross-cutting error handling and telemetry

当程序调用远端 API 失败时,最怕两件事:一是日志里信息太少,排查不了;二是日志把敏感内容原样记下来了。这个文件就在两者之间做平衡。它定义了 ResponseDebugContext,像一张小纸条,只记录请求编号、Cloudflare 的追踪编号、授权错误、授权错误码这些安全又有用的线索。它会从 HTTP 响应头里取这些信息;如果错误不是 HTTP 返回,就给一张空纸条。它还提供两类简短错误消息:传输层错误,比如超时、网络失败、HTTP 401;API 层错误,比如额度用完、限流、请求不合法。重要的是,HTTP 错误只记录状态码,不记录响应正文。可以把它理解成:报警时只报“哪扇门打不开”和“门牌号”,不把门后房间里的私人物品拍进日志。

函数细节7
extract_response_debug_context19–54 ↗
fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext

作用:这个函数从一次传输错误里提取排查用的线索,比如请求 ID、Cloudflare 追踪号、授权失败原因。有人查看日志或上报遥测时,可以用这些线索去服务端定位同一次请求。

数据流:进去的是一个 TransportError,也就是“请求在网络或 HTTP 层出问题”的错误。它先准备一个空的 ResponseDebugContext;如果这个错误不是 HTTP 返回,就直接把空结果交出去。若是 HTTP 错误,它读取响应头,找 x-request-id 或 x-oai-request-id 作为请求编号,找 cf-ray 作为边缘网络追踪号,找 x-openai-authorization-error 作为授权错误。它还会读取 x-error-json,这个头里放的是 Base64(一种把二进制内容变成普通文本的编码)后的 JSON,再解码并解析出 error.code。最后出来的是填好字段的 ResponseDebugContext,原错误本身不被改动。

调用关系:它是这个文件里提取调试上下文的核心小工具。extract_response_debug_context_from_api_error 遇到 API 错误里包着传输错误时,会把活交给它。测试 tests::extract_response_debug_context_decodes_identity_headers 会构造一组响应头来确认它确实能把这些线索取出来。

调用图:被 2 处调用(extract_response_debug_context_from_api_error, extract_response_debug_context_decodes_identity_headers);外部调用 1 个(default)。

extract_response_debug_context_from_api_error56–61 ↗
fn extract_response_debug_context_from_api_error(error: &ApiError) -> ResponseDebugContext

作用:这个函数把更外层的 ApiError 转成可排查的 ResponseDebugContext。调用者不用自己判断错误里面是不是藏着 HTTP/网络错误,直接把 ApiError 交给它即可。

数据流:进去的是一个 ApiError,也就是 API 调用失败的总错误类型。它检查这个错误是不是 Transport 类型;如果是,就把里面的 TransportError 交给 extract_response_debug_context 继续提取响应头线索。如果不是传输错误,比如额度用完或请求不合法,它就返回一个空的 ResponseDebugContext。它不会改动原错误,只产出一份调试信息摘要。

调用关系:它站在更外层,方便 API 调用失败后的统一处理。真正读取 HTTP 响应头的工作不是它做,而是交给 extract_response_debug_context;没有可读响应头时,它用默认空结果收尾。

调用图:调用 1 个内部函数(extract_response_debug_context);外部调用 1 个(default)。

telemetry_transport_error_message63–71 ↗
fn telemetry_transport_error_message(error: &TransportError) -> String

作用:这个函数把传输层错误变成一条适合写进遥测的短消息。它的重点是:HTTP 错误只写状态码,不把响应正文写进去,避免泄露敏感信息。

数据流:进去的是 TransportError。它按错误种类改写成短文本:HTTP 错误变成类似“http 401”;重试次数用完变成“retry limit reached”;超时变成“timeout”;网络错误和构建请求错误则保留原本的错误说明。出来的是一个 String 字符串。它只读错误内容,不修改任何状态。

调用关系:它是传输错误消息的统一出口。telemetry_api_error_message 遇到 ApiError::Transport 时,会调用它来生成最终遥测文字,这样 HTTP 正文不会被上层不小心记进日志。

调用图:被 1 处调用(telemetry_api_error_message);外部调用 1 个(format!)。

telemetry_api_error_message73–87 ↗
fn telemetry_api_error_message(error: &ApiError) -> String

作用:这个函数把 API 层的各种失败整理成一条短而安全的遥测消息。它让监控系统知道大概发生了什么,比如限流、额度不足、服务过载,而不需要暴露详细响应正文。

数据流:进去的是 ApiError。它先看错误种类:如果里面是 TransportError,就交给 telemetry_transport_error_message;如果是 API 返回的状态码,就生成类似“api error 400”;如果是流中断、上下文超长、额度用完、限流、无效请求等,就变成固定短语。出来的是一个 String。过程中不会改动错误对象,也不会读取或输出 HTTP 响应正文。

调用关系:它是 API 错误进入遥测系统前的总整理口。它会复用 telemetry_transport_error_message 来处理更底层的传输失败,自己则覆盖那些只有 API 层才知道的错误类型。

调用图:调用 1 个内部函数(telemetry_transport_error_message);外部调用 1 个(format!)。

tests::extract_response_debug_context_decodes_identity_headers103–132 ↗
fn extract_response_debug_context_decodes_identity_headers()

作用:这个测试确认:当 HTTP 响应头里带着请求编号、追踪号和授权错误信息时,提取函数能正确读出来。它还确认 Base64 编码的错误 JSON 能被解码成具体错误码。

数据流:进去的是测试里手工造出来的一组 HTTP 头和一个 401 未授权错误。测试把这些头塞进 TransportError::Http,再调用 extract_response_debug_context。出来的是一个 ResponseDebugContext;测试用断言检查它的 request_id、cf_ray、auth_error、auth_error_code 都和预期一致。

调用关系:它在测试运行时由测试框架调用。它直接覆盖 extract_response_debug_context 这条主路径,证明响应头提取和 x-error-json 解码这两个关键行为没有坏。

调用图:调用 1 个内部函数(extract_response_debug_context);外部调用 3 个(new, from_static, assert_eq!)。

tests::telemetry_error_messages_omit_http_bodies135–148 ↗
fn telemetry_error_messages_omit_http_bodies()

作用:这个测试确认:HTTP 错误转成遥测消息时,不会把响应正文带出来。这样即使正文里有“secret token leaked”这类敏感字样,也不会进日志。

数据流:进去的是测试手工构造的 HTTP 401 传输错误,其中 body 字段故意放了看起来像泄密的文本。测试分别调用遥测消息函数,并检查结果只包含“http 401”。出来的结果是断言通过或失败;它不改动生产代码状态。

调用关系:它由测试框架在测试阶段运行,用来守住安全边界。它验证 telemetry_transport_error_message 的行为,也验证 telemetry_api_error_message 在包了一层 ApiError::Transport 后仍然不会泄露 HTTP 正文。

调用图:外部调用 1 个(assert_eq!)。

tests::telemetry_error_messages_preserve_non_http_details151–165 ↗
fn telemetry_error_messages_preserve_non_http_details()

作用:这个测试确认:不是 HTTP 响应的错误,比如 DNS 查询失败、请求头格式错误、连接流关闭,仍然会保留有用的具体说明。也就是说,文件不是一味删信息,而是只避开容易泄露内容的 HTTP 正文。

数据流:进去的是测试构造的三种错误:网络错误、构建请求错误、流错误。测试把它们交给对应的遥测消息函数,然后用断言检查输出仍是原本那句具体错误说明。出来的是测试结果;生产数据不会被修改。

调用关系:它由测试框架调用,补上另一半安全策略:HTTP 正文要省略,但普通错误说明要保留。它间接保证 telemetry_transport_error_message 和 telemetry_api_error_message 在非 HTTP 场景下仍然对排查问题有帮助。

调用图:外部调用 4 个(assert_eq!, Stream, Build, Network)。

secrets/src/sanitizer.rs源码 ↗
utilcross-cutting

这个文件像一个“打码器”。它接收一段普通文字,检查里面有没有常见的秘密信息,比如 OpenAI 风格的 key、AWS 访问 key、Bearer token(一种常见的“拿着就能访问服务”的身份令牌),以及类似 password=xxx、api_key: xxx 这样的配置写法。发现后,它不会试图理解这些秘密具体属于谁,而是按一组常见规律去匹配,然后把敏感部分替换成 [REDACTED_SECRET]。这里用到了正则表达式,也就是“按文本规律找东西”的规则。正则通过 LazyLock 懒加载:程序启动时不急着创建,第一次用到时才编译好,之后重复使用,避免每次都重新准备。它不是百分百万能保险箱,只是“尽力而为”的防泄漏工具,但对日志和输出清洗很关键。

函数细节3
redact_secrets15–22 ↗
fn redact_secrets(input: String) -> String

作用:把输入文字里看起来像密钥、密码或访问令牌的内容打码。别人会在打印日志、显示错误、保存输出前用它,减少把秘密信息暴露出去的风险。

数据流:进去的是一整段字符串。它依次用几条预先准备好的文本匹配规则扫描:先找 OpenAI key,再找 AWS access key,再找 Bearer token,最后找 api_key、token、secret、password 这类赋值写法;每找到一类,就把敏感部分换成固定的 [REDACTED_SECRET]。出来的是一段新的字符串,原字符串本身不被改动,但返回内容已经尽量去掉了秘密。

调用关系:这是这个文件对外最重要的入口。测试函数 tests::load_regex 会调用它,目的不是检查打码效果,而是顺便触发所有正则表达式的加载,确认这些规则都能正常编译。它自己依赖文件顶部那些通过 compile_regex 准备好的规则来干活。

调用图:被 1 处调用(load_regex)。

compile_regex24–30 ↗
fn compile_regex(pattern: &str) -> Regex

作用:把一条正则表达式规则变成程序可以反复使用的匹配器。这样其他地方不用每次都重新解释文本规则。

数据流:进去的是一段规则文本,比如“以 AKIA 开头后面跟 16 个大写字母或数字”。它调用 Regex::new 尝试编译这条规则;如果成功,就返回可用的 Regex 对象;如果规则本身写错了,就直接 panic,也就是让程序立刻报错停止,因为这是代码里的固定规则,写错代表程序员需要修。

调用关系:它在每个静态正则第一次被使用时自动参与工作。redact_secrets 使用那些静态规则时,会间接触发它。tests::load_regex 的存在就是为了尽早发现这里是否会因为规则写错而 panic。

调用图:外部调用 2 个(new, panic!)。

tests::load_regex37–40 ↗
fn load_regex()

作用:这是一个测试,用来确认文件里所有正则表达式都能成功加载。它不关心打码结果,只关心这些“找秘密的规则”有没有写坏。

数据流:进去的是测试里写死的一小段字符串 secret。它调用 redact_secrets,这会迫使所有懒加载的正则表达式被创建;如果其中任何一条规则无效,compile_regex 会让测试失败。正常出来时没有特别返回值,只表示规则都能加载。

调用关系:它只在运行测试时出现。它调用 redact_secrets,而 redact_secrets 又会使用那些静态正则;这样测试就覆盖到了 compile_regex 可能出错的地方,避免坏规则悄悄留到正式运行时才爆炸。

调用图:调用 1 个内部函数(redact_secrets)。

请求与 payload 转储

这些组件将代理流量和分析 payload 的临时调试捕获保存到本地文件,便于之后检查。

responses-api-proxy/src/dump.rs源码 ↗
io_transportrequest handling / cross-cutting diagnostics

这个文件像一个“行车记录仪”。代理每收到一次请求,就先把请求方法、网址、头信息和正文写到一个 request.json 文件里;等上游服务返回响应时,再把状态码、响应头和响应正文写到对应的 response.json 文件里。文件名里有递增编号和时间戳,所以多次请求不会互相覆盖。正文如果本来是 JSON,就按 JSON 保存;如果不是,就当普通文字保存。响应正文比较特殊:它可能是一边读一边转发的流,所以这里用 ResponseBodyDump 包了一层,读多少就偷偷复制多少,等读完或对象被丢弃时再写文件。最重要的安全点是 HeaderDump:凡是 authorization 或名字里带 cookie 的头,都会变成 [REDACTED],也就是“已遮盖”。

函数细节14
ExchangeDumper::new25–32 ↗
fn new(dump_dir: PathBuf) -> io::Result<Self>

作用:创建一个“转储器”,也就是准备好一个专门存放请求和响应记录的文件夹。没有它,后面想写调试文件时可能会因为目录不存在而失败。

数据流:进去的是一个目录路径 → 它先在磁盘上创建这个目录以及缺少的父目录,然后把路径保存起来,并把请求编号从 1 开始准备好 → 出来的是一个可用的 ExchangeDumper,之后每次请求都能用它生成一组记录文件。

调用关系:测试里的 dump_request_writes_redacted_headers_and_json_body 和 response_body_dump_streams_body_and_writes_response_file 会先调用它搭好临时目录。它内部把创建目录的活儿交给系统文件函数 create_dir_all。

调用图:被 2 处调用(dump_request_writes_redacted_headers_and_json_body, response_body_dump_streams_body_and_writes_response_file);外部调用 2 个(new, create_dir_all)。

ExchangeDumper::dump_request34–60 ↗
fn dump_request(
        &self,
        method: &Method,
        url: &str,
        headers: &[Header],
        body: &[u8],
    ) -> io::Result<ExchangeDump>

作用:把一次进入代理的请求写成 JSON 文件,并预留好对应响应文件的位置。调用者拿到返回值后,就知道这次响应该写到哪里。

数据流:进去的是请求方法、网址、请求头和请求正文 → 它拿一个递增编号和当前时间拼出文件名前缀,生成 request.json 和 response.json 两个路径;然后把请求头转成可保存格式、把敏感头打码、把正文转成 JSON 或字符串,再写入请求文件 → 出来的是 ExchangeDump,里面记着响应文件路径。

调用关系:这是一次交换记录的起点。它会调用 dump_body 处理正文,调用 write_json_dump 真正落盘写文件,也会用 HeaderDump::from 间接处理每个请求头。后续 ExchangeDump::tee_response_body 会接着记录响应。

调用图:调用 2 个内部函数(dump_body, write_json_dump);外部调用 6 个(fetch_add, iter, as_str, join, now, format!)。

ExchangeDump::tee_response_body68–82 ↗
fn tee_response_body(
        self,
        status: u16,
        headers: &HeaderMap,
        response_body: R,
    ) -> ResponseBodyDump<R>

作用:把响应正文包装成一个“边读边复制”的读取器。这样代理正常把响应转给客户端时,也能顺手留一份用于写日志。

数据流:进去的是响应状态码、响应头和原始响应正文读取器 → 它把状态码和头信息存起来,把响应文件路径带上,并准备一个空缓冲区用来累计读到的正文 → 出来的是 ResponseBodyDump,外表仍然像普通读取器,但会偷偷记录读过的内容。

调用关系:它接在 ExchangeDumper::dump_request 之后使用。它不直接写文件,而是创建 ResponseBodyDump;真正写响应 JSON 的动作会在 ResponseBodyDump::read 读到结尾时,或 ResponseBodyDump::drop 被销毁时发生。

调用图:外部调用 2 个(iter, new)。

ResponseBodyDump::write_dump_if_needed95–114 ↗
fn write_dump_if_needed(&mut self)

作用:把已经收集到的响应内容写进 response.json,并保证最多只写一次。这个保护很重要,因为读到结尾和对象销毁都可能触发写文件。

数据流:进去的是 ResponseBodyDump 自己保存的状态码、响应头、已读正文和目标路径 → 如果已经写过,它什么也不做;如果没写过,它先标记为已写,再把响应组装成 ResponseDump,正文经过 dump_body 转换,最后调用 write_json_dump 写到磁盘;如果写失败,只在标准错误里打印提醒 → 出来没有正常返回值,但磁盘上会多出响应记录文件,内部的头信息会被取走。

调用关系:它是响应记录真正落盘的核心。ResponseBodyDump::read 在读到正文结束时会调用它,ResponseBodyDump::drop 在对象被清理时也会调用它,确保即使调用者没有把流读到结尾,也尽量留下记录。

调用图:调用 2 个内部函数(dump_body, write_json_dump);被 2 处调用(drop, read);外部调用 2 个(eprintln!, take)。

ResponseBodyDump::read118–127 ↗
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>

作用:让 ResponseBodyDump 表现得像普通的读取器,同时把读到的响应正文复制一份。调用者用它读数据时,业务转发不受影响,记录也自动完成。

数据流:进去的是调用者提供的一块缓冲区 → 它先从真正的响应正文里读取数据;如果读到 0 字节,说明正文结束,就调用 write_dump_if_needed 写响应文件;如果读到了数据,就把这段数据追加到内部 body 缓冲区 → 出来的是本次读到的字节数,调用者继续像读普通流一样使用。

调用关系:它实现了 Rust 的 Read 接口,也就是“可读取对象”的标准约定。它把实际读取交给内部 response_body,把收尾写文件交给 ResponseBodyDump::write_dump_if_needed。测试 response_body_dump_streams_body_and_writes_response_file 会通过读取字符串来验证它。

调用图:调用 1 个内部函数(write_dump_if_needed);外部调用 1 个(read)。

ResponseBodyDump::drop131–133 ↗
fn drop(&mut self)

作用:在 ResponseBodyDump 被销毁时兜底写响应文件。这样即使外部没有完整读完响应,也不会完全丢掉已经收集到的记录。

数据流:进去的是即将被释放的 ResponseBodyDump → 它调用 write_dump_if_needed 检查是否已经写过;没写过就把当前已收集的响应内容写出,写过就不重复写 → 出来没有返回值,但可能完成一次兜底落盘。

调用关系:它是 Rust 的 Drop 钩子,也就是对象离开作用域时自动执行的清理动作。它和 ResponseBodyDump::read 共同保证响应文件尽可能被写出来,并由 write_dump_if_needed 统一控制“只写一次”。

调用图:调用 1 个内部函数(write_dump_if_needed)。

HeaderDump::from171–183 ↗
fn from(header: (&reqwest::header::HeaderName, &reqwest::header::HeaderValue)) -> Self

作用:把不同来源的 HTTP 头转换成可以写进 JSON 的简单名字和值。转换时会主动遮盖 Authorization 和 Cookie,避免把秘密写进文件。

数据流:进去的是一个请求头或响应头 → 它取出头名,先问 should_redact_header 这个头该不该打码;如果该打码,值变成 [REDACTED],否则把原始字节尽量按文字转出来 → 出来的是 HeaderDump,里面只有 name 和 value 两个字符串。

调用关系:ExchangeDumper::dump_request 和 ExchangeDump::tee_response_body 在整理请求头、响应头时会通过迭代调用它。它把“是否敏感”的判断交给 should_redact_header,把二进制头值转文字时会用宽容的 UTF-8 转换。

调用图:调用 1 个内部函数(should_redact_header);外部调用 1 个(from_utf8_lossy)。

should_redact_header186–189 ↗
fn should_redact_header(name: &str) -> bool

作用:判断一个 HTTP 头是不是应该隐藏值。它专门保护 authorization 和各种 cookie 相关头,因为这些通常包含令牌、登录态或会话信息。

数据流:进去的是头名字符串 → 它不区分大小写地检查是否等于 authorization,或者小写后是否包含 cookie → 出来是 true 或 false,true 表示保存时必须打码。

调用关系:它被 HeaderDump::from 调用,是所有头信息脱敏规则的集中入口。这样请求头和响应头使用同一套安全判断,不会一边打码一边漏掉。

调用图:被 1 处调用(from)。

dump_body191–194 ↗
fn dump_body(body: &[u8]) -> Value

作用:把请求或响应正文变成适合写进 JSON 文件的值。它优先保留真正的 JSON 结构,不是 JSON 时就保存成普通字符串。

数据流:进去的是一段原始字节 → 它先尝试用 serde_json 解析成 JSON;如果解析成功,就输出对象、数组、数字等原样结构;如果失败,就按 UTF-8 文本尽量转换,输出一个字符串 → 出来的是 serde_json::Value,也就是可写进 JSON 文件的通用值。

调用关系:ExchangeDumper::dump_request 用它处理请求正文,ResponseBodyDump::write_dump_if_needed 用它处理响应正文。它让 dump 文件既能好看地展示 JSON 请求,也能记录事件流、纯文本等非 JSON 内容。

调用图:被 2 处调用(dump_request, write_dump_if_needed);外部调用 1 个(from_slice)。

write_json_dump196–201 ↗
fn write_json_dump(path: &PathBuf, dump: &impl Serialize) -> io::Result<()>

作用:把一个可序列化的数据结构漂亮地写成 JSON 文件。这里的“漂亮”指带缩进,方便人直接打开阅读。

数据流:进去的是目标文件路径和要保存的数据 → 它先把数据转成格式化后的 JSON 字节;如果转换失败,就包装成输入输出错误;然后在末尾加一个换行,再写到磁盘 → 出来是成功或失败的 io::Result,成功时文件内容已经落盘。

调用关系:ExchangeDumper::dump_request 用它写 request.json,ResponseBodyDump::write_dump_if_needed 用它写 response.json。它把“怎么把结构体变成 JSON 文件”的细节集中在一个地方。

调用图:被 2 处调用(dump_request, write_dump_if_needed);外部调用 2 个(write, to_vec_pretty)。

tests::dump_request_writes_redacted_headers_and_json_body225–300 ↗
fn dump_request_writes_redacted_headers_and_json_body()

作用:测试请求记录是否写得正确,尤其是敏感请求头会不会被打码、JSON 正文会不会保持 JSON 结构。

数据流:进去的是测试自己构造的一组请求头和 JSON 请求体 → 它创建临时目录和 ExchangeDumper,调用 dump_request 写文件,再读回生成的 request.json,与期望的 JSON 内容逐项比较 → 出来是测试通过或失败,最后删除临时目录。

调用关系:它先用 tests::test_dump_dir 准备目录,再调用 ExchangeDumper::new 和 ExchangeDumper::dump_request,随后通过 tests::dump_file_with_suffix 找到生成文件。它验证的是请求侧记录链路。

调用图:调用 1 个内部函数(new);外部调用 7 个(assert!, assert_eq!, read_to_string, remove_dir_all, dump_file_with_suffix, test_dump_dir, vec!)。

tests::response_body_dump_streams_body_and_writes_response_file303–355 ↗
fn response_body_dump_streams_body_and_writes_response_file()

作用:测试响应正文在正常读取时既能原样传出去,又能被记录到 response.json。它也检查响应头里的 authorization 和 set-cookie 会被打码。

数据流:进去的是测试构造的响应状态码、响应头和一段模拟响应正文 → 它先写一个请求记录拿到 ExchangeDump,再用 tee_response_body 包住响应正文,把内容读成字符串;之后读回 response.json,检查状态码、头、正文都符合预期 → 出来是测试通过或失败,最后清理临时目录。

调用关系:它覆盖响应侧的完整流程:ExchangeDumper::new 创建转储器,dump_request 建立配对文件名,ExchangeDump::tee_response_body 创建包装读取器,读取动作触发 ResponseBodyDump::read 和最终写文件。

调用图:调用 2 个内部函数(new, new);外部调用 8 个(new, from_static, new, assert_eq!, read_to_string, remove_dir_all, dump_file_with_suffix, test_dump_dir)。

tests::test_dump_dir357–365 ↗
fn test_dump_dir() -> std::path::PathBuf

作用:给每个测试创建一个独立的临时目录,避免多个测试写到同一个地方互相干扰。

数据流:进去没有业务参数,只读取进程号、系统临时目录和一个递增测试编号 → 它拼出一个唯一目录名,并在磁盘上创建目录 → 出来的是这个临时目录路径。

调用关系:两个测试函数都会先调用它准备安全的测试环境。它内部用原子计数器生成不同编号,用 create_dir_all 真正创建目录。

调用图:外部调用 3 个(format!, create_dir_all, temp_dir)。

tests::dump_file_with_suffix367–377 ↗
fn dump_file_with_suffix(dump_dir: &std::path::Path, suffix: &str) -> std::path::PathBuf

作用:在测试目录里找到唯一一个以指定后缀结尾的 dump 文件,比如 -request.json 或 -response.json。

数据流:进去的是测试目录和文件名后缀 → 它读取目录下所有文件,筛选出路径字符串以该后缀结尾的文件,排序后确认刚好只有一个 → 出来的是那个文件路径;如果不是正好一个,测试会失败。

调用关系:测试函数写完 dump 文件后用它定位实际生成的文件。它服务于断言阶段,让测试不用关心文件名前面的序号和时间戳。

调用图:外部调用 2 个(assert_eq!, read_dir)。

analytics/src/analytics_capture.rs源码 ↗
io_transportstartup 和 request handling

这个文件做的事很像“把每张出库单复印一份放进抽屉”。分析事件本来会被组装成 TrackEventsRequest,也就是“一批要追踪的事件请求”。这里会把这批请求转成 JSON(一种常见的文本数据格式,方便人和程序都读取),然后一行一条追加到指定文件。文件路径通常由 CODEX_ANALYTICS_EVENTS_CAPTURE_FILE 这个环境变量指定。initialize 只负责提前确认文件能打开或创建;append_payload 真正写入内容;open_capture_file 统一处理“创建、追加、打开文件”的细节。在 Unix 系统上,它还会把文件权限设成 0o600,也就是只有当前用户能读写,避免分析数据被其他用户随便看到。

函数细节3
initialize11–13 ↗
fn initialize(path: &Path) -> io::Result<()>

作用:这个函数用来在正式记录事件前,先确认捕获文件能不能打开或创建。它不写入事件内容,只做一次“试开门”,早点发现路径或权限问题。

数据流:进去的是一个文件路径。它把路径交给 open_capture_file 去打开或创建文件;如果成功,就立刻丢掉这个文件句柄,不留下内容变化;如果失败,就把操作系统给出的错误返回给调用者。

调用关系:它通常由 from_base_url_and_capture_file 在配置分析功能时调用,相当于启动阶段的预检查。真正打开文件的细节不在它这里做,而是交给 open_capture_file,保证初始化和后续写入使用同一套开文件规则。

调用图:调用 1 个内部函数(open_capture_file);被 1 处调用(from_base_url_and_capture_file)。

append_payload15–23 ↗
fn append_payload(path: &Path, payload: &TrackEventsRequest) -> io::Result<()>

作用:这个函数把一批分析事件写到捕获文件里。别人想把“准备上报的事件请求”保存下来时,就会用它。

数据流:进去的是文件路径和一个 TrackEventsRequest。它先用 serde_json::to_vec 把请求变成 JSON 字节;如果转换失败,就包装成输入数据错误。然后它在末尾加一个换行符,打开捕获文件,把这一整行追加进去,最后 flush,也就是强制把缓冲里的内容尽快写到文件系统。

调用关系:它由 capture_track_events_request 在捕获分析请求时调用,是实际落盘的主动作。它先请序列化工具把事件变成可保存的文本,再请 open_capture_file 打开目标文件,最后自己负责写入和刷新。

调用图:调用 1 个内部函数(open_capture_file);被 1 处调用(capture_track_events_request);外部调用 1 个(to_vec)。

open_capture_file25–34 ↗
fn open_capture_file(path: &Path) -> io::Result<File>

作用:这个函数统一规定捕获文件该怎么打开:没有就创建,有了就追加写,不覆盖旧内容。这样初始化和真正写事件时不会各自用一套不同规则。

数据流:进去的是一个文件路径。它创建一组打开文件的选项,设置为“可创建、追加写入”;在 Unix 系统上,还设置权限为 0o600,限制只有当前用户可读写。最后它尝试打开文件,成功就返回文件对象,失败就返回错误。

调用关系:initialize 和 append_payload 都依赖它。前者用它检查文件是否可用,后者用它拿到真正能写的文件。它内部使用 OpenOptions::new 这类标准库能力来搭好打开文件的方式。

调用图:被 2 处调用(append_payload, initialize);外部调用 1 个(new)。

持久日志捕获

这些文件提供本地日志后端,涵盖从 TUI 会话 JSONL 捕获,到 tracing-to-database 摄取和持久运行时日志存储。

tui/src/session_log.rs源码 ↗
io_transportstartup, main loop, teardown

终端界面运行时会不断收到事件,也会向后台发命令。出了问题时,光靠肉眼很难知道当时到底发生了什么。这个文件就是为了解决这个问题:如果环境变量打开了记录功能,它会创建一个会话日志文件,把“什么时候、谁发给谁、是什么事”写进去。日志格式是 JSON Lines,也就是每行一个 JSON,方便人看,也方便工具逐行分析。它用一个全局的 SessionLogger 保存日志文件;OnceLock(一旦设置就不再换的盒子)保证文件只打开一次,Mutex(互斥锁,一把锁,防止同时写乱)保证多处写日志时不会把内容搅在一起。它会记录会话开始、界面收到的事件、界面发出的命令、会话结束。有些事件只记摘要,比如搜索结果数量,而不是把所有细节都塞进去。

函数细节10
SessionLogger::new23–27 ↗
fn new() -> Self

作用:创建一个还没有打开文件的会话日志器。它像先准备好一本空日志本的外壳,真正的纸本文件要等后面启用记录时才放进去。

数据流:进去没有额外输入 → 它新建一个空的 OnceLock,用来以后保存日志文件和写入锁 → 出来一个 SessionLogger,此时还不能写日志,因为文件还没打开。

调用关系:全局 LOGGER 第一次被用到时会靠它生成日志器。后面的 SessionLogger::open 会把实际文件放进这个日志器里,SessionLogger::write_json_line 才能开始写内容。

调用图:外部调用 1 个(new)。

SessionLogger::open29–46 ↗
fn open(&self, path: PathBuf) -> std::io::Result<()>

作用:打开或创建真正的会话日志文件,并把它接到日志器上。没有这一步,其他记录函数都会安静地不做事。

数据流:进去一个文件路径 → 它准备写入选项:需要时创建文件、每次打开都清空旧内容、只写入;如果父目录不存在就先创建;在 Unix 系统上把权限设成 0600,也就是只有当前用户能读写 → 成功后把文件放进 OnceLock 里的 Mutex,出来是成功或失败结果,同时日志器从“未启用”变成“可写”。

调用关系:maybe_init 在确认环境变量允许记录后会调用它。它完成后,maybe_init 会马上写一条 session_start 头记录;如果打开失败,后续日志不会启动。

调用图:外部调用 4 个(get_or_init, new, parent, create_dir_all)。

SessionLogger::write_json_line48–72 ↗
fn write_json_line(&self, value: serde_json::Value)

作用:把一条 JSON 记录写进日志文件,并在末尾加换行。它是所有具体日志事件最后都会走到的“落笔”步骤。

数据流:进去一个 serde_json::Value,也就是一条已经拼好的 JSON 数据 → 它先看日志文件是否已经打开,没打开就直接返回;打开了就拿互斥锁,防止并发写乱;再把 JSON 转成字符串,写入文件、写入换行、立刻 flush(冲刷到文件,减少崩溃时丢日志的机会)→ 出来没有返回值,但文件里多了一行记录;如果序列化或写入失败,只写警告,不让主程序崩掉。

调用关系:maybe_init、log_inbound_app_event、log_session_end 和 write_record 都会把准备好的记录交给它。它是这个文件里所有日志最终进入磁盘的统一出口。

调用图:外部调用 3 个(get, to_string, warn!)。

SessionLogger::is_enabled74–76 ↗
fn is_enabled(&self) -> bool

作用:判断会话日志现在有没有真正启用。调用者用它避免在未开启记录时白白拼日志内容。

数据流:进去没有额外输入 → 它检查保存文件的 OnceLock 里是否已经有文件 → 出来 true 或 false;不会改动任何东西。

调用关系:log_inbound_app_event、log_outbound_op 和 log_session_end 在写日志前都会先问它一句。这样没有调用 maybe_init 成功打开文件时,记录函数会快速退出。

调用图:外部调用 1 个(get)。

now_ts79–82 ↗
fn now_ts() -> String

作用:生成当前时间戳,给每条日志标上“发生在什么时候”。时间用 RFC3339 这种常见格式,方便人读,也方便程序解析。

数据流:进去没有输入 → 它读取当前 UTC 时间,并格式化到毫秒级 → 出来一个字符串,比如带 Z 结尾的标准时间。

调用关系:maybe_init、log_inbound_app_event、log_session_end 和 write_record 在组装日志 JSON 时都会用它。它让不同种类的记录有统一的时间格式。

调用图:外部调用 1 个(now)。

maybe_init84–120 ↗
fn maybe_init(config: &Config)

作用:在程序启动时决定要不要打开会话日志。它看环境变量开关,如果用户没要求记录,就什么也不做。

数据流:进去一个 Config,里面有日志目录、当前工作目录、模型信息等上下文 → 它读取 CODEX_TUI_RECORD_SESSION,只有值像 1、true、yes 时才启用;再读取可选的 CODEX_TUI_SESSION_LOG_PATH,如果没给路径,就在配置的日志目录下生成 session-时间.jsonl;然后尝试打开文件;成功后写入一条 session_start,记录当前目录和模型信息 → 出来没有返回值,但可能创建了日志文件并写入第一行;失败时只打错误日志,不中断应用。

调用关系:run_ratatui_app 在启动终端应用时调用它。它是整个会话记录功能的入口:只有它成功打开文件,后面的入站事件、出站命令和结束记录才会真的写入。

调用图:被 1 处调用(run_ratatui_app);外部调用 5 个(from, format!, json!, var, error!)。

log_inbound_app_event122–211 ↗
fn log_inbound_app_event(event: &AppEvent)

作用:记录“外部或后台发给终端界面”的事件。它帮开发者复盘界面为什么变了、什么时候收到搜索结果或新会话通知。

数据流:进去一个 AppEvent → 它先检查日志是否启用;启用后根据事件种类拼一条 JSON:新会话、清屏、插入历史、开始文件搜索、文件搜索结果、宠物预览或选择加载等会写更清楚的字段;其他比较杂的事件只记事件变体名,避免日志太吵或太大 → 出来没有返回值,但日志文件可能多一行“to_tui”方向的记录。

调用关系:send 在把事件送进终端界面时会调用它。它不负责处理事件本身,只是在事件进入界面前后留下可追踪的脚印,然后把最终写文件的工作交给 SessionLogger::write_json_line。

调用图:被 1 处调用(send);外部调用 1 个(json!)。

log_outbound_op213–218 ↗
fn log_outbound_op(op: &AppCommand)

作用:记录“终端界面发出去”的命令。这样以后能看到用户操作或界面动作具体让后台做了什么。

数据流:进去一个 AppCommand → 它先确认日志已启用;如果启用,就把方向写成 from_tui、类型写成 op,并把整个命令作为 payload 交给 write_record → 出来没有返回值,但日志文件可能多一行出站命令记录。

调用关系:submit_thread_op 和 submit_op 在提交命令时会调用它。它本身只做开关检查和分类,真正组装通用 JSON 记录的活交给 write_record。

调用图:调用 1 个内部函数(write_record);被 2 处调用(submit_thread_op, submit_op)。

log_session_end220–230 ↗
fn log_session_end()

作用:在会话结束时写一条收尾记录。它像在日志本最后盖个章,说明这次记录不是突然断掉,而是正常结束。

数据流:进去没有额外输入 → 它先检查日志是否启用;启用后拼出包含当前时间、meta 方向和 session_end 类型的 JSON → 出来没有返回值,但日志文件可能多一行结束记录。

调用关系:run_ratatui_app 在终端应用结束时调用它。它和 maybe_init 写的 session_start 前后呼应,让一份日志能清楚看出会话边界。

调用图:被 1 处调用(run_ratatui_app);外部调用 1 个(json!)。

write_record232–243 ↗
fn write_record(dir: &str, kind: &str, obj: &T)

作用:把通用对象包装成统一的日志记录格式。它主要给出站命令使用,避免每种命令都重复写同样的 JSON 外壳。

数据流:进去方向 dir、类别 kind、以及一个可序列化的对象 obj;可序列化的意思是能被转成 JSON → 它加上当前时间,把对象放进 payload 字段,组成标准 JSON → 出来没有返回值,但会把这条记录交给 SessionLogger::write_json_line 写入文件。

调用关系:log_outbound_op 调用它来记录 AppCommand。它位于“业务想记一条命令”和“真正写磁盘”之间,负责把不同对象套进统一格式。

调用图:被 1 处调用(log_outbound_op);外部调用 1 个(json!)。

state/src/log_db.rs源码 ↗
io_transportcross-cutting:程序运行期间持续接收日志;后台按批量、定时或显式 flush 写入数据库

程序里到处都会打日志,但如果每打一条都立刻写数据库,主流程会被磁盘操作拖慢。这个文件像一个“日志收件箱”:前台只把日志快速丢进一个有上限的队列,后台任务再攒一批一起写入 SQLite。它接入的是 tracing(一套 Rust 常用的日志和跟踪框架),把事件、时间、级别、来源文件、线程号、当前 span(可以理解成“当前正在做哪件事”的上下文)整理成 LogEntry。它还会过滤掉一些特别吵的低级别 OpenTelemetry 计时日志,避免数据库被无用内容塞满。队列满时,新日志会被丢弃,这是为了保护主程序不卡住;但调用 flush 时,会等已进入队列的日志真正处理完,适合测试或退出前确保落库。

函数细节47
LogSinkQueueConfig::default59–65 ↗
fn default() -> Self

作用:给日志后台队列提供一套默认参数。使用者不想细调时,就用这组安全的默认值。

数据流:没有输入 → 填好队列容量、批量写入大小、定时刷新间隔 → 返回一个默认的 LogSinkQueueConfig。

调用关系:LogDbLayer::start 会用它拿到默认配置,再交给带配置的启动函数继续创建日志层。

调用图:被 1 处调用(start)。

LogSinkQueueConfig::normalized69–79 ↗
fn normalized(self) -> Self

作用:把配置修正到可用范围,防止传入 0 这类会让队列或批量写入失效的值。

数据流:输入一个配置 → 队列容量和批量大小至少改成 1;刷新间隔如果是 0,就改回默认间隔 → 输出修正后的配置。

调用关系:LogDbLayer::start_with_config 在真正创建队列和后台任务前调用它,确保后面的运行逻辑不用再担心坏配置。

调用图:被 1 处调用(start_with_config);外部调用 1 个(is_zero)。

start99–101 ↗
fn start(state_db: std::sync::Arc<StateRuntime>) -> LogDbLayer

作用:这是给外部最方便用的入口:传入状态数据库运行时,就得到一个能收集日志的层。

数据流:输入 StateRuntime,也就是本地状态数据库的运行对象 → 调用 LogDbLayer::start 创建日志层 → 返回 LogDbLayer。

调用关系:测试和实际使用代码会调用它;它只是薄薄包装一层,把工作交给 LogDbLayer::start。

调用图:调用 1 个内部函数(start);被 3 处调用(tool_call_logs_include_thread_id, flush_persists_logs_for_query, sqlite_feedback_logs_match_feedback_formatter_shape)。

LogDbLayer::clone104–109 ↗
fn clone(&self) -> Self

作用:复制一个日志层句柄,让多个地方可以共享同一个后台日志队列。

数据流:输入已有的 LogDbLayer → 复制发送端和当前进程的日志标识 → 返回一个新的 LogDbLayer 句柄;后台任务本身不会复制。

调用关系:tracing 订阅器和测试里会克隆它,这样既能安装日志层,又能保留一个句柄用来 flush。

调用图:外部调用 1 个(clone)。

LogDbLayer::start113–115 ↗
fn start(state_db: std::sync::Arc<StateRuntime>) -> Self

作用:用默认配置启动日志数据库写入功能。适合大多数不需要调队列大小的场景。

数据流:输入 StateRuntime → 取默认 LogSinkQueueConfig → 调用 LogDbLayer::start_with_config → 返回可以挂到 tracing 上的 LogDbLayer。

调用关系:顶层 start 函数会调用它;真正创建后台任务的细节交给 start_with_config。

调用图:调用 1 个内部函数(default);被 1 处调用(start);外部调用 1 个(start_with_config)。

LogDbLayer::start_with_config117–128 ↗
fn start_with_config(
        state_db: std::sync::Arc<StateRuntime>,
        config: LogSinkQueueConfig,
    ) -> Self

作用:按指定配置启动日志层和后台写库任务。需要控制队列大小、批量大小或刷新频率时会用它。

数据流:输入数据库运行时和配置 → 先修正配置 → 创建异步消息队列 → 启动 run_inserter 后台任务 → 生成本进程唯一日志标识 → 返回 LogDbLayer。

调用关系:LogDbLayer::start 和相关测试会调用它;它负责把前台收日志的 Layer 和后台写数据库的 run_inserter 接起来。

调用图:调用 3 个内部函数(normalized, current_process_log_uuid, run_inserter);被 2 处调用(configured_batch_size_flushes_without_explicit_flush, configured_flush_interval_persists_buffered_logs);外部调用 2 个(channel, spawn)。

LogDbLayer::try_send137–139 ↗
fn try_send(&self, entry: LogEntry)

作用:把一条整理好的日志尽快塞进后台队列。它不会等待,队列满了就直接放弃这条新日志。

数据流:输入 LogEntry → 包成 Entry 命令 → 尝试立即发送到队列 → 成功则后台稍后写库,失败则静默丢弃。

调用关系:LogDbLayer::on_event 在每次收到日志事件后调用它;这样普通打日志不会因为数据库或队列拥堵而卡住。

调用图:被 1 处调用(on_event);外部调用 3 个(new, try_send, Entry)。

LogDbLayer::on_new_span146–162 ↗
fn on_new_span(
        &self,
        attrs: &Attributes<'_>,
        id: &Id,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    )

作用:当 tracing 新建一个 span,也就是一段“正在做某件事”的上下文时,保存它的名字、字段和可能的线程号。

数据流:输入 span 的属性、编号和 tracing 上下文 → 读取字段,特别寻找 thread_id → 格式化这些字段 → 把 SpanLogContext 存到这个 span 的扩展数据里。

调用关系:tracing 框架在新 span 出现时自动调用它;之后 on_event 和 format_feedback_log_body 会借这些上下文拼出更完整的日志内容。

调用图:调用 1 个内部函数(format_fields);外部调用 3 个(record, span, default)。

LogDbLayer::on_record164–188 ↗
fn on_record(
        &self,
        id: &Id,
        values: &Record<'_>,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    )

作用:当已有 span 后续补充字段时,更新这个 span 里保存的日志上下文。

数据流:输入 span 编号、新字段和 tracing 上下文 → 读取新字段里的 thread_id → 找到原来的 SpanLogContext 并追加字段;如果没有,就新建一份 → 修改 span 内部扩展数据。

调用关系:tracing 框架在 span 被追加记录时调用它;它依赖 append_fields 和 format_fields 维护之后事件要用的上下文。

调用图:调用 2 个内部函数(append_fields, format_fields);外部调用 3 个(span, record, default)。

LogDbLayer::on_event190–229 ↗
fn on_event(&self, event: &Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>)

作用:这是日志事件进入数据库管道的关键入口。它把一条 tracing 事件变成数据库能保存的 LogEntry。

数据流:输入一条日志事件和上下文 → 先过滤掉吵闹的低级别 OpenTelemetry 日志 → 读取 message、thread_id、时间、级别、来源位置和 span 上下文 → 组装 LogEntry → 调用 try_send 放进队列。

调用关系:tracing 框架每次打日志都会调用它;它把格式化工作交给 MessageVisitor、event_thread_id 和 format_feedback_log_body,最后交给后台队列。

调用图:调用 2 个内部函数(try_send, format_feedback_log_body);外部调用 5 个(now, matches!, metadata, record, default)。

LogDbLayer::flush236–238 ↗
fn flush(&self) -> impl Future<Output = ()> + Send + '_

作用:要求后台把已经进入队列的日志处理完。常用于测试、导出前或程序准备退出前。

数据流:没有业务输入,只使用当前日志层的发送端 → 创建一次性回复通道 → 发送 Flush 命令 → 等后台回复;如果队列已关闭,就直接结束。

调用关系:LogWriter trait 和测试都会用它;后台 run_inserter 收到 Flush 后会调用真正的 flush 写数据库,再回信表示完成。

调用图:外部调用 3 个(send, channel, Flush)。

SpanFieldVisitor::record_field259–263 ↗
fn record_field(&mut self, field: &Field, value: String)

作用:从 span 的字段里挑出 thread_id。thread_id 可以理解成某个会话或线程的标记,方便之后按它找反馈日志。

数据流:输入字段名和值 → 如果字段名是 thread_id,并且之前还没记录过 → 保存这个值;其他字段忽略。

调用关系:各种 record_i64、record_str 等类型专用函数都会把值转成字符串后交给它统一判断。

调用图:被 7 处调用(record_bool, record_debug, record_error, record_f64, record_i64, record_str, record_u64);外部调用 1 个(name)。

SpanFieldVisitor::record_i64267–269 ↗
fn record_i64(&mut self, field: &Field, value: i64)

作用:处理 span 里 64 位有符号整数类型的字段,并尝试把它当作 thread_id 保存。

数据流:输入字段和值 → 把数字转成字符串 → 交给 record_field 判断是否要保存 → 可能更新 visitor 的 thread_id。

调用关系:tracing 在记录整数 span 字段时调用它;实际筛选逻辑集中在 record_field。

调用图:调用 1 个内部函数(record_field)。

SpanFieldVisitor::record_u64271–273 ↗
fn record_u64(&mut self, field: &Field, value: u64)

作用:处理 span 里 64 位无符号整数类型的字段,并尝试识别 thread_id。

数据流:输入字段和值 → 转成字符串 → 交给 record_field → 可能保存为 thread_id。

调用关系:由 tracing 字段访问机制调用;它只是类型适配,判断工作交给 record_field。

调用图:调用 1 个内部函数(record_field)。

SpanFieldVisitor::record_bool275–277 ↗
fn record_bool(&mut self, field: &Field, value: bool)

作用:处理 span 里的真假值字段,并检查它是不是 thread_id。

数据流:输入字段和值 → 把 true 或 false 转成字符串 → 交给 record_field → 可能更新 thread_id。

调用关系:tracing 记录布尔字段时调用它;统一筛选逻辑仍在 record_field。

调用图:调用 1 个内部函数(record_field)。

SpanFieldVisitor::record_f64279–281 ↗
fn record_f64(&mut self, field: &Field, value: f64)

作用:处理 span 里的小数字段,并尝试识别 thread_id。

数据流:输入字段和值 → 把浮点数转成字符串 → 交给 record_field → 可能保存 thread_id。

调用关系:tracing 记录浮点字段时调用它;它服务于 on_new_span 和 on_record 收集上下文。

调用图:调用 1 个内部函数(record_field)。

SpanFieldVisitor::record_str283–285 ↗
fn record_str(&mut self, field: &Field, value: &str)

作用:处理 span 里的文本字段,这也是 thread_id 最常见的形式。

数据流:输入字段和值 → 复制成字符串 → 交给 record_field → 如果字段名匹配,就保存。

调用关系:tracing 记录字符串字段时调用它;on_new_span 和 on_record 借它拿到线程标记。

调用图:调用 1 个内部函数(record_field)。

SpanFieldVisitor::record_error287–289 ↗
fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static))

作用:处理 span 里的错误对象字段,并把错误文字化后检查是否是 thread_id。

数据流:输入字段和错误对象 → 把错误转成字符串 → 交给 record_field → 可能保存 thread_id。

调用关系:tracing 遇到错误类型字段时调用它;它把特殊类型变成普通字符串,方便统一处理。

调用图:调用 1 个内部函数(record_field);外部调用 1 个(to_string)。

SpanFieldVisitor::record_debug291–293 ↗
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug)

作用:处理只能用调试格式显示的 span 字段,并检查是否是 thread_id。

数据流:输入字段和值 → 用调试格式转成字符串 → 交给 record_field → 可能更新 thread_id。

调用关系:tracing 遇到通用调试字段时调用它;它保证各种字段类型都能走同一套 thread_id 提取逻辑。

调用图:调用 1 个内部函数(record_field);外部调用 1 个(format!)。

event_thread_id296–315 ↗
fn event_thread_id(
    event: &Event<'_>,
    ctx: &tracing_subscriber::layer::Context<'_, S>,
) -> Option<String>

作用:当日志事件自己没有 thread_id 时,从它所在的 span 链路里找一个最近的 thread_id。

数据流:输入日志事件和 tracing 上下文 → 从根 span 到当前 span 逐个查看保存的 SpanLogContext → 发现 thread_id 就记下来,后面的可以覆盖前面的 → 返回找到的线程号或空。

调用关系:LogDbLayer::on_event 在事件字段没给 thread_id 时调用它;它依赖 on_new_span 和 on_record 之前存下的上下文。

调用图:外部调用 1 个(event_scope)。

format_feedback_log_body317–346 ↗
fn format_feedback_log_body(
    event: &Event<'_>,
    ctx: &tracing_subscriber::layer::Context<'_, S>,
) -> String

作用:把一条日志整理成反馈日志需要的正文格式,包含 span 路径和事件字段。

数据流:输入事件和上下文 → 先按从外到内的顺序拼接 span 名称和字段 → 再追加当前事件的字段文本 → 输出一整行适合反馈导出的日志正文。

调用关系:LogDbLayer::on_event 会调用它填充 LogEntry.feedback_log_body;它内部用 format_fields 把字段变成人能读的文字。

调用图:调用 1 个内部函数(format_fields);被 1 处调用(on_event);外部调用 2 个(event_scope, new)。

format_fields348–356 ↗
fn format_fields(fields: R) -> String

作用:把 tracing 字段格式化成普通文本。这样数据库里保存的不是内部结构,而是人能读懂的一串内容。

数据流:输入一组 tracing 字段 → 使用默认字段格式器写成字符串 → 返回格式化后的字段文本。

调用关系:on_new_span、on_record 和 format_feedback_log_body 都会用它;它是 span 字段和事件字段变成日志文字的公共小工具。

调用图:被 3 处调用(on_new_span, on_record, format_feedback_log_body);外部调用 3 个(default, new, new)。

append_fields358–363 ↗
fn append_fields(fields: &mut String, values: &Record<'_>)

作用:把新记录的字段追加到 span 已有的字段字符串里。

数据流:输入已有字段字符串和新 Record → 暂时取出原字符串 → 用默认格式器追加新字段 → 把合并后的字符串放回去。

调用关系:LogDbLayer::on_record 在 span 后续补字段时调用它,保证之后生成日志正文时能看到完整上下文。

调用图:被 1 处调用(on_record);外部调用 3 个(default, new, take)。

current_process_log_uuid365–372 ↗
fn current_process_log_uuid() -> &'static str

作用:给当前进程生成并缓存一个唯一标识,用来区分不同进程写出的日志。

数据流:没有输入 → 第一次调用时读取进程号并生成随机 UUID → 拼成类似 pid:进程号:随机值 的字符串 → 后续调用都返回同一个值。

调用关系:LogDbLayer::start_with_config 在创建日志层时调用它;每条 LogEntry 都会带上这个进程标识。

调用图:被 1 处调用(start_with_config);外部调用 1 个(new)。

run_inserter374–408 ↗
async fn run_inserter(
    state_db: std::sync::Arc<StateRuntime>,
    mut receiver: mpsc::Receiver<LogDbCommand>,
    config: LogSinkQueueConfig,
)

作用:这是后台写库循环。它从队列收日志,攒够一批或等到时间就写进数据库。

数据流:输入 StateRuntime、接收队列和配置 → 建立日志缓冲区和定时器 → 收到 Entry 就放进缓冲区,满批量就写库;收到 Flush 就立即写库并回复;队列关闭时写完剩余日志后退出。

调用关系:LogDbLayer::start_with_config 用 tokio 启动它;它是前台 try_send 和数据库 insert_logs 之间的后台工人。

调用图:被 1 处调用(start_with_config);外部调用 3 个(with_capacity, select!, interval)。

flush410–416 ↗
async fn flush(state_db: &StateRuntime, buffer: &mut Vec<LogEntry>)

作用:把后台缓冲区里已有的日志真正写入数据库。

数据流:输入数据库运行时和日志缓冲区 → 如果缓冲区为空就不做事 → 否则取出全部日志 → 调用 insert_logs 批量写入 SQLite;写入错误会被忽略。

调用关系:run_inserter 在批量满、定时到、收到 Flush 或退出时调用它;它是最终落库的那一步。

调用图:外部调用 1 个(insert_logs)。

MessageVisitor::record_field425–432 ↗
fn record_field(&mut self, field: &Field, value: String)

作用:从一条日志事件的字段里提取 message 和 thread_id。

数据流:输入字段名和值 → 如果字段名是 message 且还没保存,就保存为日志消息;如果字段名是 thread_id 且还没保存,就保存线程号 → 其他字段不影响这两个结果。

调用关系:MessageVisitor 的各种 record_* 函数都会调用它;LogDbLayer::on_event 用这个 visitor 从事件里拿核心信息。

调用图:被 7 处调用(record_bool, record_debug, record_error, record_f64, record_i64, record_str, record_u64);外部调用 1 个(name)。

MessageVisitor::record_i64436–438 ↗
fn record_i64(&mut self, field: &Field, value: i64)

作用:处理日志事件里的整数字段,并尝试提取 message 或 thread_id。

数据流:输入字段和值 → 把整数转成字符串 → 交给 record_field → 可能更新 message 或 thread_id。

调用关系:tracing 在记录整数事件字段时调用它;真正的字段名判断交给 record_field。

调用图:调用 1 个内部函数(record_field)。

MessageVisitor::record_u64440–442 ↗
fn record_u64(&mut self, field: &Field, value: u64)

作用:处理日志事件里的无符号整数字段,并尝试提取关键信息。

数据流:输入字段和值 → 转成字符串 → 交给 record_field → 可能保存为 message 或 thread_id。

调用关系:由 tracing 字段访问机制调用;它是 MessageVisitor 支持多种字段类型的一部分。

调用图:调用 1 个内部函数(record_field)。

MessageVisitor::record_bool444–446 ↗
fn record_bool(&mut self, field: &Field, value: bool)

作用:处理日志事件里的真假值字段,并尝试提取 message 或 thread_id。

数据流:输入字段和值 → 把布尔值转成字符串 → 交给 record_field → 可能更新提取结果。

调用关系:tracing 记录布尔事件字段时调用它;它把具体类型转换后交给统一逻辑。

调用图:调用 1 个内部函数(record_field)。

MessageVisitor::record_f64448–450 ↗
fn record_f64(&mut self, field: &Field, value: f64)

作用:处理日志事件里的小数字段,并尝试提取 message 或 thread_id。

数据流:输入字段和值 → 把浮点数转成字符串 → 调用 record_field → 可能保存核心字段。

调用关系:tracing 记录浮点事件字段时调用它;LogDbLayer::on_event 间接受益于这些提取结果。

调用图:调用 1 个内部函数(record_field)。

MessageVisitor::record_str452–454 ↗
fn record_str(&mut self, field: &Field, value: &str)

作用:处理日志事件里的文本字段,这是提取 message 和 thread_id 最常见的路径。

数据流:输入字段和值 → 复制成字符串 → 交给 record_field → 如果字段名匹配,就保存。

调用关系:tracing 记录字符串事件字段时调用它;普通 tracing::info!("...") 的消息通常会经由这里被取出。

调用图:调用 1 个内部函数(record_field)。

MessageVisitor::record_error456–458 ↗
fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static))

作用:处理日志事件里的错误对象字段,把错误转成文字后再检查是否是关键字段。

数据流:输入字段和错误对象 → 转成字符串 → 调用 record_field → 可能保存为 message 或 thread_id。

调用关系:tracing 遇到错误字段时调用它;它让错误类型也能进入统一的字段提取流程。

调用图:调用 1 个内部函数(record_field);外部调用 1 个(to_string)。

MessageVisitor::record_debug460–462 ↗
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug)

作用:处理需要用调试格式显示的日志事件字段,并尝试提取 message 或 thread_id。

数据流:输入字段和值 → 用调试格式变成字符串 → 调用 record_field → 可能更新 message 或 thread_id。

调用关系:tracing 处理通用调试字段时调用它;它补齐了 MessageVisitor 对各种字段类型的支持。

调用图:调用 1 个内部函数(record_field);外部调用 1 个(format!)。

tests::temp_codex_home483–485 ↗
fn temp_codex_home() -> std::path::PathBuf

作用:给测试创建一个临时的 Codex 主目录路径,避免污染真实用户数据。

数据流:没有输入 → 读取系统临时目录 → 拼上随机 UUID 组成唯一目录名 → 返回这个路径。

调用关系:多个数据库相关测试启动 StateRuntime 前都会调用它;测试结束后通常会删除这个目录。

调用图:外部调用 2 个(format!, temp_dir)。

tests::wait_for_log_count487–504 ↗
async fn wait_for_log_count(runtime: &StateRuntime, expected: usize) -> Vec<crate::LogRow>

作用:测试用的小帮手:等待数据库里出现指定数量的日志。

数据流:输入 StateRuntime 和期望条数 → 在最多 2 秒内反复查询日志 → 数量达标就返回这些行;超时则让测试失败。

调用关系:批量刷新和定时刷新测试会调用它,因为后台写库是异步的,不能假设日志立刻可查。

调用图:外部调用 7 个(assert!, default, query_logs, from_millis, from_secs, now, sleep)。

tests::test_entry506–520 ↗
fn test_entry(message: &str) -> LogEntry

作用:快速造一条固定格式的测试日志,方便测试队列行为。

数据流:输入一段消息文本 → 填入固定时间、级别、目标、线程号、进程号、文件行号等字段 → 返回 LogEntry。

调用关系:队列满丢弃和 flush 等待测试会调用它,避免每个测试都手写完整 LogEntry。

tests::SharedWriter::snapshot528–531 ↗
fn snapshot(&self) -> String

作用:读取测试用内存写入器里已经收集到的日志文本。

数据流:没有业务输入,读取内部共享字节数组 → 加锁复制字节 → 按 UTF-8 转成字符串 → 返回日志文本。

调用关系:sqlite_feedback_logs_match_feedback_formatter_shape 用它拿到 tracing 格式化层输出的日志,再和 SQLite 中的反馈日志比较。

调用图:外部调用 1 个(from_utf8)。

tests::SharedWriter::make_writer541–545 ↗
fn make_writer(&'a self) -> Self::Writer

作用:为 tracing 的格式化层提供一个写入目标,让测试能把日志写进内存而不是终端。

数据流:输入 SharedWriter 自身引用 → 复制内部共享字节数组的引用 → 返回 SharedWriterGuard,后续写入会进同一块内存。

调用关系:tracing_subscriber 的 MakeWriter 机制会调用它;测试借此捕获标准格式日志内容。

调用图:外部调用 1 个(clone)。

tests::SharedWriterGuard::write549–555 ↗
fn write(&mut self, buf: &[u8]) -> io::Result<usize>

作用:把 tracing 写来的字节追加到共享内存缓冲区。

数据流:输入一段字节 → 加锁访问共享 Vec<u8> → 追加这些字节 → 返回写入的字节数。

调用关系:tracing 格式化层输出日志时会调用它;SharedWriter::snapshot 之后会读取这些内容。

tests::SharedWriterGuard::flush557–559 ↗
fn flush(&mut self) -> io::Result<()>

作用:满足写入接口要求的刷新函数。这里写的是内存,不需要真正刷新。

数据流:没有实际输入变化 → 直接返回成功 → 不改动缓冲区。

调用关系:tracing 或标准写入流程可能调用它;它只是让 SharedWriterGuard 符合 io::Write 接口。

tests::sqlite_feedback_logs_match_feedback_formatter_shape563–618 ↗
async fn sqlite_feedback_logs_match_feedback_formatter_shape()

作用:验证写进 SQLite 的反馈日志格式,和 tracing 普通格式化输出的形状一致。

数据流:创建临时数据库和内存日志写入器 → 同时安装普通格式化层和 LogDbLayer → 打几条带 thread_id 的日志 → flush 后分别读取内存日志和数据库反馈日志 → 去掉时间戳后比较是否相同 → 清理临时目录。

调用关系:它覆盖 start、format_feedback_log_body、span 上下文保存和数据库查询的组合行为,确保反馈导出的日志看起来和用户熟悉的日志一致。

调用图:调用 2 个内部函数(start, init);外部调用 11 个(from_utf8, new, assert_eq!, default, temp_codex_home, remove_dir_all, debug!, info_span!, trace!, layer (+1 more))。

tests::flush_persists_logs_for_query621–649 ↗
async fn flush_persists_logs_for_query()

作用:验证显式调用 flush 后,刚打出的日志能被数据库查询到。

数据流:创建临时数据库和日志层 → 打一条 info 日志 → 调用 layer.flush 等后台写完 → 查询日志表 → 断言只有一条且消息正确 → 清理目录。

调用关系:它直接检验 start、on_event、队列、run_inserter 和 flush 的基本闭环。

调用图:调用 2 个内部函数(start, init);外部调用 7 个(new, assert_eq!, temp_codex_home, default, remove_dir_all, info!, registry)。

tests::configured_batch_size_flushes_without_explicit_flush652–698 ↗
async fn configured_batch_size_flushes_without_explicit_flush()

作用:验证批量大小达到配置值时,即使不手动 flush,日志也会自动写入数据库。

数据流:用 batch_size=2 启动日志层 → 打第一条日志后确认数据库还没有 → 打第二条日志 → 等数据库出现两条 → 检查消息顺序 → 清理目录。

调用关系:它专门测试 LogDbLayer::start_with_config 和 run_inserter 的“攒够一批就写库”路径。

调用图:调用 2 个内部函数(start_with_config, init);外部调用 10 个(new, assert_eq!, temp_codex_home, wait_for_log_count, from_millis, from_secs, remove_dir_all, sleep, info!, registry)。

tests::configured_flush_interval_persists_buffered_logs701–731 ↗
async fn configured_flush_interval_persists_buffered_logs()

作用:验证没攒够一批时,定时器到点也会把缓冲日志写进数据库。

数据流:用很短的 flush_interval 启动日志层 → 打一条日志 → 等数据库出现一条 → 检查消息内容 → 清理目录。

调用关系:它覆盖 run_inserter 的定时刷新分支,说明日志不会因为数量少就永远留在内存里。

调用图:调用 2 个内部函数(start_with_config, init);外部调用 9 个(new, assert_eq!, temp_codex_home, wait_for_log_count, from_millis, remove_dir_all, sleep, info!, registry)。

tests::event_queue_drops_new_entries_when_full734–751 ↗
async fn event_queue_drops_new_entries_when_full()

作用:验证日志队列满时,新来的日志会被丢弃,而不是阻塞程序。

数据流:创建容量为 1 的队列和日志层 → 连续 try_send 两条测试日志 → 从队列读出第一条 → 确认第二条没有进入队列。

调用关系:它直接测试 LogDbLayer::try_send 的保护策略:日志可丢,但主流程不能被日志系统拖死。

调用图:外部调用 5 个(assert!, assert_eq!, channel, panic!, test_entry)。

tests::flush_waits_for_queue_capacity_and_receiver_processing754–791 ↗
async fn flush_waits_for_queue_capacity_and_receiver_processing()

作用:验证 flush 会排在已有日志后面,并且会等后台明确回复才结束。

数据流:创建容量为 1 的队列 → 先塞一条日志占住队列 → 另起任务调用 flush → 确认 flush 没立刻完成 → 依次取出日志命令和 Flush 命令 → 手动回复 Flush → 确认 flush 任务完成。

调用关系:它测试 LogDbLayer::flush 的等待语义,保证调用者知道:flush 返回时,之前进入队列的日志已经被后台处理过。

调用图:外部调用 10 个(assert!, assert_eq!, channel, panic!, test_entry, from_millis, from_secs, spawn, sleep, timeout)。

state/src/runtime/logs.rs源码 ↗
domain_logicstartup, request handling, cross-cutting

日志就像系统的行车记录仪:出问题时要能回看,但也不能把硬盘塞满。这个文件就是 StateRuntime 里专门处理日志的部分。它会把一条或一批 LogEntry 存到独立的 logs 表里,保存时间、级别、来源、线程号、进程号、正文等信息。写入后,它会立刻做“修剪”:同一个对话线程、同一个无线程进程,都只能保留一定大小和一定行数以内的新日志,旧的会被删掉。启动时还会清掉 10 天前的日志,并做一次轻量数据库整理。查询时,它支持按级别、时间、文件、模块、线程、关键词等过滤。给反馈系统用的日志会被格式化成普通文本,并严格控制总字节数,避免一次反馈带走过大的日志包。

函数细节37
StateRuntime::insert_log6–8 ↗
async fn insert_log(&self, entry: &LogEntry) -> anyhow::Result<()>

作用:把一条日志写进日志数据库。它是给只想写单条日志的调用者用的便捷入口。

数据流:进去的是一条 LogEntry 日志记录 → 它把这条记录临时看成只有一个元素的列表 → 交给批量写入函数处理,最终数据库多出这条日志,或返回错误。

调用关系:它自己不直接碰数据库,而是把活儿交给 StateRuntime::insert_logs。这样单条写入和批量写入走同一套保存、修剪规则,不会出现两种行为。

调用图:调用 1 个内部函数(insert_logs);外部调用 1 个(from_ref)。

StateRuntime::insert_logs11–47 ↗
async fn insert_logs(&self, entries: &[LogEntry]) -> anyhow::Result<()>

作用:把一批日志一次性写入 SQLite 日志表。它还会在同一个事务里清理超额日志,保证别人看到的总是已经整理好的结果。

数据流:进去的是一组 LogEntry → 如果为空就直接结束;否则开启数据库事务,把每条日志的时间、级别、正文、线程、进程等字段插入 logs 表,并估算这条日志占多少字节 → 调用修剪函数删掉超出预算的旧日志 → 提交事务,返回成功或错误。

调用关系:StateRuntime::insert_log 会调用它。它写完后立刻调用 StateRuntime::prune_logs_after_insert,让“写入”和“清理”成为一件原子事情,也就是要么一起成功,要么一起失败。

调用图:调用 1 个内部函数(prune_logs_after_insert);被 1 处调用(insert_log);外部调用 2 个(new, is_empty)。

StateRuntime::prune_logs_after_insert62–286 ↗
async fn prune_logs_after_insert(
        &self,
        entries: &[LogEntry],
        tx: &mut SqliteConnection,
    ) -> anyhow::Result<()>

作用:在新日志插入后,按分区删掉太旧、太多、太大的日志。这里的分区可以理解成“每个线程一本小账本;没有线程的日志按进程另算一本账”。

数据流:进去的是刚插入的日志列表和正在使用的数据库事务 → 它先找出受影响的线程和无线程进程,只检查这些地方是否超过大小或行数限制 → 对超限的部分按时间从新到旧累计,超过预算的旧行会被删除 → 数据库里留下每个分区最新且不超额的日志。

调用关系:它只由 StateRuntime::insert_logs 调用,并且运行在同一个事务中。这样调用者不会短暂看到“新日志已经插入但旧日志还没删”的中间状态。

调用图:被 1 处调用(insert_logs);外部调用 2 个(new, iter)。

StateRuntime::delete_logs_before288–294 ↗
async fn delete_logs_before(&self, cutoff_ts: i64) -> anyhow::Result<u64>

作用:删除某个时间点以前的日志。它用于做按日期的粗粒度清理,比如清掉 10 天前的记录。

数据流:进去的是 cutoff_ts,一个 Unix 秒级时间戳 → 它执行 DELETE FROM logs WHERE ts < cutoff_ts → 出来的是被删除的行数。

调用关系:StateRuntime::run_logs_startup_maintenance 会调用它。它只负责按时间删,具体什么时候删、删多久以前的日志由启动维护函数决定。

调用图:被 1 处调用(run_logs_startup_maintenance);外部调用 1 个(query)。

StateRuntime::run_logs_startup_maintenance296–310 ↗
async fn run_logs_startup_maintenance(&self) -> anyhow::Result<()>

作用:程序启动时整理日志数据库。它会删掉过期日志,并让 SQLite 做一次不抢锁的轻量检查点整理。

数据流:进去的是当前运行时对象 → 它取当前时间,往前减 10 天得到截止时间 → 调用 StateRuntime::delete_logs_before 删除更早日志 → 再执行 PRAGMA wal_checkpoint(PASSIVE),把 SQLite 的写入日志尽量合并回主库但不强等别人 → 返回成功或错误。

调用关系:它通常在运行时启动阶段被调用。它把删除旧日志的具体动作交给 StateRuntime::delete_logs_before,自己负责决定时间窗口和数据库维护方式。

调用图:调用 1 个内部函数(delete_logs_before);外部调用 3 个(now, days, query)。

StateRuntime::query_logs313–332 ↗
async fn query_logs(&self, query: &LogQuery) -> anyhow::Result<Vec<LogRow>>

作用:按条件查询普通日志列表。它给界面、命令或其他内部功能提供“查日志”的能力。

数据流:进去的是 LogQuery 查询条件 → 它拼出 SELECT 语句,调用 push_log_filters 加上级别、时间、线程、文件、关键词等过滤条件,再按要求升序或降序排序并加上数量限制 → 从数据库取出 LogRow 列表返回。

调用关系:它是日志读取的主要入口之一。过滤条件的拼接交给 push_log_filters,模糊匹配又间接交给 push_like_filters,避免每个查询函数重复写同样的筛选规则。

调用图:调用 1 个内部函数(push_log_filters);外部调用 1 个(new)。

StateRuntime::query_feedback_logs_for_threads335–431 ↗
async fn query_feedback_logs_for_threads(
        &self,
        thread_ids: &[&str],
    ) -> anyhow::Result<Vec<u8>>

作用:为一个或多个线程收集反馈用日志,并返回一段字节内容。反馈日志会被限制大小,避免上传或展示时过大。

数据流:进去的是线程 ID 列表 → 如果列表为空就返回空字节;否则它查询这些线程的日志,还会把这些线程最近所属进程里的“无线程日志”一起纳入 → 先在 SQL 里按估算大小截住,再逐行格式化,超过最大字节数就停止 → 最后按正常时间顺序输出字节数组。

调用关系:StateRuntime::query_feedback_logs 会调用它来处理单个线程。它把每行文本的样子交给 format_feedback_log_line,自己负责从数据库挑出该带走哪些日志。

调用图:调用 1 个内部函数(format_feedback_log_line);被 1 处调用(query_feedback_logs);外部调用 4 个(new, new, with_capacity, try_from)。

StateRuntime::query_feedback_logs434–436 ↗
async fn query_feedback_logs(&self, thread_id: &str) -> anyhow::Result<Vec<u8>>

作用:查询某一个线程的反馈日志。它是单线程场景下更简单的包装函数。

数据流:进去的是一个 thread_id → 它把这个线程 ID 包成只有一个元素的列表 → 调用 StateRuntime::query_feedback_logs_for_threads,返回同样的字节内容。

调用关系:它不自己写 SQL,而是复用 StateRuntime::query_feedback_logs_for_threads。这样单线程和多线程反馈日志的规则完全一致。

调用图:调用 1 个内部函数(query_feedback_logs_for_threads)。

StateRuntime::max_log_id439–446 ↗
async fn max_log_id(&self, query: &LogQuery) -> anyhow::Result<i64>

作用:找出符合条件的日志里最大的 id。这个 id 可以理解成日志表里的最新编号,常用于增量读取。

数据流:进去的是 LogQuery 查询条件 → 它拼出 SELECT MAX(id) 查询,并调用 push_log_filters 加上同样的过滤条件 → 从数据库读出最大 id;如果没有匹配日志,就返回 0。

调用关系:它和 StateRuntime::query_logs 使用同一套过滤函数 push_log_filters,所以“查最大编号”和“查日志列表”对条件的理解一致。

调用图:调用 1 个内部函数(push_log_filters);外部调用 1 个(new)。

format_feedback_log_line457–473 ↗
fn format_feedback_log_line(
    ts: i64,
    ts_nanos: i64,
    level: &str,
    feedback_log_body: &str,
) -> String

作用:把一条反馈日志格式化成人能读的一行文本。它会补上时间、级别和换行符。

数据流:进去的是秒级时间、纳秒、日志级别和正文 → 它尽量把时间转成 RFC3339 格式,也就是类似 1970-01-01T00:00:01.123456Z 的标准时间;如果时间不合法,就退回到原始数字格式 → 拼成“时间 级别 正文”,并确保最后有换行 → 返回字符串。

调用关系:StateRuntime::query_feedback_logs_for_threads 在生成反馈日志文件内容时会逐行调用它。测试也会检查它的输出格式,保证反馈里的日志长相稳定。

调用图:被 1 处调用(query_feedback_logs_for_threads);外部调用 3 个(from_timestamp, format!, try_from)。

push_log_filters475–521 ↗
fn push_log_filters(builder: &mut QueryBuilder<Sqlite>, query: &LogQuery)

作用:把 LogQuery 里的筛选条件追加到 SQL 查询里。它像给查询语句不断加“只看这些日志”的条件。

数据流:进去的是一个正在构造的 SQL 查询 builder 和 LogQuery → 它根据查询条件追加级别、起止时间、模块名、文件名、线程、是否包含无线程日志、起始 id、正文搜索等 WHERE 条件 → builder 被原地改好,不直接返回数据。

调用关系:StateRuntime::query_logs 和 StateRuntime::max_log_id 都调用它。遇到模块名和文件名这种“包含某段文字”的条件时,它把细节交给 push_like_filters。

调用图:调用 1 个内部函数(push_like_filters);被 2 处调用(max_log_id, query_logs);外部调用 3 个(push, push_bind, separated)。

push_like_filters523–539 ↗
fn push_like_filters(builder: &mut QueryBuilder<Sqlite>, column: &str, filters: &[String])

作用:给某个数据库列追加多个“包含某段文字”的模糊匹配条件。比如文件名里包含 main 或 lib。

数据流:进去的是 SQL builder、列名、过滤词列表 → 如果过滤词为空就不做事;否则追加 AND (...),里面用 OR 连接多个 LIKE 条件 → builder 被原地加上这些模糊匹配条件。

调用关系:它只由 push_log_filters 调用。这样模块路径和文件路径的模糊搜索可以共用同一段小工具逻辑。

调用图:被 1 处调用(push_log_filters);外部调用 1 个(push)。

tests::open_db_pool558–566 ↗
async fn open_db_pool(path: &Path) -> SqlitePool

作用:测试里打开一个 SQLite 连接池。连接池可以理解成一组可复用的数据库连接,方便测试直接检查数据库内容。

数据流:进去的是数据库文件路径 → 它用这个路径创建 SQLite 连接选项,并要求文件必须已经存在 → 连接成功后返回 SqlitePool;失败时测试直接报错。

调用关系:它是测试辅助函数,被 tests::log_row_count 和部分迁移、配置测试使用。生产代码不会调用它。

调用图:外部调用 2 个(new, connect_with)。

tests::log_row_count568–576 ↗
async fn log_row_count(path: &Path) -> i64

作用:测试里统计 logs 表有多少行。它用来确认日志是否真的写到了正确的数据库。

数据流:进去的是数据库路径 → 它调用 tests::open_db_pool 打开数据库 → 执行 SELECT COUNT(*) FROM logs → 关闭连接池并返回行数。

调用关系:tests::insert_logs_use_dedicated_log_database 会用它验证日志写入位置。它依赖 tests::open_db_pool 完成打开数据库这一步。

调用图:外部调用 1 个(open_db_pool)。

tests::insert_logs_use_dedicated_log_database579–607 ↗
async fn insert_logs_use_dedicated_log_database()

作用:测试日志是否写进专门的日志数据库,而不是混进别的状态数据库。

数据流:进去的是测试框架启动的临时环境 → 它创建临时目录,初始化 StateRuntime,插入一条日志 → 打开 logs_db_path 指向的数据库统计行数 → 期望行数为 1,最后删除临时目录。

调用关系:这是自动测试的一部分。它调用 StateRuntime::init、日志插入能力和 tests::log_row_count,验证这个文件的写入路径没有走错。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 4 个(assert_eq!, logs_db_path, log_row_count, remove_dir_all)。

tests::init_migrates_message_only_logs_db_to_feedback_log_body_schema610–706 ↗
async fn init_migrates_message_only_logs_db_to_feedback_log_body_schema()

作用:测试旧版日志数据库能否被迁移到新版字段结构。这样老用户升级后,原来的日志内容不会丢。

数据流:进去的是临时测试目录 → 它先手工创建一个只有旧 message 字段的日志库并插入旧日志 → 再初始化 StateRuntime 触发迁移 → 查询日志确认正文还在,并检查表字段和索引已经变成新结构 → 最后清理目录。

调用关系:它通过 StateRuntime::init 间接测试迁移流程,再用 StateRuntime::query_logs 验证迁移结果。它还直接读取 SQLite 元信息,确认数据库结构符合预期。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 12 个(Owned, new, connect_with, now, assert_eq!, logs_db_path, query, default, open_db_pool, create_dir_all (+2 more))。

tests::init_configures_logs_db_with_incremental_auto_vacuum709–724 ↗
async fn init_configures_logs_db_with_incremental_auto_vacuum()

作用:测试日志数据库是否开启了增量自动回收空间。这样删掉大量日志后,数据库文件更容易被慢慢瘦身。

数据流:进去的是临时测试目录 → 初始化 StateRuntime 创建日志库 → 打开日志数据库读取 PRAGMA auto_vacuum → 期望值是 2,也就是 SQLite 的 incremental 模式 → 关闭并删除临时目录。

调用关系:它调用 StateRuntime::init 和 tests::open_db_pool。它验证的是启动建库配置,不直接测试插入或查询日志。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 4 个(assert_eq!, logs_db_path, open_db_pool, remove_dir_all)。

tests::format_feedback_log_line_matches_feedback_formatter_shape727–737 ↗
fn format_feedback_log_line_matches_feedback_formatter_shape()

作用:测试反馈日志单行格式是否符合预期。这样反馈日志的文本样子不会被无意改坏。

数据流:进去的是固定时间、级别和正文样例 → 调用 format_feedback_log_line → 把结果和预期字符串比较。

调用关系:它直接覆盖 format_feedback_log_line。这个测试保护 StateRuntime::query_feedback_logs_for_threads 最终输出的行格式。

调用图:外部调用 1 个(assert_eq!)。

tests::format_feedback_log_line_preserves_existing_trailing_newline740–750 ↗
fn format_feedback_log_line_preserves_existing_trailing_newline()

作用:测试正文本来已有换行时,格式化函数不会再多加一个空行。

数据流:进去的是带结尾换行的正文 → 调用 format_feedback_log_line → 期望输出只保留一个结尾换行。

调用关系:它直接测试 format_feedback_log_line 的边界行为,避免反馈日志出现多余空行。

调用图:外部调用 1 个(assert_eq!)。

tests::query_logs_with_search_matches_rendered_body_substring753–803 ↗
async fn query_logs_with_search_matches_rendered_body_substring()

作用:测试按关键词搜索日志时,查的是反馈正文里的实际展示内容。

数据流:进去的是临时运行时 → 插入两条正文不同的日志 → 用 search 条件查询包含 foo=2 的日志 → 期望只返回第二条,并确认返回正文正确 → 清理目录。

调用关系:它通过 StateRuntime::insert_logs 准备数据,再调用 StateRuntime::query_logs。它验证 push_log_filters 里的搜索条件是否用对了字段。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 3 个(default, assert_eq!, remove_dir_all)。

tests::query_logs_filters_level_set_without_rewriting_stored_level806–888 ↗
async fn query_logs_filters_level_set_without_rewriting_stored_level()

作用:测试按日志级别过滤时大小写不影响匹配,但数据库里原来的级别写法不会被改掉。

数据流:进去的是临时运行时 → 插入 TRACE、INFO、warn、ERROR 四条日志 → 查询 WARN 和 ERROR → 期望返回 warn 和 ERROR,并保留它们原来的大小写。

调用关系:它测试 StateRuntime::query_logs 和 push_log_filters 对 level 条件的处理。重点是查询时比较用大写,但存储值不被重写。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 4 个(default, assert_eq!, remove_dir_all, vec!)。

tests::insert_logs_prunes_old_rows_when_thread_exceeds_size_limit891–942 ↗
async fn insert_logs_prunes_old_rows_when_thread_exceeds_size_limit()

作用:测试同一个线程的日志超过大小限制时,会删掉较旧的记录。

数据流:进去的是临时运行时 → 插入同一线程两条大日志,总大小超过预算 → 查询这个线程 → 期望只剩时间更新的那条。

调用关系:它主要覆盖 StateRuntime::insert_logs 之后调用的 StateRuntime::prune_logs_after_insert。查询用 StateRuntime::query_logs 验证修剪结果。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 4 个(default, assert_eq!, remove_dir_all, vec!)。

tests::insert_logs_prunes_single_thread_row_when_it_exceeds_size_limit945–980 ↗
async fn insert_logs_prunes_single_thread_row_when_it_exceeds_size_limit()

作用:测试单条线程日志如果自己就超过大小限制,也会被删掉。

数据流:进去的是临时运行时 → 插入一条超大的有线程日志 → 查询该线程 → 期望结果为空。

调用关系:它验证 StateRuntime::prune_logs_after_insert 的严格预算规则:不是“至少保留一条”,而是超限就不能留下。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 4 个(default, assert!, remove_dir_all, vec!)。

tests::insert_logs_prunes_threadless_rows_per_process_uuid_only983–1049 ↗
async fn insert_logs_prunes_threadless_rows_per_process_uuid_only()

作用:测试没有线程 ID 的日志按进程 ID 单独限额,不会和同进程的线程日志混在一起算。

数据流:进去的是临时运行时 → 插入两条同进程无线程大日志和一条有线程日志 → 修剪后查询线程日志加无线程日志 → 期望旧的无线程日志被删,但有线程日志还在。

调用关系:它验证 StateRuntime::prune_logs_after_insert 对“线程日志”和“无线程进程日志”使用两套独立预算。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 4 个(default, assert_eq!, remove_dir_all, vec!)。

tests::insert_logs_prunes_single_threadless_process_row_when_it_exceeds_size_limit1052–1087 ↗
async fn insert_logs_prunes_single_threadless_process_row_when_it_exceeds_size_limit()

作用:测试一条无线程、但带进程 ID 的日志如果太大,也会被删除。

数据流:进去的是临时运行时 → 插入一条超过大小限制的无线程进程日志 → 查询无线程日志 → 期望没有返回任何记录。

调用关系:它覆盖 StateRuntime::prune_logs_after_insert 中按 process_uuid 修剪无线程日志的分支。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 3 个(default, assert!, remove_dir_all)。

tests::insert_logs_prunes_threadless_rows_with_null_process_uuid1090–1155 ↗
async fn insert_logs_prunes_threadless_rows_with_null_process_uuid()

作用:测试没有线程 ID、也没有进程 ID 的日志仍然会被当作一个独立分区来限额。

数据流:进去的是临时运行时 → 插入两条无线程无进程的大日志,再插入一条有进程的无线程小日志 → 查询无线程日志 → 期望无进程分区只剩较新一条,有进程那条不受影响。

调用关系:它验证 StateRuntime::prune_logs_after_insert 里专门处理 process_uuid 为 NULL 的分支。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 3 个(default, assert_eq!, remove_dir_all)。

tests::insert_logs_prunes_single_threadless_null_process_row_when_it_exceeds_limit1158–1193 ↗
async fn insert_logs_prunes_single_threadless_null_process_row_when_it_exceeds_limit()

作用:测试无线程、无进程 ID 的单条超大日志也会被删掉。

数据流:进去的是临时运行时 → 插入一条没有 thread_id、没有 process_uuid 的超大日志 → 查询无线程日志 → 期望为空。

调用关系:它补齐了 NULL process_uuid 分区的边界测试,确认这类日志和其他分区一样严格受限。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 3 个(default, assert!, remove_dir_all)。

tests::insert_logs_prunes_old_rows_when_thread_exceeds_row_limit1196–1236 ↗
async fn insert_logs_prunes_old_rows_when_thread_exceeds_row_limit()

作用:测试同一个线程的日志行数超过上限时,会删除最旧的行。

数据流:进去的是临时运行时 → 给同一线程插入 1001 条日志 → 查询该线程 → 期望只剩 1000 条,时间从 2 到 1001。

调用关系:它验证 StateRuntime::prune_logs_after_insert 不只按字节数修剪,也按行数修剪。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 4 个(default, assert_eq!, remove_dir_all, vec!)。

tests::insert_logs_prunes_old_threadless_rows_when_process_exceeds_row_limit1239–1283 ↗
async fn insert_logs_prunes_old_threadless_rows_when_process_exceeds_row_limit()

作用:测试同一进程的无线程日志超过行数上限时,会删掉最旧的记录。

数据流:进去的是临时运行时 → 插入 1001 条同 process_uuid 的无线程日志 → 查询无线程日志并筛出该进程 → 期望只剩 1000 条,最旧的第 1 条被删。

调用关系:它覆盖 StateRuntime::prune_logs_after_insert 对无线程进程分区的行数限制。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 3 个(default, assert_eq!, remove_dir_all)。

tests::insert_logs_prunes_old_threadless_null_process_rows_when_row_limit_exceeded1286–1330 ↗
async fn insert_logs_prunes_old_threadless_null_process_rows_when_row_limit_exceeded()

作用:测试无线程且无进程 ID 的日志超过行数上限时,也会删掉最旧行。

数据流:进去的是临时运行时 → 插入 1001 条 thread_id 和 process_uuid 都为空的日志 → 查询并筛选这类记录 → 期望剩 1000 条,时间从 2 到 1001。

调用关系:它覆盖 StateRuntime::prune_logs_after_insert 中 NULL process_uuid 分区的行数限制。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 3 个(default, assert_eq!, remove_dir_all)。

tests::query_feedback_logs_returns_newest_lines_within_limit_in_order1333–1400 ↗
async fn query_feedback_logs_returns_newest_lines_within_limit_in_order()

作用:测试反馈日志会选取限制内的日志,并按从旧到新的正常阅读顺序输出。

数据流:进去的是临时运行时 → 插入同一线程三条日志 → 调用 StateRuntime::query_feedback_logs → 把返回字节转成字符串后,期望三行按时间 1、2、3 排列。

调用关系:它验证 StateRuntime::query_feedback_logs、StateRuntime::query_feedback_logs_for_threads 和 format_feedback_log_line 的配合结果。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 2 个(assert_eq!, remove_dir_all)。

tests::query_feedback_logs_excludes_oversized_newest_row1403–1450 ↗
async fn query_feedback_logs_excludes_oversized_newest_row()

作用:测试最新日志如果太大,反馈日志不会硬塞进去。

数据流:进去的是临时运行时 → 插入一条小日志和一条更新的超大日志 → 查询反馈日志 → 期望返回空字节,因为按最新优先累计时超大行已经超过预算。

调用关系:它验证 StateRuntime::query_feedback_logs_for_threads 的大小截断规则,尤其是超大最新行的边界情况。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 2 个(assert_eq!, remove_dir_all)。

tests::query_feedback_logs_includes_threadless_rows_from_same_process1453–1548 ↗
async fn query_feedback_logs_includes_threadless_rows_from_same_process()

作用:测试反馈日志会带上同一进程里的无线程日志。这样排查问题时,不会漏掉没有归属到某个对话线程的运行信息。

数据流:进去的是临时运行时 → 插入同进程的无线程日志、线程日志,以及另一个进程的无线程日志 → 查询某线程反馈日志 → 期望包含同进程的三条相关日志,不包含其他进程的日志。

调用关系:它验证 StateRuntime::query_feedback_logs_for_threads 中“线程日志 + 最近进程的无线程日志”的合并逻辑。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 2 个(assert_eq!, remove_dir_all)。

tests::query_feedback_logs_excludes_threadless_rows_from_prior_processes1551–1646 ↗
async fn query_feedback_logs_excludes_threadless_rows_from_prior_processes()

作用:测试一个线程跨进程后,反馈日志只把无线程日志关联到最新进程,而不会把老进程的无线程日志也带上。

数据流:进去的是临时运行时 → 插入旧进程无线程日志、旧进程线程日志、新进程线程日志、新进程无线程日志 → 查询该线程反馈日志 → 期望保留线程自己的旧新日志,但无线程部分只包含新进程。

调用关系:它细化验证 StateRuntime::query_feedback_logs_for_threads 查 latest_processes 的规则:无线程日志按线程最近出现的 process_uuid 来补充。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 2 个(assert_eq!, remove_dir_all)。

tests::query_feedback_logs_keeps_newest_suffix_across_thread_and_threadless_logs1649–1721 ↗
async fn query_feedback_logs_keeps_newest_suffix_across_thread_and_threadless_logs()

作用:测试反馈日志在大小限制下保留的是整体最新的一段,而不是简单按线程或无线程分别截取。

数据流:进去的是临时运行时 → 插入一条较旧的线程大日志和两条较新的无线程大日志 → 查询反馈日志 → 期望较旧线程标记不出现,较新的两个无线程标记出现,并且只有两行。

调用关系:它验证 StateRuntime::query_feedback_logs_for_threads 对线程日志和无线程日志合并后统一按时间、大小截断。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 5 个(from_utf8, assert!, assert_eq!, format!, remove_dir_all)。

tests::query_feedback_logs_for_threads_merges_requested_threads_and_threadless_rows1724–1841 ↗
async fn query_feedback_logs_for_threads_merges_requested_threads_and_threadless_rows()

作用:测试一次请求多个线程反馈日志时,会合并这些线程,以及它们各自最新进程的无线程日志。

数据流:进去的是临时运行时 → 插入三个线程和三个进程的相关日志 → 请求 thread-1 和 thread-2 的反馈日志 → 期望返回这两个线程及 proc-1、proc-2 的无线程日志,不包含 thread-3 或 proc-3。

调用关系:它直接测试 StateRuntime::query_feedback_logs_for_threads 的多线程入口,覆盖 StateRuntime::query_feedback_logs 单线程包装无法覆盖的情况。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 2 个(assert_eq!, remove_dir_all)。

tests::query_feedback_logs_for_threads_returns_empty_for_empty_thread_list1844–1858 ↗
async fn query_feedback_logs_for_threads_returns_empty_for_empty_thread_list()

作用:测试请求的线程列表为空时,反馈日志查询会立刻返回空结果。

数据流:进去的是临时运行时和空线程列表 → 调用 StateRuntime::query_feedback_logs_for_threads → 期望得到空字节数组,不访问或拼接任何实际日志。

调用关系:它验证 StateRuntime::query_feedback_logs_for_threads 的快速返回分支,避免空输入生成奇怪 SQL 或误返回其他日志。

调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 2 个(assert_eq!, remove_dir_all)。