会话遥测与功能专项埋点
这一阶段像系统的“黑匣子”和仪表盘,属于幕后支撑,不直接干活,却把一次会话里发生了什么记清楚。会话遥测记录请求、用户输入、工具调用和连接事件;回合计时拆出模型慢、工具慢还是调度慢;登录环境只报“配没配”,不泄露密钥。app-server、沙箱、工具分发给请求和运行环境贴标签。Guardian、云配置、目标、记忆功能各自记成功失败、耗时和用量。SQLite 启动和指标则记录数据库健康状况。
会话遥测基础
这些文件建立共享的会话级遥测接口,以及其他插桩所依赖的核心计时和环境上下文。
login/src/auth_env_telemetry.rs源码 ↗
程序登录时,经常要靠环境变量(一种由操作系统提供给程序的配置项)拿到 API Key、刷新令牌地址等认证信息。这个文件就像一个安全检查表:它查看几个关键环境变量是否存在、是否为空、某个供应商有没有配置自己的密钥变量,然后把这些结果整理成一份遥测数据。遥测可以理解成“程序运行时的健康和使用情况记录”。这里特别重要的一点是,它不会把真实密钥名字或密钥值直接传出去;比如供应商配置了 env_key,它只记成“configured”,意思是“配过了”,而不是记录原文。这样既能帮助开发者判断用户为什么登录失败,也能降低泄密风险。文件里的结构体 AuthEnvTelemetry 保存检查结果,collect_auth_env_telemetry 负责实际检查,to_otel_metadata 把结果转成 OpenTelemetry(常见的观测数据格式)需要的样子。
AuthEnvTelemetry::to_otel_metadata19–28 ↗
fn to_otel_metadata(&self) -> AuthEnvTelemetryMetadata
作用:把本文件内部使用的认证环境检查结果,转换成遥测系统能接收的格式。有人要把这些信息发给 OpenTelemetry 之类的观测系统时,就会用它。
数据流:进去的是一个 AuthEnvTelemetry 对象,里面已经有各种“环境变量是否存在”的布尔值和少量脱敏信息。函数把这些字段逐个复制到 AuthEnvTelemetryMetadata 里,其中字符串字段会克隆一份。出来的是一份新的遥测元数据对象,原来的 AuthEnvTelemetry 不会被改动。
调用关系:它位于“收集完信息之后、上报之前”这一步。collect_auth_env_telemetry 先生成 AuthEnvTelemetry,后续如果要交给观测系统,就通过 AuthEnvTelemetry::to_otel_metadata 换成外部遥测库认识的类型。
collect_auth_env_telemetry31–43 ↗
fn collect_auth_env_telemetry(
provider: &ModelProviderInfo,
codex_api_key_env_enabled: bool,
) -> AuthEnvTelemetry
作用:检查认证相关的环境配置是否存在,并整理成一份安全的遥测记录。它让程序能知道用户大概配置了哪些认证入口,但不暴露真正的密钥。
数据流:进去的是一个模型供应商配置 provider,以及一个开关 codex_api_key_env_enabled,表示 Codex API Key 环境变量是否被启用。函数会读取几个固定环境变量,比如 OpenAI API Key、Codex API Key、刷新令牌地址覆盖项,也会查看 provider 里是否声明了自己的 env_key。它把每项检查结果填进 AuthEnvTelemetry 返回;如果 provider 有 env_key,只记录 provider_env_key_name 为“configured”,不会返回真实名字或密钥内容。
调用关系:它是这个文件的核心入口。创建认证相关对象时,new 和 new_with_provider 会调用它来收集现场信息;测试函数 tests::collect_auth_env_telemetry_buckets_provider_env_key_name 也会调用它确认脱敏行为。它自己把“某个环境变量是否真的有值”的小检查交给 env_var_present 来做。
调用图:调用 1 个内部函数(env_var_present);被 3 处调用(new, collect_auth_env_telemetry_buckets_provider_env_key_name, new_with_provider)。
env_var_present45–51 ↗
fn env_var_present(name: &str) -> bool
作用:判断某个环境变量是不是“真的配置了”。它不关心变量的具体内容,只关心是否存在且不是空白。
数据流:进去的是环境变量名字。函数向操作系统读取这个变量:如果读到了普通字符串,就去掉前后空白后判断是否为空;如果变量不存在,就返回 false;如果变量存在但内容不是合法 Unicode 字符串,就保守地认为它存在,返回 true。出来的是一个布尔值,表示这个变量能不能算作已配置。
调用关系:它是 collect_auth_env_telemetry 的底层小工具,专门负责把操作系统环境变量的各种情况统一成“有”或“没有”。它内部调用标准库的 std::env::var,也就是向系统要环境变量值。
调用图:被 1 处调用(collect_auth_env_telemetry);外部调用 1 个(var)。
tests::collect_auth_env_telemetry_buckets_provider_env_key_name60–88 ↗
fn collect_auth_env_telemetry_buckets_provider_env_key_name()
作用:这是一个测试,用来确认供应商自定义的 env_key 不会被原样写进遥测数据。它保护的是安全边界:遥测里只能出现“configured”,不能泄露可能敏感的配置名或内容。
数据流:进去的是测试代码手工构造的 ModelProviderInfo,其中 env_key 故意写成“sk-should-not-leak”。测试调用 collect_auth_env_telemetry 收集信息,然后检查 telemetry.provider_env_key_name 是否等于 Some("configured")。结果是:如果函数正确脱敏,测试通过;如果把原值泄露出来,测试失败。
调用关系:它只在测试时运行,不参与正式登录流程。它调用 collect_auth_env_telemetry,并用 assert_eq! 做断言,确保这个文件最重要的隐私行为不会被以后改代码时破坏。
调用图:调用 1 个内部函数(collect_auth_env_telemetry);外部调用 1 个(assert_eq!)。
otel/src/events/session_telemetry.rs源码 ↗
这份文件定义了 SessionTelemetry,也就是“会话遥测器”。遥测可以理解成给程序装仪表盘:它不会直接完成聊天或调用工具,但会记录每一步发生了什么、花了多久、有没有失败。没有它,线上出了慢、断流、鉴权失败、工具报错等问题时,开发者就很难知道卡在哪里。它保存一份会话元信息,比如模型名、账号、来源、版本和终端类型;还可以接上指标客户端,把计数器、耗时直方图和追踪事件发出去。文件里的方法大多是围绕具体场景做记录:启动阶段、API 请求、WebSocket/SSE 流事件、插件安装提示、用户提示词、工具执行结果等。重要的是,它会尽量不让“记录失败”影响主流程:指标写失败通常只打警告,不中断用户正在做的事。
trace_field_value69–73 ↗
fn trace_field_value(fields: &'a [(&str, &str)], key: &str) -> Option<&'a str>
作用:从一组“字段名和值”里找出指定字段的值。它像在一张小表格里按列名查内容。
数据流:输入是一组字段键值对和一个要找的键名 → 它逐个比较字段名 → 找到就返回对应字符串,找不到就返回空结果,不改动任何数据。
调用关系:它被 SessionTelemetry::tool_result_with_tags 使用,用来从额外追踪字段里取出 mcp_server、mcp_server_origin 这类信息,好决定工具结果日志里该写什么。
调用图:被 1 处调用(tool_result_with_tags)。
SessionTelemetry::with_auth_env110–113 ↗
fn with_auth_env(mut self, auth_env: AuthEnvTelemetryMetadata) -> Self
作用:给一个已有的会话遥测器补上“鉴权环境”信息,比如环境变量里有没有 API Key。常用于创建后再把启动时探测到的登录环境塞进去。
数据流:输入是当前 SessionTelemetry 和一份 AuthEnvTelemetryMetadata → 它替换 metadata.auth_env → 输出更新后的 SessionTelemetry。
调用关系:这是构建 SessionTelemetry 时的链式配置方法;后面的 conversation_starts、record_api_request、record_websocket_connect 等记录会读取这些鉴权环境信息。
SessionTelemetry::with_model115–119 ↗
fn with_model(mut self, model: &str, slug: &str) -> Self
作用:更新这次会话使用的模型名和模型标识。配置或运行中模型被覆盖时,可以用它让遥测记录跟实际模型一致。
数据流:输入是当前 SessionTelemetry、model 和 slug → 它把这两个字符串写入会话元信息 → 输出更新后的 SessionTelemetry。
调用关系:这是会话遥测器的配置入口之一;后续指标标签和事件会带上这里设置的模型信息。
SessionTelemetry::with_metrics_service_name121–124 ↗
fn with_metrics_service_name(mut self, service_name: &str) -> Self
作用:设置指标里使用的服务名。它会先清洗服务名,避免指标系统不接受奇怪字符。
数据流:输入是当前 SessionTelemetry 和 service_name → 它调用 sanitize_metric_tag_value 清洗字符串 → 把结果存进 metadata.service_name → 输出更新后的 SessionTelemetry。
调用关系:它服务于指标标签生成;之后 metadata_tag_refs 会把 service_name 放进通用指标标签里。
调用图:外部调用 1 个(sanitize_metric_tag_value)。
SessionTelemetry::with_metrics126–130 ↗
fn with_metrics(mut self, metrics: MetricsClient) -> Self
作用:给会话遥测器接上一个指标客户端,让它真的能发计数和耗时指标。默认也会把会话元信息作为指标标签带上。
数据流:输入是当前 SessionTelemetry 和 MetricsClient → 它保存这个客户端,并开启 metadata 标签 → 输出更新后的 SessionTelemetry。
调用关系:SessionTelemetry::with_metrics_config 和 SessionTelemetry::with_provider_metrics 都会调用它,把刚创建或从 provider 拿到的指标客户端装到会话遥测器上。
调用图:被 2 处调用(with_metrics_config, with_provider_metrics)。
SessionTelemetry::with_metrics_without_metadata_tags132–136 ↗
fn with_metrics_without_metadata_tags(mut self, metrics: MetricsClient) -> Self
作用:接上指标客户端,但不自动给每条指标加会话元信息标签。适合不想让 model、来源、版本等字段影响指标维度的场景。
数据流:输入是当前 SessionTelemetry 和 MetricsClient → 它保存客户端,并关闭 metadata 标签 → 输出更新后的 SessionTelemetry。
调用关系:这是 with_metrics 的变体;后续 counter、histogram、record_duration 仍能发指标,但 tags_with_metadata 不会再追加会话级标签。
SessionTelemetry::with_metrics_config138–141 ↗
fn with_metrics_config(self, config: MetricsConfig) -> MetricsResult<Self>
作用:用一份指标配置创建指标客户端,并装到当前会话遥测器上。调用方不用自己手动创建 MetricsClient。
数据流:输入是当前 SessionTelemetry 和 MetricsConfig → 它创建 MetricsClient → 成功后交给 with_metrics → 输出带指标能力的 SessionTelemetry;创建失败则返回错误。
调用关系:它把配置层和会话遥测连接起来;内部把真正安装客户端的动作交给 SessionTelemetry::with_metrics。
调用图:调用 2 个内部函数(with_metrics, new)。
SessionTelemetry::with_provider_metrics143–148 ↗
fn with_provider_metrics(self, provider: &OtelProvider) -> Self
作用:从 OtelProvider 里拿指标客户端,并接到会话遥测器上。OtelProvider 可以理解成 OpenTelemetry 的总开关和资源提供者。
数据流:输入是当前 SessionTelemetry 和 provider → 它询问 provider.metrics() 是否有指标客户端 → 有就 clone 后交给 with_metrics,没有就原样返回。
调用关系:它在遥测 provider 已经初始化后使用;如果 provider 提供指标,它把后续发指标的能力交给 SessionTelemetry::with_metrics 打开。
调用图:调用 2 个内部函数(with_metrics, metrics)。
SessionTelemetry::counter150–163 ↗
fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)])
作用:记录一个“次数类”指标,比如 API 调了几次、工具执行了几次。它是这个文件里很多统计的基础按钮。
数据流:输入是指标名、增加值和标签 → 如果没有指标客户端就什么也不做 → 如果有,就合并会话元信息标签并调用 MetricsClient.counter → 失败时只写警告日志。
调用关系:它被很多地方调用,包括工具、网络、压缩、守护审查等统计;本文件里的 record_api_request、record_websocket_event、sse_event、tool_result_with_tags 等也通过它记次数。
调用图:被 17 处调用(force_http_fallback, emit_guardian_review_metrics, emit_compact_metric, emit_turn_memory_metric, emit_turn_network_proxy_metric, emit_unified_exec_tty_metric, emit_metrics, counter, counter, record_api_request (+7 more));外部调用 1 个(warn!)。
SessionTelemetry::histogram165–178 ↗
fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)])
作用:记录一个数值分布指标,比如 token 数量这类需要看大致范围和分布的数据。直方图可以理解成把很多数值按区间装桶。
数据流:输入是指标名、数值和标签 → 如果没有指标客户端就跳过 → 如果有,就合并会话标签并写入 MetricsClient.histogram → 出错只打警告。
调用关系:它被守护审查 token 使用量等统计调用;作用和 counter 类似,但记录的是数值大小而不是次数。
调用图:被 3 处调用(emit_guardian_token_usage_histograms, histogram, histogram);外部调用 1 个(warn!)。
SessionTelemetry::record_duration180–193 ↗
fn record_duration(&self, name: &str, duration: Duration, tags: &[(&str, &str)])
作用:记录一段耗时,比如请求用了多久、工具跑了多久。它是性能观测的通用入口。
数据流:输入是指标名、Duration 耗时和标签 → 如果指标不可用就跳过 → 否则合并会话标签并调用 MetricsClient.record_duration → 失败时写警告但不中断流程。
调用关系:它被启动阶段、API 请求、WebSocket/SSE 事件、工具调用和 Responses API 细分耗时记录复用,是本文件的核心耗时记录工具。
调用图:被 11 处调用(emit_guardian_review_metrics, resolve, record_api_request, record_responses_websocket_timing_metrics, record_startup_phase, record_turn_ttft, record_websocket_event, record_websocket_request, sse_event, sse_event_failed (+1 more));外部调用 1 个(warn!)。
SessionTelemetry::record_startup_phase196–218 ↗
fn record_startup_phase(
&self,
phase: &'static str,
duration: Duration,
status: Option<&'static str>,
)
作用:记录启动过程某个阶段花了多久,以及是否成功。它帮助开发者知道程序启动慢是慢在哪一段。
数据流:输入是阶段名、耗时和可选状态 → 它组装 phase/status 标签 → 调用 record_duration 写启动耗时指标 → 再写一条日志和追踪事件。
调用关系:它会在启动解析等流程中被 resolve 调用;内部把指标写入交给 SessionTelemetry::record_duration,把事件写入交给日志/追踪宏。
调用图:调用 1 个内部函数(record_duration);被 1 处调用(resolve);外部调用 2 个(log_and_trace_event!, vec!)。
SessionTelemetry::record_turn_ttft221–232 ↗
fn record_turn_ttft(&self, duration: Duration)
作用:记录一轮对话从开始到第一个 token 出现的时间。TTFT 是 Time To First Token,意思是“等到第一段回复出来要多久”。
数据流:输入是一段 Duration → 它写入 TURN_TTFT_DURATION_METRIC 指标 → 同时发出 codex.turn_ttft 日志和追踪事件。
调用关系:它用于对话响应性能观察;内部复用 SessionTelemetry::record_duration,保证指标和事件两边都有记录。
调用图:调用 1 个内部函数(record_duration);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::record_plugin_install_elicitation_sent235–257 ↗
fn record_plugin_install_elicitation_sent(
&self,
tool_type: &str,
tool_id: &str,
tool_name: &str,
)
作用:记录系统向用户发出了插件或连接器安装询问。这样可以统计哪些工具安装提示被展示过。
数据流:输入是工具类型、工具 ID 和工具名 → 它给安装询问计数指标加 1,并按工具类型打标签 → 然后写日志和追踪事件,带上工具信息。
调用关系:它面向插件安装提示流程;内部用 SessionTelemetry::counter 记次数,再用统一事件宏把细节写出去。
调用图:调用 1 个内部函数(counter);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::record_plugin_install_suggestion260–293 ↗
fn record_plugin_install_suggestion(
&self,
tool_type: &str,
tool_id: &str,
tool_name: &str,
response_action: &str,
user_confirmed: bool,
comple
作用:记录一次插件或连接器安装建议的结果,比如用户是否确认、流程是否完成。它用来衡量建议是否真的转化成安装。
数据流:输入是工具信息、响应动作、用户是否确认、是否完成 → 它把 completed 转成 true/false 标签 → 增加建议计数指标 → 写一条包含完整结果的日志和追踪事件。
调用关系:它接在安装建议被展示和处理之后;内部通过 SessionTelemetry::counter 写统计,通过事件宏写可排查的上下文。
调用图:调用 1 个内部函数(counter);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::start_timer295–301 ↗
fn start_timer(&self, name: &str, tags: &[(&str, &str)]) -> Result<Timer, MetricsError>
作用:启动一个计时器,让调用方稍后自动或手动记录耗时。适合包住一段还没执行完的操作。
数据流:输入是指标名和标签 → 如果没有指标客户端就返回 ExporterDisabled 错误 → 否则调用 tags_with_metadata 合并标签 → 让 MetricsClient 创建 Timer 并返回。
调用关系:它被外层 start_timer 包装调用;内部依赖 SessionTelemetry::tags_with_metadata 给计时器补齐会话标签。
调用图:调用 1 个内部函数(tags_with_metadata);被 2 处调用(start_timer, start_timer)。
SessionTelemetry::shutdown_metrics303–308 ↗
fn shutdown_metrics(&self) -> MetricsResult<()>
作用:关闭指标客户端,把该收尾的指标导出工作做完。通常用于程序退出或遥测结束时。
数据流:输入是当前 SessionTelemetry → 如果没有指标客户端就返回成功 → 如果有,就调用 metrics.shutdown() → 返回关闭结果。
调用关系:它处在收尾阶段;和 snapshot_metrics 不同,它不是读取快照,而是让指标系统完成关闭。
SessionTelemetry::snapshot_metrics310–315 ↗
fn snapshot_metrics(&self) -> MetricsResult<ResourceMetrics>
作用:抓取当前指标快照。快照像给仪表盘拍一张照片,方便调试或测试查看当前累计值。
数据流:输入是当前 SessionTelemetry → 如果没有指标客户端就返回 ExporterDisabled 错误 → 如果有,就调用 metrics.snapshot() → 输出 ResourceMetrics。
调用关系:它被 SessionTelemetry::reset_runtime_metrics 和 SessionTelemetry::runtime_metrics_summary 调用,分别用于重置增量和生成运行时摘要。
调用图:被 2 处调用(reset_runtime_metrics, runtime_metrics_summary)。
SessionTelemetry::reset_runtime_metrics318–325 ↗
fn reset_runtime_metrics(&self)
作用:读取并丢弃一次运行时指标快照,用来清掉某些增量累计。可以理解成把临时计数器归零。
数据流:输入是当前 SessionTelemetry → 没有指标客户端就直接返回 → 有的话调用 snapshot_metrics → 如果失败,只写 debug 日志,不打断程序。
调用关系:它依赖 SessionTelemetry::snapshot_metrics;主要用于调试或采样前的准备,让下一次摘要更像“从现在开始统计”。
调用图:调用 1 个内部函数(snapshot_metrics);外部调用 1 个(debug!)。
SessionTelemetry::runtime_metrics_summary328–341 ↗
fn runtime_metrics_summary(&self) -> Option<RuntimeMetricsSummary>
作用:生成一份运行时指标摘要,供调试查看。摘要比完整快照更简短,适合展示给开发者。
数据流:输入是当前 SessionTelemetry → 调用 snapshot_metrics 取快照 → 用 RuntimeMetricsSummary::from_snapshot 转成摘要 → 如果摘要为空则返回 None,否则返回摘要。
调用关系:它把底层 ResourceMetrics 转成更好读的 RuntimeMetricsSummary;依赖 SessionTelemetry::snapshot_metrics 拿原始数据。
调用图:调用 2 个内部函数(snapshot_metrics, from_snapshot)。
SessionTelemetry::tags_with_metadata343–350 ↗
fn tags_with_metadata(
&'a self,
tags: &'a [(&'a str, &'a str)],
) -> MetricsResult<Vec<(&'a str, &'a str)>>
作用:把调用方传来的指标标签和会话自带标签合在一起。这样每条指标都能知道它来自哪个模型、哪个来源、哪个版本等。
数据流:输入是额外标签列表 → 它先调用 metadata_tag_refs 取会话标签 → 再把额外标签追加进去 → 输出合并后的标签数组。
调用关系:它被 SessionTelemetry::start_timer 使用;counter、histogram、record_duration 也在逻辑上做同样的标签合并。它把具体会话标签来源交给 metadata_tag_refs。
调用图:调用 1 个内部函数(metadata_tag_refs);被 1 处调用(start_timer)。
SessionTelemetry::metadata_tag_refs352–365 ↗
fn metadata_tag_refs(&self) -> MetricsResult<Vec<(&str, &str)>>
作用:生成会话级指标标签,比如 auth_mode、session_source、originator、service_name、model、app_version。标签能帮助后台按维度筛选指标。
数据流:输入是当前 SessionTelemetry → 如果关闭了 metadata 标签,就返回空列表 → 否则从 metadata 取字段,交给 SessionMetricTagValues.into_tags() 校验和转换 → 输出标签列表或错误。
调用关系:它被 SessionTelemetry::tags_with_metadata 调用,是所有会话级指标标签的来源。
调用图:被 1 处调用(tags_with_metadata);外部调用 1 个(new)。
SessionTelemetry::new368–399 ↗
fn new(
conversation_id: ThreadId,
model: &str,
slug: &str,
account_id: Option<String>,
account_email: Option<String>,
auth_mode: Option<TelemetryAuthMo
作用:创建一份新的会话遥测器,并填好会话的基本身份信息。每次新对话通常都需要这样一个记录器。
数据流:输入包括 conversation_id、模型、账号、鉴权模式、来源、是否记录用户提示词、终端类型和会话来源 → 它清洗 originator,填入版本号和默认鉴权环境,并尝试使用全局指标客户端 → 输出 SessionTelemetry。
调用关系:它被测试、会话创建和响应流等多处调用,是 SessionTelemetry 的主要入口;后续可用 with_auth_env、with_model、with_metrics 等方法继续补配置。
调用图:调用 1 个内部函数(global);被 25 处调用(test_session_telemetry, test_session_telemetry, new, session_telemetry, test_session_telemetry_without_metadata, test_session_telemetry, responses_respects_model_info_overrides_from_config, responses_stream_includes_subagent_header_on_other, responses_stream_includes_subagent_header_on_review, azure_responses_request_includes_store_and_reasoning_ids (+15 more));外部调用 4 个(to_string, sanitize_metric_tag_value, env!, default)。
SessionTelemetry::record_responses401–436 ↗
fn record_responses(&self, handle_responses_span: &Span, event: &ResponseEvent)
作用:把 Responses API 返回的事件信息写到当前追踪 Span 里。Span 可以理解成一次操作的时间线记录。
数据流:输入是一个追踪 Span 和一个 ResponseEvent → 它先用 responses_type 给事件分类并写入 otel.name → 如果是输出项,会记录来源和工具名;如果是完成事件,会记录 token 用量 → 不返回值,只更新 Span。
调用关系:它在处理 Responses API 事件时调用;内部用 SessionTelemetry::responses_type 和 responses_item_type 判断事件类型。
调用图:调用 1 个内部函数(responses_type);外部调用 1 个(record)。
SessionTelemetry::conversation_starts439–475 ↗
fn conversation_starts(
&self,
provider_name: &str,
reasoning_effort: Option<ReasoningEffort>,
reasoning_summary: ReasoningSummary,
context_window: Option<i64>,
作用:记录一次会话刚开始时的配置和环境。它像开局登记表,后面排查问题时能知道这局用的模型、权限策略、沙箱策略和服务器配置。
数据流:输入包括 provider 名、推理强度、摘要设置、上下文窗口、压缩阈值、审批策略、沙箱策略和 MCP 服务器列表 → 它连同鉴权环境一起写成日志和追踪事件 → 日志里记录服务器名列表,追踪里记录服务器数量。
调用关系:它通常在对话开始时调用;不负责改变会话,只把启动上下文送进统一遥测管道。
调用图:外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::log_request477–508 ↗
async fn log_request(&self, attempt: u64, f: F) -> Result<Response, Error>
作用:包住一次 HTTP 请求,自动测量耗时并记录结果。调用方只需要把真正发请求的异步函数交给它。
数据流:输入是尝试次数 attempt 和一个会返回 reqwest::Response 的异步函数 → 它记录开始时间,等待请求完成,计算耗时 → 从成功响应或错误里提取状态码和错误文字 → 调用 record_api_request 记录 → 最后原样返回请求结果。
调用关系:它是便捷包装器;真正的 API 请求遥测由 SessionTelemetry::record_api_request 完成。
调用图:调用 1 个内部函数(record_api_request);外部调用 1 个(now)。
SessionTelemetry::record_api_request511–571 ↗
fn record_api_request(
&self,
attempt: u64,
status: Option<u16>,
error: Option<&str>,
duration: Duration,
auth_header_attached: bool,
auth_heade
作用:记录一次普通 API 请求的成败、耗时、状态码和鉴权细节。它是排查 HTTP 请求失败、401 恢复、服务端错误的重要记录点。
数据流:输入包括尝试次数、HTTP 状态码、错误、耗时、鉴权头信息、恢复信息、接口名、request_id、cf_ray 和鉴权错误 → 它判断 2xx 且无错误才算成功 → 写 API 调用次数和耗时指标 → 写一条包含网络和鉴权上下文的日志/追踪事件。
调用关系:它被 on_request 和 SessionTelemetry::log_request 调用;内部用 SessionTelemetry::counter 和 record_duration 写指标。
调用图:调用 2 个内部函数(counter, record_duration);被 2 处调用(on_request, log_request);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::record_websocket_connect574–625 ↗
fn record_websocket_connect(
&self,
duration: Duration,
status: Option<u16>,
error: Option<&str>,
auth_header_attached: bool,
auth_header_name: Option<&
作用:记录 WebSocket 连接建立过程的结果。WebSocket 是一种长连接,常用于持续接收模型返回事件。
数据流:输入包括连接耗时、状态码、错误、鉴权头、恢复信息、endpoint、连接是否复用、request_id、cf_ray 和鉴权错误 → 它根据错误和状态码判断成功 → 写 codex.websocket_connect 日志和追踪事件。
调用关系:它被 connect_websocket 调用;和 record_websocket_request、record_websocket_event 一起覆盖 WebSocket 从连接、发送请求到接收事件的完整链路。
调用图:被 1 处调用(connect_websocket);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::record_websocket_request627–662 ↗
fn record_websocket_request(
&self,
duration: Duration,
error: Option<&str>,
connection_reused: bool,
)
作用:记录一次通过 WebSocket 发出的请求是否成功以及花了多久。它关注的是长连接上的单次请求,而不是连接本身。
数据流:输入是耗时、可选错误和连接是否复用 → 没有错误就算成功 → 写请求次数指标和耗时指标 → 再写日志/追踪事件,附带鉴权环境和连接复用信息。
调用关系:它被 on_ws_request 调用;内部通过 SessionTelemetry::counter 和 record_duration 把请求统计送入指标系统。
调用图:调用 2 个内部函数(counter, record_duration);被 1 处调用(on_ws_request);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::record_auth_recovery665–694 ↗
fn record_auth_recovery(
&self,
mode: &str,
step: &str,
outcome: &str,
request_id: Option<&str>,
cf_ray: Option<&str>,
auth_error: Option<&str>,
作用:记录一次鉴权恢复过程,比如遇到未授权后尝试刷新或切换凭据。它帮助判断登录状态修复有没有生效。
数据流:输入是恢复模式、步骤、结果、request_id、cf_ray、鉴权错误、错误码、恢复原因和鉴权状态是否变化 → 它把这些字段写成 codex.auth_recovery 日志和追踪事件 → 不返回额外结果。
调用关系:它被 handle_unauthorized 调用;通常发生在请求收到未授权错误后的恢复流程中。
调用图:被 1 处调用(handle_unauthorized);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::record_websocket_event696–790 ↗
fn record_websocket_event(
&self,
result: &Result<
Option<
Result<
tokio_tungstenite::tungstenite::Message,
tokio_tu
作用:记录从 WebSocket 收到的每个事件,判断它是什么类型、是否成功、有没有解析失败。它还能从特殊 timing 事件里提取服务端细分耗时。
数据流:输入是 WebSocket 读取结果和耗时 → 它处理文本、二进制、关闭、错误、空流等情况 → 文本会尝试解析 JSON 并读取 type 字段;遇到 timing 事件会调用 record_responses_websocket_timing_metrics;遇到 response.failed 会标记失败 → 最后写事件次数、耗时指标和日志/追踪事件。
调用关系:它被 on_ws_event 调用;内部用 counter、record_duration 记通用事件统计,并把 Responses API 的特殊耗时交给 record_responses_websocket_timing_metrics。
调用图:调用 3 个内部函数(counter, record_duration, record_responses_websocket_timing_metrics);被 1 处调用(on_ws_event);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::log_sse_event792–841 ↗
fn log_sse_event(
&self,
response: &Result<Option<Result<StreamEvent, StreamError<E>>>, Elapsed>,
duration: Duration,
)
作用:记录一次 SSE 流事件的读取结果。SSE 是 Server-Sent Events,意思是服务器持续往客户端推送一条条文本事件。
数据流:输入是一次 SSE 轮询结果和耗时 → 如果收到正常事件,会按事件内容判断完成、失败、输出项解析是否正常 → 成功时调用 sse_event,失败或超时时调用 sse_event_failed → 如果流暂时没有事件则不记录。
调用关系:它被 on_sse_poll 调用;本身负责分类,实际成功和失败的指标/日志分别交给 SessionTelemetry::sse_event 和 sse_event_failed。
调用图:调用 2 个内部函数(sse_event, sse_event_failed);被 1 处调用(on_sse_poll)。
SessionTelemetry::sse_event843–860 ↗
fn sse_event(&self, kind: &str, duration: Duration)
作用:记录一个成功的 SSE 事件。它会统计事件类型和耗时。
数据流:输入是事件类型 kind 和耗时 → 它写 SSE 事件次数指标,success=true → 写 SSE 事件耗时指标 → 写 codex.sse_event 日志。
调用关系:它只被 SessionTelemetry::log_sse_event 调用,用于正常解析并处理成功的 SSE 事件。
调用图:调用 2 个内部函数(counter, record_duration);被 1 处调用(log_sse_event);外部调用 1 个(log_event!)。
SessionTelemetry::sse_event_failed862–899 ↗
fn sse_event_failed(&self, kind: Option<&String>, duration: Duration, error: &T)
作用:记录一个失败的 SSE 事件,比如解析失败、流错误或等待超时。它把错误信息写进日志和追踪,方便排查流式响应为什么断了。
数据流:输入是可选事件类型、耗时和错误对象 → 如果没有类型就用 unknown → 写 success=false 的 SSE 次数和耗时指标 → 根据是否有 kind 写日志 → 再写追踪事件。
调用关系:它被 SessionTelemetry::log_sse_event 调用;和 sse_event 成对出现,一个处理成功,一个处理失败。
调用图:调用 2 个内部函数(counter, record_duration);被 1 处调用(log_sse_event);外部调用 2 个(log_event!, trace_event!)。
SessionTelemetry::see_event_completed_failed901–915 ↗
fn see_event_completed_failed(&self, error: &T)
作用:记录 response.completed 这个完成事件本身处理失败了。函数名里 see 看起来像 sse 的拼写误差,但行为是记录 SSE completed 失败。
数据流:输入是一个可显示的错误 → 它写 codex.sse_event 事件,kind 固定为 response.completed,并带上错误消息 → 不改动状态。
调用关系:它被 map_response_events 调用;用于把响应完成阶段的异常写入统一日志和追踪。
调用图:被 1 处调用(map_response_events);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::sse_event_completed917–939 ↗
fn sse_event_completed(
&self,
input_token_count: i64,
output_token_count: i64,
cached_token_count: Option<i64>,
reasoning_token_count: Option<i64>,
too
作用:记录 SSE 流里的 response.completed 完成事件,并带上 token 用量。token 可以理解成模型处理文本时的基本小片段。
数据流:输入是输入 token、输出 token、缓存 token、推理 token 和工具 token 数量 → 它把这些数字写成 codex.sse_event 日志和追踪事件,kind 固定为 response.completed。
调用关系:它被 map_response_events 调用;用于在响应结束时留下用量信息,和 record_responses 里的 Span token 记录互补。
调用图:被 1 处调用(map_response_events);外部调用 1 个(log_and_trace_event!)。
SessionTelemetry::user_prompt941–982 ↗
fn user_prompt(&self, items: &[UserInput])
作用:记录用户这轮输入的基本情况。它可以选择记录原文,也可以出于隐私只记录长度和类型数量。
数据流:输入是一组 UserInput → 它拼接所有文本输入,统计文本、图片、本地图片数量 → 如果 log_user_prompts 为真就记录原文,否则记录 [REDACTED] → 日志写提示词长度和可选原文,追踪写输入类型计数。
调用关系:它在用户提交提示词时使用;依赖会话元信息里的 log_user_prompts 决定是否保留敏感内容。
调用图:外部调用 3 个(iter, log_event!, trace_event!)。
SessionTelemetry::tool_decision984–999 ↗
fn tool_decision(
&self,
tool_name: &str,
call_id: &str,
decision: &ReviewDecision,
source: ToolDecisionSource,
)
作用:记录一次工具调用审批决定,比如允许、拒绝或由谁做出的决定。它帮助审计为什么某个工具被执行或没被执行。
数据流:输入是工具名、调用 ID、审批决定和决策来源 → 它把决定转成小写字符串,把来源转成字符串 → 写 codex.tool_decision 日志。
调用关系:它被 request_approval 调用;通常发生在工具执行前的用户或策略审批阶段。
调用图:被 1 处调用(request_approval);外部调用 1 个(log_event!)。
SessionTelemetry::sandbox_outcome1001–1030 ↗
fn sandbox_outcome(
&self,
tool_name: &str,
call_id: &str,
outcome: &str,
initial_duration: Duration,
escalated_duration: Option<Duration>,
)
作用:记录工具在沙箱里的执行结果和耗时。沙箱可以理解成隔离的小房间,用来限制工具对系统的影响。
数据流:输入是工具名、调用 ID、结果、初始执行耗时和可选升级执行耗时 → 它把耗时转成毫秒并限制在 i64 最大值内 → 写日志和追踪事件。
调用关系:它用于工具沙箱执行结束后;和 tool_result_with_tags 关注工具输出不同,它更关注沙箱策略下的执行结果和时间。
调用图:外部调用 3 个(as_millis, log_event!, trace_event!)。
SessionTelemetry::log_tool_result_with_tags1033–1068 ↗
async fn log_tool_result_with_tags(
&self,
tool_name: &str,
call_id: &str,
arguments: &str,
extra_tags: &[(&str, &str)],
extra_trace_fields: &[(&str, &s
作用:包住一次异步工具执行,自动测量耗时并记录工具结果。调用方把真正执行工具的函数交给它即可。
数据流:输入是工具名、调用 ID、参数、额外指标标签、额外追踪字段和异步执行函数 → 它记录开始时间,等待工具执行完成,计算耗时 → 成功时取输出预览和成功标记,失败时把错误转成输出文本且 success=false → 调用 tool_result_with_tags 记录 → 最后原样返回工具执行结果。
调用关系:它是工具执行遥测的便捷包装器;具体写指标、日志和追踪的工作交给 SessionTelemetry::tool_result_with_tags。
调用图:调用 1 个内部函数(tool_result_with_tags);外部调用 3 个(Borrowed, Owned, now)。
SessionTelemetry::log_tool_failed1070–1092 ↗
fn log_tool_failed(&self, tool_name: &str, error: &str)
作用:记录一个还没真正跑起来就失败的工具调用。比如参数准备失败或工具无法启动时,可以用它留下失败日志。
数据流:输入是工具名和错误文本 → 它用 0 毫秒耗时、success=false 写 codex.tool_result 日志 → 追踪里记录错误长度、行数、工具来源 builtin 和错误消息。
调用关系:它和 tool_result_with_tags 都记录工具结果;区别是这里没有真实执行耗时和调用 ID,适合早期失败。
调用图:外部调用 2 个(log_event!, trace_event!)。
SessionTelemetry::tool_result_with_tags1095–1141 ↗
fn tool_result_with_tags(
&self,
tool_name: &str,
call_id: &str,
arguments: &str,
duration: Duration,
success: bool,
output: &str,
extra
作用:正式记录一次工具调用的结果,包括次数、耗时、参数长度、输出长度、是否来自 MCP 服务器等。MCP 可以理解成外部工具服务接入协议。
数据流:输入是工具名、调用 ID、参数、耗时、是否成功、输出文本、额外指标标签和额外追踪字段 → 它组装 tool/success 标签并追加额外标签 → 写工具调用次数和耗时指标 → 用 trace_field_value 查 mcp_server 信息 → 写详细日志和追踪事件。
调用关系:它被 SessionTelemetry::log_tool_result_with_tags 调用;内部复用 counter、record_duration 和 trace_field_value,是工具结果遥测的核心实现。
调用图:调用 3 个内部函数(counter, record_duration, trace_field_value);被 1 处调用(log_tool_result_with_tags);外部调用 3 个(with_capacity, log_event!, trace_event!)。
SessionTelemetry::record_responses_websocket_timing_metrics1143–1193 ↗
fn record_responses_websocket_timing_metrics(&self, value: &serde_json::Value)
作用:从 WebSocket 的特殊 timing 事件里提取 Responses API 的细分耗时。这样能看清时间花在接口开销、推理、首 token、逐 token 等哪一段。
数据流:输入是一段 JSON 值 → 它读取 timing_metrics 字段 → 分别查找 overhead、inference、engine iapi/service 的 TTFT 和 TBT 等毫秒字段 → 每个字段用 duration_from_ms_value 转成 Duration,成功后写对应耗时指标。
调用关系:它被 SessionTelemetry::record_websocket_event 在遇到 responsesapi.websocket_timing 事件时调用;内部把每个合法耗时交给 record_duration。
调用图:调用 2 个内部函数(record_duration, duration_from_ms_value);被 1 处调用(record_websocket_event);外部调用 1 个(get)。
SessionTelemetry::responses_type1195–1216 ↗
fn responses_type(event: &ResponseEvent) -> String
作用:把 ResponseEvent 转成一个简短类型名,方便写到追踪里。比如 completed、text_delta、function_call 等。
数据流:输入是 ResponseEvent → 它按事件枚举分支匹配 → 对输出项事件会继续调用 responses_item_type 细分 → 输出一个字符串类型名。
调用关系:它被 SessionTelemetry::record_responses 调用;当事件里包含 ResponseItem 时,它把细分判断交给 SessionTelemetry::responses_item_type。
调用图:调用 1 个内部函数(responses_item_type);被 1 处调用(record_responses)。
SessionTelemetry::responses_item_type1218–1237 ↗
fn responses_item_type(item: &ResponseItem) -> String
作用:把 Responses API 的输出项转成简短类型名,比如消息、推理、函数调用、网页搜索、图片生成等。
数据流:输入是 ResponseItem → 它按输出项种类匹配 → 对普通消息会带上 role,生成类似 message_from_assistant 的名字 → 输出类型字符串。
调用关系:它被 SessionTelemetry::responses_type 调用,用来细化 OutputItemDone 和 OutputItemAdded 这类事件。
调用图:被 1 处调用(responses_type);外部调用 1 个(format!)。
duration_from_ms_value1240–1251 ↗
fn duration_from_ms_value(value: Option<&serde_json::Value>) -> Option<Duration>
作用:把 JSON 里的毫秒数转换成 Rust 的 Duration 耗时对象。它会过滤掉负数、无穷大这类不靠谱的值。
数据流:输入是可选 JSON 值 → 没有值就返回 None → 尝试按浮点数、有符号整数、无符号整数读取毫秒 → 如果不是有限非负数就返回 None → 否则四舍五入并限制到 u64 最大值,生成 Duration::from_millis。
调用关系:它被 SessionTelemetry::record_responses_websocket_timing_metrics 调用,专门为 WebSocket timing JSON 里的毫秒字段做安全转换。
调用图:被 1 处调用(record_responses_websocket_timing_metrics);外部调用 1 个(from_millis)。
core/src/turn_timing.rs源码 ↗
可以把一次回合想成一次外卖订单:下单后,先等商家接单,再等做饭,再等骑手。这个文件就是给这些阶段打点的秒表。它记录回合什么时候开始,第一次有可见输出用了多久,也记录第一次完整助手消息用了多久。另一个更细的“画像”会把时间拆成:第一次请求模型前、模型采样中、两次采样之间的空档、工具阻塞、最后一次采样之后。文件里用互斥锁(一把锁,防止两个任务同时改同一份数据)保护这些时间。TurnProfileTimingGuard 像“临时工牌”:进入某个阶段时拿到它,离开作用域时自动归还,于是阶段结束时间不会漏记。
record_turn_ttft_metric18–27 ↗
async fn record_turn_ttft_metric(turn_context: &TurnContext, event: &ResponseEvent)
作用:记录 TTFT,也就是“从回合开始到第一次有可见输出”的时间。有人在模型返回事件时调用它,用来判断用户第一次看到回应等了多久。
数据流:进去的是当前回合上下文和一个响应事件 → 它先问计时状态:这个事件算不算第一次输出,如果算就拿到耗时 → 出来没有直接返回值,但会把这段耗时写进会话遥测数据里;如果已经记过或事件不算输出,就什么也不改。
调用关系:它由 try_run_sampling_request 在处理模型采样返回时调用。它自己不判断所有细节,而是把判断交给 TurnTimingState::record_ttft_for_response_event,拿到结果后再交给 session_telemetry 记录。
调用图:被 1 处调用(try_run_sampling_request)。
record_turn_ttfm_metric29–40 ↗
async fn record_turn_ttfm_metric(turn_context: &TurnContext, item: &TurnItem)
作用:记录 TTFM,也就是“从回合开始到第一条助手消息完成”的时间。它帮助区分“很快开始吐字”和“完整答案很晚才形成”这两种情况。
数据流:进去的是当前回合上下文和一个回合条目 → 它只在条目是助手消息时尝试计时 → 出来没有直接返回值;如果这是第一条助手消息,就把耗时写入 TURN_TTFM_DURATION_METRIC 指标,否则不做事。
调用关系:它由 emit_turn_item_completed 在某个回合条目完成时调用。它把识别第一条消息的工作交给 TurnTimingState::record_ttfm_for_turn_item,再把得到的时间交给遥测系统。
调用图:被 1 处调用(emit_turn_item_completed)。
TurnTimingState::mark_turn_started86–95 ↗
async fn mark_turn_started(&self, started_at: Instant) -> i64
作用:宣布一个新回合开始,并把所有“第一次输出、第一次消息”等旧记录清空。没有这一步,后面的计时就不知道从哪里算起。
数据流:进去的是单调时钟 Instant 表示的开始时间 → 它再取当前 Unix 毫秒时间,保存开始秒数,清空 first_token_at 和 first_message_at,并启动详细画像计时 → 出来返回开始时刻的 Unix 毫秒数,同时内部状态变成一个全新的回合。
调用关系:这是每个回合计时的起点。它会调用 now_unix_timestamp_ms 取得可写入日志的墙钟时间,也会通过 profile_state 拿到画像状态并调用 TurnProfileState::start。
调用图:调用 2 个内部函数(profile_state, now_unix_timestamp_ms)。
TurnTimingState::started_at_unix_secs97–99 ↗
async fn started_at_unix_secs(&self) -> Option<i64>
作用:取出当前回合开始时的 Unix 秒时间。外部需要把回合开始时间写进记录或事件时会用它。
数据流:进去没有额外参数 → 它读取被锁保护的 started_at_unix_secs → 出来是一个可选秒数;如果回合还没开始,就返回空。
调用关系:它是一个简单查询口,不推动流程,也不调用别的本文件函数。它依赖 mark_turn_started 之前已经把开始时间存好。
TurnTimingState::completed_at_and_duration_ms101–108 ↗
async fn completed_at_and_duration_ms(&self) -> (Option<i64>, Option<i64>)
作用:在回合结束时,给出“结束时间”和“总共用了多少毫秒”。这通常用于最终记录、日志或上报。
数据流:进去没有额外参数 → 它读取回合开始的 Instant,再取当前 Unix 秒时间 → 出来是一对值:完成时间一定按当前时间给出;耗时只有在回合已经开始时才有。
调用关系:它在收尾阶段提供总耗时。它调用 now_unix_timestamp_secs 得到适合记录的结束时间,并用 started_at.elapsed 计算从开始到现在的长度。
调用图:调用 1 个内部函数(now_unix_timestamp_secs)。
TurnTimingState::time_to_first_token_ms110–115 ↗
async fn time_to_first_token_ms(&self) -> Option<i64>
作用:查询已经记录下来的 TTFT 毫秒数。它不会主动打点,只是把已有的“第一次输出时间”换算成毫秒给别人看。
数据流:进去没有额外参数 → 它读取 started_at 和 first_token_at,并计算两者差值 → 出来是可选毫秒数;还没开始或还没出现第一次输出时返回空。
调用关系:它是状态查询函数,依赖 TurnTimingStateInner::record_turn_ttft 曾经成功记录过 first_token_at。它不调用外部流程。
TurnTimingState::complete_profile117–119 ↗
fn complete_profile(&self) -> TurnProfile
作用:结束并生成本回合的详细耗时画像。画像会说明时间分别花在模型采样、工具等待和空档等地方。
数据流:进去没有额外参数 → 它取当前 Instant,锁住画像状态,让画像状态补齐最后一段时间并汇总 → 出来是一个 TurnProfile;如果之前已经完成过,会拿到同一份完成结果。
调用关系:它是详细画像的收尾入口。它通过 profile_state 取到 TurnProfileState,然后把真正的汇总工作交给 TurnProfileState::complete。
调用图:调用 1 个内部函数(profile_state);外部调用 1 个(now)。
TurnTimingState::begin_sampling121–128 ↗
fn begin_sampling(self: &Arc<Self>) -> TurnProfileTimingGuard
作用:标记系统进入“模型采样”阶段,也就是正在请求模型生成内容。它返回一个守卫对象,守卫消失时会自动结束这个阶段。
数据流:进去的是共享的 TurnTimingState 引用 → 它取当前时间,尝试让画像状态进入 Sampling 阶段 → 出来是 TurnProfileTimingGuard,里面记着阶段和是否真的生效;如果当前状态不允许进入,守卫会是非激活的。
调用关系:它负责打开采样阶段,调用 TurnProfileState::begin_sampling 做状态切换,并用 Arc clone 保住计时对象。后续 TurnProfileTimingGuard::drop 会在作用域结束时自动关闭阶段。
调用图:调用 1 个内部函数(profile_state);外部调用 2 个(clone, now)。
TurnTimingState::record_sampling_retry130–132 ↗
fn record_sampling_retry(&self)
作用:记录一次模型采样重试。这样画像不只知道采样花了多久,也知道请求模型时重试了多少次。
数据流:进去没有额外参数 → 它锁住画像状态,把重试次数加一,前提是回合已经开始且画像还没完成 → 出来没有返回值,只改变内部计数。
调用关系:它是采样流程里遇到重试时的计数入口。它通过 profile_state 拿到 TurnProfileState,再交给 TurnProfileState::record_sampling_retry。
调用图:调用 1 个内部函数(profile_state)。
TurnTimingState::begin_tool_blocking134–141 ↗
fn begin_tool_blocking(self: &Arc<Self>) -> TurnProfileTimingGuard
作用:标记系统进入“等工具”的阶段,比如模型要求执行命令或调用工具,主流程被工具结果挡住了。它同样用守卫来自动结束计时。
数据流:进去的是共享的 TurnTimingState 引用 → 它取当前时间,尝试把画像状态切到 ToolBlocking → 出来是一个 TurnProfileTimingGuard;如果已经有别的阶段在进行或回合没开始,就返回非激活守卫。
调用关系:它和 begin_sampling 是一对相似入口,只是阶段不同。它调用 TurnProfileState::begin_tool_blocking 开始计时,之后由 TurnProfileTimingGuard::drop 调用 end_phase 完成收尾。
调用图:调用 1 个内部函数(profile_state);外部调用 2 个(clone, now)。
TurnTimingState::record_ttft_for_response_event143–152 ↗
async fn record_ttft_for_response_event(
&self,
event: &ResponseEvent,
) -> Option<Duration>
作用:看到一个响应事件时,判断它能不能算“第一次有输出”,如果能,就记录 TTFT。它避免把纯状态通知误当成用户可见输出。
数据流:进去的是一个 ResponseEvent → 它先用 response_event_records_turn_ttft 判断事件是否该触发 TTFT,再锁住内部状态尝试记录第一次 token 时间 → 出来是可选 Duration;只有第一次有效输出才返回耗时。
调用关系:它被外层 record_turn_ttft_metric 使用。它把事件分类交给 response_event_records_turn_ttft,把真正落笔记录交给 TurnTimingStateInner::record_turn_ttft。
调用图:调用 1 个内部函数(response_event_records_turn_ttft)。
TurnTimingState::record_ttfm_for_turn_item154–160 ↗
async fn record_ttfm_for_turn_item(&self, item: &TurnItem) -> Option<Duration>
作用:看到一个回合条目时,判断它是不是助手消息;如果是第一次助手消息,就记录 TTFM。这样工具输出或其他杂项不会污染这个指标。
数据流:进去的是 TurnItem → 它先检查这个条目是否是 AgentMessage → 如果是,就锁住内部状态记录第一条消息时间;出来是可选 Duration,只有第一次助手消息才有值。
调用关系:它被 record_turn_ttfm_metric 调用。外层负责把返回的耗时上报,这里只负责“是否该记”和“只记一次”。
调用图:外部调用 1 个(matches!)。
TurnTimingState::profile_state162–166 ↗
fn profile_state(&self) -> std::sync::MutexGuard<'_, TurnProfileState>
作用:安全地拿到详细画像状态的锁。它是修改采样、工具等待等画像字段前必须经过的小门。
数据流:进去没有额外参数 → 它尝试锁住标准互斥锁;如果锁曾经因为线程 panic 而中毒,也会取回里面的数据继续用 → 出来是一个可修改 TurnProfileState 的锁守卫。
调用关系:它被 begin_sampling、begin_tool_blocking、complete_profile、mark_turn_started 和 record_sampling_retry 使用。它把“怎么安全拿锁”的细节集中在一处,调用者只关心改画像。
调用图:被 5 处调用(begin_sampling, begin_tool_blocking, complete_profile, mark_turn_started, record_sampling_retry);外部调用 1 个(lock)。
TurnProfileTimingGuard::drop170–176 ↗
fn drop(&mut self)
作用:当阶段守卫被丢弃时,自动结束对应的计时阶段。这样调用方不需要手写“结束采样”或“结束工具等待”,不容易忘。
数据流:进去的是即将销毁的守卫自身 → 如果守卫生效,它取当前时间,锁住画像状态,并结束自己记录的阶段 → 出来没有返回值,但画像里的阶段耗时被补上。
调用关系:它是 begin_sampling 和 begin_tool_blocking 返回的守卫的自动收尾动作。它调用 TurnProfileState::end_phase,让阶段计时形成闭环。
调用图:外部调用 1 个(now)。
now_unix_timestamp_secs179–181 ↗
fn now_unix_timestamp_secs() -> i64
作用:取得当前 Unix 秒时间,也就是从 1970 年 1 月 1 日到现在过了多少秒。它适合写进日志、事件或持久化记录。
数据流:进去没有参数 → 它先取 Unix 毫秒时间,再除以 1000 → 出来是当前 Unix 秒数。
调用关系:它被 TurnTimingState::completed_at_and_duration_ms 用来标记回合完成时间。它的底层时间来源是 now_unix_timestamp_ms。
调用图:调用 1 个内部函数(now_unix_timestamp_ms);被 1 处调用(completed_at_and_duration_ms)。
now_unix_timestamp_ms183–188 ↗
fn now_unix_timestamp_ms() -> i64
作用:取得当前 Unix 毫秒时间。项目里很多地方需要给事件盖时间戳,都会用这个统一的小工具。
数据流:进去没有参数 → 它读取系统墙钟时间,计算距离 Unix 纪元的毫秒数,并尽量转成 i64;如果时间异常就给默认值,数字太大就封顶 → 出来是一个毫秒时间戳。
调用关系:它被 mark_turn_started、now_unix_timestamp_secs,以及 stamp_ws_stream_request_start_ms、run_guardian_review、emit_turn_item_completed、emit_turn_item_started、request_command_approval 等多处调用,是跨流程的时间戳工具。
调用图:被 22 处调用(stamp_ws_stream_request_start_ms, run_guardian_review, emit_turn_item_completed, emit_turn_item_started, request_command_approval, request_patch_approval, request_permissions_for_environment, execute_user_shell_command, emit_exec_command_begin, emit_exec_end (+12 more));外部调用 2 个(now, try_from)。
duration_to_u64_ms190–192 ↗
fn duration_to_u64_ms(duration: Duration) -> u64
作用:把一段 Duration 时间换成 u64 毫秒数。TurnProfile 里的字段需要整数毫秒,所以要做这一步转换。
数据流:进去的是 Duration → 它取出毫秒数并尝试转成 u64;如果太大就用 u64 最大值兜底 → 出来是可安全存放的毫秒整数。
调用关系:它被 TurnProfileState::complete 调用,用来把内部计时用的 Duration 转成最终画像里的数字字段。
TurnProfileState::start195–201 ↗
fn start(&mut self, started_at: Instant)
作用:重置并开始一份新的回合画像。它把旧回合留下的阶段、计数和完成结果全部清掉。
数据流:进去的是回合开始的 Instant → 它用默认空状态覆盖自己,只保留 started_at 和 last_transition_at 为开始时间 → 出来没有返回值,但画像状态变成“刚开始”。
调用关系:它由 TurnTimingState::mark_turn_started 调用,是详细画像的起点。后面的 begin_sampling、begin_tool_blocking 和 complete 都基于它设置的开始时间运行。
调用图:外部调用 1 个(default)。
TurnProfileState::begin_sampling203–218 ↗
fn begin_sampling(&mut self, now: Instant) -> bool
作用:让画像进入“模型采样中”阶段,并记录这是第几次采样请求。它也会把进入采样前的空档归到正确类别里。
数据流:进去的是当前 Instant → 如果画像已完成、没开始,或已有阶段在进行,就返回 false;否则先用 advance 结算上一段空闲时间,再设置 active_phase 为 Sampling,并把采样请求次数加一 → 出来是是否成功进入阶段。
调用关系:它由 TurnTimingState::begin_sampling 调用。它内部调用 advance 来把时间切片,若不是第一次采样,还会把上次采样后的待定空闲时间放进 between_sampling_overhead。
TurnProfileState::record_sampling_retry220–224 ↗
fn record_sampling_retry(&mut self)
作用:给当前回合画像增加一次采样重试计数。它只数次数,不改变当前阶段。
数据流:进去没有参数 → 它检查画像尚未完成且已经开始 → 满足条件就把 sampling_retry_count 饱和加一;不满足就不动 → 出来没有返回值。
调用关系:它由 TurnTimingState::record_sampling_retry 调用。这个计数最终会在 TurnProfileState::complete 生成的 TurnProfile 里出现。
TurnProfileState::begin_tool_blocking226–236 ↗
fn begin_tool_blocking(&mut self, now: Instant) -> bool
作用:让画像进入“工具阻塞”阶段,用来记录系统等待工具、命令或外部动作时花的时间。
数据流:进去的是当前 Instant → 如果画像已完成、没开始,或已有阶段在进行,就返回 false;否则先结算上一段时间,再把 active_phase 设为 ToolBlocking → 出来是是否成功进入阶段。
调用关系:它由 TurnTimingState::begin_tool_blocking 调用。它和 begin_sampling 一样依赖 advance 来保证时间不会丢段。
调用图:调用 1 个内部函数(advance)。
TurnProfileState::end_phase238–244 ↗
fn end_phase(&mut self, now: Instant, phase: TurnProfilePhase)
作用:结束当前正在进行的某个阶段。它会确认要结束的阶段确实是当前阶段,防止把别的阶段误关掉。
数据流:进去的是当前时间和要结束的阶段名 → 如果画像已完成,或当前阶段不是这个阶段,就不做事;否则先用 advance 把这一段耗时加到账上,再清空 active_phase → 出来没有返回值。
调用关系:它由 TurnProfileTimingGuard::drop 自动调用。也就是说,begin_sampling 或 begin_tool_blocking 创建的守卫离开作用域时,会走到这里做最后结算。
调用图:调用 1 个内部函数(advance)。
TurnProfileState::advance246–257 ↗
fn advance(&mut self, now: Instant)
作用:把“上一次状态变化到现在”的时间归类到账本里。它是详细画像能分清各阶段耗时的核心记账动作。
数据流:进去的是当前 Instant → 它拿出上一次转换时间,计算经过了多久,再根据当前 active_phase 把这段时间加到 sampling、tool_blocking、第一次采样前或采样后的待定空闲里 → 出来没有返回值,但各类耗时被更新,last_transition_at 变成现在。
调用关系:它被 begin_sampling、begin_tool_blocking、end_phase 和 complete 调用。每次阶段切换或最终完成前,都先通过它把上一段时间结清。
调用图:被 4 处调用(begin_sampling, begin_tool_blocking, complete, end_phase);外部调用 1 个(saturating_duration_since)。
TurnProfileState::complete259–302 ↗
fn complete(&mut self, now: Instant) -> TurnProfile
作用:把详细画像最终封账,生成 TurnProfile。它保证同一个回合完成多次时返回同一份结果,不会重复计算。
数据流:进去的是完成时的 Instant → 如果已有完成画像就直接克隆返回;否则先 advance 结清最后一段,整理最后一次采样后的空闲时间,把各类 Duration 转成毫秒,并用总时长补齐可能的取整差 → 出来是 TurnProfile,同时内部标记为已完成。
调用关系:它由 TurnTimingState::complete_profile 调用。它依赖 advance 做最后结算,依赖 duration_to_u64_ms 做格式转换,并把采样次数、重试次数一起放进最终结果。
调用图:调用 2 个内部函数(advance, duration_to_u64_ms);外部调用 1 个(take)。
TurnTimingStateInner::time_to_first_token306–308 ↗
fn time_to_first_token(&self) -> Option<Duration>
作用:计算从回合开始到第一次 token 的时间差。token 可以理解为模型流式输出里的第一小片文字或内容。
数据流:进去没有参数 → 它读取 started_at 和 first_token_at,两个都有才相减 → 出来是可选 Duration;缺任意一个就返回空。
调用关系:它被 TurnTimingStateInner::record_turn_ttft 调用。它只做计算,不负责判断事件是否算输出,也不修改状态。
调用图:被 1 处调用(record_turn_ttft)。
TurnTimingStateInner::record_turn_ttft310–317 ↗
fn record_turn_ttft(&mut self) -> Option<Duration>
作用:真正落笔记录第一次输出时间。它保证 TTFT 只会记录一次。
数据流:进去没有参数 → 如果 first_token_at 已经有值,就返回空;如果回合还没开始,也返回空;否则把当前 Instant 存成 first_token_at,再计算从开始到现在的耗时 → 出来是这次新记录的 Duration。
调用关系:它由 TurnTimingState::record_ttft_for_response_event 在事件被判定有效后调用。它再调用 time_to_first_token 来得到最终耗时。
调用图:调用 1 个内部函数(time_to_first_token);外部调用 1 个(now)。
TurnTimingStateInner::record_turn_ttfm319–327 ↗
fn record_turn_ttfm(&mut self) -> Option<Duration>
作用:真正记录第一条助手消息出现的时间。它和 TTFT 分开,因为第一次吐字和第一条完整消息不是一回事。
数据流:进去没有参数 → 如果 first_message_at 已经记录过就返回空;如果回合未开始也返回空;否则取当前 Instant,保存为 first_message_at,并计算它距离 started_at 的时间 → 出来是这次新记录的 Duration。
调用关系:它由 TurnTimingState::record_ttfm_for_turn_item 在确认条目是 AgentMessage 后调用。外层再负责把这个时间上报为指标。
调用图:外部调用 1 个(now)。
response_event_records_turn_ttft330–349 ↗
fn response_event_records_turn_ttft(event: &ResponseEvent) -> bool
作用:判断一个响应事件是否应该算作“用户第一次看到了模型输出”。它把纯元数据、完成通知、限流信息等排除掉。
数据流:进去的是 ResponseEvent → 它按事件类型分类:文字增量、推理内容增量等直接算;新增或完成的输出项再交给 response_item_records_turn_ttft 细看;纯状态类事件返回 false → 出来是布尔值。
调用关系:它被 TurnTimingState::record_ttft_for_response_event 调用。它是 TTFT 打点前的过滤器,必要时把具体条目的判断交给 response_item_records_turn_ttft。
调用图:调用 1 个内部函数(response_item_records_turn_ttft);被 1 处调用(record_ttft_for_response_event)。
response_item_records_turn_ttft351–387 ↗
fn response_item_records_turn_ttft(item: &ResponseItem) -> bool
作用:判断某个响应条目是否能代表第一次有效输出。它会区分空消息、推理文本、工具调用和工具结果等不同情况。
数据流:进去的是 ResponseItem → 对普通消息,它提取助手输出文字并要求非空;对推理条目,它检查摘要或内容里是否有非空文本;对工具调用类条目多半算作输出;对 AgentMessage、压缩触发、工具结果和 Other 等不算 → 出来是布尔值。
调用关系:它由 response_event_records_turn_ttft 调用。它还会用 raw_assistant_output_text_from_item 帮忙从消息条目里取出真实文本,避免空壳消息误触发 TTFT。
调用图:调用 1 个内部函数(raw_assistant_output_text_from_item);被 1 处调用(response_event_records_turn_ttft)。
请求与工具跟踪
这些文件将遥测附加到请求处理和工具执行路径,包括 app-server span、沙箱特征描述和 rollout 跟踪发送。
app-server/src/app_server_tracing.rs源码 ↗
这个文件专门统一 app-server 请求的追踪信息。这里的“追踪”可以理解成给每个请求贴一张快递面单:上面写着请求方法、请求编号、连接编号、传输方式、客户端名字和版本,还会尽量接上上游传来的追踪上下文,这样一个请求从别的系统一路走到这里时,监控工具能把它连成一条线。它同时照顾两类入口:一种是标准 JSON-RPC 请求,比如 stdio、Unix socket、WebSocket;另一种是进程内直接调用的 typed request。两边都会生成形状一致的 span(一次可被监控系统记录的工作片段),这样不同入口的数据可以放在一起比较。文件里的小函数分工很清楚:先识别传输方式,创建统一模板,再补客户端信息,最后接上父级追踪信息。
request_span24–55 ↗
fn request_span(
request: &JSONRPCRequest,
transport: &AppServerTransport,
connection_id: ConnectionId,
session: &ConnectionSessionState,
) -> Span
作用:给一个普通 JSON-RPC 请求创建追踪 span,也就是给这次请求准备一份可被日志和监控系统识别的“身份卡”。它会记录请求方法、请求编号、连接、传输方式、客户端信息,以及可能来自上游的追踪链路。
数据流:进去的是 JSON-RPC 请求、它通过的传输方式、连接编号和当前会话状态。函数先从请求里尝试读 initialize 参数里的客户端信息,再把传输方式翻译成固定名字,创建基础 span;然后把客户端名称和版本写进去;最后如果请求自带 W3C trace context(跨系统追踪用的一组标准字段),就把它接成父级链路,否则尝试从环境变量里找。出来的是一个已经填好主要字段的 Span。
调用关系:它在 process_request 处理网络或 JSON-RPC 请求时被调用,是请求正式处理前的追踪入口。它把建模板的活交给 app_server_request_span_template,把客户端字段交给 client_name、client_version 和 record_client_info,把链路串接交给 attach_parent_context。
调用图:调用 7 个内部函数(app_server_request_span_template, attach_parent_context, client_name, client_version, initialize_client_info, record_client_info, transport_name);被 1 处调用(process_request)。
typed_request_span62–83 ↗
fn typed_request_span(
request: &ClientRequest,
connection_id: ConnectionId,
session: &ConnectionSessionState,
) -> Span
作用:给进程内直接传来的 typed request 创建追踪 span。它的目的和 request_span 一样,但会把传输方式固定标成 in-process,表示这个请求不是从 socket 或标准输入来的。
数据流:进去的是类型化的客户端请求、连接编号和会话状态。函数先从请求对象上拿方法名和请求编号,创建一个基础 span;如果这是初始化请求,就直接从里面取客户端名称和版本,否则用会话里已有的信息;之后写入这些客户端字段,并尝试接上环境里的父级追踪上下文。出来的是一份和 JSON-RPC 请求格式尽量一致的 Span。
调用关系:它在 process_client_request 处理进程内请求时被调用。它复用 app_server_request_span_template 和 record_client_info,保证进程内调用和 JSON-RPC 调用在监控里长得一样,方便一起分析。
调用图:调用 6 个内部函数(app_server_request_span_template, attach_parent_context, initialize_client_info_from_typed_request, record_client_info, app_server_client_name, client_version);被 1 处调用(process_client_request);外部调用 2 个(id, method)。
transport_name85–92 ↗
fn transport_name(transport: &AppServerTransport) -> &'static str
作用:把内部的传输方式枚举翻译成监控里使用的短字符串。比如标准输入输出会变成 stdio,WebSocket 会变成 websocket。
数据流:进去的是 AppServerTransport,也就是服务器当前使用的通信通道。函数根据具体类型做匹配,选出一个固定文本。出来的是一个稳定的传输方式名字,用来写进 span。
调用关系:它只被 request_span 使用。request_span 需要先知道请求是从哪里来的,再把这个信息交给 app_server_request_span_template 写进统一的追踪字段。
调用图:被 1 处调用(request_span)。
app_server_request_span_template94–114 ↗
fn app_server_request_span_template(
method: &str,
transport: &'static str,
request_id: &impl std::fmt::Display,
connection_id: ConnectionId,
) -> Span
作用:创建 app-server 请求追踪 span 的统一模板。它像一张预印好的表格,先把请求方法、传输方式、请求编号、连接编号等固定栏位填好,同时留出客户端信息和 turn.id 这些之后补写的位置。
数据流:进去的是请求方法、传输方式、请求 ID 和连接 ID。函数调用 tracing 的 info_span! 宏创建一个名为 app_server.request 的 span,并写入 OpenTelemetry 相关字段;OpenTelemetry 是常见的监控追踪标准,用来让不同系统的调用链能接起来。出来的是一个 Span 对象,后续函数会继续往里面补字段。
调用关系:它是 request_span 和 typed_request_span 共同使用的底座。两种入口虽然请求来源不同,但都通过它生成同样结构的追踪记录,避免监控数据一边一个样。
调用图:被 2 处调用(request_span, typed_request_span);外部调用 1 个(info_span!)。
record_client_info116–123 ↗
fn record_client_info(span: &Span, client_name: Option<&str>, client_version: Option<&str>)
作用:把客户端名称和客户端版本写进 span。这样后面看监控时,可以知道请求是哪个客户端、哪个版本发来的。
数据流:进去的是一个 Span,以及可选的客户端名称和版本。函数只在这些值确实存在时才写入 span,不会强行填空值。出来没有新的返回值,但传入的 span 会多出 app_server.client_name 和 app_server.client_version 字段。
调用关系:它被 request_span 和 typed_request_span 调用。前面的函数负责找客户端信息,它负责把找到的信息真正记录到追踪数据里。
调用图:被 2 处调用(request_span, typed_request_span);外部调用 1 个(record)。
attach_parent_context125–142 ↗
fn attach_parent_context(
span: &Span,
method: &str,
request_id: &impl std::fmt::Display,
parent_trace: Option<&W3cTraceContext>,
)
作用:把当前请求接到已有的追踪链路上。简单说,如果这个请求是别的系统转过来的,它会尽量告诉监控工具:这不是孤立的一次请求,它有上游来源。
数据流:进去的是当前 span、请求方法、请求 ID,以及可选的 W3CTraceContext。函数如果拿到请求自带的 traceparent,就尝试把它设为父级;如果这个 trace 信息无效,就写一条警告日志。若请求没带父级信息,它还会尝试从环境变量里读取 traceparent 上下文并接上。出来没有返回值,但 span 的父子关系可能被设置好。
调用关系:它被 request_span 和 typed_request_span 调用,是追踪链路“接线”的步骤。它把具体接线动作交给 codex_otel 里的 set_parent_from_w3c_trace_context、set_parent_from_context 和 traceparent_context_from_env。
调用图:被 2 处调用(request_span, typed_request_span);外部调用 4 个(set_parent_from_context, set_parent_from_w3c_trace_context, traceparent_context_from_env, warn!)。
client_name144–152 ↗
fn client_name(
initialize_client_info: Option<&'a InitializeParams>,
session: &'a ConnectionSessionState,
) -> Option<&'a str>
作用:决定这次请求应该记录哪个客户端名称。初始化请求里如果直接带了客户端名称,就优先用它;否则就用当前会话里之前保存的名称。
数据流:进去的是可选的初始化参数和会话状态。函数先看初始化参数是否存在,存在就取 params.client_info.name;不存在就去 session.app_server_client_name() 里找。出来的是一个可选的客户端名称文本。
调用关系:它被 request_span 调用。request_span 先用 initialize_client_info 尝试解析初始化参数,再交给 client_name 做“新请求信息优先,旧会话信息兜底”的选择。
调用图:调用 1 个内部函数(app_server_client_name);被 1 处调用(request_span)。
client_version154–162 ↗
fn client_version(
initialize_client_info: Option<&'a InitializeParams>,
session: &'a ConnectionSessionState,
) -> Option<&'a str>
作用:决定这次请求应该记录哪个客户端版本。它和 client_name 的规则一样:初始化请求里的版本优先,否则使用会话中已有的版本。
数据流:进去的是可选的初始化参数和会话状态。函数如果看到初始化参数,就取 params.client_info.version;否则调用会话状态里的 client_version 方法。出来的是一个可选的版本号文本。
调用关系:它被 request_span 调用,用来给 record_client_info 提供版本字段。这样初始化请求可以立刻记录最新客户端版本,普通请求也能沿用会话里保存的信息。
调用图:调用 1 个内部函数(client_version);被 1 处调用(request_span)。
initialize_client_info164–170 ↗
fn initialize_client_info(request: &JSONRPCRequest) -> Option<InitializeParams>
作用:从 JSON-RPC 的 initialize 请求里解析客户端信息。只有 initialize 方法才会带这类信息,所以它会先确认方法名,再尝试把 params 转成 InitializeParams。
数据流:进去的是一个 JSONRPCRequest。函数先检查 method 是否等于 initialize;如果不是,直接返回空。若是初始化请求,它会复制 params,并用 serde_json::from_value 尝试解析成 InitializeParams;解析成功就返回这些参数,失败就返回空。它不修改原请求。
调用关系:它被 request_span 调用,是 JSON-RPC 路径里获取客户端名称和版本的第一步。后续 client_name 和 client_version 会根据它的结果决定用请求里的新信息,还是用会话里的旧信息。
调用图:被 1 处调用(request_span);外部调用 1 个(from_value)。
initialize_client_info_from_typed_request172–180 ↗
fn initialize_client_info_from_typed_request(request: &ClientRequest) -> Option<(&str, &str)>
作用:从进程内 typed request 的初始化请求里直接取客户端名称和版本。因为 typed request 已经是强类型对象,不需要再从 JSON 里解析。
数据流:进去的是 ClientRequest。函数检查它是不是 Initialize 变体;如果是,就从 params.client_info 里拿 name 和 version 的字符串引用;如果不是初始化请求,就返回空。它不复制大块数据,也不修改请求。
调用关系:它被 typed_request_span 调用。typed_request_span 用它来判断当前进程内请求是否自带新的客户端信息,如果没有,就退回使用会话里保存的信息。
调用图:被 1 处调用(typed_request_span)。
core/src/sandbox_tags.rs源码 ↗
这份文件像一个“权限翻译员”。项目里有一套权限配置,里面会说文件能不能写、网络能不能用、是否交给外部沙箱等;但记录指标时不需要整套细节,只需要一个稳定、好读的标签。这里提供两个判断:一个判断是否用了平台沙箱,也就是操作系统提供的隔离环境;另一个判断文件权限大概是什么级别。它会先看权限档案是不是完全关闭限制、是不是外部沙箱、还是项目自己管理的限制。对于自己管理的情况,它还会结合文件系统策略、网络限制、当前目录,以及 Windows 的特殊沙箱级别,给出像“none”“workspace-write”“read-only”这样的标签。重点是:它不真正执行沙箱,只负责把复杂状态归类成可上报、可比较的名字。
permission_profile_sandbox_tag8–38 ↗
fn permission_profile_sandbox_tag(
profile: &PermissionProfile,
windows_sandbox_level: WindowsSandboxLevel,
enforce_managed_network: bool,
) -> &'static str
作用:这个函数判断当前权限配置在“沙箱”层面应该记成什么标签。沙箱可以理解成把程序关进一个受限制的房间,防止它乱访问系统资源;这个函数只负责给这种限制状态起一个简短名字。
数据流:输入是一份权限档案、Windows 沙箱级别,以及是否强制使用受管理网络。它先看权限是不是完全不限制,若是就给出“none”;如果是外部提供的沙箱,就给出“external”。如果是项目自己管理权限,它会把文件权限转成沙箱策略,再询问是否真的需要平台沙箱;不需要就仍然是“none”。如果需要,它会特别处理 Windows 的提升级别,或者查询当前平台支持的沙箱类型,最后输出一个固定字符串标签;它不修改任何配置。
调用关系:它会被发送终局结果的流程调用,用来把本次运行的沙箱状态写进对外结果或指标;也会在创建相关状态时使用,并被测试用例检查是否正确使用平台沙箱标签。函数内部把“是否需要平台沙箱”的判断交给 should_require_platform_sandbox,把“当前系统能用哪种沙箱”的判断交给 get_platform_sandbox,自己负责把这些判断汇总成一个标签。
调用图:调用 1 个内部函数(should_require_platform_sandbox);被 3 处调用(dispatch_any_with_terminal_outcome, new, turn_metadata_state_uses_platform_sandbox_tag);外部调用 3 个(cfg!, get_platform_sandbox, matches!)。
permission_profile_policy_tag40–61 ↗
fn permission_profile_policy_tag(
profile: &PermissionProfile,
cwd: &Path,
) -> &'static str
作用:这个函数把文件访问权限归成几个外行也能理解的等级标签,比如全盘可写、只读、或只能写工作区。它主要用来说明这次运行对磁盘文件有多大权力。
数据流:输入是一份权限档案和当前工作目录。它先看权限是否完全关闭限制:如果是,就输出“danger-full-access”,意思是风险较高的完全访问;如果使用外部沙箱,就输出“external-sandbox”。如果是项目自己管理权限,它会取出文件系统沙箱策略,先判断是否能写整个磁盘;能写就标成全权限。否则再看在当前目录下有没有可写位置:没有就是“read-only”,有就是“workspace-write”。函数只返回标签,不改变文件系统。
调用关系:它在发送终局结果的流程中被调用,用来把本次运行的文件权限级别写成清楚的标签。它自己不做底层权限计算,而是向权限档案要 file_system_sandbox_policy,再根据这个策略给出的能力做最后归类。
调用图:调用 1 个内部函数(file_system_sandbox_policy);被 1 处调用(dispatch_any_with_terminal_outcome)。
core/src/tools/tool_dispatch_trace.rs源码 ↗
系统调用工具时,除了真正执行工具,还需要留下“谁调用了什么、传了什么、最后成功还是失败”的痕迹,方便之后回放、排查问题或做审计。这个文件就像一个翻译员:核心代码里用的是 ToolInvocation、ToolPayload、ToolOutput 这些内部对象,而 codex-rollout-trace 需要的是另一套事件格式。ToolDispatchTrace 先在工具开始派发时建立一段追踪上下文;工具结束时,它再把不同来源的调用,比如模型直接调用或代码单元调用,转换成对应的结果事件。如果追踪没开启,它会立刻跳过,避免多做无用功。这样主派发流程只关心“工具怎么跑”,这里专门关心“怎么把过程记清楚”。
ToolDispatchTrace::start25–32 ↗
fn start(invocation: &ToolInvocation) -> Self
作用:在一次工具调用刚开始时,打开一段追踪记录。它把当前调用的信息交给 rollout trace 系统,让后面能把这次调用的结果接到同一条记录上。
数据流:进去的是一个 ToolInvocation,也就是“这次工具调用的完整说明”,里面有会话、轮次、工具名、调用来源和参数。函数从会话服务里找到 rollout_thread_trace,并用 tool_dispatch_invocation 把内部调用说明翻译成追踪事件需要的格式。出来的是一个 ToolDispatchTrace,里面包着追踪上下文,后续用它记录成功或失败。
调用关系:dispatch_any_with_terminal_outcome 在真正派发工具前会调用它,相当于先放一个“开始记录”的书签。它把具体的格式转换交给 tool_dispatch_invocation,然后保存返回的 ToolDispatchTraceContext,供后续结束记录使用。
调用图:被 1 处调用(dispatch_any_with_terminal_outcome)。
ToolDispatchTrace::record_completed34–55 ↗
fn record_completed(
&self,
invocation: &ToolInvocation,
call_id: &str,
payload: &ToolPayload,
result: &dyn ToolOutput,
)
作用:在工具跑完后,记录这次调用是成功结束还是失败结束,并把工具返回的内容写进追踪事件。这里的“失败”是从日志角度判断的,不一定等同于程序崩溃。
数据流:进去的是原始调用信息 invocation、调用编号 call_id、当时传入工具的 payload,以及工具返回结果 result。它先问追踪上下文 is_enabled 是否真的开启;没开启就什么也不做。开启时,它用 tool_dispatch_result 把返回值转成追踪系统认识的结果格式,再用 result.success_for_logging 判断状态是 Completed 还是 Failed,最后把状态和结果交给上下文的 record_completed。输出没有普通返回值,但会在追踪系统里留下完成事件。
调用关系:它处在工具执行结束之后,是 start 建立的那段追踪的收尾动作。它自己不理解所有返回格式,而是把“按调用来源生成结果”的活交给 tool_dispatch_result,再把最终事件交给 ToolDispatchTraceContext 的 record_completed 写入。
调用图:调用 3 个内部函数(tool_dispatch_result, is_enabled, record_completed);外部调用 1 个(success_for_logging)。
ToolDispatchTrace::record_failed57–59 ↗
fn record_failed(&self, error: &FunctionCallError)
作用:在工具派发过程中出现错误、没能正常产出工具结果时,记录一次失败。它保证早早返回或异常中断的路径也不会丢掉结束记录。
数据流:进去的是 FunctionCallError,也就是工具调用失败的错误信息。函数不改写错误,只把它交给追踪上下文的 record_failed。出来没有普通返回值,但追踪系统会得到一条失败事件。
调用关系:它和 record_completed 是一对收尾方式:工具有结果时走 completed,派发阶段直接出错时走 failed。真正记录失败事件的细节由 ToolDispatchTraceContext 的 record_failed 处理。
调用图:调用 1 个内部函数(record_failed)。
tool_dispatch_invocation62–85 ↗
fn tool_dispatch_invocation(invocation: &ToolInvocation) -> Option<ToolDispatchInvocation>
作用:把核心系统内部的工具调用说明,转换成 rollout trace 的“工具调用开始事件”。简单说,就是把一张内部工单改写成追踪系统能看懂的登记表。
数据流:进去的是 ToolInvocation。函数先看调用来源:如果是 Direct,就说明是模型直接请求工具,并带上模型可见的调用编号;如果是 CodeMode,就说明来自代码单元,并带上代码单元编号和运行时工具调用编号。接着它取出线程、轮次、工具名、命名空间和参数,并用 tool_dispatch_payload 转换参数格式。出来的是 Some(ToolDispatchInvocation),也就是可记录的开始事件。
调用关系:ToolDispatchTrace::start 会通过它生成追踪起点。它负责拼好事件的大框架,而参数部分交给 tool_dispatch_payload,避免每种 payload 的复制规则散在外面。
调用图:调用 1 个内部函数(tool_dispatch_payload)。
tool_dispatch_result87–101 ↗
fn tool_dispatch_result(
invocation: &ToolInvocation,
call_id: &str,
payload: &ToolPayload,
result: &dyn ToolOutput,
) -> Option<ToolDispatchResult>
作用:把工具执行后的返回值,转换成 rollout trace 的“工具调用结果事件”。它会根据调用来源决定结果长什么样,因为模型直接调用和代码模式调用需要的返回格式不同。
数据流:进去的是原始 invocation、call_id、payload 和工具输出 result。函数检查 invocation.source:如果是 Direct,就调用 result.to_response_item,把结果做成模型能看到的响应项;如果是 CodeMode,就调用 result.code_mode_result,把结果做成代码模式使用的值。出来的是 Some(ToolDispatchResult),里面装着对应来源的结果格式。
调用关系:ToolDispatchTrace::record_completed 在收尾时调用它。它不决定成功还是失败,只负责把“结果内容”翻译对;状态判断由 record_completed 用 success_for_logging 另行完成。
调用图:调用 1 个内部函数(code_mode_result);被 1 处调用(record_completed);外部调用 1 个(to_response_item)。
tool_dispatch_payload103–115 ↗
fn tool_dispatch_payload(payload: &ToolPayload) -> ToolDispatchPayload
作用:把工具调用参数从核心内部格式,转换成 rollout trace 的参数格式。它主要做一件事:保留原参数内容,但换成追踪系统使用的类型。
数据流:进去的是 ToolPayload,可能是普通函数参数、工具搜索参数,或者自定义输入。函数按类型分别复制 arguments 或 input,装进对应的 ToolDispatchPayload。出来的是转换后的 payload;原始 payload 不会被改动。
调用关系:tool_dispatch_invocation 在生成开始事件时会调用它。它是一个小的格式转换零件,让开始事件构造函数不用关心每种 payload 该怎么拷贝。
调用图:被 1 处调用(tool_dispatch_invocation)。
功能指标发送器
这些文件为特定产品领域提供专项插桩,例如 guardian 评审、cloud-config 活动和目标生命周期事件。
core/src/guardian/metrics.rs源码 ↗
Guardian 像一道安全检查门,会在某些敏感操作前判断能不能放行。这个文件做的事,就是给每次检查贴上清楚的标签,再把数字送进遥测系统(telemetry,意思是程序运行时自动记录的数据)。没有它,系统可能只知道“审查发生过”,却不知道是哪个动作、什么风险、模型用了多少 token、哪里超时或失败。核心函数会先整理一组标签,比如决定结果、动作类型、风险等级、使用的 Guardian 模型等;然后记录次数、总耗时、首次输出等待时间;如果有 token 用量,还会分成输入、缓存输入、输出等几类分别记录。文件后半部分是一堆“小翻译器”,把代码里的枚举值翻成稳定、干净、适合做统计标签的短字符串。
emit_guardian_review_metrics21–52 ↗
fn emit_guardian_review_metrics(
session_telemetry: &SessionTelemetry,
result: &GuardianReviewAnalyticsResult,
approval_request_source: GuardianApprovalRequestSource,
reviewed_action:
作用:这是本文件的主入口:每完成一次 Guardian 审查,就用它把这次审查的结果写进指标系统。别人用它是为了后续能统计通过率、失败原因、耗时和 token 消耗。
数据流:进去的是当前会话的遥测对象、审查结果、请求来源、被审查的动作,以及总耗时毫秒数。它先调用 guardian_review_metric_tags 做出一组标签,再记录一次计数和一次耗时;如果结果里有“首次 token 时间”,也记录;如果有 token 用量,就把 token 统计交给 emit_guardian_token_usage_histograms。出来没有普通返回值,但遥测系统里多了这些指标数据。
调用关系:正常运行时,track_guardian_review 会在 Guardian 审查结束后调用它;测试 guardian_review_metrics_record_counts_durations_and_token_usage 也会调用它验证记录是否正确。它自己负责搭主流程,把标签生成交给 guardian_review_metric_tags,把 token 细分记录交给 emit_guardian_token_usage_histograms,并通过 SessionTelemetry 的 counter 和 record_duration 真正写指标。
调用图:调用 4 个内部函数(emit_guardian_token_usage_histograms, guardian_review_metric_tags, counter, record_duration);被 2 处调用(guardian_review_metrics_record_counts_durations_and_token_usage, track_guardian_review);外部调用 1 个(from_millis)。
emit_guardian_token_usage_histograms54–78 ↗
fn emit_guardian_token_usage_histograms(
session_telemetry: &SessionTelemetry,
token_usage: &TokenUsage,
base_tags: Vec<(&'static str, String)>,
)
作用:这个函数专门记录模型 token 用量。token 可以理解成模型读写文字时的计费小单位,它把总量、输入、输出、缓存输入等分门别类记下来。
数据流:进去的是遥测对象、一份 TokenUsage,以及前面已经做好的基础标签。它依次取出 total、input、cached_input、non_cached_input、output、reasoning_output 这些数值,每一种都额外加上 token_type 标签,然后写入同一个 token 用量直方图指标。出来没有返回值,但指标系统里会多出多条带不同 token_type 的记录。
调用关系:它只在 emit_guardian_review_metrics 发现审查结果里确实有 token_usage 时被调用。它会读取 TokenUsage 自带的 cached_input 和 non_cached_input 计算方法,然后把最终数字交给 SessionTelemetry 的 histogram。
调用图:调用 3 个内部函数(histogram, cached_input, non_cached_input);被 1 处调用(emit_guardian_review_metrics)。
guardian_review_metric_tags80–135 ↗
fn guardian_review_metric_tags(
result: &GuardianReviewAnalyticsResult,
approval_request_source: GuardianApprovalRequestSource,
reviewed_action: &GuardianReviewedAction,
) -> Vec<(&'static
作用:这个函数把一次 Guardian 审查的各种信息整理成一组统一标签。标签就像统计表里的列名和值,用来以后按“动作类型”“失败原因”“风险等级”等维度筛选。
数据流:进去的是审查结果、审批请求来源、被审查动作。它把决定、终止状态、失败原因、来源、动作、会话类型、风险等级、用户授权、最终 outcome、模型名、推理力度等信息逐一转成字符串;模型名和推理力度还会做清洗,避免出现不适合指标系统的字符。出来是一组键值对标签,供后续计数、耗时、token 指标共用。
调用关系:emit_guardian_review_metrics 会先调用它准备公共标签,然后把同一批标签用于多种指标。它内部依赖多个小的 tag 转换函数,比如 decision_tag、risk_level_tag、outcome_tag 等,把代码里的枚举翻译成稳定字符串。
调用图:被 1 处调用(emit_guardian_review_metrics);外部调用 1 个(vec!)。
decision_tag137–143 ↗
fn decision_tag(decision: GuardianReviewDecision) -> &'static str
作用:这个函数把 Guardian 的最终决定翻译成指标标签文字。它让“批准、拒绝、中止”在统计系统里有固定写法。
数据流:进去的是一个 GuardianReviewDecision。它按值判断是 Approved、Denied 还是 Aborted,然后分别输出 approved、denied、aborted。它不改任何外部状态。
调用关系:它是 guardian_review_metric_tags 使用的一个小翻译器,帮助生成 decision 标签;这个标签之后会跟着 emit_guardian_review_metrics 写进所有相关指标。
terminal_status_tag145–153 ↗
fn terminal_status_tag(status: GuardianReviewTerminalStatus) -> &'static str
作用:这个函数把审查结束时的状态翻成短标签。它比 decision 更细,可以区分正常拒绝、超时、失败关闭等情况。
数据流:进去的是 GuardianReviewTerminalStatus。它根据状态输出 approved、denied、aborted、timed_out 或 failed_closed。出来的是一个固定字符串,不产生副作用。
调用关系:它配合 guardian_review_metric_tags 生成 terminal_status 标签。这样 emit_guardian_review_metrics 写出的指标就能看出审查是怎样结束的,而不只是结果好坏。
failure_reason_tag155–164 ↗
fn failure_reason_tag(reason: Option<GuardianReviewFailureReason>) -> &'static str
作用:这个函数把失败原因翻成统计用文字。如果没有失败原因,也会明确写成 none,避免指标里出现空白。
数据流:进去的是一个可有可无的 GuardianReviewFailureReason。它把超时、取消、提示词构建错误、会话错误、解析错误分别转成对应字符串;如果是 None,就输出 none。它只返回字符串。
调用关系:guardian_review_metric_tags 用它生成 failure_reason 标签。之后 emit_guardian_review_metrics 会把这个标签附到计数、耗时和 token 指标上,方便排查失败集中在哪类问题。
approval_request_source_tag166–171 ↗
fn approval_request_source_tag(source: GuardianApprovalRequestSource) -> &'static str
作用:这个函数说明这次审批请求是从哪里来的。它把主流程和被委派的子代理区分开,方便看问题发生在谁身上。
数据流:进去的是 GuardianApprovalRequestSource。它把 MainTurn 转成 main_turn,把 DelegatedSubagent 转成 delegated_subagent。输出是固定标签字符串。
调用关系:它服务于 guardian_review_metric_tags 里的 approval_request_source 标签。emit_guardian_review_metrics 最后会用这个标签帮助统计不同来源的审查情况。
reviewed_action_tag173–183 ↗
fn reviewed_action_tag(action: &GuardianReviewedAction) -> &'static str
作用:这个函数把“被审查的动作”翻成一个动作类别。比如执行 shell 命令、访问网络、调用 MCP 工具等,都能在指标里分开看。
数据流:进去的是 GuardianReviewedAction。它只看动作属于哪一类,不关心动作里的详细参数,然后输出 shell、network_access、mcp_tool_call 等固定字符串。它不修改动作本身。
调用关系:guardian_review_metric_tags 用它生成 action 标签。这个标签跟随 emit_guardian_review_metrics 写入指标后,可以让人知道哪些类型的操作最常触发 Guardian 审查。
session_kind_tag185–192 ↗
fn session_kind_tag(kind: Option<GuardianReviewSessionKind>) -> &'static str
作用:这个函数标记 Guardian 审查会话的类型。它能区分新开的主干会话、复用会话、临时分叉会话,或者没有这类信息。
数据流:进去的是一个可有可无的 GuardianReviewSessionKind。它把不同会话类型转成 trunk_new、trunk_reused、ephemeral_forked;如果没有值,就输出 none。它只做翻译。
调用关系:guardian_review_metric_tags 用它生成 session_kind 标签。这样后续指标能帮助判断不同会话复用方式对耗时或失败率有没有影响。
optional_bool_tag194–200 ↗
fn optional_bool_tag(value: Option<bool>) -> &'static str
作用:这个函数把“可能不知道”的真假值转成标签。它比普通布尔值多处理一种 unknown,也就是信息缺失。
数据流:进去的是 Option<bool>,也就是可能是 true、false,也可能没有值。它分别输出 true、false 或 unknown。出来的是字符串,不改变外部数据。
调用关系:guardian_review_metric_tags 用它处理 had_prior_review_context 这类字段,因为有时系统知道之前有没有审查上下文,有时不知道。
bool_tag202–204 ↗
fn bool_tag(value: bool) -> &'static str
作用:这个函数把普通真假值转成指标标签文字。它用于那些一定有值、不会缺失的布尔字段。
数据流:进去的是 bool。true 输出 true,false 输出 false。它没有其他行为。
调用关系:guardian_review_metric_tags 用它处理 reviewed_action_truncated,也就是被审查动作内容有没有因为太长而被截断。
risk_level_tag206–214 ↗
fn risk_level_tag(risk_level: Option<GuardianRiskLevel>) -> &'static str
作用:这个函数把 Guardian 判断出的风险等级翻成标签。这样指标系统能按低、中、高、严重风险分组查看。
数据流:进去的是一个可有可无的 GuardianRiskLevel。它把 Low、Medium、High、Critical 转成 low、medium、high、critical;如果没有风险等级,就输出 none。
调用关系:guardian_review_metric_tags 用它生成 risk_level 标签。emit_guardian_review_metrics 写入指标后,运维或开发者可以看高风险审查是否更慢、更容易被拒绝。
user_authorization_tag216–224 ↗
fn user_authorization_tag(user_authorization: Option<GuardianUserAuthorization>) -> &'static str
作用:这个函数把用户授权级别翻成标签。它用于记录用户当前允许系统做多大权限的事。
数据流:进去的是一个可有可无的 GuardianUserAuthorization。它把 Unknown、Low、Medium、High 转成对应小写字符串;没有值时输出 none。它只返回标签文字。
调用关系:guardian_review_metric_tags 用它生成 user_authorization 标签。这个标签会被 emit_guardian_review_metrics 附到审查指标上,方便分析授权级别和审查结果之间的关系。
outcome_tag226–232 ↗
fn outcome_tag(outcome: Option<GuardianAssessmentOutcome>) -> &'static str
作用:这个函数把 Guardian 的评估结论翻成 allow 或 deny。它表示审查判断这个动作应该放行还是拒绝。
数据流:进去的是一个可有可无的 GuardianAssessmentOutcome。Allow 输出 allow,Deny 输出 deny,没有结论时输出 none。它不改任何状态。
调用关系:guardian_review_metric_tags 用它生成 outcome 标签。之后 emit_guardian_review_metrics 会把这个标签写进指标,帮助区分模型评估结论和最终终止状态。
tests::test_session_telemetry251–271 ↗
fn test_session_telemetry() -> SessionTelemetry
作用:这是测试用的准备函数,用来造一个只在内存里工作的遥测会话。它让测试不用真的把指标发到外部服务,也能检查记录结果。
数据流:进去没有参数。它创建一个内存指标导出器,创建 MetricsClient,再创建 SessionTelemetry,并把两者接起来。出来是一份可用于测试的 SessionTelemetry,测试可以往里面写指标并马上读取快照。
调用关系:测试 guardian_review_metrics_record_counts_durations_and_token_usage 会先调用它拿到测试遥测对象。它内部调用多个 new、in_memory、default 等构造函数,把测试环境搭好。
调用图:调用 4 个内部函数(new, new, in_memory, new);外部调用 2 个(default, env!)。
tests::find_metric273–282 ↗
fn find_metric(resource_metrics: &'a ResourceMetrics, name: &str) -> &'a Metric
作用:这个测试辅助函数在一堆指标里按名字找出目标指标。找不到时直接让测试失败。
数据流:进去的是一次指标快照和要找的指标名。它遍历所有 scope metrics,再遍历里面的 metric;名字匹配就返回该指标,全部找完还没有就 panic,也就是让测试报错停止。
调用关系:counter_point 和 histogram_sums 都靠它先找到具体指标,再继续读取计数或直方图数据。它在测试里相当于“从账本里翻到指定那一页”。
调用图:外部调用 2 个(scope_metrics, panic!)。
tests::attributes_to_map284–290 ↗
fn attributes_to_map(
attributes: impl Iterator<Item = &'a KeyValue>,
) -> BTreeMap<String, String>
作用:这个测试辅助函数把指标上的标签列表变成普通字典。这样测试比较标签时更直观,也不受原始迭代顺序影响。
数据流:进去的是一串 KeyValue 标签。它把每个标签的 key 和 value 都转成 String,然后收集成 BTreeMap。出来的是按键排序的字符串字典。
调用关系:counter_point 和 histogram_sums 在读取指标点时会用它整理标签。这样测试里的 assert_eq 可以直接比较清楚的键值表。
调用图:外部调用 1 个(map)。
tests::counter_point292–309 ↗
fn counter_point(
resource_metrics: &ResourceMetrics,
name: &str,
) -> (BTreeMap<String, String>, u64)
作用:这个测试辅助函数读取某个计数器指标的唯一数据点。它用来确认“审查次数”这类计数是否真的加了 1,以及标签是否正确。
数据流:进去的是指标快照和指标名。它先用 find_metric 找指标,再确认数据类型是 u64 的 Sum,并断言只有一个数据点;然后用 attributes_to_map 整理标签,连同计数值一起返回。遇到类型不对或数量不对就让测试失败。
调用关系:主测试 guardian_review_metrics_record_counts_durations_and_token_usage 用它读取 GUARDIAN_REVIEW_COUNT_METRIC。它把查找指标和解析计数器这两步封装起来,让主测试只关心结果对不对。
调用图:外部调用 4 个(assert_eq!, attributes_to_map, find_metric, panic!)。
tests::histogram_sums311–332 ↗
fn histogram_sums(resource_metrics: &ResourceMetrics, name: &str) -> BTreeMap<String, u64>
作用:这个测试辅助函数读取直方图指标里每类数据的总和。直方图可以理解成用来记录耗时或用量分布的统计桶,这里测试只关心总和。
数据流:进去的是指标快照和指标名。它先用 find_metric 找到指标,再确认这是 f64 直方图;然后遍历每个数据点,取 token_type 标签作为键,没有 token_type 时用 sample,当作普通样本;最后把每个点的 sum 转成 u64 放进表里返回。类型不对会让测试失败。
调用关系:主测试用它检查 token 用量、审查总耗时、首次 token 时间这些直方图。它和 counter_point 一起构成测试读取指标结果的工具箱。
调用图:外部调用 2 个(find_metric, panic!)。
tests::guardian_review_metrics_record_counts_durations_and_token_usage335–417 ↗
fn guardian_review_metrics_record_counts_durations_and_token_usage()
作用:这是本文件的核心测试,验证一次 Guardian 审查会正确记录次数、耗时、首次 token 时间和各种 token 用量。它还检查标签是否按预期清洗和填写。
数据流:进去没有参数。它先用 test_session_telemetry 创建测试遥测环境,再构造一份“批准、低风险、有 token 用量”的审查结果,调用 emit_guardian_review_metrics 写入指标;随后读取指标快照,用 counter_point 检查计数和标签,用 histogram_sums 检查 token 与耗时数值。测试通过表示这些指标写法没有跑偏。
调用关系:它直接覆盖 emit_guardian_review_metrics 这条主流程,也间接覆盖标签生成和 token 直方图记录。它调用 without_session 构造默认审查结果,再调用 assert_eq 对实际指标和期望值做精确比较。
调用图:调用 2 个内部函数(without_session, emit_guardian_review_metrics);外部调用 3 个(assert_eq!, counter_point, test_session_telemetry)。
cloud-config/src/metrics.rs源码 ↗
云端配置可能来自网络,也可能会失败、重试、超时,或者拿到一份空配置。这个文件就像给这条流程装了几个计数器:每次尝试拉取时记一笔,最后结束时记一笔,真正加载配置时也记一笔。每笔记录都会带上一些“标签”,比如是谁触发的、尝试第几次、结果是什么、HTTP 状态码是多少。HTTP 状态码就是网络请求返回的数字结果,比如 200 表示成功、401 表示未授权。它还会把配置包的大致内容压成一个简单标签,例如没有配置、空配置,或者包含企业配置/企业要求。真正发送指标的地方是 emit_metric,它会先看看全局监控系统是否已经启用;如果没有启用,就什么也不做,避免影响正常功能。
emit_fetch_attempt_metric7–24 ↗
fn emit_fetch_attempt_metric(
trigger: &str,
attempt: usize,
outcome: &str,
status_code: Option<u16>,
)
作用:记录一次“尝试拉取云端配置”的事件。别人会用它来知道每一次请求是成功、失败,还是因为某种网络状态卡住了。
数据流:进去的是触发原因、当前第几次尝试、这次尝试的结果,以及可选的 HTTP 状态码 → 它把尝试次数和状态码转成适合监控标签的文字 → 最后调用 emit_metric 发出一条名为云端配置拉取尝试的计数指标,不直接返回有用数据。
调用关系:它在拉取流程的中途被调用。handle_unauthorized、retry_after_request_failure 和 validate_and_cache_remote_bundle 会在遇到未授权、请求失败后准备重试、或校验远端配置时用它记一笔;它自己把状态码交给 status_code_tag 整理,再把完整指标交给 emit_metric 发送。
调用图:调用 2 个内部函数(emit_metric, status_code_tag);被 3 处调用(handle_unauthorized, retry_after_request_failure, validate_and_cache_remote_bundle);外部调用 1 个(vec!)。
emit_fetch_final_metric26–47 ↗
fn emit_fetch_final_metric(
trigger: &str,
outcome: &str,
reason: &str,
attempt_count: usize,
status_code: Option<u16>,
bundle: Option<&CloudConfigBundle>,
)
作用:记录一次“拉取云端配置最终结束”的结果。它回答的是:这轮拉取最后到底成没成,为什么结束,试了几次,拿到的配置大概长什么样。
数据流:进去的是触发原因、最终结果、结束原因、总尝试次数、可选状态码,以及可选的配置包 → 它把次数、状态码、配置包形状都转成标签文字 → 最后通过 emit_metric 发出一条最终结果指标,不返回业务数据。
调用关系:它通常在一轮拉取流程收尾时被调用。fetch_remote_bundle_and_update_cache_with_retries 会在带重试的拉取结束后用它总结全局结果;handle_unauthorized 和 validate_and_cache_remote_bundle 也会在特殊失败或校验阶段结束时记录最终状态。它依赖 status_code_tag 整理状态码,也通过 bundle_shape_tag 概括配置内容。
调用图:调用 2 个内部函数(emit_metric, status_code_tag);被 3 处调用(fetch_remote_bundle_and_update_cache_with_retries, handle_unauthorized, validate_and_cache_remote_bundle);外部调用 1 个(vec!)。
emit_load_metric49–58 ↗
fn emit_load_metric(trigger: &str, outcome: &str, bundle: Option<&CloudConfigBundle>)
作用:记录“加载云端配置”的结果。它让监控里能看见配置是否真的被加载进来了,而不只是网络上有没有拉到。
数据流:进去的是触发原因、加载结果,以及可选的配置包 → 它把配置包概括成一个 bundle_shape 标签 → 然后调用 emit_metric 发出一条加载指标,本身不产出返回值。
调用关系:它发生在读取或刷新配置时。load_startup_bundle_with_timeout 会在启动阶段加载配置时用它;refresh_cache_in_background 和 refresh_cache_once 会在后台或单次刷新缓存时用它。它把最终发送工作交给 emit_metric。
调用图:调用 1 个内部函数(emit_metric);被 3 处调用(load_startup_bundle_with_timeout, refresh_cache_in_background, refresh_cache_once);外部调用 1 个(vec!)。
bundle_shape_tag60–79 ↗
fn bundle_shape_tag(bundle: Option<&CloudConfigBundle>) -> String
作用:把一份云端配置包简化成一个短标签,方便放进监控系统。它不关心配置的具体内容,只关心里面有没有企业配置、企业要求,或者是不是空的。
数据流:进去的是一个可能存在、也可能不存在的 CloudConfigBundle → 如果没有配置包,就输出 none;如果有,就检查配置文件部分和要求文件部分里是否有企业托管内容 → 最后输出 empty、enterprise_config、enterprise_requirements,或两者用逗号拼起来的文字。
调用关系:它是指标标签的小帮手。emit_fetch_final_metric 和 emit_load_metric 需要描述配置包大概形状时,会用它把复杂配置变成一段简单文字,避免把完整配置内容塞进监控数据里。
调用图:外部调用 1 个(new)。
status_code_tag81–85 ↗
fn status_code_tag(status_code: Option<u16>) -> String
作用:把可选的 HTTP 状态码变成监控标签。这样无论有没有状态码,指标里都有一个统一字段,后续统计更方便。
数据流:进去的是一个可能存在的状态码数字 → 如果有数字,就转成字符串;如果没有,就用 none 这个文字代替 → 输出这个标签字符串,不改动其他东西。
调用关系:它被 emit_fetch_attempt_metric 和 emit_fetch_final_metric 调用。拉取尝试和最终结果都可能带网络状态码,所以这两个函数都用它来统一格式。
调用图:被 2 处调用(emit_fetch_attempt_metric, emit_fetch_final_metric)。
emit_metric87–95 ↗
fn emit_metric(metric_name: &str, tags: Vec<(&str, String)>)
作用:真正把指标送到监控系统里。它像统一出口,前面的函数只负责准备指标名称和标签,最后都从这里发出去。
数据流:进去的是指标名称和一组标签 → 它先询问 codex_otel 的全局监控对象是否存在;codex_otel 可以理解为项目接入的监控/遥测工具 → 如果存在,就把标签整理成监控库需要的格式,并把这个计数器加 1;如果不存在,就安静地跳过。
调用关系:它是本文件所有打点函数的底层出口。emit_fetch_attempt_metric、emit_fetch_final_metric 和 emit_load_metric 都会把准备好的指标交给它;它再调用 codex_otel::global 去找全局监控系统。这样上层流程不用关心监控系统有没有初始化。
调用图:被 3 处调用(emit_fetch_attempt_metric, emit_fetch_final_metric, emit_load_metric);外部调用 1 个(global)。
ext/goal/src/metrics.rs源码 ↗
可以把这个文件理解成目标系统旁边的“计数员”。主流程在创建或更新目标时,不想自己操心怎么上报监控,就把事情交给这里。这里包了一层 GoalMetrics,里面可能有一个 MetricsClient,也就是“把数字送到监控系统的客户端”。如果没有这个客户端,它就安静地什么都不做,这样本地运行或关闭监控时不会报错。它会记录几类事:目标刚创建、从暂停或受限状态恢复、进入最终状态。最终状态包括被阻塞、用量受限、预算受限、完成。除了给这些事件加一,还会把目标用掉的 token 数和耗时用直方图记录下来。直方图可以理解成把很多次耗时或用量放进桶里,方便看分布。一个重要细节是:它只在状态真的变了时才记录,避免同一个状态被重复上报,导致数据虚高。
GoalMetrics::new17–19 ↗
fn new(metrics_client: Option<MetricsClient>) -> Self
作用:创建一个 GoalMetrics 计数员,把外部传进来的监控客户端保存起来。有人想让目标系统能上报指标时,会先用它做出这个小工具。
数据流:进去的是一个可能存在、也可能不存在的 MetricsClient。函数不做复杂加工,只把它放进 GoalMetrics 结构里。出来的是一个新的 GoalMetrics;之后其他记录函数会靠里面这个客户端决定是上报指标,还是因为没有客户端而什么都不做。
调用关系:它由 new_with_host_capabilities 调用,通常是在目标功能初始化、知道宿主环境有没有监控能力之后创建。它本身不调用别人,只是把后面所有指标记录需要的“监控通道”准备好。
调用图:被 1 处调用(new_with_host_capabilities)。
GoalMetrics::record_created21–26 ↗
fn record_created(&self)
作用:记录“新目标被创建了一次”。这样系统之后能统计一共新建了多少目标。
数据流:进去的是当前这个 GoalMetrics 自己。它先查看里面有没有 MetricsClient;如果没有,就直接结束。若有,就把 GOAL_CREATED_METRIC 这个计数器加 1。出来没有业务返回值,但监控系统里会多一次“目标创建”的计数。
调用关系:它由 handle_create 调用,也就是在真正处理创建目标请求的时候触发。它不再把工作交给本文件里的其他函数,而是直接通过 metrics_client.counter 把数字送出去。
调用图:被 1 处调用(handle_create)。
GoalMetrics::record_resumed28–33 ↗
fn record_resumed(&self)
作用:记录“目标恢复运行了一次”。这里的恢复通常指目标之前暂停、被阻塞或用量受限,现在又回到活跃状态。
数据流:进去的是当前 GoalMetrics。它检查是否有 MetricsClient;没有就跳过。有的话,就把 GOAL_RESUMED_METRIC 计数器加 1。出来没有普通返回值,但监控系统会看到一次恢复事件。
调用关系:它被 record_resumed_if_status_changed 调用。也就是说,外面一般不会无脑调用它,而是先由 record_resumed_if_status_changed 判断状态变化是否真的算“恢复”,确认后才交给它上报。
调用图:被 1 处调用(record_resumed_if_status_changed)。
GoalMetrics::record_resumed_if_status_changed35–52 ↗
fn record_resumed_if_status_changed(
&self,
previous_status: Option<codex_state::ThreadGoalStatus>,
goal_status: codex_state::ThreadGoalStatus,
)
作用:判断一次状态变化是不是“目标恢复了”,如果是,才记录恢复指标。它的价值是防止把不相关的状态更新误算成恢复。
数据流:进去的是上一次状态 previous_status 和现在的新状态 goal_status。它检查现在是否是 Active,也就是活跃中;同时检查之前是否是 Paused、Blocked 或 UsageLimited。如果这两个条件都满足,就调用 record_resumed 记录一次恢复。出来没有返回值;可能产生一次监控上报,也可能什么都不做。
调用关系:它位于状态更新流程中的“判断关口”。它会用 matches! 这种 Rust 写法来判断旧状态是否属于一组指定状态;当判断成立时,把真正上报的活交给 record_resumed。这样调用方只需要告诉它新旧状态,不需要自己理解哪些变化算恢复。
调用图:调用 1 个内部函数(record_resumed);外部调用 1 个(matches!)。
GoalMetrics::record_terminal_if_status_changed54–83 ↗
fn record_terminal_if_status_changed(
&self,
previous_status: Option<codex_state::ThreadGoalStatus>,
goal: &codex_state::ThreadGoal,
)
作用:当目标进入某种结束类状态时,记录对应指标,并顺便记录这个目标用了多少 token、花了多少秒。它让系统不仅知道“结束了多少”,还知道这些结束的目标大概消耗了多少资源。
数据流:进去的是旧状态 previous_status 和完整的目标 goal。它先比较旧状态和当前状态;如果没变,就直接结束,避免重复计数。然后它看当前状态是哪一种:Blocked、UsageLimited、BudgetLimited、Complete 会对应不同计数器;Active 和 Paused 不算结束类状态,所以跳过。若有 MetricsClient,它会把对应事件计数加 1,再按当前状态打上 status 标签,记录 tokens_used 和 time_used_seconds 两个数值。出来没有普通返回值,但监控系统会收到事件次数、token 用量和耗时数据。
调用关系:它由 account_active_goal_progress 和 handle_update 调用,通常发生在目标进展被结算或目标状态被更新时。它自己不调用本文件的其他函数,而是直接选择合适的监控指标,再交给 metrics_client.counter 和 metrics_client.histogram 上报。
调用图:被 2 处调用(account_active_goal_progress, handle_update)。
记忆使用遥测
这些文件定义记忆读取和工具驱动的记忆使用的指标词汇与分类逻辑,然后发送相应计数器。
ext/memories/src/metrics.rs源码 ↗
这个文件像一个“记账员”,每次记忆工具被调用时,它把这次调用记成一条指标。指标就是给监控系统看的数字记录,比如“某个工具被调用了 1 次”。它还会把路径翻译成更好统计的类别:比如 MEMORY.md 归为 memory_md,skills/ 归为 skills,其他不认识的归为 other。这样监控图表不会被一堆零散路径弄乱。它也把成功或失败变成固定标签,把“结果是否被截断”变成 true、false 或 unknown。一个重要细节是:如果没有传入 MetricsClient(指标客户端,也就是负责把统计数据送出去的对象),它会直接什么都不做,不会让正常功能因为统计失败而中断。
record_tool_call7–30 ↗
fn record_tool_call(
metrics_client: Option<&MetricsClient>,
operation: &str,
scope: &str,
success: bool,
truncated: &str,
)
作用:记录一次记忆工具调用的统计信息,比如调用的是哪个操作、作用范围是什么、成功还是失败、结果有没有被截断。有人会用它来给监控系统留下“这次工具调用发生过”的痕迹。
数据流:进去的是一个可选的 MetricsClient、操作名、范围名、成功标记和截断标记。它先检查有没有指标客户端;没有就直接返回。有的话,它把工具名拼成“命名空间/操作名”的形式,再把操作、范围、状态、截断情况做成标签,最后把计数器加 1。出来没有业务结果,但监控系统多了一条调用计数。
调用关系:它是在 handle_call 处理记忆工具请求时被多处调用的收尾记录员。它自己会请 status_tag 把 true/false 的成功状态翻译成 succeeded/failed,也会用格式化拼出完整工具名,然后把结果交给 MetricsClient 记录。
调用图:调用 1 个内部函数(status_tag);被 4 处调用(handle_call, handle_call, handle_call, handle_call);外部调用 1 个(format!)。
scope_from_path32–53 ↗
fn scope_from_path(path: &str) -> &'static str
作用:把一个具体文件路径归类成稳定的统计范围名称。这样监控里看到的是 memory_md、skills 这类清楚的类别,而不是成百上千个不同路径。
数据流:进去的是一个路径字符串。它先去掉开头结尾多余的斜杠,再去掉开头的 ./,然后按规则判断:空路径算 root,MEMORY.md 算 memory_md,skills 或 skills 下面的文件算 skills,rollout_summaries 下面的算 rollout_summaries,等等。出来的是一个固定的范围标签;不符合已知规则的路径会变成 other。
调用关系:它在 handle_call 需要根据实际路径给指标分类时使用。它不负责记录指标,只负责把杂乱路径变成干净标签,之后这些标签通常会被交给 record_tool_call 记录。
调用图:被 1 处调用(handle_call)。
scope_from_optional_path55–57 ↗
fn scope_from_optional_path(path: Option<&str>, default: &'static str) -> &'static str
作用:处理“路径可能不存在”的情况:有路径就按路径分类,没有路径就使用调用者给的默认范围。它让上层代码不用反复写同样的判断。
数据流:进去的是一个可选路径和一个默认范围。如果路径存在,它把路径交给 scope_from_path 做分类;如果路径不存在,它直接返回默认范围。出来一定是一个可用于指标标签的范围字符串。
调用关系:它被 handle_call 在路径不一定存在的工具调用里使用。它把“有没有路径”的小判断包起来;真正的路径分类工作还是交给 scope_from_path。
调用图:被 2 处调用(handle_call, handle_call)。
truncated_tag59–65 ↗
fn truncated_tag(truncated: Option<bool>) -> &'static str
作用:把“内容是否被截断”转换成监控标签。这样指标里可以区分结果完整、结果被截短、以及根本不知道有没有截断这三种情况。
数据流:进去的是一个可选的布尔值:Some(true) 表示被截断,Some(false) 表示没被截断,None 表示未知。它把这三种情况分别变成字符串 true、false、unknown。出来的是一个固定标签,方便后续统计。
调用关系:它被 handle_call 在记录工具调用指标前使用。它不直接接触监控系统,只是把程序里的状态翻译成 record_tool_call 能写进指标标签的文字。
调用图:被 3 处调用(handle_call, handle_call, handle_call)。
status_tag67–69 ↗
fn status_tag(success: bool) -> &'static str
作用:把成功或失败这个布尔值转换成更适合人看的状态标签。成功变成 succeeded,失败变成 failed。
数据流:进去的是 success 这个 true/false 值。它做一个简单判断:true 输出 succeeded,false 输出 failed。它不改动任何外部数据,只返回这个状态文字。
调用关系:它是 record_tool_call 的内部小帮手。record_tool_call 在真正写指标前调用它,把原始的成功标记变成监控系统里更清楚的标签。
调用图:被 1 处调用(record_tool_call)。
memories/read/src/metrics.rs源码 ↗
这个文件很小,但作用像给一个统计表贴上固定标签。这里定义的 MEMORIES_USAGE_METRIC 是一个常量字符串,内容是 "codex.memories.usage"。当系统想统计“memories read”相关功能有没有被用、用了多少次,或把数据交给监控系统时,就可以引用这个名字。这样做的好处是集中管理:如果大家都直接手写这个字符串,时间久了很容易出现拼写不一致,监控数据就会被拆成几份,查问题时也会混乱。把名字放在这里,相当于规定了一个标准口径。
memories/read/src/usage.rs源码 ↗
这份文件像一个“门卫加登记员”。它先看传进来的命令是不是能正常解析的 shell 命令,再确认里面的每个小命令都是已知安全命令;如果命令看不懂或不安全,就直接不记录,避免把危险或奇怪命令误判成正常读取。确认安全后,它会把命令拆成“读文件”“搜索文件”“列文件”等更好理解的动作。只有读文件和搜索文件会继续检查路径。路径里如果包含 memories/MEMORY.md、memories/memory_summary.md、memories/raw_memories.md、memories/rollout_summaries/ 或 memories/skills/,就分别归到对应的 MemoriesUsageKind。这个分类最后可以变成指标标签,也就是统计系统里用来分组的小名字。这样系统不用真的理解文件内容,也能知道是哪种记忆被用到了。
MemoriesUsageKind::as_tag18–26 ↗
fn as_tag(self) -> &'static str
作用:把一种“记忆使用类型”转换成统计系统容易保存的短字符串。有人要上报指标时,就用它把枚举值变成像 memory_md 这样的标签。
数据流:进去的是一个 MemoriesUsageKind,也就是已经判断好的记忆类别;函数按类别做一一对应转换;出来的是固定的英文小写标签字符串,不改动任何外部数据。
调用关系:它通常是在统计上报阶段被用到:前面代码先判断读到了哪类记忆,后面上报指标时需要一个稳定的标签名,这个函数就负责把内部分类翻译成指标能用的名字。
memories_usage_kinds_from_command29–48 ↗
fn memories_usage_kinds_from_command(command: &str) -> Vec<MemoriesUsageKind>
作用:从一整条 shell 命令里找出它读取或搜索了哪些记忆文件。它会先过滤掉解析不了或不安全的命令,防止统计结果被乱七八糟的命令污染。
数据流:进去的是一段命令文本;它先用 parse_shell_script_into_commands 尝试拆成 shell 命令,再用 is_known_safe_command 检查每个命令是不是已知安全;不通过就返回空列表。通过后,它再用 parse_shell_script 拆成更具体的动作,只看读取和搜索动作里的路径,并交给 get_memory_kind 判断路径属于哪种记忆;出来的是一个 MemoriesUsageKind 列表,表示这条命令碰到了哪些记忆类别。
调用关系:它会被 emit_metric_for_tool_read 调用,也就是工具读取内容后准备记录指标时会来问它:“这次读取算用了哪种记忆?”它自己把命令解析的工作交给 shell 命令解析器,把路径分类的细活交给 get_memory_kind。
调用图:调用 2 个内部函数(parse_shell_script_into_commands, parse_shell_script);被 1 处调用(emit_metric_for_tool_read);外部调用 1 个(new)。
get_memory_kind50–64 ↗
fn get_memory_kind(path: String) -> Option<MemoriesUsageKind>
作用:根据文件路径判断它是不是某种已知的记忆文件或记忆目录。它是一个小型分类器,把路径字符串翻译成具体的记忆类别。
数据流:进去的是一个路径字符串;它依次检查路径里是否包含几个固定片段,比如 memories/MEMORY.md 或 memories/skills/;匹配到就返回对应的 MemoriesUsageKind,完全匹配不到就返回 None,表示这不是它关心的记忆文件。
调用关系:它是在 memories_usage_kinds_from_command 解析出读文件或搜索文件路径后被使用的。上层函数负责判断命令安不安全、是什么动作;这个函数只负责最后一步:看路径属于哪一种记忆。
core/src/memory_usage.rs源码 ↗
项目里有些工具会执行命令,而命令里可能会读取“记忆”数据。这个文件的作用就像门口的计数器:每次工具跑完后,它看看这次调用是不是能提取出一段命令;如果能,就判断这段命令涉及哪类记忆读取,再把“读取类型、用了哪个工具、是否成功”这些标签写进遥测指标。遥测指标可以理解成系统运行时自动收集的统计数字,用来之后分析功能有没有被用、用得是否顺利。这里不会真的执行命令,也不会改变工具结果,只是旁路地做记录。一个重要细节是:它只认识没有命名空间的 shell_command 和 exec_command 两种工具;如果参数不是函数调用格式,或者 JSON 解析失败,它会安静地跳过,不影响主流程。
emit_metric_for_tool_read9–27 ↗
fn emit_metric_for_tool_read(invocation: &ToolInvocation, success: bool)
作用:这个函数在一次工具调用结束后,尝试给“记忆读取”行为打点。打点就是往统计系统里加一条计数,方便以后知道哪些工具读取了哪类记忆,以及这次是否成功。
数据流:进去的是一次工具调用记录和一个成功/失败标记。它先让 shell_script_for_invocation 从调用参数里取出真正执行的命令;如果取不到,就什么也不做。取到命令后,它把工具名压平成适合当标签的名字,再用 memories_usage_kinds_from_command 判断命令涉及哪些记忆读取类型。最后,它对每一种类型都把计数器加 1,并附上读取类型、工具名、成功与否这些标签。
调用关系:它是在 dispatch_any_with_terminal_outcome 处理完工具最终结果后被叫到的,属于主流程旁边的记录员。它自己不判断命令细节,而是把“取命令”的活交给 shell_script_for_invocation,把“判断记忆类型”的活交给 memories_usage_kinds_from_command,把“整理工具名”的活交给 flat_tool_name。
调用图:调用 3 个内部函数(shell_script_for_invocation, flat_tool_name, memories_usage_kinds_from_command);被 1 处调用(dispatch_any_with_terminal_outcome)。
shell_script_for_invocation29–46 ↗
fn shell_script_for_invocation(invocation: &ToolInvocation) -> Option<String>
作用:这个函数负责从一次工具调用里抠出实际要执行的命令文本。只有拿到这段命令,后面才能判断它有没有读取记忆。
数据流:进去的是一份工具调用记录。它先确认这次调用的参数是函数形式;如果不是,就返回空。然后它看工具名:如果是 shell_command,就按 ShellCommandToolCallParams 这种格式解析 JSON 参数并取出 command;如果是 exec_command,就按 ExecCommandArgs 解析并取出 cmd。工具名不认识、带命名空间、或者 JSON 解析失败时,都返回空。
调用关系:它只服务于 emit_metric_for_tool_read,是打点流程的第一道筛子。它把复杂的工具参数转换成一段普通命令字符串,让上层函数不用关心不同工具的参数格式差异。
调用图:被 1 处调用(emit_metric_for_tool_read)。
memories/write/src/metrics.rs源码 ↗
这个文件本身不做计算,也不执行流程,它像一张“指标名称清单”。项目在写入记忆时,会经历启动、第一阶段、第二阶段等步骤,每一步都可能需要记录一些信息,比如跑了多少任务、总共花了多少毫秒、输入输出有多少、用了多少模型 token(可以理解成模型处理文字时消耗的计量单位)。这些字符串常量就是上报监控时使用的固定标签名。把它们统一放在这里,好处是以后如果要改指标名,只需要改一个地方;其他代码引用这些常量,就不容易因为拼写不一致造成数据分裂。类比一下,它就像仓库里每个货架的标准编号,大家按编号登记货物,统计时才不会乱。
SQLite 启动遥测
这些文件适配指标接收器并分类数据库初始化结果,以便一致报告轻量级 SQLite 启动和回退行为。
rollout/src/sqlite_metrics.rs源码 ↗
这份文件像一个“转接头”。数据库层只认识 DbTelemetry 这套简单接口:记一次计数,或者记一段耗时;而真正往外报监控的是 codex_otel::MetricsClient。OtelDbTelemetry 就夹在中间,把数据库给的指标名、数值、标签接过来,再补上一个 originator 标签,也就是“来源是谁”。这样同一个指标如果来自不同模块,监控系统里也能分清。recorder 用来创建这个转接头,并把来源字符串先限制到安全长度,避免标签值太长。with_originator 则专门负责把原有标签复制一份,再追加来源标签。一个重要细节是,真正发送指标时返回值被忽略了;也就是说,监控上报失败不会打断数据库正常工作,指标只是辅助观察,不应该影响主流程。
OtelDbTelemetry::counter15–18 ↗
fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)])
作用:这个函数用来记录一个“次数类”数据库指标,比如某个操作发生了几次。它会自动给指标加上来源标签,方便后面在监控里按来源区分。
数据流:外面传进来指标名字、增加的数量、已有标签;它先调用 with_originator 给标签补上 originator 来源;然后把这些信息交给 metrics.counter 发送出去。发送结果不往外返回,也不会因为发送失败改变数据库流程。
调用关系:它是 DbTelemetry 接口的一部分,数据库代码想记次数时会走到这里。它自己不拼接底层监控协议,而是先请 with_originator 准备好标签,再把真正上报的活交给 MetricsClient 的 counter。
调用图:调用 2 个内部函数(counter, with_originator)。
OtelDbTelemetry::record_duration20–23 ↗
fn record_duration(&self, name: &str, duration: Duration, tags: &[(&str, &str)])
作用:这个函数用来记录一个“耗时类”数据库指标,比如一次查询用了多久。它同样会加上来源标签,让监控数据更容易追踪。
数据流:外面传进来指标名字、持续时间和已有标签;它调用 with_originator 追加来源标签;然后把指标名、耗时和新标签交给 metrics.record_duration 上报。上报结果被丢弃,所以监控失败不会影响调用者。
调用关系:它也是 DbTelemetry 接口的一部分,数据库代码需要记录耗时时会用它。它依赖 with_originator 补齐标签,再把实际发送工作交给 MetricsClient 的 record_duration。
调用图:调用 2 个内部函数(record_duration, with_originator)。
recorder26–31 ↗
fn recorder(metrics: codex_otel::MetricsClient, originator: &str) -> DbTelemetryHandle
作用:这个函数负责造出一个数据库监控记录器,给别的代码使用。简单说,它把“监控客户端”和“这个指标来自哪里”包装成一个统一的 DbTelemetryHandle。
数据流:调用者传入 MetricsClient 和 originator 来源字符串;它先用 bounded_originator_tag_value 把来源值变成受限制的安全标签值;再创建 OtelDbTelemetry,并用 Arc 包起来返回。Arc 可以理解成“多人共用的一份对象”,方便不同地方同时拿着用。
调用关系:sqlite_telemetry_recorder 会调用它来准备 SQLite 的指标记录器。它不直接记录指标,而是在启动或配置阶段把 OtelDbTelemetry 搭好,后续 counter 和 record_duration 才会真正干上报的活。
调用图:被 1 处调用(sqlite_telemetry_recorder);外部调用 2 个(new, bounded_originator_tag_value)。
with_originator33–40 ↗
fn with_originator(
tags: &[(&'a str, &'a str)],
originator: &'static str,
) -> Vec<(&'a str, &'a str)>
作用:这个小函数专门给一组指标标签追加“来源”标签。它保证每次上报数据库指标时,都带着是谁发出来的这条线索。
数据流:输入是一组原始标签和一个 originator 来源值;它先复制原始标签,避免直接改调用者给的那份;然后把固定的 ORIGINATOR_TAG 和来源值加到末尾;最后返回这组新的标签。
调用关系:OtelDbTelemetry::counter 和 OtelDbTelemetry::record_duration 都会先调用它。它不负责发指标,只负责把标签准备好,让后面的 MetricsClient 能带着完整信息上报。
调用图:被 2 处调用(counter, record_duration)。
state/src/telemetry.rs源码 ↗
这份文件做的是数据库遥测,也就是把关键运行情况记成指标,方便之后看监控和排查问题。可以把它想成汽车仪表盘:车照样开,但仪表盘会告诉你启动是否顺利、哪里报警、花了多久。文件先定义了 DbTelemetry 这套接口,外部可以接入真正的指标系统。install_process_db_telemetry 会安装一个全进程共用的指标出口,而且只认第一次安装,避免不同地方互相覆盖。数据库初始化时,record_init_result 会把结果变成几个简单标签,比如成功或失败、哪个数据库、哪个阶段、错误大类,并同时记录次数和耗时。文件还专门把复杂错误归成少数几类,比如 SQLite 忙、锁住、只读、磁盘 IO、JSON 解析错误等。这样监控标签不会爆炸,也更容易看懂。
install_process_db_telemetry29–36 ↗
fn install_process_db_telemetry(telemetry: DbTelemetryHandle) -> bool
作用:安装一个全进程共用的数据库遥测出口。别人之后记录数据库指标时,如果没有单独指定出口,就会用这里安装的这个。
数据流:进去的是一个遥测对象,也就是会接收计数和耗时记录的东西。函数尝试把它放进全局的一次性位置里;如果这是第一次安装,就保存成功并返回 true;如果之前已经装过,就不替换,只写一条调试日志并返回 false。
调用关系:它通常在系统启动、遥测系统准备好之后被调用。后面的 record_counter 和 record_duration 不直接认识具体指标系统,而是通过 resolve_telemetry 找到这里装好的出口。
调用图:外部调用 1 个(debug!)。
DbKind::as_str47–54 ↗
fn as_str(self) -> &'static str
作用:把数据库种类变成稳定的短文本,比如 state、logs。这样写到监控标签里时,人和机器都容易识别。
数据流:进去的是一个 DbKind 枚举值,也就是“这是哪一种数据库”。函数按种类查表式地返回对应字符串,不改动任何状态。
调用关系:record_init_result 在记录初始化指标时会调用它,把内部的数据库类型转成监控标签里的 db 字段。
调用图:被 1 处调用(record_init_result)。
record_init_result57–73 ↗
fn record_init_result(
telemetry: Option<&dyn DbTelemetry>,
db: DbKind,
phase: &'static str,
duration: Duration,
result: &anyhow::Result<T>,
)
作用:记录一次数据库初始化或打开过程的结果。它会同时记“发生了一次”和“花了多久”,并带上成功失败、阶段、数据库名字、错误类型这些标签。
数据流:进去的是可选的遥测出口、数据库种类、阶段名、耗时,以及一个结果对象。函数先用 DbOutcomeTags::from_result 判断成功还是失败、失败属于哪类;再用 DbKind::as_str 得到数据库名字;最后调用 record_counter 记次数,调用 record_duration 记耗时。出来没有业务返回值,效果是把指标送到遥测出口。
调用关系:它是数据库启动监控的核心入口,会被 init_inner、open_sqlite 以及 record_backfill_gate 使用。它自己不关心指标系统在哪,而是把发送动作交给 record_counter 和 record_duration。
调用图:调用 4 个内部函数(as_str, from_result, record_counter, record_duration);被 3 处调用(init_inner, open_sqlite, record_backfill_gate)。
record_backfill_gate75–81 ↗
fn record_backfill_gate(
telemetry: Option<&dyn DbTelemetry>,
duration: Duration,
result: &anyhow::Result<()>,
)
作用:专门记录 state 数据库的 backfill_gate 阶段结果。backfill 可以理解成“补旧数据”,gate 是一道检查门。
数据流:进去的是可选遥测出口、这个检查花的时间,以及成功或失败的结果。函数把数据库固定为 State,把阶段固定为 backfill_gate,然后交给 record_init_result 统一记录。
调用关系:它是一个更好用的小入口,让调用方不用每次都手写数据库种类和阶段名。真正的指标组织和发送仍由 record_init_result 完成。
调用图:调用 1 个内部函数(record_init_result)。
record_fallback83–93 ↗
fn record_fallback(
caller: &'static str,
reason: &'static str,
telemetry_override: Option<&dyn DbTelemetry>,
)
作用:记录一次数据库降级行为。降级就是主方案不能用时,系统退到备用方案;这个函数会记下是谁触发的、为什么触发。
数据流:进去的是调用者名字、降级原因,以及可选的遥测出口。函数把这些变成 caller 和 reason 两个标签,然后调用 record_counter 让对应指标加一。
调用关系:当低层数据库代码发现需要走备用路径时会用它。它只负责组织标签,真正找到遥测出口和发送计数由 record_counter 完成。
调用图:调用 1 个内部函数(record_counter)。
record_counter95–99 ↗
fn record_counter(telemetry: Option<&dyn DbTelemetry>, name: &str, tags: &[(&str, &str)])
作用:把某个指标计数加一。它是文件里所有“记一次事件”的统一小帮手。
数据流:进去的是可选遥测出口、指标名、标签列表。函数先调用 resolve_telemetry 找到应该用哪个出口;如果找到了,就调用出口的 counter 方法,把这个指标加 1;如果没有出口,就什么也不做。
调用关系:record_init_result 用它记录初始化次数,record_fallback 用它记录降级次数。它把“找出口”的工作交给 resolve_telemetry,自己只负责发计数。
调用图:调用 1 个内部函数(resolve_telemetry);被 2 处调用(record_fallback, record_init_result)。
record_duration101–110 ↗
fn record_duration(
telemetry: Option<&dyn DbTelemetry>,
name: &str,
duration: Duration,
tags: &[(&str, &str)],
)
作用:记录某个动作花了多长时间。它是文件里所有“记耗时”的统一小帮手。
数据流:进去的是可选遥测出口、指标名、具体时长、标签列表。函数先通过 resolve_telemetry 找出口;如果有出口,就调用出口的 record_duration 方法;如果没有,就安静跳过。
调用关系:record_init_result 在记录数据库初始化结果时会用它补上一条耗时指标。它和 record_counter 配合,一个记次数,一个记时间。
调用图:调用 1 个内部函数(resolve_telemetry);被 1 处调用(record_init_result)。
resolve_telemetry112–114 ↗
fn resolve_telemetry(telemetry: Option<&dyn DbTelemetry>) -> Option<&dyn DbTelemetry>
作用:决定这次记录指标该发给谁。它优先使用调用方临时传进来的出口,没有的话才用全局安装的出口。
数据流:进去的是一个可能为空的遥测出口引用。函数先看这个引用有没有值;有就直接返回它;没有就去全局 PROCESS_DB_TELEMETRY 里找;如果全局也没安装,就返回空。
调用关系:record_counter 和 record_duration 都靠它统一选择遥测出口。这样调用方既可以临时指定出口,也可以依赖启动时 install_process_db_telemetry 安装的全局出口。
调用图:被 2 处调用(record_counter, record_duration)。
DbOutcomeTags::from_result122–133 ↗
fn from_result(result: &anyhow::Result<T>) -> Self
作用:把一个操作结果翻译成监控标签。成功就是 success 和 none,失败就标成 failed,并尽量给出错误大类。
数据流:进去的是一个 anyhow::Result,也就是“成功值或错误”的通用结果。函数检查它是 Ok 还是 Err;成功时直接产出成功标签;失败时调用 classify_error 分析错误链,产出失败标签和错误分类。
调用关系:record_init_result 在组织初始化指标时会先调用它。它自己只判断结果,不发送指标;错误细分工作交给 classify_error。
调用图:调用 1 个内部函数(classify_error);被 1 处调用(record_init_result)。
classify_error136–155 ↗
fn classify_error(err: &anyhow::Error) -> &'static str
作用:把复杂错误归成少数几种好懂的类别,比如数据库迁移、JSON、磁盘 IO、未知。这样监控里不会出现一大堆杂乱错误文本。
数据流:进去的是一个 anyhow::Error。函数沿着错误链一层层查看原始原因:如果发现 sqlx 数据库错误,就交给 classify_sqlx_error;如果发现迁移错误、JSON 错误或 IO 错误,就返回对应类别;都认不出来就返回 unknown。
调用关系:DbOutcomeTags::from_result 在失败时调用它。它像第一层分拣员,遇到 sqlx::Error 这种更复杂的数据库错误,就交给 classify_sqlx_error 继续细分。
调用图:调用 1 个内部函数(classify_sqlx_error);被 1 处调用(from_result);外部调用 1 个(chain)。
classify_sqlx_error157–172 ↗
fn classify_sqlx_error(err: &sqlx::Error) -> &'static str
作用:专门给 sqlx 数据库库返回的错误分类。sqlx 是这里用来访问数据库的 Rust 库。
数据流:进去的是一个 sqlx::Error。函数看错误具体是哪种:数据库返回的错误会取出错误码并交给 classify_sqlite_code;连接池超时返回 pool_timeout;IO 错误返回 io;JSON 解码失败返回 serde;其他情况返回 unknown。
调用关系:classify_error 发现错误来自 sqlx 时会调用它。它继续把 SQLite 错误码交给 classify_sqlite_code,因为数字错误码还需要再翻译成人能看懂的类别。
调用图:调用 1 个内部函数(classify_sqlite_code);被 1 处调用(classify_error);外部调用 1 个(Borrowed)。
classify_sqlite_code174–190 ↗
fn classify_sqlite_code(code: &str) -> &'static str
作用:把 SQLite 的数字错误码翻译成简单标签,比如 busy、locked、readonly、constraint。这样监控里看到的不是难记的数字。
数据流:进去的是 SQLite 返回的错误码字符串。函数尝试把它转成数字,并取低 8 位得到主错误码;然后按 SQLite 官方错误码表映射到固定文字;不认识或无法解析时返回 unknown。
调用关系:classify_sqlx_error 在遇到数据库原始错误码时调用它。测试函数 tests::classifies_extended_sqlite_codes 会检查它能正确识别普通码和扩展码。
调用图:被 1 处调用(classify_sqlx_error)。
tests::classifies_extended_sqlite_codes198–202 ↗
fn classifies_extended_sqlite_codes()
作用:验证 SQLite 错误码分类是否正确,尤其是扩展错误码也能归到正确的大类。
数据流:进去没有外部输入。测试直接把 5、6、2067 这些字符串喂给 classify_sqlite_code,并用断言检查结果分别是不是 busy、locked、constraint。测试通过时不产生业务输出;失败时测试框架会报错。
调用关系:它只在运行测试时由测试框架调用,不参与正常程序运行。它保护 classify_sqlite_code 的关键行为,避免以后改代码时把错误码翻译弄坏。
调用图:外部调用 1 个(assert_eq!)。