Codex 系统手册

OpenTelemetry 运行时、提供方与指标基础

stage-20.222 个文件

这一阶段像给系统装仪表盘和记录仪,启动时先接好线,之后在后台持续帮忙。配置部分把观测开关、地址、凭证整理成可用设置,写错了只警告不阻止启动;初始化和提供器把日志、指标、追踪接到 OpenTelemetry(统一观测出口),再由 OTLP 配好网络发送。指标模块管名字、标签、校验、计时器和汇总。追踪上下文把一次请求串起来,事件和请求回调记录耗时、状态和结果。

本阶段的文件22

运行时配置桥接

这些文件清理面向用户的 OTEL 设置,并将其转换为启动期间使用的具体运行时 provider 配置。

core/src/config/otel.rs源码 ↗
configconfig load / startup

这个文件像一个“配置安检口”。用户可以在配置文件里写 OTEL 相关内容,比如是否记录用户提示词、运行环境名、日志/链路/指标要发到哪里,以及给链路追踪附加的元数据。问题是,这些内容有些有严格格式,特别是 span attributes(给一次操作贴的标签)和 tracestate(跨服务传递追踪信息的标准字段)。如果直接拿用户配置去初始化 OTEL,格式错了可能导致启动失败,甚至影响全局观测状态。所以这里先补默认值,再逐项检查。合法的留下,不合法的丢掉,并把原因写进启动警告和日志。这样系统既能尽量按用户意图工作,又不会因为一个观测配置的小错误把整个程序弄挂。

函数细节5
resolve_config9–37 ↗
fn resolve_config(
    config: OtelConfigToml,
    startup_warnings: &mut Vec<String>,
) -> OtelConfig

作用:把配置文件里读到的 OTEL 原始配置,变成程序内部稳定可用的 OTEL 配置。它会补上默认值,并把容易写错的追踪元数据先清洗一遍。

数据流:进去的是用户配置 OtelConfigToml,以及一个用来收集启动警告的列表。它读取每个可选项:没写的就填默认值,比如环境名、导出方式;写了的就采用用户值。然后它把 span attributes 和 tracestate 交给专门的检查函数过滤。出来的是一个完整的 OtelConfig;同时,坏配置对应的警告会被追加到启动警告列表里。

调用关系:它是在总配置加载流程 load_config_with_layer_stack 中被调用的。它自己不做所有细节检查,而是把 span attributes 交给 resolve_span_attributes,把 tracestate 交给 resolve_tracestate,最后把这些结果装配成最终配置。

调用图:调用 2 个内部函数(resolve_span_attributes, resolve_tracestate);被 1 处调用(load_config_with_layer_stack)。

resolve_span_attributes39–58 ↗
fn resolve_span_attributes(
    span_attributes: Option<BTreeMap<String, String>>,
    startup_warnings: &mut Vec<String>,
) -> BTreeMap<String, String>

作用:检查用户给链路追踪 span 添加的标签是否合法。span 可以理解为“一段被追踪的工作”,这些标签就是贴在这段工作上的说明纸条。

数据流:进去的是可选的键值表,以及启动警告列表。如果用户没写,它直接给出空表。如果写了,它会一条一条拿出来,用 OTEL 校验工具检查这一条标签格式对不对。合法的放进新表;不合法的丢掉,并写一条警告。出来的是只包含合法标签的新表,警告列表可能被增加内容。

调用关系:它由 resolve_config 调用,是整理总 OTEL 配置时的一个小检查站。遇到坏标签时,它把写警告的活交给 push_invalid_config_warning,校验规则则交给外部的 validate_span_attributes

调用图:调用 1 个内部函数(push_invalid_config_warning);被 1 处调用(resolve_config);外部调用 3 个(from, new, validate_span_attributes)。

resolve_tracestate60–90 ↗
fn resolve_tracestate(
    tracestate: Option<BTreeMap<String, BTreeMap<String, String>>>,
    startup_warnings: &mut Vec<String>,
) -> BTreeMap<String, BTreeMap<String, String>>

作用:检查用户配置的 tracestate 是否能安全用于链路追踪。tracestate 是 W3C 标准里的一个传递字段,用来让多个系统在一次请求流转时共享追踪信息。

数据流:进去的是可选的 tracestate 多层键值表,以及启动警告列表。如果用户没写,它返回空表。如果写了,它先逐个成员处理:每个成员里的字段先交给 resolve_tracestate_member_fields 过滤;空成员会被跳过。然后它检查每个成员整体是否合法。最后还会检查所有成员合在一起后是否仍然符合标准。出来的是过滤后的合法 tracestate;如果整体组合不合法,它会警告并返回空表。

调用关系:它由 resolve_config 调用,用来处理比普通标签更复杂的追踪状态配置。它会继续调用 resolve_tracestate_member_fields 做成员内部字段过滤,也会在发现问题时调用 push_invalid_config_warning。具体格式规则由外部的 OTEL 校验函数判断。

调用图:调用 2 个内部函数(push_invalid_config_warning, resolve_tracestate_member_fields);被 1 处调用(resolve_config);外部调用 3 个(new, validate_tracestate_entries, validate_tracestate_member)。

resolve_tracestate_member_fields92–107 ↗
fn resolve_tracestate_member_fields(
    member_key: &str,
    fields: BTreeMap<String, String>,
    startup_warnings: &mut Vec<String>,
) -> BTreeMap<String, String>

作用:检查某一个 tracestate 成员里面的字段,去掉格式不合法的小字段。可以把它理解成先检查“表格里每一格”,再让上层检查“整张表”。

数据流:进去的是成员名、这个成员下面的一组字段,以及启动警告列表。它逐个字段构造成一个小表,然后用 OTEL 校验工具判断这个字段放在该成员下是否有效。合法字段留下,不合法字段丢掉并产生警告。出来的是该成员过滤后的字段表。

调用关系:它只由 resolve_tracestate 调用,是 tracestate 检查流程的内层步骤。遇到坏字段时,它调用 push_invalid_config_warning 统一记录警告;字段格式的真正判断交给外部的 validate_tracestate_member

调用图:调用 1 个内部函数(push_invalid_config_warning);被 1 处调用(resolve_tracestate);外部调用 3 个(from, new, validate_tracestate_member)。

push_invalid_config_warning109–117 ↗
fn push_invalid_config_warning(
    config_key: &str,
    err: impl Display,
    startup_warnings: &mut Vec<String>,
)

作用:把“某个 OTEL 配置不合法,已被忽略”这件事用统一格式记录下来。这样用户启动时能看见问题,程序也能继续运行。

数据流:进去的是配置项名字、错误原因,以及启动警告列表。它把这些拼成一句清楚的话:正在忽略哪个配置、为什么忽略。然后它把这句话写入日志警告,并追加到启动警告列表。出来没有单独返回值,但日志和警告列表都被更新了。

调用关系:它是这个文件里各个校验函数共用的“报错话术”。resolve_span_attributesresolve_tracestateresolve_tracestate_member_fields 一旦发现坏配置,都会调用它,避免每个地方各写一套不同的警告格式。

调用图:被 3 处调用(resolve_span_attributes, resolve_tracestate, resolve_tracestate_member_fields);外部调用 2 个(format!, warn!)。

core/src/otel_init.rs源码 ↗
orchestrationstartup

OpenTelemetry,简称 OTEL,可以理解成程序里的“行车记录仪”:它把运行过程中的追踪、指标、事件发到外部系统,方便排查问题和观察使用情况。这个文件主要做启动阶段的接线工作。它先读取 Config 里的 OTEL 设置,判断要用哪种出口:不上报、发到 Statsig、通过 HTTP 发 OTLP、或通过 gRPC 发 OTLP。OTLP 是 OpenTelemetry 常用的传输格式。它还会处理请求头、TLS 证书这些连接外部服务需要的安全信息。接着,它根据是否允许 analytics(分析统计)决定要不要开启指标上报,并设置服务名、版本、运行时指标等信息。除此之外,它还提供两个小入口:一个记录“进程启动了”这个指标,另一个把 SQLite 数据库的遥测也挂到同一套指标系统里。整体上,它不是业务功能本身,而是让整个程序从一开始就具备可观察性。

函数细节4
build_provider16–95 ↗
fn build_provider(
    config: &Config,
    service_version: &str,
    service_name_override: Option<&str>,
    default_analytics_enabled: bool,
) -> Result<Option<OtelProvider>, Box<dyn Error>>

作用:根据应用配置创建一个 OpenTelemetry 提供者,也就是后续记录日志、指标、追踪时要用的“遥测总开关和管道”。如果配置说不导出遥测,它会返回空,表示不用接这套系统。

数据流:输入是一份 Config、服务版本号、可选的服务名覆盖值,以及默认是否开启分析统计。它读取配置里的导出方式、HTTP 或 gRPC 地址、请求头、TLS 证书、环境名、附加属性等信息,把配置里的类型转换成 codex_otel 真正认识的设置。它还会读取 originator 作为默认服务名,并检查 RuntimeMetrics 功能开关。最后交给 OtelProvider::from 创建结果,出来的是一个可能存在的 OtelProvider;如果遥测关闭,就是 None;如果配置有问题,则返回错误。

调用关系:这是启动遥测的核心拼装点。initialize、run_main_with_transport_options、run_main 这些启动流程会调用它来准备遥测系统,测试代码也会用它确认配置能正确生成提供者。它自己会调用 originator 取得默认来源名,再调用 OtelProvider::from 把整理好的设置变成真正可运行的遥测提供者。

调用图:调用 2 个内部函数(originator, from);被 6 处调用(initialize, run_main_with_transport_options, app_server_default_analytics_disabled_without_flag, app_server_default_analytics_enabled_with_flag, run_main, mcp_server_builds_otel_provider_with_logs_traces_and_metrics)。

codex_export_filter99–101 ↗
fn codex_export_filter(meta: &tracing::Metadata<'_>) -> bool

作用:判断一条 tracing 事件是不是 Codex 自己的 OTEL 事件,只允许这类事件被导出。这样可以避免把别的库产生的杂音也一股脑发出去。

数据流:输入是一条 tracing::Metadata,也就是一条日志或追踪事件的说明信息。它读取这条事件的 target,target 可以理解成事件来自哪个模块。然后检查它是不是以 codex_otel 开头。输出是 true 或 false:true 表示保留并导出,false 表示过滤掉。

调用关系:它是遥测导出时的过滤门卫。外部 tracing 系统在决定哪些事件能走 OTEL 管道时会用到这个判断。它只做一件事:通过 metadata.target() 看事件来源,不再把工作交给别的本项目函数。

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

record_process_start103–108 ↗
fn record_process_start(otel: Option<&OtelProvider>, originator: &str)

作用:记录一次“程序进程启动了”的指标。这个指标可以帮助统计程序启动次数,也能用来观察不同来源的启动行为。

数据流:输入是一个可选的 OtelProvider 和 originator 字符串。它先看看遥测提供者是否存在,再看看里面有没有 metrics,也就是指标记录能力。如果没有,就直接什么都不做;如果有,就调用 codex_otel::record_process_start_once,把启动事件按 originator 记录进去。它不返回有意义的数据,只是在指标系统里留下记录。

调用关系:run_main_with_transport_options 和 run_main 这些主启动流程会在程序开始运行时调用它。它的位置很靠前,通常是在遥测系统已经建好之后。真正写入指标的动作交给外部函数 record_process_start_once,这个函数只负责检查有没有指标管道并触发记录。

调用图:被 3 处调用(run_main_with_transport_options, run_main, run_main);外部调用 1 个(record_process_start_once)。

install_sqlite_telemetry110–116 ↗
fn install_sqlite_telemetry(otel: Option<&OtelProvider>, originator: &str)

作用:把 SQLite 数据库的运行情况接入 OpenTelemetry 指标系统。SQLite 是一种嵌入式数据库,这样做可以让程序知道数据库相关操作也发生了什么。

数据流:输入是一个可选的 OtelProvider 和 originator 字符串。它先确认遥测提供者存在,并且里面有 metrics 指标功能;如果没有,就安静退出。若有,它用 codex_rollout::sqlite_telemetry_recorder 创建一个 SQLite 遥测记录器,再把这个记录器安装到 codex_state::install_process_db_telemetry。结果是进程里的数据库遥测被挂上了指标系统;函数本身不返回业务数据。

调用关系:run_main_with_transport_options 和 run_main 会在启动阶段调用它,让数据库层也能被观测到。它自己不直接监听数据库,而是先请 codex_rollout 创建记录器,再把记录器交给 codex_state 安装到进程数据库遥测位置。也就是说,它是数据库遥测和 OTEL 指标之间的接线员。

调用图:被 3 处调用(run_main_with_transport_options, run_main, run_main);外部调用 2 个(sqlite_telemetry_recorder, install_process_db_telemetry)。

Provider 和 crate 表面

这些文件定义 OTEL crate 的公共集成边界,并根据已解析设置构建进程级 tracing、logging 和 metrics provider。

otel/src/config.rs源码 ↗
configstartup / config load

OTEL 是 OpenTelemetry 的缩写,可以理解成一套“给程序装仪表盘”的通用标准,用来收集指标、追踪调用过程。这个文件就是这套仪表盘的配置说明书。它定义了几种导出方式:不导出、发到内置的 Statsig、通过 OTLP HTTP 或 OTLP gRPC 发到指定地址。OTLP 是 OpenTelemetry 的传输协议,HTTP 和 gRPC 是两种不同的网络传法。文件里还保存了内置 Statsig 的地址、请求头名字和 API key,并且有一个重要保护:在 debug 调试版本里,默认 Statsig 会被关掉,避免本地开发和测试时偷偷发遥测数据。它还定义了 TLS 配置,TLS 可以理解成网络传输时的加密证书设置。另一个小检查是 span attributes,也就是附加在追踪片段上的键值信息,要求键名不能是空字符串,防止发出无效数据。

函数细节3
resolve_exporter13–36 ↗
fn resolve_exporter(exporter: &OtelExporter) -> OtelExporter

作用:这个函数把用户或默认配置里的“导出器”变成真正可用的导出器配置。特别是遇到 Statsig 这种内置简写时,它会展开成具体的 HTTP 地址、请求头和协议;但在调试版本里会直接关掉。

数据流:进去的是一个 OtelExporter 配置。函数先看它是不是 Statsig:如果是,并且当前是 debug 调试构建,就输出 None,表示不导出;如果是正式构建,就把它改写成一个 OTLP HTTP 配置,里面带上 Statsig 的固定地址、API key 请求头、JSON 协议和无 TLS 特殊配置。其他导出器不改内容,只复制一份原样输出。

调用关系:它是在搭建日志、追踪和指标导出时被调用的,例如 build_otlp_metric_exporter、build_logger、build_tracer_provider,以及配置转换的 from 流程会用它。它自己主要依赖编译模式判断 cfg! 和复制 clone,把“简写配置”交还给后续真正创建导出器的代码使用。

调用图:被 4 处调用(build_otlp_metric_exporter, from, build_logger, build_tracer_provider);外部调用 3 个(from, cfg!, clone)。

validate_span_attributes39–48 ↗
fn validate_span_attributes(attributes: &BTreeMap<String, String>) -> std::io::Result<()>

作用:这个函数检查用户配置的追踪附加信息是否合法。它主要防止出现空的属性名,因为这种数据发到遥测系统里没有意义,也可能让后续处理出错。

数据流:进去的是一张按键排序的字符串键值表,也就是 span attributes。函数查看所有键名,只要发现有空字符串,就生成一个 InvalidInput 类型的输入错误并返回;如果所有键名都有内容,就返回成功,不修改这张表。

调用关系:它在配置转换的 from 流程里被调用,属于把配置真正用于运行前的一道检查门。它不继续调用复杂逻辑,只是在发现坏配置时创建一个标准输入错误,让上层知道配置不能继续用。

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

tests::statsig_default_metrics_exporter_is_disabled_in_debug_builds113–118 ↗
fn statsig_default_metrics_exporter_is_disabled_in_debug_builds()

作用:这是一个测试,确认调试版本里内置 Statsig 导出器会被自动关闭。它保护开发者,避免本地测试时意外发送遥测数据。

数据流:测试里创建一个 Statsig 导出器配置,把它交给 resolve_exporter,然后检查返回结果是不是 None。结果符合预期就通过,不符合就让测试失败。

调用关系:它只在测试时运行,用来验证 resolve_exporter 的安全行为。它通过 assert! 做断言,等于是给这个配置文件里最容易出意外的默认上报行为加了一道自动检查。

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

otel/src/lib.rs源码 ↗
othercross-cutting

这个文件像一家店的前台。真正干活的代码分散在 config、metrics、provider、trace_context 等模块里,但外部使用者不需要知道每个柜台在哪里,这里把常用类型和函数统一重新导出。遥测指的是把程序运行时发生了什么记录出来,比如性能指标、追踪链路、会话信息,方便排查问题和观察系统状态。文件里还定义了 TelemetryAuthMode,用来把更复杂的登录方式简化成遥测系统关心的两类:API Key 或 ChatGPT 登录。这样做可以避免 otel 反过来依赖核心业务代码,减少模块之间互相牵扯。最后,它提供了两个全局入口:一个用已安装好的指标客户端启动计时器,一个读取全局 Statsig 指标配置。

函数细节3
TelemetryAuthMode::from55–64 ↗
fn from(mode: codex_app_server_protocol::AuthMode) -> Self

作用:这个函数把应用服务器里的登录方式,转换成遥测系统自己认识的登录分类。它的用处是让遥测只记录必要的认证类别,而不用理解所有具体登录细节。

数据流:进去的是一个 codex_app_server_protocol::AuthMode,也就是外部协议里定义的认证方式。函数检查它是哪一种:如果是普通 API Key 或 Bedrock API Key,就变成 TelemetryAuthMode::ApiKey;其他几种 ChatGPT、令牌、身份或个人访问令牌相关方式,都归成 TelemetryAuthMode::Chatgpt。出来的是一个更简单的 TelemetryAuthMode,不会改动外部状态。

调用关系:它位于协议层认证信息和遥测记录之间,像一个翻译员。调用方在准备写遥测字段时会用它,把复杂认证枚举翻译成 otel 库自己的小枚举,从而避免这个库直接依赖更大的核心业务模块。

start_global_timer68–73 ↗
fn start_global_timer(name: &str, tags: &[(&str, &str)]) -> MetricsResult<Timer>

作用:这个函数用全局已经安装好的指标客户端开始计时。调用方可以用它测量某段代码花了多久,比如一次请求、一次工具调用或一个后台任务。

数据流:进去的是计时器名字和一组标签,标签就是给指标附上的说明文字,比如类别或来源。函数先调用 global 取当前全局指标客户端;如果没有安装指标客户端,就返回 MetricsError::ExporterDisabled,表示指标导出没开。若拿到了客户端,它会把名字和标签交给客户端创建 Timer,返回这个计时器给调用方。

调用关系:它是外部代码使用指标系统的快捷入口。它自己不真正计时,而是先通过 global 找到全局 metrics 客户端,再把 start_timer 的工作交给那个客户端;如果全局客户端不存在,它就提前告诉调用方这次不能计时。

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

global_statsig_metrics_settings77–79 ↗
fn global_statsig_metrics_settings() -> Option<StatsigMetricsSettings>

作用:这个函数读取当前全局 OTEL 指标客户端对应的 Statsig 指标设置。只有当前指标导出器确实是 Statsig 时,才会拿到配置。

数据流:它不需要输入参数。函数调用 global_statsig_settings 去询问全局指标系统:现在有没有 Statsig 的指标配置。结果可能是 Some(StatsigMetricsSettings),表示找到了已解析好的配置;也可能是 None,表示没有全局指标客户端,或者当前用的不是 Statsig。

调用关系:它是查看全局 Statsig 指标配置的薄包装。真正查找配置的工作交给 global_statsig_settings;调用方只需要通过这个统一入口,就能知道当前遥测指标是否走 Statsig 以及具体设置是什么。

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

otel/src/provider.rs源码 ↗
orchestrationstartup, cross-cutting, teardown

这个文件像一位“总接线员”:启动时读 OtelSettings 配置,判断日志、追踪、指标哪些要打开,然后分别装好对应的发送器。日志是程序说了什么,追踪是一次请求或任务走过哪些步骤,指标是数字状态,比如计数和耗时。它还会给这些数据贴上服务名、版本、环境、主机名等标签,方便外部系统按机器或环境筛选。比较重要的是,它会先检查追踪相关配置,避免 OpenTelemetry 的全局状态被半路装坏;因为这些全局设置一旦装上,通常不能轻松撤回。OtelProvider 保存装好的 logger、tracer 和 metrics,并在手动 shutdown 或对象销毁时尽量把缓存数据刷出去,防止程序退出时丢遥测数据。文件底部还有一些测试,确认主机名和日志/追踪过滤规则没有跑偏。

函数细节25
OtelProvider::shutdown64–75 ↗
fn shutdown(&self)

作用:主动关闭这个 OpenTelemetry 提供者,把还没发出去的追踪、指标和日志尽量发完。有人在程序结束前调用它,可以减少最后一批遥测数据丢失的机会。

数据流:进去的是当前 OtelProvider 里保存的可选 logger、tracer_provider 和 metrics → 它逐个检查哪些存在,先让追踪强制刷新并关闭,再关闭指标,最后关闭日志 → 出来没有返回值,但外部收集器更可能收到退出前缓存的数据。

调用关系:它是 OtelProvider::from 创建出的提供者的收尾动作。它不把工作交给本文件其他函数,而是调用 OpenTelemetry SDK 和 MetricsClient 自己的关闭能力,和 OtelProvider::drop 起到类似的兜底作用。

OtelProvider::from77–154 ↗
fn from(settings: &OtelSettings) -> Result<Option<Self>, Box<dyn Error>>

作用:根据配置创建一个 OtelProvider,也就是把日志、追踪、指标的出口真正搭起来。没有它,配置只是纸面说明,程序不会真的把遥测数据发出去。

数据流:进去的是 OtelSettings,里面有服务名、环境、版本、各种 exporter(发送目标)和追踪标签等 → 它先判断哪些功能启用,校验追踪配置,再创建指标客户端、日志资源、追踪资源、日志发送器和追踪发送器,并安装全局追踪传播器和全局指标客户端 → 出来是 Ok(Some(OtelProvider));如果三类遥测都没开,就清空 tracestate 后返回 Ok(None)。

调用关系:这是本文件的核心入口,build_provider、若干集成测试和 build_wfp_metrics_provider 会调用它。它把具体活儿分给 make_resource、build_logger、build_tracer_provider、MetricsClient::new、install_global 等函数和模块,自己负责决定顺序,尤其是先校验、再安装全局状态。

调用图:调用 9 个内部函数(resolve_exporter, validate_span_attributes, new, otlp, install_global, install_global_statsig_settings, make_resource, set_tracestate_entries, validate_tracestate_entries);被 6 处调用(build_provider, otel_provider_rejects_header_unsafe_configured_tracestate, otlp_http_exporter_sends_logs_to_collector, otlp_http_exporter_sends_traces_to_collector, otlp_http_exporter_sends_traces_to_collector_in_tokio_runtime, build_wfp_metrics_provider);外部调用 6 个(new, new, debug!, set_text_map_propagator, set_tracer_provider, matches!)。

OtelProvider::logger_layer156–165 ↗
fn logger_layer(&self) -> Option<impl Layer<S> + Send + Sync>

作用:给 tracing 日志系统生成一个“日志层”,让符合条件的日志能被 OpenTelemetry 收走。这里的 layer 可以理解成插在日志流水线上的一个过滤和转发器。

数据流:进去的是 OtelProvider 自己,以及外部 tracing subscriber 的类型信息 → 如果 provider 里有 logger,它创建 OpenTelemetryTracingBridge,并套上 log_export_filter 过滤规则;如果没有 logger,就返回 None → 出来是一个可选的日志转发层。

调用关系:它通常在应用组装 tracing_subscriber 时被调用。它不负责创建 logger,logger 已由 OtelProvider::from 和 build_logger 准备好;它只把 logger 接入运行中的日志管道。

OtelProvider::tracing_layer167–178 ↗
fn tracing_layer(&self) -> Option<impl Layer<S> + Send + Sync>

作用:给 tracing 系统生成一个“追踪层”,把程序里的 span(一次操作的时间段)发给 OpenTelemetry。这样外部系统就能看到任务从开始到结束的路径。

数据流:进去的是 OtelProvider 自己,以及 subscriber 类型信息 → 如果 provider 里有 tracer,它创建 tracing_opentelemetry layer,绑定 tracer,并套上 trace_export_filter;如果没有 tracer,就返回 None → 出来是一个可选的追踪转发层。

调用关系:它通常和 logger_layer 一起在启动时装进 tracing_subscriber。tracer 本身来自 OtelProvider::from 创建的 tracer_provider;这个函数只负责把它接到 tracing 的事件流里。

OtelProvider::codex_export_filter180–182 ↗
fn codex_export_filter(meta: &tracing::Metadata<'_>) -> bool

作用:提供一个兼容用的日志过滤入口,判断某条 tracing 元数据是否应该作为 Codex 日志导出。它本质上复用普通日志过滤规则。

数据流:进去的是一条 tracing Metadata,里面包含日志或 span 的来源信息 → 它直接交给 log_export_filter 判断 → 出来是 true 或 false,表示这条内容要不要进日志出口。

调用关系:它位于外部调用者和 log_export_filter 之间,像一个别名入口。真正的判断逻辑不在这里,而是在 log_export_filter 和 targets 模块里。

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

OtelProvider::log_export_filter184–186 ↗
fn log_export_filter(meta: &tracing::Metadata<'_>) -> bool

作用:判断某条日志是否允许被导出到日志收集系统。这样可以避免把专门给追踪用、或不该作为日志发出的内容混进去。

数据流:进去的是 tracing Metadata → 它读取 metadata 的 target,也就是这条日志来自哪个命名区域,再交给 is_log_export_target 判断 → 出来是布尔值,true 表示允许导出为日志。

调用关系:logger_layer 会把它装进日志层里,codex_export_filter 也会复用它。真正的白名单/黑名单规则由 crate::targets::is_log_export_target 决定。

调用图:调用 1 个内部函数(is_log_export_target);外部调用 1 个(target)。

OtelProvider::trace_export_filter188–190 ↗
fn trace_export_filter(meta: &tracing::Metadata<'_>) -> bool

作用:判断某条 tracing 内容是否可以进入追踪出口。它允许 span 本身通过,也允许来自“追踪安全”目标的事件通过。

数据流:进去的是 tracing Metadata → 它先看这是不是 span;如果不是,再看 target 是否属于 trace-safe,也就是被认为适合放进追踪里的来源 → 出来是 true 或 false。

调用关系:tracing_layer 会用它过滤追踪数据。它把具体目标名判断交给 crate::targets::is_trace_safe_target,避免普通日志内容误混进追踪。

调用图:调用 1 个内部函数(is_trace_safe_target);外部调用 2 个(is_span, target)。

OtelProvider::metrics192–194 ↗
fn metrics(&self) -> Option<&MetricsClient>

作用:取出当前 provider 里的指标客户端,供别的代码记录数字型运行数据。比如请求数、耗时、错误次数这类东西。

数据流:进去的是 OtelProvider 自己 → 它查看 metrics 字段是否存在,并把里面的 MetricsClient 借出来 → 出来是 Option<&MetricsClient>,有指标功能就返回引用,没有启用就返回 None。

调用关系:with_provider_metrics 会调用它,把全局或上下文里的指标客户端拿出来用。它不创建指标客户端,创建工作在 OtelProvider::from 里完成。

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

OtelProvider::drop198–209 ↗
fn drop(&mut self)

作用:当 OtelProvider 被销毁时自动做清理,作为忘记手动 shutdown 时的保险。它尽量把缓存的遥测数据送出去再结束。

数据流:进去的是即将被释放的 OtelProvider → 它检查 tracer_provider、metrics、logger 是否存在,逐个刷新或关闭 → 出来没有返回值,但会改变这些底层客户端的状态,让它们进入关闭状态。

调用关系:这是 Rust 的 Drop 机制自动调用的函数,不需要业务代码手动调用。它和 OtelProvider::shutdown 做几乎同样的收尾工作,保证生命周期结束时不会悄悄丢掉太多数据。

make_resource212–221 ↗
fn make_resource(settings: &OtelSettings, kind: ResourceKind) -> Resource

作用:创建 OpenTelemetry 的 Resource,也就是给一批日志或追踪统一贴上的“这是谁发的”身份信息。比如服务名、版本、环境和主机名。

数据流:进去的是 OtelSettings 和 ResourceKind(日志还是追踪)→ 它读取服务名,检测主机名,再调用 resource_attributes 生成标签列表,最后用 OpenTelemetry 的 builder 组装 Resource → 出来是一个 Resource 对象。

调用关系:OtelProvider::from 会分别为日志和追踪调用它。它把主机名检测交给 detected_host_name,把标签细节交给 resource_attributes,自己负责把这些信息包成 SDK 要的资源对象。

调用图:调用 2 个内部函数(detected_host_name, resource_attributes);被 1 处调用(from);外部调用 1 个(builder)。

resource_attributes223–241 ↗
fn resource_attributes(
    settings: &OtelSettings,
    host_name: Option<&str>,
    kind: ResourceKind,
) -> Vec<KeyValue>

作用:生成资源标签列表,让外部遥测系统知道数据属于哪个版本、哪个环境,以及必要时来自哪台机器。标签就像快递面单上的字段,方便后面分类查询。

数据流:进去的是配置、可选主机名和资源类型 → 它总是加入 service.version 和 env;如果是日志资源,并且主机名不是空白,就额外加入 host.name → 出来是一组 KeyValue 标签。

调用关系:make_resource 会调用它来准备 Resource 的标签。测试 resource_attributes_include_host_name_when_present 和 resource_attributes_omit_host_name_when_missing_or_empty 专门检查它对主机名的处理是否符合预期。

调用图:被 3 处调用(make_resource, resource_attributes_include_host_name_when_present, resource_attributes_omit_host_name_when_missing_or_empty);外部调用 2 个(new, vec!)。

detected_host_name243–246 ↗
fn detected_host_name() -> Option<String>

作用:从当前机器上读取主机名,并把空白或无效结果过滤掉。这样日志里可以带上机器名,但不会带一个空字符串这种没用信息。

数据流:进去没有参数 → 它调用系统主机名读取函数 gethostname,把结果转成字符串,再交给 normalize_host_name 清理 → 出来是 Some(主机名) 或 None。

调用关系:make_resource 在创建资源时调用它。它只负责“探测当前机器叫什么”,真正决定是否把主机名写进标签的是 resource_attributes。

调用图:调用 1 个内部函数(normalize_host_name);被 1 处调用(make_resource);外部调用 1 个(gethostname)。

normalize_host_name248–251 ↗
fn normalize_host_name(host_name: &str) -> Option<String>

作用:清理主机名字符串,把前后空格去掉,并把空结果当成没有主机名。它防止遥测标签里出现看起来有值、其实没内容的字段。

数据流:进去的是一个主机名字符串 → 它 trim 掉前后空白,然后检查是否为空 → 出来是 Some(干净的主机名) 或 None。

调用关系:detected_host_name 会用它清理系统读到的主机名,resource_attributes 也通过 host_name.and_then(normalize_host_name) 复用同样规则。

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

tracer_provider_builder253–265 ↗
fn tracer_provider_builder(
    resource: &Resource,
    span_attributes: BTreeMap<String, String>,
) -> TracerProviderBuilder

作用:创建追踪提供者的 builder,并按需加上每个 span 都要带的自定义标签。builder 可以理解成“还没完工的组装器”。

数据流:进去的是 Resource 和 span_attributes 这组自定义键值 → 它先创建带 Resource 的 SdkTracerProvider builder;如果自定义标签为空,就直接返回;否则加上 SpanAttributesProcessor → 出来是可继续添加导出器的 TracerProviderBuilder。

调用关系:build_tracer_provider 会调用它,把资源信息和每个 span 的额外标签统一装进去。SpanAttributesProcessor 是它在需要时塞进追踪流水线的小处理器。

调用图:被 1 处调用(build_tracer_provider);外部调用 2 个(builder, clone)。

SpanAttributesProcessor::on_start277–281 ↗
fn on_start(&self, span: &mut Span, _cx: &Context)

作用:每当一个 span 开始时,给它加上配置里的自定义标签。span 是一段操作的记录,加标签能让外部系统按项目、租户或其他维度筛选。

数据流:进去的是正在开始的 span、当前上下文,以及处理器里保存的 attributes → 它遍历每个键值,调用 span.set_attribute 贴到这个 span 上 → 出来没有返回值,但这个 span 多了这些属性。

调用关系:tracer_provider_builder 在配置了 span_attributes 时会安装这个处理器。OpenTelemetry SDK 在 span 启动时自动调用它,不需要业务代码直接调用。

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

SpanAttributesProcessor::on_end283–283 ↗
fn on_end(&self, _span: SpanData)

作用:这是 SpanProcessor 接口要求实现的结束回调,但这里结束时不需要额外动作。也就是说,标签只在 span 开始时加一次。

数据流:进去的是已经结束的 SpanData → 它什么也不改、什么也不发 → 出来没有返回值,也没有副作用。

调用关系:OpenTelemetry SDK 会在 span 结束时调用它。它存在是为了满足 SpanProcessor 这套接口,实际工作集中在 on_start。

SpanAttributesProcessor::force_flush285–287 ↗
fn force_flush(&self) -> OTelSdkResult

作用:这是 SpanProcessor 接口里的“强制刷新”方法,但这个处理器自己不缓存数据,所以直接报告成功。

数据流:进去的是处理器自己 → 它不读取外部数据,也不执行刷新操作 → 出来是 Ok(()),表示没有待刷新的东西。

调用关系:当 tracer_provider 被 force_flush 时,OpenTelemetry SDK 可能调用它。真正需要刷新的通常是批量导出器,而不是这个只贴标签的小处理器。

SpanAttributesProcessor::shutdown_with_timeout289–291 ↗
fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult

作用:这是 SpanProcessor 接口里的关闭方法,但这个处理器没有网络连接或后台线程要关,所以直接成功。

数据流:进去的是处理器自己和一个超时时间 → 它忽略超时时间,不做清理 → 出来是 Ok(())。

调用关系:当 tracer_provider 关闭时,OpenTelemetry SDK 会调用它。它只是配合 SDK 生命周期,真正的关闭工作在导出器和批处理器那里。

build_logger294–361 ↗
fn build_logger(
    resource: &Resource,
    exporter: &OtelExporter,
) -> Result<SdkLoggerProvider, Box<dyn Error>>

作用:按配置创建日志提供者,让日志可以通过 OTLP gRPC 或 OTLP HTTP 发到外部收集器。OTLP 是 OpenTelemetry 常用的传输协议,可以理解成遥测数据的标准快递格式。

数据流:进去的是 Resource 和日志 exporter 配置 → 它先创建带资源信息的日志 builder;如果 exporter 是 None,就只返回本地 provider;如果是 gRPC,就准备 headers、TLS 加密配置和 gRPC exporter;如果是 HTTP,就选择二进制或 JSON 协议,准备 headers、可选 TLS HTTP 客户端和 HTTP exporter → 出来是 SdkLoggerProvider。

调用关系:OtelProvider::from 在日志启用时调用它。它把底层细节交给 crate::otlp::build_header_map、build_grpc_tls_config、build_http_client 等工具函数,自己负责按 exporter 类型走不同装配路线。

调用图:调用 4 个内部函数(resolve_exporter, build_grpc_tls_config, build_header_map, build_http_client);外部调用 7 个(new, builder, from_headers, builder, debug!, clone, unreachable!)。

build_tracer_provider363–457 ↗
fn build_tracer_provider(
    resource: &Resource,
    exporter: &OtelExporter,
    span_attributes: BTreeMap<String, String>,
) -> Result<SdkTracerProvider, Box<dyn Error>>

作用:按配置创建追踪提供者,让 span 能被批量发送到外部收集器。它还会根据运行时环境选择更合适的 HTTP 发送方式。

数据流:进去的是 Resource、追踪 exporter 配置和每个 span 要加的自定义属性 → 如果 exporter 是 None,就返回只带资源和属性处理器的 provider;如果是 gRPC,就准备 headers、TLS 和 gRPC SpanExporter;如果是 HTTP,它会选择协议,构建 HTTP exporter,并在多线程 Tokio 运行时里使用异步批处理器,否则使用普通批处理器 → 出来是 SdkTracerProvider。

调用关系:OtelProvider::from 在追踪启用时调用它。它依赖 tracer_provider_builder 安装资源和 span 属性处理器,也依赖 crate::otlp 的 TLS、header、HTTP 客户端和运行时判断函数完成底层传输准备。

调用图:调用 7 个内部函数(resolve_exporter, build_async_http_client, build_grpc_tls_config, build_header_map, build_http_client, current_tokio_runtime_is_multi_thread, tracer_provider_builder);外部调用 7 个(builder, new, from_headers, builder, builder, debug!, unreachable!)。

tests::resource_attributes_include_host_name_when_present466–479 ↗
fn resource_attributes_include_host_name_when_present()

作用:测试日志资源在传入有效主机名时,会真的把 host.name 标签加进去。这样可以防止以后改代码时不小心丢掉机器名。

数据流:进去的是测试构造的 OtelSettings 和主机名 opentelemetry-test → 它调用 resource_attributes 生成标签,再从结果里查找 host.name → 出来通过 assert_eq! 确认找到的值正是传入的主机名。

调用关系:这是针对 resource_attributes 的单元测试。它使用 tests::test_otel_settings 提供最小配置,只关注主机名这一条规则。

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

tests::resource_attributes_omit_host_name_when_missing_or_empty482–510 ↗
fn resource_attributes_omit_host_name_when_missing_or_empty()

作用:测试没有主机名、主机名全是空格,或者资源类型是追踪时,都不会加入 host.name。这样日志和追踪的标签行为保持清楚边界。

数据流:进去的是测试配置、None 主机名、空白主机名和追踪资源场景 → 它分别调用 resource_attributes,再检查结果里不存在 host.name → 出来是三组 assert!,全部成立才表示规则正确。

调用关系:这是 resource_attributes 的另一组保护测试。它和 include_host_name 测试互补,一个确认该加时会加,一个确认不该加时不会加。

调用图:调用 1 个内部函数(resource_attributes);外部调用 2 个(assert!, test_otel_settings)。

tests::log_export_target_excludes_trace_safe_events513–518 ↗
fn log_export_target_excludes_trace_safe_events()

作用:测试日志导出目标不会包含 trace_safe 这类专门给追踪用的事件。这样日志出口和追踪出口不会互相串内容。

数据流:进去的是几组硬编码的 target 字符串 → 它调用 is_log_export_target,并用 assert! 检查 log_only、network_proxy 允许,trace_safe 和其子路径不允许 → 出来是测试通过或失败。

调用关系:这个测试验证 targets 模块的日志过滤规则,而这些规则会被 OtelProvider::log_export_filter 用在 logger_layer 中。

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

tests::trace_export_target_only_includes_trace_safe_prefix521–526 ↗
fn trace_export_target_only_includes_trace_safe_prefix()

作用:测试追踪导出目标只接受 trace_safe 前缀的事件,不接受普通日志目标。这样追踪里不会混进不适合的日志内容。

数据流:进去的是几组 target 字符串 → 它调用 is_trace_safe_target,并断言 trace_safe 和子路径允许,log_only、network_proxy 不允许 → 出来是测试通过或失败。

调用关系:这个测试验证 targets 模块的追踪过滤规则,而这些规则会被 OtelProvider::trace_export_filter 用在 tracing_layer 中。

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

tests::test_otel_settings528–541 ↗
fn test_otel_settings() -> OtelSettings

作用:生成一份测试用的最小 OtelSettings,避免每个测试都手写一大段配置。它让测试更短,也让默认测试条件更一致。

数据流:进去没有参数 → 它填入 environment、service_name、service_version、codex_home,并把日志、追踪、指标 exporter 都设为 None,span_attributes 和 tracestate 设为空 → 出来是一份 OtelSettings。

调用关系:resource_attributes 相关测试会调用它。它只是测试辅助函数,不参与正式运行时的 provider 创建流程。

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

指标基础

这些文件建立指标子系统的共享类型、验证规则、命名约定、全局协调以及标签/配置模型。

otel/src/metrics/error.rs源码 ↗
data_modelcross-cutting

这个文件专门描述 metrics(指标,也就是系统运行时的数字记录,比如计数器、耗时、运行状态)相关的错误。没有它,别的代码遇到“指标名为空”“标签里有非法字符”“导出器没打开”“关闭指标系统失败”这类问题时,就可能各写各的错误,排查起来很乱。这里用 MetricsError 这个枚举把所有常见失败情况集中列出来;每一种错误都带有给人看的提示文字,有些还会带上原始底层错误,方便继续追根溯源。Result<T> 是一个简写,意思是“要么成功得到 T,要么失败得到 MetricsError”。可以把它理解成指标系统里的统一报错格式:所有零件坏了,都贴同一种风格的故障标签。

otel/src/metrics/validation.rs源码 ↗
domain_logiccross-cutting

监控指标就像仓库里的货物,名字和标签就是货物上的条码。如果条码为空,或者混进了系统不认识的符号,后面分类、统计、发送到外部监控平台时就容易乱套。这个文件专门做这层检查:指标名不能为空,只能用英文字母、数字、点、下划线和短横线;标签的键和值也不能为空,允许的字符类似,但额外允许斜杠。它把检查拆成几块:整组标签逐个查,单个标签键和值共用一套基础检查,字符规则则由小函数判断。检查通过就什么也不改,返回成功;检查失败就返回明确的 MetricsError,告诉调用方到底是名字空了,还是某个标签不合规。

函数细节7
validate_tags5–11 ↗
fn validate_tags(tags: &BTreeMap<String, String>) -> Result<()>

作用:检查一整组标签是否都能被指标系统接受。有人创建带标签的指标时会用它,防止一批标签里藏着空键、空值或非法字符。

数据流:进去的是一个按键排序的标签表,里面每项都是“标签名 → 标签值”。它逐个拿出键和值,分别交给标签键检查和标签值检查;只要有一个不合格,就立刻返回错误。全部合格时,不修改原标签,返回成功。

调用关系:它通常在创建带标签对象时由 new 调用,像一道总闸门。它自己不细看每个字符,而是把键交给 validate_tag_key,把值交给 validate_tag_value,让这两个更小的检查器完成具体判断。

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

validate_metric_name13–23 ↗
fn validate_metric_name(name: &str) -> Result<()>

作用:检查指标名字是否合法。计数器、仪表盘数值、直方图这类指标在创建前都会靠它确认名字不是空的,也没有监控系统不接受的字符。

数据流:进去的是一个指标名字字符串。它先看是不是空字符串;如果是,就返回“指标名为空”的错误。再逐个看字符是否符合指标名规则;如果有不合格字符,就带着原名字返回错误。全部通过则返回成功,不改变名字本身。

调用关系:它位于各种指标创建函数的入口处,会被 counter、gauge、histogram、duration_histogram 调用。它相当于给所有指标类型共用的一把尺子,保证不同指标用同一套命名规则。

调用图:被 4 处调用(counter, duration_histogram, gauge, histogram)。

validate_tag_key25–28 ↗
fn validate_tag_key(key: &str) -> Result<()>

作用:检查单个标签的名字是否合法。标签名是用来分类指标的,比如“服务名”或“地区”,所以不能为空,也不能有乱七八糟的字符。

数据流:进去的是一个标签键字符串。它把这个字符串连同“tag key”这个说明文字交给通用的标签组件检查函数;如果不合格,错误里会说明出问题的是标签键。检查通过后返回成功,不修改输入。

调用关系:它会被 attributes、with_tag、push_optional_tag 这些添加标签的地方调用,也会被 validate_tags 在批量检查时调用。真正的空值和字符判断交给 validate_tag_component 完成。

调用图:调用 1 个内部函数(validate_tag_component);被 4 处调用(attributes, with_tag, push_optional_tag, validate_tags)。

validate_tag_value30–32 ↗
fn validate_tag_value(value: &str) -> Result<()>

作用:检查单个标签的值是否合法。标签值是真正的分类内容,比如某个服务叫“api”或某个环境叫“prod”,这里保证它不是空的,也符合允许的字符范围。

数据流:进去的是一个标签值字符串。它把值和“tag value”这个说明文字交给通用检查函数;如果为空或包含非法字符,就返回能说明问题位置的错误。通过时返回成功,不改动原值。

调用关系:它会被 attributes、with_tag、push_optional_tag 在添加标签时使用,也会被 validate_tags 做整组标签检查时使用。它和 validate_tag_key 共用 validate_tag_component,避免键和值的基础规则写两遍。

调用图:调用 1 个内部函数(validate_tag_component);被 4 处调用(attributes, with_tag, push_optional_tag, validate_tags)。

validate_tag_component34–47 ↗
fn validate_tag_component(value: &str, label: &str) -> Result<()>

作用:这是标签键和值共用的底层检查器。它负责判断一段标签文字是不是空的,以及每个字符是不是允许出现。

数据流:进去的是要检查的文字,以及一个说明它身份的标签,比如“tag key”或“tag value”。它先检查文字是否为空;为空就返回带身份说明的错误。然后逐个检查字符是否符合标签字符规则;有非法字符就返回错误并带上原值。全部没问题就返回成功。

调用关系:它不直接面对外部创建指标的代码,而是被 validate_tag_key 和 validate_tag_value 包起来使用。这样外层函数能告诉它当前查的是键还是值,错误信息也更容易看懂。

调用图:被 2 处调用(validate_tag_key, validate_tag_value)。

is_metric_char49–51 ↗
fn is_metric_char(c: char) -> bool

作用:判断一个字符能不能出现在指标名字里。它把指标命名规则压缩成一个很小的判断:字母、数字、点、下划线、短横线才可以。

数据流:进去的是一个字符。它检查这个字符是不是 ASCII 字母或数字,ASCII 可以简单理解为常见英文字符集;如果不是,再看它是不是点、下划线或短横线。结果出来的是 true 或 false,表示能不能用。

调用关系:它是 validate_metric_name 背后的字符尺子。指标名检查会逐个字符套用这个规则;其中对符号的判断借助 Rust 的 matches! 宏,也就是一种简短写法,用来判断字符是不是几个指定符号之一。

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

is_tag_char53–55 ↗
fn is_tag_char(c: char) -> bool

作用:判断一个字符能不能出现在标签键或标签值里。它和指标名规则很像,但额外允许斜杠,方便标签里表达类似路径或分组的内容。

数据流:进去的是一个字符。它先接受常见英文字母和数字,再接受点、下划线、短横线和斜杠。最后返回 true 或 false,告诉上层这个字符是否合格。

调用关系:它服务于 validate_tag_component,也就是标签键和值的共同检查流程。符号判断同样通过 Rust 的 matches! 宏完成,让允许的符号列表写得很直接。

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

otel/src/metrics/names.rs源码 ↗
data_modelcross-cutting

这个文件像一本“指标名字通讯录”。系统里很多地方都会上报运行数据,如果每个地方自己手写名字,很容易出现拼错、大小写不一致、同一个意思用了两个名字等问题。这样监控平台就会把数据分散成不同项目,后面查问题会很麻烦。这里把所有指标名统一写成常量,也就是固定不变的字符串。别的代码只要引用这些常量,就能保证大家说的是同一种指标。它覆盖了工具调用、进程启动、API 请求、SSE 事件、WebSocket、模型响应耗时、一次对话回合耗时、守护审核、目标状态、插件、钩子、启动预热、线程技能等很多运行场景。文件本身不做计算,也不发送数据;它的重要性在于给整套观测系统提供统一命名。

otel/src/metrics/config.rs源码 ↗
configstartup / config load

指标可以理解成程序的“健康数据”,比如请求数、耗时、错误数。这个文件就是给这些健康数据准备一张填写好的“投递单”。MetricsConfig 里放着运行环境、服务名、版本号、导出器、导出间隔、是否允许手动抓取运行时快照,以及每条指标都要附带的默认标签。导出器有两种:一种是 OTLP,也就是把指标发给外部观测系统的通用协议;另一种是 InMemory,把指标留在内存里,主要方便测试检查。这个文件还提供了一组链式设置方法,像填表一样一步步补充配置。特别重要的是 with_tag:它不会随便接受标签,而是先检查标签名和值是否合法,避免后面发指标时因为脏数据出问题。

函数细节5
MetricsConfig::otlp27–42 ↗
fn otlp(
        environment: impl Into<String>,
        service_name: impl Into<String>,
        service_version: impl Into<String>,
        exporter: OtelExporter,
    ) -> Self

作用:创建一个把指标发到外部观测系统的配置。有人想让真实服务把指标送去收集器时,就会用这个函数。

数据流:进去的是环境名、服务名、服务版本,以及一个 OTLP 导出器配置;函数把前三个值转成字符串,包进 MetricsConfig,并把导出器标记成 Otlp;出来的是一份默认配置,导出间隔还没改、运行时手动读取没开、默认标签为空。

调用关系:它通常在上层配置转换或集成测试里被调用,用来搭出真实导出链路的起点。它自己不做网络发送,只是把“以后要用 OTLP 发出去”这件事写进配置里,后续指标初始化流程会读取这份配置。

调用图:被 2 处调用(from, otlp_http_exporter_sends_metrics_to_collector);外部调用 3 个(new, into, Otlp)。

MetricsConfig::in_memory45–60 ↗
fn in_memory(
        environment: impl Into<String>,
        service_name: impl Into<String>,
        service_version: impl Into<String>,
        exporter: InMemoryMetricExporter,
    ) -> Self

作用:创建一个把指标暂存在内存里的配置。它主要给测试用,因为测试可以直接查看内存里的指标,而不用真的连外部服务。

数据流:进去的是环境名、服务名、服务版本,以及一个内存指标导出器;函数把文字信息转成字符串,把导出器包装成 InMemory;出来的是一份可用于测试的 MetricsConfig,其他选项保持默认状态。

调用关系:很多测试和测试工具会调用它,比如会话遥测测试、WebSocket 测试夹具、运行时指标快照测试等。它让这些测试能验证“指标有没有产生”,而不需要搭建真实的指标收集服务器。

调用图:被 10 处调用(test_session_telemetry, test_session_telemetry_without_metadata, test_session_telemetry, websocket_harness_with_provider_options, build_metrics_with_defaults, runtime_metrics_summary_collects_tool_api_and_streaming_metrics, manager_snapshot_metrics_collects_without_shutdown, snapshot_collects_metrics_without_shutdown, build_in_memory_client, invalid_tag_component_is_rejected);外部调用 3 个(new, into, InMemory)。

MetricsConfig::with_export_interval63–66 ↗
fn with_export_interval(mut self, interval: Duration) -> Self

作用:设置周期性导出指标的时间间隔。也就是说,告诉系统隔多久把攒到的指标发出去一次。

数据流:进去的是已经有的 MetricsConfig 和一个时间长度;函数把这个时间长度写到 export_interval 字段里;出来的是修改后的同一份配置,方便继续接着调用别的设置方法。

调用关系:它是配置创建之后的补充步骤,适合用在链式写法里,比如先创建 otlp 配置,再追加导出间隔。它不调用别的项目函数,只改配置里的一个选项。

MetricsConfig::with_runtime_reader69–72 ↗
fn with_runtime_reader(mut self) -> Self

作用:打开“按需读取运行时指标快照”的开关。简单说,就是允许程序在需要时临时抓一份当前指标,而不是只等定时导出。

数据流:进去的是一份 MetricsConfig;函数把 runtime_reader 这个布尔开关设为 true;出来的是已经开启该能力的配置。

调用关系:它也是配置创建后的链式补充项。后面的指标系统初始化时会看到这个开关,并据此准备手动读取指标的能力;这个函数本身只负责把开关打开。

MetricsConfig::with_tag75–82 ↗
fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Result<Self>

作用:给所有指标加一个默认标签。标签就是随指标一起带上的键值信息,比如环境、区域、组件名,方便后面筛选和分组。

数据流:进去的是一份 MetricsConfig、一个标签名和一个标签值;函数先把它们转成字符串,再分别检查标签名和值是否合法;如果检查通过,就把这对标签放进 default_tags;出来的是 Ok 包着的新配置,如果标签不合规,就返回错误。

调用关系:它会把校验工作交给 validate_tag_key 和 validate_tag_value,避免非法标签混进全局配置。后续每条指标都会带上这里存下的默认标签,所以这个函数处在“配置入口”和“实际发指标”之间,是防止坏标签扩散的关口。

调用图:调用 2 个内部函数(validate_tag_key, validate_tag_value);外部调用 1 个(into)。

otel/src/metrics/tags.rs源码 ↗
domain_logiccross-cutting:在记录遥测指标、尤其是会话或进程启动指标时使用

做监控时,光记录“发生了一次请求”还不够,还要知道这次请求来自哪里、用的什么模型、是什么版本,这些补充信息就叫标签。这个文件把常用标签名统一定义好,避免各处手写时写错。它还提供了 SessionMetricTagValues 这个小容器,用来收集一次会话相关的标签,并按固定顺序变成真正要上报的标签列表。可选标签没有值时会被跳过,不会硬塞空值。每个标签放进去前都会检查键和值是否合法,像门口保安一样挡掉不合规的数据。另一个重要点是 originator,也就是“发起方”标签,会被限制在一小组已知值里;不认识的统一记成 other,防止出现成千上万个不同标签值,把指标系统拖垮。

函数细节5
bounded_originator_tag_value29–36 ↗
fn bounded_originator_tag_value(originator: &str) -> &'static str

作用:这个函数把“发起方”名字收敛成安全、已知的一小组值。这样监控系统不会因为用户传来各种奇怪名字,而产生太多分类。

数据流:进去的是一个发起方字符串,比如某个客户端名字;它先用 sanitize_metric_tag_value 把字符串清理成适合做指标标签的样子,再拿它和内置的已知名单逐个比较;如果匹配上,就出来这个已知值,如果没匹配上,就统一出来 other。它不修改外部数据。

调用关系:record_process_start_once 在记录进程启动指标时会用它,把来源信息先规整好再上报。它自己把清理字符串的活交给 sanitize_metric_tag_value,然后只负责判断这个值是不是系统认可的来源。

调用图:被 1 处调用(record_process_start_once);外部调用 1 个(sanitize_metric_tag_value)。

SessionMetricTagValues::into_tags48–57 ↗
fn into_tags(self) -> Result<Vec<(&'static str, &'a str)>>

作用:这个方法把一次会话里分散的标签字段,打包成指标系统能直接使用的标签列表。它保证标签按固定顺序出现,并且自动跳过没有填写的可选项。

数据流:进去的是一个 SessionMetricTagValues,里面有认证方式、会话来源、发起方、服务名、模型名、应用版本等信息;它先创建一个最多容纳 6 个标签的列表,然后把每一项交给 push_optional_tag 检查并加入;出来的是一个标签数组,或者在某个标签不合法时出来一个错误。

调用关系:它是这个文件里把结构化字段变成实际标签列表的主入口。调用者准备好会话信息后调用它;它不自己做每个标签的细查,而是反复调用 SessionMetricTagValues::push_optional_tag 来完成“有值才加入、加入前先验证”的小步骤。

调用图:外部调用 2 个(push_optional_tag, with_capacity)。

SessionMetricTagValues::push_optional_tag59–71 ↗
fn push_optional_tag(
        tags: &mut Vec<(&'static str, &'a str)>,
        key: &'static str,
        value: Option<&'a str>,
    ) -> Result<()>

作用:这个辅助方法负责把单个标签安全地放进列表。它会先判断这个标签有没有值,有值才检查并加入,没有值就安静跳过。

数据流:进去的是当前标签列表、一个固定标签名,以及一个可能为空的标签值;如果值为空,它什么都不加,直接成功返回;如果有值,它先调用 validate_tag_key 检查标签名是否合法,再调用 validate_tag_value 检查标签值是否合法,最后把这一对键和值追加到列表里;出来的是成功结果或验证失败的错误,同时可能改变传入的标签列表。

调用关系:它主要被 SessionMetricTagValues::into_tags 使用,是组装标签时的“守门员”。into_tags 负责决定要处理哪些标签,它负责每个标签能不能进列表,并把具体校验工作交给 validate_tag_key 和 validate_tag_value。

调用图:调用 2 个内部函数(validate_tag_key, validate_tag_value)。

tests::session_metric_tags_include_expected_tags_in_order86–109 ↗
fn session_metric_tags_include_expected_tags_in_order()

作用:这个测试确认:当所有标签都有值时,生成出来的标签既没有漏项,也保持预期顺序。顺序很重要,因为它能让结果稳定,方便比较和排查问题。

数据流:进去的是测试里手工构造的一组完整会话标签值;它调用 into_tags 生成标签列表;然后用 assert_eq! 把实际结果和预期列表逐项比较;出来的是测试通过,或者在内容、顺序不一致时测试失败。

调用关系:它在测试阶段运行,用来保护 SessionMetricTagValues::into_tags 的行为不被以后改坏。它不参与线上记录指标,只是给开发者一个报警:完整标签的输出格式必须保持一致。

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

tests::session_metric_tags_skip_missing_optional_tags112–133 ↗
fn session_metric_tags_skip_missing_optional_tags()

作用:这个测试确认:可选标签没有值时会被跳过,而不是生成空标签或占位标签。这样上报出去的数据更干净,也不会误导监控分析。

数据流:进去的是一组缺少 auth_mode 和 service_name 的会话标签值;它调用 into_tags 生成标签列表;然后用 assert_eq! 检查结果里只剩下有值的标签,并且顺序正确;出来的是测试通过,或者实际行为和预期不同时测试失败。

调用关系:它同样只在测试阶段运行,重点保护 SessionMetricTagValues::push_optional_tag 对 None 值的处理规则。换句话说,它保证“没填就不报”这个约定不会被无意中破坏。

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

otel/src/metrics/mod.rs源码 ↗
orchestrationstartup 和 cross-cutting

项目里很多地方都可能想记录“发生了多少次”“用了多久”“进程什么时候启动”这类指标。如果每个地方都自己创建一套指标工具,就会乱套。这个文件像一个前台接待处:它把 metrics 子模块里的重要类型统一摆出来,别人不用知道里面分了多少文件;同时用 OnceLock(一种只能成功写入一次的保险箱)保存全局的 MetricsClient 和 StatsigMetricsSettings。启动或初始化阶段会把客户端和设置放进去,之后其他代码只需要调用 global 或 global_statsig_settings,就能拿到同一份配置好的对象。这里有个重要行为:重复安装不会覆盖旧值,因为 set 的结果被忽略了,所以第一次放进去的全局对象才算数。

函数细节4
install_global27–29 ↗
fn install_global(metrics: MetricsClient)

作用:把已经创建好的 MetricsClient 放进全局位置,让项目其他地方都能用同一个指标客户端。它通常在初始化时调用,避免每个地方各建各的。

数据流:进去的是一个 MetricsClient,也就是已经配置好的指标记录工具。函数尝试把它放进 GLOBAL_METRICS 这个只能设置一次的全局格子里;如果之前已经放过了,这次不会替换。出来没有返回值,但第一次成功时会改变全局状态,让后续 global 能取到它。

调用关系:它会被 from 调用,说明某个配置或构建流程在生成指标客户端后,会顺手把它安装成全局对象。之后 new、start_global_timer 这类需要记录指标的流程,会通过 global 来取这份对象继续工作。

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

global31–33 ↗
fn global() -> Option<MetricsClient>

作用:取出当前全局的 MetricsClient,给想记录指标的代码使用。如果还没安装,就返回空,调用者可以据此选择不记录或走备用逻辑。

数据流:它不需要外部输入,只读取 GLOBAL_METRICS 这个全局格子。里面有客户端时,它复制一份可用的句柄返回;里面没有时,返回 None,也就是“现在还没有全局指标客户端”。它不修改任何状态。

调用关系:它会被 new 和 start_global_timer 调用。也就是说,创建某些对象或启动全局计时器时,会先来这里看看有没有已经准备好的指标客户端;有就接着记录,没有就不能使用全局指标能力。

调用图:被 2 处调用(new, start_global_timer)。

install_global_statsig_settings35–37 ↗
fn install_global_statsig_settings(settings: StatsigMetricsSettings)

作用:把 Statsig 的指标设置保存到全局位置,方便后面需要这些设置的代码统一读取。这样配置只需要装一次,不用到处传来传去。

数据流:进去的是 StatsigMetricsSettings,也就是 Statsig 相关指标功能的配置。函数尝试把它放进 GLOBAL_STATSIG_METRICS_SETTINGS 这个只能设置一次的全局格子;如果已经有值,就不会覆盖。出来没有返回值,但第一次成功时会留下全局设置。

调用关系:它会被 from 调用,表示在某个配置转换或初始化流程中,系统拿到 Statsig 指标设置后会把它登记为全局设置。后续需要这些设置的代码,会通过 global_statsig_settings 间接取用。

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

global_statsig_settings39–41 ↗
fn global_statsig_settings() -> Option<StatsigMetricsSettings>

作用:读取全局保存的 Statsig 指标设置。如果系统还没有安装这份设置,它就返回空,让调用者知道现在没有可用配置。

数据流:它不接收参数,只查看 GLOBAL_STATSIG_METRICS_SETTINGS。里面有设置时,复制一份返回;没有时返回 None。它只读取全局状态,不会改动任何东西。

调用关系:它会被 global_statsig_metrics_settings 调用。也就是说,外层可能有一个更面向业务的取配置函数,而真正从全局 OnceLock 里拿值的动作由这里完成。

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

指标传输与记录

这些文件实现 OTLP 传输设置,以及用于发出计数器、计时器、启动信号和运行时摘要的具体指标客户端功能。

otel/src/otlp.rs源码 ↗
io_transporttelemetry setup / startup

这个文件像是“发快递前的打包和验票处”。项目要把日志、指标、追踪发送到外部 OTLP 服务端,不能只知道地址,还要处理请求头、超时时间、TLS(加密连接)证书,以及 mTLS(客户端也拿证书证明自己身份)。这里会把普通的字符串配置变成真正能用的 HTTP 或 gRPC 客户端。它还特别照顾 Tokio(Rust 常用的异步运行环境):如果当前已经在异步运行时里,就用安全的方式创建阻塞式 HTTP 客户端,避免把运行时卡死。重要行为是:证书文件读不到、证书格式不对、客户端证书和私钥只给了一个,都会立刻报配置错误,而不是等发送数据时才失败。

函数细节12
build_header_map22–32 ↗
fn build_header_map(headers: &std::collections::HashMap<String, String>) -> HeaderMap

作用:把配置里写的请求头从普通键值对,转换成 HTTP 客户端真正能使用的请求头表。无效的请求头名字或值会被跳过,避免把坏配置塞进网络请求里。

数据流:输入是一组字符串形式的头信息,比如“Authorization → token”。它逐个检查名字和值能不能成为合法 HTTP 头;合法的放进 HeaderMap,不合法的忽略。输出是一份可直接交给 OTLP HTTP/gRPC 导出器使用的请求头集合,不会改动原始配置。

调用关系:当指标导出器、日志导出器、追踪提供器准备连接 OTLP 服务时,会调用它把用户配置的头信息变成网络库认识的格式。它自己只做格式转换,不负责发送数据。

调用图:被 3 处调用(build_otlp_metric_exporter, build_logger, build_tracer_provider);外部调用 3 个(new, from_bytes, from_str)。

build_grpc_tls_config34–68 ↗
fn build_grpc_tls_config(
    endpoint: &str,
    tls_config: ClientTlsConfig,
    tls: &OtelTlsConfig,
) -> Result<ClientTlsConfig, Box<dyn Error>>

作用:给 OTLP 的 gRPC 连接准备 TLS 加密配置。它会确认服务端地址里有主机名,并按需要加载 CA 证书、客户端证书和私钥。

数据流:输入是 OTLP gRPC 地址、一个已有的 TLS 配置壳子,以及用户写的证书路径配置。它先从地址里取出主机名,作为证书校验用的域名;然后读取 CA 证书;如果配置了客户端证书和私钥,就一起读入并组成客户端身份。输出是补全后的 gRPC TLS 配置;如果地址没主机名、证书读不了,或 mTLS 只给了证书/私钥的一半,就返回配置错误。

调用关系:指标、日志、追踪在选择 gRPC 方式发送 OTLP 数据时会用到它。它会把读文件的工作交给 read_bytes,把生成统一配置错误的工作交给 config_error。

调用图:调用 2 个内部函数(config_error, read_bytes);被 3 处调用(build_otlp_metric_exporter, build_logger, build_tracer_provider);外部调用 3 个(domain_name, from_pem, from_pem)。

build_http_client74–92 ↗
fn build_http_client(
    tls: &OtelTlsConfig,
    timeout_var: &str,
) -> Result<reqwest::blocking::Client, Box<dyn Error>>

作用:创建一个阻塞式 HTTP 客户端,用来给 OTLP HTTP 导出器发送数据。它的重点不是只建客户端,而是避免在 Tokio 异步运行时里错误地做阻塞操作。

数据流:输入是 TLS 配置和一个表示超时时间环境变量名字的字符串。它先判断当前是否处在多线程 Tokio 运行时;如果是,就用 block_in_place 安全地做阻塞初始化;如果是在单线程运行时里,就另开一个系统线程去建客户端;如果不在 Tokio 里,就直接建。输出是一个 reqwest 阻塞式 HTTP 客户端,或者一个说明原因的错误。

调用关系:指标、日志、追踪导出器需要 HTTP 客户端时会从这里入口拿。它会调用 current_tokio_runtime_is_multi_thread 判断环境,并把真正的证书、超时、客户端构建细节交给 build_http_client_inner。测试 build_http_client_works_in_current_thread_runtime 专门验证它在单线程 Tokio 里也能正常工作。

调用图:调用 2 个内部函数(build_http_client_inner, current_tokio_runtime_is_multi_thread);被 4 处调用(build_otlp_metric_exporter, build_http_client_works_in_current_thread_runtime, build_logger, build_tracer_provider);外部调用 4 个(clone, spawn, try_current, block_in_place)。

current_tokio_runtime_is_multi_thread94–99 ↗
fn current_tokio_runtime_is_multi_thread() -> bool

作用:判断当前代码是不是运行在 Tokio 的多线程运行时里。Tokio 是异步任务调度器,多线程版本允许用更安全的方式临时做阻塞工作。

数据流:它不需要业务输入,只尝试读取当前线程上的 Tokio 运行时句柄。读到了就检查运行时类型是不是 MultiThread;读不到说明不在 Tokio 里。输出是 true 或 false,不修改任何状态。

调用关系:build_http_client 用它决定该怎样创建阻塞式 HTTP 客户端,build_tracer_provider 也会参考这个判断。测试 current_tokio_runtime_is_multi_thread_detects_runtime_flavor 会分别在无运行时、单线程运行时、多线程运行时下验证结果。

调用图:被 2 处调用(build_http_client, build_tracer_provider);外部调用 1 个(try_current)。

build_http_client_inner101–146 ↗
fn build_http_client_inner(
    tls: &OtelTlsConfig,
    timeout_var: &str,
) -> Result<reqwest::blocking::Client, Box<dyn Error>>

作用:真正动手创建阻塞式 reqwest HTTP 客户端,并把超时和 TLS 证书配置进去。它是 build_http_client 背后的“施工队”。

数据流:输入是 TLS 配置和超时环境变量名。它先解析超时时间,再按配置读取 CA 证书;如果有客户端证书和私钥,就把两者合在一起生成客户端身份,并强制只走 HTTPS。最后输出建好的阻塞式 HTTP 客户端;如果证书文件读不到、格式不对、mTLS 配置不完整,就输出错误。

调用关系:它只由 build_http_client 调用,因为外层函数要先处理 Tokio 运行时环境。它会调用 resolve_otlp_timeout 决定超时时间,调用 read_bytes 读证书文件,调用 config_error 生成更清楚的配置错误。

调用图:调用 3 个内部函数(config_error, read_bytes, resolve_otlp_timeout);被 1 处调用(build_http_client);外部调用 3 个(from_pem, from_pem, builder)。

build_async_http_client148–194 ↗
fn build_async_http_client(
    tls: Option<&OtelTlsConfig>,
    timeout_var: &str,
) -> Result<reqwest::Client, Box<dyn Error>>

作用:创建异步版 HTTP 客户端,用于不需要阻塞线程的 OTLP HTTP 发送场景。它同样支持超时、CA 证书和 mTLS 客户端证书。

数据流:输入是可选的 TLS 配置和超时环境变量名。它先设置超时;如果提供了 TLS 配置,就读取 CA 证书并替换默认根证书;再检查客户端证书和私钥是否成对出现,成对就生成客户端身份。输出是异步 reqwest 客户端;遇到证书或配置问题则返回错误。

调用关系:追踪提供器在需要异步 HTTP 客户端时会调用它。它和 build_http_client_inner 做的证书处理很像,但创建的是异步客户端;共同依赖 resolve_otlp_timeout、read_bytes 和 config_error。

调用图:调用 3 个内部函数(config_error, read_bytes, resolve_otlp_timeout);被 1 处调用(build_tracer_provider);外部调用 3 个(from_pem, from_pem, builder)。

resolve_otlp_timeout196–204 ↗
fn resolve_otlp_timeout(signal_var: &str) -> Duration

作用:决定 OTLP 请求最多等多久。它先看某一类信号自己的超时设置,再看 OTLP 通用超时设置,最后才用默认值。

数据流:输入是一个环境变量名,通常代表日志、指标或追踪自己的超时配置。它先读取这个变量;没有或无效时,再读取通用的 OTEL_EXPORTER_OTLP_TIMEOUT;还没有就使用库提供的默认超时时间。输出是一个 Duration(时间长度)。

调用关系:build_http_client_inner 和 build_async_http_client 在创建客户端时都会用它设置网络请求超时。它把具体读取和解析环境变量的动作交给 read_timeout_env。

调用图:调用 1 个内部函数(read_timeout_env);被 2 处调用(build_async_http_client, build_http_client_inner)。

read_timeout_env206–213 ↗
fn read_timeout_env(var: &str) -> Option<Duration>

作用:从环境变量里读取毫秒数,并转换成程序能用的时间长度。负数或无法解析的内容会被当作没配置。

数据流:输入是环境变量名字。它尝试读取该变量的字符串值,再把它解析成整数毫秒;如果变量不存在、不是整数、或是负数,就输出 None;如果合法,就输出对应的 Duration。它不修改环境变量。

调用关系:它只服务于 resolve_otlp_timeout,是超时解析链条里的小工具。上层函数不用关心环境变量里写了什么怪值,只看有没有得到可用时间。

调用图:被 1 处调用(resolve_otlp_timeout);外部调用 2 个(from_millis, var)。

read_bytes215–223 ↗
fn read_bytes(path: &AbsolutePathBuf) -> Result<(Vec<u8>, PathBuf), Box<dyn Error>>

作用:读取证书或私钥文件的原始字节,并在失败时给出带文件路径的清楚错误。这样排查配置问题时能知道到底哪个文件出错。

数据流:输入是一个绝对路径。它调用文件系统读取该路径内容;成功时输出文件字节和对应路径;失败时把原始读文件错误包装成“failed to read 某路径”的错误。它不解析文件内容,只负责安全地读出来。

调用关系:gRPC TLS、阻塞 HTTP 客户端、异步 HTTP 客户端都会通过它读取 CA 证书、客户端证书和私钥。后续解析证书的工作由各自的构建函数继续完成。

调用图:调用 1 个内部函数(to_path_buf);被 3 处调用(build_async_http_client, build_grpc_tls_config, build_http_client_inner);外部调用 4 个(new, new, format!, read)。

config_error225–227 ↗
fn config_error(message: impl Into<String>) -> Box<dyn Error>

作用:把一段配置错误说明包装成统一的错误对象。这样上层看到的是“配置数据无效”,而不是零散的普通字符串。

数据流:输入是一段错误文字。它把文字转成 io::Error,并标记为 InvalidData(数据不合法)。输出是一个可统一返回的错误对象,不产生其他副作用。

调用关系:当 gRPC TLS、HTTP TLS 或 mTLS 配置不完整时,相关构建函数会调用它生成错误。它让这些错误在外层看起来格式一致,也更容易展示给用户。

调用图:被 3 处调用(build_async_http_client, build_grpc_tls_config, build_http_client_inner);外部调用 3 个(new, into, new)。

tests::current_tokio_runtime_is_multi_thread_detects_runtime_flavor236–257 ↗
fn current_tokio_runtime_is_multi_thread_detects_runtime_flavor()

作用:验证 current_tokio_runtime_is_multi_thread 的判断是否靠谱。它分别检查没有 Tokio、单线程 Tokio、多线程 Tokio 三种情况。

数据流:测试先在没有运行时的环境里调用目标函数,期待 false;再创建单线程 Tokio 运行时,进入里面调用,期待 false;最后创建多线程 Tokio 运行时,期待 true。输出不是业务数据,而是测试通过或失败。

调用关系:它是 current_tokio_runtime_is_multi_thread 的安全网。因为 build_http_client 会根据这个判断选择不同建客户端方式,所以这个测试能防止运行时判断出错后引发阻塞问题。

调用图:外部调用 4 个(new_current_thread, new_multi_thread, assert!, assert_eq!)。

tests::build_http_client_works_in_current_thread_runtime260–271 ↗
fn build_http_client_works_in_current_thread_runtime()

作用:验证 build_http_client 在单线程 Tokio 运行时里也能创建客户端。这个场景容易出问题,因为阻塞式客户端如果直接创建,可能和异步运行时冲突。

数据流:测试创建一个单线程 Tokio 运行时,在里面用默认 TLS 配置调用 build_http_client。它期待返回结果是成功的客户端,而不是错误。测试本身不发送网络请求,只验证构建过程能走通。

调用关系:它直接覆盖 build_http_client 的特殊分支:在单线程运行时里通过额外线程创建阻塞式客户端。这样可以保护日志、指标、追踪初始化时在类似环境下不会崩。

调用图:调用 1 个内部函数(build_http_client);外部调用 3 个(new_current_thread, assert!, default)。

otel/src/metrics/client.rs源码 ↗
domain_logiccross-cutting:启动时创建,运行中随时记录指标,关闭时冲刷并停止

程序跑起来后,需要知道自己做了多少事、哪里慢、当前状态是多少。这个文件就像一个统一的仪表盘入口:外面的代码只要调用 MetricsClient,就能记一次计数器、写一个当前值、记录一次耗时。它会先检查指标名和标签是否合法,再把默认标签和本次标签合并,避免上报出脏数据。内部会缓存已经创建过的“仪表”,比如计数器、直方图(把数值按区间统计,方便看分布)和仪表盘式数值,避免每次都重新建。启动时它根据配置选择导出方式:可以放在内存里,也可以用 OTLP(OpenTelemetry 的传输协议)发到外部收集器。它还支持运行中手动抓一份快照,以及关闭时先冲刷再停掉,尽量不丢最后一批指标。

函数细节26
SharedManualReader::new70–72 ↗
fn new(inner: Arc<ManualReader>) -> Self

作用:把一个可共享的手动指标读取器包起来,让它能作为 OpenTelemetry 的读取器使用。简单说,就是给已有读数器套一层外壳。

数据流:进去一个 Arc<ManualReader>,也就是可以被多处共享的手动读取器 → 函数把它放进 SharedManualReader 结构里 → 出来一个新的 SharedManualReader,不改变原读取器本身。

调用关系:build_provider 在需要支持运行时快照时会调用它,把手动读取器接入 OpenTelemetry 的 provider。之后真正的读取、刷新、关闭动作都转交给里面的 ManualReader。

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

SharedManualReader::register_pipeline76–78 ↗
fn register_pipeline(&self, pipeline: Weak<Pipeline>)

作用:把指标读取器登记到 OpenTelemetry 的指标管道里。管道可以理解成“指标从产生到导出的流水线”。

数据流:进去一个弱引用形式的 Pipeline → 它不自己处理,而是交给内部的 ManualReader 去登记 → 结果是内部读取器知道自己属于哪条指标流水线。

调用关系:这是 MetricReader 接口要求的方法,通常由 OpenTelemetry SDK 在搭建指标系统时调用。SharedManualReader 只是中转,把工作交给 inner。

SharedManualReader::collect80–82 ↗
fn collect(&self, rm: &mut ResourceMetrics) -> opentelemetry_sdk::error::OTelSdkResult

作用:从当前指标系统里收集一份数据。它用于运行中手动拿快照,而不是等定时导出。

数据流:进去一个可写的 ResourceMetrics 容器 → 它让内部 ManualReader 把当前收集到的指标填进去 → 出来是成功或失败结果,同时这个容器被写入指标数据。

调用关系:这是 MetricReader 接口的一部分。MetricsClient::snapshot 最终会依赖 ManualReader 的 collect 能力;这里负责把接口调用转给内部读取器。

SharedManualReader::force_flush84–86 ↗
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult

作用:强制把暂存的指标刷新出去。可以理解成让系统“别等了,现在就把手里的数据交出去”。

数据流:没有额外业务输入 → 它调用内部 ManualReader 的 force_flush → 返回 OpenTelemetry SDK 的成功或失败结果。

调用关系:这是 OpenTelemetry SDK 可能在刷新流程中调用的接口方法。SharedManualReader 不做额外逻辑,只负责转发。

SharedManualReader::shutdown_with_timeout88–90 ↗
fn shutdown_with_timeout(&self, timeout: Duration) -> opentelemetry_sdk::error::OTelSdkResult

作用:在限定时间内关闭这个读取器。这样关闭程序时不会无限卡住。

数据流:进去一个超时时间 Duration → 它把这个时间交给内部 ManualReader 的关闭方法 → 返回关闭是否成功。

调用关系:这是 MetricReader 接口要求的关闭方法。OpenTelemetry provider 关闭时可能调用它,SharedManualReader 把关闭动作交给 inner。

SharedManualReader::temporality92–94 ↗
fn temporality(&self, kind: InstrumentKind) -> Temporality

作用:告诉 OpenTelemetry 某类指标该用什么“时间口径”。比如是累计总量,还是每一段时间的增量。

数据流:进去一个 InstrumentKind,表示指标工具的种类 → 它询问内部 ManualReader 对这种工具采用什么 Temporality → 返回对应口径。

调用关系:这是 OpenTelemetry SDK 在导出或收集指标时会查询的接口方法。这里不自己决定规则,而是沿用内部 ManualReader 的规则。

MetricsClientInner::counter110–144 ↗
fn counter(
        &self,
        name: &str,
        description: Option<&str>,
        inc: i64,
        tags: &[(&str, &str)],
    ) -> Result<()>

作用:记录一次“只增不减”的计数。比如请求数、启动次数、错误次数,都适合用它。

数据流:进去指标名、可选说明、增加量 inc、标签 → 先检查指标名是否合法,再拒绝负数增加量,然后合并默认标签和本次标签 → 找到或创建对应 Counter,把增加量加进去;成功返回 Ok,非法则返回错误。

调用关系:MetricsClient::counter 和 MetricsClient::counter_with_description 会把公开调用转给它。它会调用 validate_metric_name 检查名字,并调用 MetricsClientInner::attributes 准备标签。

调用图:调用 2 个内部函数(attributes, validate_metric_name)。

MetricsClientInner::histogram146–159 ↗
fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)]) -> Result<()>

作用:记录一个普通数值样本,用来观察数值分布。比如一次处理了多少条数据,值大多落在哪些范围。

数据流:进去指标名、整数值、标签 → 检查指标名,合并和校验标签 → 从缓存里拿到同名 Histogram,拿不到就创建一个,然后记录这个值。

调用关系:MetricsClient::histogram 是外部入口,它把请求交给这里。这里依赖 MetricsClientInner::attributes 做标签准备,也依赖 validate_metric_name 防止非法指标名进入系统。

调用图:调用 2 个内部函数(attributes, validate_metric_name)。

MetricsClientInner::gauge161–189 ↗
fn gauge(
        &self,
        name: &str,
        description: Option<&str>,
        value: i64,
        tags: &[(&str, &str)],
    ) -> Result<()>

作用:记录一个“当前值”。比如当前连接数、队列长度、内存里的某个数量。

数据流:进去指标名、可选说明、当前值、标签 → 检查指标名,合并标签 → 按名字和说明找到或创建 Gauge,然后把当前值写进去。

调用关系:MetricsClient::gauge 和 MetricsClient::gauge_with_description 会调用它。它和 counter 类似,也会通过 MetricsClientInner::attributes 准备标签。

调用图:调用 2 个内部函数(attributes, validate_metric_name)。

MetricsClientInner::duration_histogram191–222 ↗
fn duration_histogram(
        &self,
        name: &str,
        value: f64,
        unit: &'static str,
        description: &str,
        boundaries: &'static [f64],
        tags: &[(&str, &str)],

作用:专门记录耗时,并按预设区间统计。它让人能看出操作通常多快、偶尔有多慢。

数据流:进去指标名、耗时数值、单位、说明、区间边界、标签 → 检查名字并准备标签 → 找到或创建带单位和区间设置的 Histogram → 把这次耗时记录进去。

调用关系:MetricsClient::record_duration 和 MetricsClient::record_duration_seconds_with_description 会调用它。它是耗时类指标的内部通用实现。

调用图:调用 2 个内部函数(attributes, validate_metric_name)。

MetricsClientInner::attributes224–244 ↗
fn attributes(&self, tags: &[(&str, &str)]) -> Result<Vec<KeyValue>>

作用:把默认标签和本次传入的标签合成 OpenTelemetry 能识别的属性列表。标签就像给指标贴的小纸条,用来区分环境、用户类型、操作种类等。

数据流:进去一组标签键值对,同时读取客户端保存的 default_tags → 如果本次没有标签,就直接把默认标签转成 KeyValue;如果有,就先复制默认标签,再逐个检查本次标签的键和值,最后覆盖或追加进去 → 出来一个 KeyValue 列表,或返回标签非法的错误。

调用关系:counter、histogram、gauge、duration_histogram 都会先找它拿整理好的标签。它把 validate_tag_key 和 validate_tag_value 用在每个外部传入的标签上。

调用图:调用 2 个内部函数(validate_tag_key, validate_tag_value);被 4 处调用(counter, duration_histogram, gauge, histogram)。

MetricsClientInner::shutdown246–255 ↗
fn shutdown(&self) -> Result<()>

作用:关闭指标系统前,先尽量把还没发出去的指标冲刷出去,再停止底层 provider。这样可以减少程序退出时丢数据。

数据流:没有业务输入 → 先写一条调试日志,然后调用 meter_provider.force_flush,再调用 meter_provider.shutdown → 成功返回 Ok;任一步失败都会包装成 MetricsError::ProviderShutdown。

调用关系:MetricsClient::shutdown 会调用它。它处在程序收尾阶段,负责把 OpenTelemetry provider 的刷新和关闭动作串起来。

调用图:外部调用 3 个(force_flush, shutdown, debug!)。

MetricsClient::new264–318 ↗
fn new(config: MetricsConfig) -> Result<Self>

作用:根据配置创建一个可用的 MetricsClient。没有它,外部代码就没有统一入口来记录和导出指标。

数据流:进去 MetricsConfig,里面有环境名、服务名、版本、导出器、导出间隔、是否支持快照、默认标签 → 先校验默认标签,再组装服务和操作系统等资源信息;如果需要快照就创建 ManualReader;然后按配置选择内存导出或 OTLP 导出,构建 provider 和 meter → 出来一个带缓存、默认标签、读取器和导出管道的 MetricsClient。

调用关系:这是创建指标客户端的主入口。测试、会话遥测、配置转换等地方会调用它;它会调用 os_resource_attributes、build_otlp_metric_exporter、build_provider 和 validate_tags。

调用图:调用 4 个内部函数(build_otlp_metric_exporter, build_provider, os_resource_attributes, validate_tags);被 12 处调用(test_session_telemetry, test_session_telemetry_without_metadata, test_session_telemetry, websocket_harness_with_provider_options, with_metrics_config, from, build_metrics_with_defaults, otlp_http_exporter_sends_metrics_to_collector, runtime_metrics_summary_collects_tool_api_and_streaming_metrics, manager_snapshot_metrics_collects_without_shutdown (+2 more));外部调用 6 个(new, new, new, with_capacity, builder, new)。

MetricsClient::counter321–323 ↗
fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)]) -> Result<()>

作用:给外部代码用的简单计数入口。调用者只需要给名字、增加量和标签,就能记录一次计数。

数据流:进去指标名、增加量、标签 → 它不自己建指标,而是把说明设为空,转交给 MetricsClientInner::counter → 返回内部记录结果或错误。

调用关系:record_process_start_once 等上层功能会调用它。它是公开 API,隐藏了内部缓存、校验和 OpenTelemetry 细节。

调用图:被 2 处调用(record_process_start_once, counter)。

MetricsClient::counter_with_description326–334 ↗
fn counter_with_description(
        &self,
        name: &str,
        description: &str,
        inc: i64,
        tags: &[(&str, &str)],
    ) -> Result<()>

作用:记录计数器,同时给这个计数器附上一段说明。说明能帮助外部监控系统或读指标的人理解它代表什么。

数据流:进去指标名、说明文字、增加量、标签 → 把说明包装成 Some,交给 MetricsClientInner::counter → 内部会创建或复用带说明的计数器并记录增加量。

调用关系:这是 counter 的增强版公开入口。它把实际工作交给 MetricsClientInner::counter,自己只负责把参数整理成内部需要的形式。

MetricsClient::histogram337–339 ↗
fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)]) -> Result<()>

作用:给外部代码用的普通直方图入口。直方图会把很多样本放进不同范围里,方便看分布。

数据流:进去指标名、整数样本值、标签 → 直接交给 MetricsClientInner::histogram → 返回记录是否成功。

调用关系:上层代码需要记录一批数值的分布时会调用它。内部创建和复用 Histogram 的细节都藏在 MetricsClientInner::histogram 里。

MetricsClient::gauge342–344 ↗
fn gauge(&self, name: &str, value: i64, tags: &[(&str, &str)]) -> Result<()>

作用:记录一个当前状态值,比如现在有多少个任务。它适合会升会降的数值。

数据流:进去指标名、当前值、标签 → 把说明设为空,交给 MetricsClientInner::gauge → 内部把这个值写入对应 Gauge。

调用关系:这是公开 API。它让调用者不用接触 OpenTelemetry 的 Gauge 类型,只用普通参数就能上报当前值。

MetricsClient::gauge_with_description347–355 ↗
fn gauge_with_description(
        &self,
        name: &str,
        description: &str,
        value: i64,
        tags: &[(&str, &str)],
    ) -> Result<()>

作用:记录当前值,同时给这个指标附说明。适合需要让监控端清楚展示含义的状态指标。

数据流:进去指标名、说明、当前值、标签 → 把这些传给 MetricsClientInner::gauge → 内部创建或复用带说明的 Gauge 并记录值。

调用关系:这是 gauge 的增强版。它本身不处理校验和缓存,而是交给 MetricsClientInner::gauge。

MetricsClient::record_duration358–372 ↗
fn record_duration(
        &self,
        name: &str,
        duration: Duration,
        tags: &[(&str, &str)],
    ) -> Result<()>

作用:记录一次耗时,单位是毫秒。比如某个操作用了多久,可以用它统计快慢分布。

数据流:进去指标名、Duration 耗时、标签 → 把 Duration 转成毫秒,并限制在 i64 最大值以内,避免过大溢出 → 交给 MetricsClientInner::duration_histogram,用毫秒单位和预设毫秒区间记录。

调用关系:Timer 的 record 流程和一些上层 record_duration 包装会调用它。它把“时间对象”转换成适合指标系统记录的数字。

调用图:被 2 处调用(record, record_duration);外部调用 1 个(as_millis)。

MetricsClient::record_duration_seconds_with_description375–390 ↗
fn record_duration_seconds_with_description(
        &self,
        name: &str,
        description: &str,
        duration: Duration,
        tags: &[(&str, &str)],
    ) -> Result<()>

作用:记录一次耗时,单位是秒,并带说明。它适合需要按秒展示、同时希望指标自带解释的场景。

数据流:进去指标名、说明、Duration、标签 → 把 Duration 转成浮点秒数 → 交给 MetricsClientInner::duration_histogram,用秒单位和预设秒区间记录。

调用关系:这是耗时记录的公开增强入口。它复用内部的 duration_histogram,只是选择秒作为单位并传入调用者给的说明。

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

MetricsClient::start_timer392–398 ↗
fn start_timer(
        &self,
        name: &str,
        tags: &[(&str, &str)],
    ) -> std::result::Result<Timer, MetricsError>

作用:启动一个计时器,方便调用者不用自己算开始和结束时间。像按下秒表开始键。

数据流:进去指标名和标签 → 调用 Timer::new,把当前客户端也交给计时器 → 出来一个 Timer;之后计时器结束或记录时,会用这个客户端写入耗时指标。

调用关系:它把“手动记录 Duration”的流程包装成更顺手的计时器流程。实际创建工作交给 Timer::new。

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

MetricsClient::snapshot401–410 ↗
fn snapshot(&self) -> Result<ResourceMetrics>

作用:在不关闭指标系统的情况下,手动抓一份当前指标快照。常用于测试或运行时查看。

数据流:没有业务输入,但会读取客户端里的 runtime_reader → 如果没启用运行时读取器,就返回 RuntimeSnapshotUnavailable;如果有,就创建空的 ResourceMetrics,让 reader.collect 填入当前指标 → 返回这份快照或收集错误。

调用关系:它依赖 MetricsClient::new 里按配置创建的 ManualReader。和正常定时导出不同,它是主动拉取一份数据。

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

MetricsClient::shutdown413–415 ↗
fn shutdown(&self) -> Result<()>

作用:公开的关闭入口。调用者用它告诉指标客户端:程序要结束了,请把数据收尾。

数据流:没有业务输入 → 直接调用 MetricsClientInner::shutdown → 返回刷新和关闭的结果。

调用关系:这是外部代码在 teardown,也就是收尾阶段会调用的方法。真正的 force_flush 和 provider shutdown 在内部完成。

os_resource_attributes418–432 ↗
fn os_resource_attributes() -> Vec<KeyValue>

作用:收集操作系统信息,作为指标的固定属性。这样看指标时能知道数据来自什么系统环境。

数据流:没有输入 → 调用 os_info::get 读取系统类型和版本,把它们转成字符串,再用 sanitize_metric_tag_value 清洗成适合指标标签的值 → 如果不是 unspecified,就输出 os 和 os_version 这两个 KeyValue。

调用关系:MetricsClient::new 会调用它,把操作系统信息加入 Resource。它让每条指标天然带上机器环境背景。

调用图:被 1 处调用(new);外部调用 4 个(new, new, sanitize_metric_tag_value, get)。

build_provider434–455 ↗
fn build_provider(
    resource: Resource,
    exporter: E,
    interval: Option<Duration>,
    runtime_reader: Option<Arc<ManualReader>>,
) -> (SdkMeterProvider, Meter)

作用:搭好 OpenTelemetry 的指标 provider 和 meter。provider 像指标系统的发动机,meter 像用来制造各种仪表的工具箱。

数据流:进去资源信息、导出器、可选导出间隔、可选手动读取器 → 先创建定时读取器 PeriodicReader,并按配置设置导出间隔;再创建 SdkMeterProvider,挂上资源、可选手动读取器和定时读取器 → 出来 provider 和名为 codex 的 meter。

调用关系:MetricsClient::new 在选好导出器后会调用它。它也会调用 SharedManualReader::new,把运行时快照读取器接到 provider 上。

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

build_otlp_metric_exporter457–531 ↗
fn build_otlp_metric_exporter(
    exporter: OtelExporter,
    temporality: Temporality,
) -> Result<opentelemetry_otlp::MetricExporter>

作用:根据配置创建 OTLP 指标导出器。OTLP 是 OpenTelemetry 用来把指标发给收集器的协议。

数据流:进去 OtelExporter 配置和时间口径 temporality → 如果导出被禁用就返回错误;如果是 Statsig,就先解析成实际导出配置再递归创建;如果是 gRPC,就设置端点、请求头、TLS 加密配置和时间口径;如果是 HTTP,就设置端点、协议格式、请求头,并按需创建带 TLS 的 HTTP 客户端 → 出来一个 MetricExporter,或返回配置/构建错误。

调用关系:MetricsClient::new 在配置要求 OTLP 导出时会调用它。它会借助 resolve_exporter、build_header_map、build_grpc_tls_config、build_http_client 等工具,把高层配置变成真正能发网络请求的导出器。

调用图:调用 4 个内部函数(resolve_exporter, build_grpc_tls_config, build_header_map, build_http_client);被 1 处调用(new);外部调用 4 个(new, from_headers, debug!, builder)。

otel/src/metrics/timer.rs源码 ↗
domain_logiccross-cutting

这个文件解决的是“怎么可靠记录耗时”的问题。很多地方都需要知道一次请求、一次操作、一个步骤花了多长时间,如果每次都手写开始时间、结束时间、上报指标,很容易漏掉。这里的 Timer 就像一个秒表:创建时记下当前时间,里面保存指标名字、标签和 MetricsClient(指标客户端,用来把数据送到指标系统)。需要时可以调用 record 手动记录耗时;更特别的是,当 Timer 被销毁时,Rust 会自动调用 drop,它会尝试记录一次耗时。如果上报失败,不会让程序崩掉,而是写一条错误日志。标签可以理解成给指标贴的小纸条,比如接口名、状态等,方便之后按不同维度查看耗时。

函数细节3
Timer::drop14–18 ↗
fn drop(&mut self)

作用:这是 Timer 离开作用范围、准备被销毁时自动执行的收尾动作。它的作用是兜底:哪怕使用者忘了手动记录耗时,也会尽量把这段时间报出去。

数据流:进去的是这个 Timer 自己保存的信息:指标名字、固定标签、指标客户端、开始时间。它调用 record,并传入空的额外标签,意思是只用 Timer 创建时带的标签来记录耗时。如果记录成功,就什么也不做;如果失败,就把错误写进日志,避免悄悄丢失问题线索。

调用关系:它是 Rust 的 Drop 机制自动触发的函数,不需要别人显式调用。它把真正的记录工作交给 Timer::record;如果 Timer::record 返回错误,它再调用日志里的 error! 把问题报出来。

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

Timer::new22–32 ↗
fn new(name: &str, tags: &[(&str, &str)], client: &MetricsClient) -> Self

作用:这是创建 Timer 的入口。调用者给它一个指标名字、一组标签和一个指标客户端,它就开始计时。

数据流:进去的是指标名称、标签列表,以及 MetricsClient。它把名字和标签复制成 Timer 自己能长期保存的形式,克隆一份指标客户端引用,并用当前时间作为 start_time。出来的是一个新的 Timer,从这一刻开始算耗时。

调用关系:它会被 start_timer 使用,通常是外部想开始测量某段代码耗时时调用。它内部用 Instant::now 取得当前时刻,并 clone 指标客户端,让这个 Timer 以后能独立把耗时交给指标系统。

调用图:被 1 处调用(start_timer);外部调用 2 个(now, clone)。

Timer::record34–40 ↗
fn record(&self, additional_tags: &[(&str, &str)]) -> Result<()>

作用:这个函数真正负责把“从创建 Timer 到现在过了多久”上报成一条耗时指标。调用者还可以临时追加一些标签,让这次记录更具体。

数据流:进去的是额外标签,以及 Timer 已经保存的固定标签、指标名、开始时间和指标客户端。它先准备一个标签列表,把额外标签和固定标签合在一起;然后计算 start_time 到现在的耗时;最后调用 MetricsClient 的 record_duration,把指标名、耗时和标签送出去。出来的是一个 Result:成功表示记录完成,失败表示指标客户端上报时出了问题。

调用关系:它是 Timer 的核心动作。Timer::drop 会在自动收尾时调用它;其他代码也可以在需要提前记录时手动调用它。它本身不直接知道指标系统怎么传输数据,而是把最终上报交给 MetricsClient::record_duration。

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

otel/src/metrics/process.rs源码 ↗
domain_logicstartup / cross-cutting

这个文件像一个“只按一次的计数按钮”。程序启动后,可能有多个地方都想告诉监控系统:“我启动了”。如果每个地方都报一次,监控里看到的启动次数就会虚高。这里用一个全局的原子布尔值(一种能被多个线程安全读写的真假开关)记住“启动指标是否已经报过”。第一次调用时,它把开关从“没报过”改成“报过”,然后通过 MetricsClient 把名为 PROCESS_START_METRIC 的计数器加 1,并带上 originator 标签,说明是谁触发的上报。标签值会先经过 bounded_originator_tag_value 处理,避免太长或不合规。之后再调用,就直接返回 false,不再发指标。

函数细节1
record_process_start_once13–27 ↗
fn record_process_start_once(metrics: &MetricsClient, originator: &str) -> Result<bool>

作用:这个函数负责把“本进程已经启动”这件事上报给指标系统,而且保证整个进程生命周期里最多只上报一次。调用者可以通过返回值知道这次调用是不是真的完成了上报。

数据流:输入是一个 MetricsClient(用来发送指标的客户端)和 originator 字符串(说明是谁触发的)。函数先查看并尝试修改全局开关 PROCESS_START_RECORDED:如果之前已经是 true,就什么也不发,返回 Ok(false);如果之前是 false,就改成 true,然后把 PROCESS_START_METRIC 这个计数器加 1,并附带 ORIGINATOR_TAG 标签,标签值由 bounded_originator_tag_value 规整后再发送。发送成功后返回 Ok(true);如果发送指标失败,就把错误返回给调用者。

调用关系:它通常会在程序启动或某个组件初始化时被调用,用来留下“进程启动过”的监控记录。它自己把两件小活交出去:先调用 bounded_originator_tag_value 把来源名字处理成适合放进标签的值,再调用 MetricsClient 的 counter 方法真正发送计数指标。这个函数是防重复的关口,外面的代码不用自己判断是否已经报过。

调用图:调用 2 个内部函数(counter, bounded_originator_tag_value)。

otel/src/metrics/runtime_metrics.rs源码 ↗
domain_logiccross-cutting

OpenTelemetry 是一套常用的“遥测”工具,可以记录程序运行时发生了什么,比如调用次数和耗时。但它给出的数据像流水账,分散在很多指标里,不方便直接拿来展示或写日志。这个文件就像一个会计,把这些原始账目按名字找出来,加总成 RuntimeMetricsSummary。它区分两类数据:一种是“次数 + 总耗时”,比如工具调用、API 调用、流式事件、WebSocket 请求;另一种是某些单独的耗时字段,比如 Responses API 的额外开销、推理时间、引擎内部耗时等。它还提供合并功能,可以把多次采样的结果叠到一起。需要注意的是,普通计数会累加;但某些 Responses API 的耗时如果新值大于 0,会用新值覆盖旧值,表示保留最近一次有效结果。

函数细节11
RuntimeMetricTotals::is_empty31–33 ↗
fn is_empty(self) -> bool

作用:判断一组“次数 + 耗时”的指标是不是完全没有内容。别人可以用它来决定要不要显示或记录这组数据。

数据流:进去的是一个 RuntimeMetricTotals,里面有 count 和 duration_ms 两个数字。它检查这两个数字是不是都等于 0;如果都为 0,就出来 true,表示这组数据是空的;否则出来 false。它不改动任何数据。

调用关系:它是 RuntimeMetricsSummary::is_empty 的小帮手。汇总表在判断自己是否为空时,会分别请每一组 RuntimeMetricTotals 自己说一句“我有没有数据”。

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

RuntimeMetricTotals::merge35–38 ↗
fn merge(&mut self, other: Self)

作用:把另一组“次数 + 耗时”加到当前这组里。适合把多次采样、多个阶段或多个来源的同类指标合成一份总数。

数据流:进去的是当前这组指标和另一组 other。它把 other.count 加到当前 count 上,把 other.duration_ms 加到当前 duration_ms 上;加法使用 saturating_add,也就是如果数字大到快溢出,就停在最大值,不会绕回 0。出来没有新对象,但当前对象被更新了。

调用关系:它被 RuntimeMetricsSummary::merge 调用。大的汇总表合并时,会把工具调用、API 调用、流式事件、WebSocket 等每个小计都交给这个函数来安全相加。

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

RuntimeMetricsSummary::is_empty59–73 ↗
fn is_empty(self) -> bool

作用:判断整份运行指标汇总是不是完全没有可报告的数据。这样日志或上报代码可以跳过空结果,避免制造无意义的输出。

数据流:进去的是一份 RuntimeMetricsSummary。它先检查里面几组“次数 + 耗时”是否为空,再检查每个单独的毫秒耗时字段是否都是 0;全部为空才返回 true,只要有一个字段有值就返回 false。它只读取数据,不修改数据。

调用关系:它会调用 RuntimeMetricTotals::is_empty 来检查各个小计。它通常处在“要不要继续展示/记录这份指标”的门口,像一个过滤器。

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

RuntimeMetricsSummary::merge75–105 ↗
fn merge(&mut self, other: Self)

作用:把另一份运行指标汇总合并到当前汇总里。它用来把分散得到的运行数据拼成一份更完整的结果。

数据流:进去的是当前汇总和另一个 other 汇总。对于工具调用、API 调用、流式事件、WebSocket 这类累计数据,它会把次数和耗时相加;对于 Responses API 和 turn 相关的单项耗时,如果 other 里的值大于 0,就用 other 的值覆盖当前值。出来没有新对象,但当前汇总被更新。

调用关系:它把小计合并工作交给 RuntimeMetricTotals::merge。它处在多份指标汇总汇合的地方,像把几张小票合并成一张总账。

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

RuntimeMetricsSummary::responses_api_summary107–117 ↗
fn responses_api_summary(&self) -> RuntimeMetricsSummary

作用:从完整汇总里单独抽出 Responses API 相关的耗时部分。这样调用方可以只关注 Responses API 的性能,而不混入工具调用、WebSocket 等别的数据。

数据流:进去的是一份完整的 RuntimeMetricsSummary。它复制 Responses API 的几个毫秒耗时字段,其余字段用默认空值填上;出来是一份新的 RuntimeMetricsSummary,但只带 Responses API 相关内容。原始汇总不变。

调用关系:它会使用默认值来清空无关字段,并被 log_websocket_timing_totals 使用。也就是说,记录 WebSocket 时序日志时,可以借它把 Responses API 的关键耗时单独拎出来。

调用图:被 1 处调用(log_websocket_timing_totals);外部调用 1 个(default)。

RuntimeMetricsSummary::from_snapshot119–169 ↗
fn from_snapshot(snapshot: &ResourceMetrics) -> Self

作用:把 OpenTelemetry 的原始快照转换成这个项目自己的运行指标汇总。没有它,后面的代码就得直接面对复杂的 OpenTelemetry 数据结构。

数据流:进去的是 ResourceMetrics,也就是 OpenTelemetry 保存的一次指标快照。它按固定的指标名字去找对应数据:计数类用 sum_counter 求总次数,耗时类用 sum_histogram_ms 求总毫秒数;最后把这些数字装进 RuntimeMetricsSummary 返回。它不修改快照,只生成一份整理后的汇总。

调用关系:它被 runtime_metrics_summary 调用,是“原始遥测数据”进入本项目汇总格式的入口。它把具体查找和加总工作分给 sum_counter 和 sum_histogram_ms。

调用图:调用 2 个内部函数(sum_counter, sum_histogram_ms);被 1 处调用(runtime_metrics_summary)。

sum_counter172–179 ↗
fn sum_counter(snapshot: &ResourceMetrics, name: &str) -> u64

作用:在一份 OpenTelemetry 快照里,按指标名字找出所有计数器,并把它们的值加起来。计数器就是只增不减的数字,比如“调用了多少次”。

数据流:进去的是指标快照 snapshot 和一个指标名 name。它遍历快照里的所有 scope metrics,也就是按采集范围分组的指标;只保留名字匹配的指标;再把每个匹配指标的计数值求和。出来是一个 u64 总数。

调用关系:它由 RuntimeMetricsSummary::from_snapshot 调用,用来读取工具调用次数、API 调用次数、SSE 事件次数、WebSocket 次数等。它会把单个指标的解析交给 sum_counter_metric。

调用图:被 1 处调用(from_snapshot);外部调用 1 个(scope_metrics)。

sum_counter_metric181–189 ↗
fn sum_counter_metric(metric: &Metric) -> u64

作用:从一个单独的 OpenTelemetry 指标里取出计数器的数值总和。如果这个指标不是计数器,它就当作 0 处理。

数据流:进去的是一个 Metric。它读取 metric.data(),确认里面是不是 u64 类型的 Sum,也就是整数计数总和;如果是,就把所有数据点的 value 加起来返回;如果不是,就返回 0。它不修改指标。

调用关系:它是 sum_counter 的内部小工具。sum_counter 先负责按名字找到指标,再让它负责判断“这个指标到底是不是计数器,以及数值是多少”。

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

sum_histogram_ms191–198 ↗
fn sum_histogram_ms(snapshot: &ResourceMetrics, name: &str) -> u64

作用:在一份 OpenTelemetry 快照里,按指标名字找出所有耗时直方图,并把它们记录的总耗时加起来。直方图可以理解成用来统计一堆耗时样本的桶,这里只关心总和。

数据流:进去的是指标快照 snapshot 和一个指标名 name。它遍历快照里的所有指标,筛出名字匹配的那些,再把每个匹配指标里的耗时总和加起来。出来是一个 u64,单位按本文件的约定是毫秒。

调用关系:它由 RuntimeMetricsSummary::from_snapshot 调用,用来读取工具调用耗时、API 调用耗时、Responses API 各段耗时、turn 相关耗时等。它把单个直方图指标的读取交给 sum_histogram_metric_ms。

调用图:被 1 处调用(from_snapshot);外部调用 1 个(scope_metrics)。

sum_histogram_metric_ms200–208 ↗
fn sum_histogram_metric_ms(metric: &Metric) -> u64

作用:从一个单独的 OpenTelemetry 指标里取出直方图记录的总耗时,并转成整数毫秒。如果这个指标不是浮点直方图,它就返回 0。

数据流:进去的是一个 Metric。它读取 metric.data(),确认是不是 f64 类型的 Histogram,也就是用浮点数记录的直方图;如果是,就取每个数据点的 sum,把它转成 u64 后加起来;如果不是,就返回 0。它不修改指标。

调用关系:它是 sum_histogram_ms 的内部小工具。sum_histogram_ms 负责找名字匹配的指标,它负责真正把直方图里的总耗时读出来,并在需要时使用 f64_to_u64 做安全转换。

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

f64_to_u64210–216 ↗
fn f64_to_u64(value: f64) -> u64

作用:把浮点数耗时安全地转成无符号整数。这样可以避免 NaN、无穷大、负数或过大的数把统计结果弄坏。

数据流:进去的是一个 f64 浮点数 value。它先检查这个数是不是正常有限数字,并且大于 0;不满足就返回 0。然后它把数值限制在 u64 能表示的最大范围内,四舍五入后转成 u64 返回。它不改动外部数据。

调用关系:它服务于耗时统计的读取流程,尤其是把 OpenTelemetry 直方图里的浮点 sum 变成项目里使用的整数毫秒。它像一道保险丝,保证异常数字不会继续往汇总结果里传。

跟踪与事件管道

这些文件提供 trace-context 传播,以及会话遥测使用的共享事件目标和事件发送辅助工具。

otel/src/targets.rs源码 ↗
utilcross-cutting

这个文件解决的是“哪些遥测信息能往哪里送”的问题。遥测可以理解成程序运行时留下的记录,比如日志和追踪;追踪是把一次操作的过程串起来看,所以更容易暴露敏感细节。这里用 target,也就是记录来源的名字,来做简单分类:名字以 codex_otel 开头,说明它属于这套遥测;名字以 codex_otel.trace_safe 开头,说明它被明确标成“适合进入追踪”。关键点是:不是所有遥测日志都适合放进追踪。is_log_export_target 会挑出只适合导出到日志的目标,并排除那些已经标为追踪安全的目标;is_trace_safe_target 则专门判断一个目标是不是追踪安全。它像是在文件柜上贴标签:有的文件只能归到普通日志柜,有的才能进更敏感的追踪柜。

函数细节2
is_log_export_target5–7 ↗
fn is_log_export_target(target: &str) -> bool

作用:判断某个遥测来源是不是应该作为“日志导出”的目标。它会把属于 codex_otel 这套遥测、但没有标成追踪安全的内容挑出来,避免同一类信息被错误送进追踪通道。

数据流:输入是一个 target 字符串,也就是一条记录的来源名字。它先看这个名字是不是以 codex_otel 开头;如果是,再调用 is_trace_safe_target 看它是不是追踪安全。最后返回一个真假值:真表示它适合走日志导出,假表示不属于这里,或者应该走追踪安全那条路。

调用关系log_export_filter 在筛选要导出的日志时会用它。它自己会把“是否追踪安全”的判断交给 is_trace_safe_target,这样日志筛选和追踪安全筛选用的是同一套口径,不容易出现前后矛盾。

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

is_trace_safe_target9–11 ↗
fn is_trace_safe_target(target: &str) -> bool

作用:判断某个遥测来源是不是被明确标记为“追踪安全”。追踪安全的意思是:这类信息可以进入追踪数据里,不太担心带入不该暴露的内容。

数据流:输入是一个 target 字符串。它只做一件事:检查这个字符串是不是以 codex_otel.trace_safe 开头。检查结果以真假值返回;它不修改任何数据。

调用关系trace_export_filter 会用它来决定哪些记录可以进入追踪导出流程;is_log_export_target 也会用它来排除这些追踪安全目标,防止同一个目标被当成普通日志专用目标处理。

调用图:被 2 处调用(trace_export_filter, is_log_export_target)。

otel/src/trace_context.rs源码 ↗
io_transportcross-cutting,启动时可能读取环境变量,运行中每次创建或传播追踪信息时都会用到

这份文件处理的是 W3C Trace Context(一套通用的追踪信息格式)。可以把它想成快递单号:请求一路经过很多地方,每个地方都把同一个单号带上,最后就能查到完整路线。文件主要做三件事:第一,从当前 tracing Span(一次正在记录的工作片段)里取出 traceparent 和 tracestate,方便传给别的进程;第二,从收到的 traceparent/tracestate 或环境变量里还原父级追踪,让当前工作接到已有链路上;第三,校验和合并配置里的 tracestate,避免生成别人看不懂的追踪头。这里还用 OnceLock 和 RwLock 保存全局配置:OnceLock 表示只初始化一次,RwLock 是读写锁,防止多个任务同时改同一份配置。

函数细节25
current_span_w3c_trace_context29–31 ↗
fn current_span_w3c_trace_context() -> Option<W3cTraceContext>

作用:把当前正在运行的追踪片段转换成可传给外部的 W3C 追踪信息。别人想把当前请求继续传到下游服务时,会用它拿到标准格式的 traceparent/tracestate。

数据流:进去时不需要参数,它读取当前 Span(当前工作片段)→ 把这个 Span 交给 span_w3c_trace_context 做真正转换 → 出来的是一个可选的 W3cTraceContext;如果当前没有有效追踪,就返回空。

调用关系:它是最方便的入口函数:先通过 tracing 的 current 拿到当前 Span,再把细活交给 span_w3c_trace_context。调用者不用关心当前 Span 怎么被编码成 HTTP 风格的追踪头。

调用图:调用 1 个内部函数(span_w3c_trace_context);外部调用 1 个(current)。

span_w3c_trace_context33–50 ↗
fn span_w3c_trace_context(span: &Span) -> Option<W3cTraceContext>

作用:把指定的 Span 转成 W3C Trace Context。它不仅导出原本的 traceparent,还会把程序配置的 tracestate 合进去。

数据流:进去的是一个 Span → 它读取 Span 里的 OpenTelemetry Context,检查这个追踪是不是有效;有效就用 TraceContextPropagator 生成 traceparent/tracestate 头,再读取全局配置的 tracestate 条目并合并 → 出来的是 W3cTraceContext;无效则返回空。

调用关系:current_span_w3c_trace_context 会调用它。它自己会拿到全局 tracestate_entries,并把合并工作交给 merge_tracestate_entries。

调用图:调用 2 个内部函数(merge_tracestate_entries, tracestate_entries);被 1 处调用(current_span_w3c_trace_context);外部调用 3 个(new, context, new)。

set_tracestate_entries52–61 ↗
fn set_tracestate_entries(
    entries: BTreeMap<String, BTreeMap<String, String>>,
) -> Result<(), Box<dyn std::error::Error>>

作用:安装全局的 tracestate 配置。tracestate 可以理解为追踪链路上的附加标签,比如某个厂商或系统想带的额外字段。

数据流:进去的是一张按名字分组的配置表 → 先调用 validate_tracestate_entries 检查这些配置会不会生成非法追踪头;检查通过后拿写锁,把全局保存的配置替换掉 → 成功返回空结果,失败返回错误。

调用关系:它通常在配置转换或加载流程中被调用。它先让 validate_tracestate_entries 把关,再通过 tracestate_entries 找到全局存放位置。

调用图:调用 2 个内部函数(tracestate_entries, validate_tracestate_entries);被 1 处调用(from)。

current_span_trace_id63–72 ↗
fn current_span_trace_id() -> Option<String>

作用:取出当前追踪的 trace id,也就是整条请求链路的唯一编号。这个编号常用于日志里,方便把同一次请求的记录搜出来。

数据流:进去时不需要参数,它读取当前 Span 的上下文 → 检查里面的 span_context 是否有效 → 出来的是 32 位十六进制字符串;如果当前没有有效追踪,就返回空。

调用关系:测试 tests::current_span_trace_id_returns_hex_trace_id 会验证它能返回像样的十六进制编号。运行中它是给日志、诊断或外部展示拿 trace id 的小入口。

调用图:被 1 处调用(current_span_trace_id_returns_hex_trace_id);外部调用 1 个(current)。

context_from_w3c_trace_context74–76 ↗
fn context_from_w3c_trace_context(trace: &W3cTraceContext) -> Option<Context>

作用:把协议对象 W3cTraceContext 还原成 OpenTelemetry Context。简单说,就是把别人传来的“追踪快递单”变回程序内部能接着用的追踪上下文。

数据流:进去的是 W3cTraceContext,里面可能有 traceparent 和 tracestate → 它取出这两个字符串并交给 context_from_trace_headers 解析 → 出来是可选的 Context;缺少或无效时返回空。

调用关系:set_parent_from_w3c_trace_context 会用它先解析外部追踪信息,测试 tests::parses_valid_w3c_trace_context 也用它验证标准格式能被正确读懂。

调用图:调用 1 个内部函数(context_from_trace_headers);被 2 处调用(set_parent_from_w3c_trace_context, parses_valid_w3c_trace_context)。

set_parent_from_w3c_trace_context78–85 ↗
fn set_parent_from_w3c_trace_context(span: &Span, trace: &W3cTraceContext) -> bool

作用:把一个 Span 接到外部传来的父级追踪下面。这样当前工作就不会变成一条新链,而是成为原有请求链路的一段。

数据流:进去的是要设置父级的 Span,以及外部传来的 W3cTraceContext → 先调用 context_from_w3c_trace_context 解析;解析成功就调用 set_parent_from_context 设置父级 → 出来是布尔值,true 表示接上了,false 表示外部信息无效或缺失。

调用关系:它把两个步骤串起来:先解析外部格式,再真正设置父子关系。具体设置动作交给 set_parent_from_context。

调用图:调用 2 个内部函数(context_from_w3c_trace_context, set_parent_from_context)。

set_parent_from_context87–89 ↗
fn set_parent_from_context(span: &Span, context: Context)

作用:把给定的 Context 设为某个 Span 的父级。它是最直接的“接线”函数。

数据流:进去的是一个 Span 和一个已经解析好的 Context → 调用 tracing 提供的 set_parent,把 Span 挂到这个父级下面 → 没有返回有意义的值,只改变这个 Span 的父级关系。

调用关系:set_parent_from_w3c_trace_context 在解析成功后会调用它。它自己不负责检查格式,只负责把已经确认可用的 Context 接上。

调用图:被 1 处调用(set_parent_from_w3c_trace_context);外部调用 1 个(set_parent)。

traceparent_context_from_env91–95 ↗
fn traceparent_context_from_env() -> Option<Context>

作用:从环境变量里读取父级追踪上下文。它让命令行启动的新进程也能接上启动它的那条追踪链。

数据流:进去时不需要参数,它通过全局 OnceLock 只加载一次 TRACEPARENT 和 TRACESTATE 环境变量 → 第一次调用时执行 load_traceparent_context,之后复用缓存结果 → 出来是可选的 Context。

调用关系:它是环境变量追踪入口。虽然调用图没有列出直接调用者,但它通常会在启动或初始化追踪时被用来决定当前进程是否继承父进程的追踪。

context_from_trace_headers97–113 ↗
fn context_from_trace_headers(
    traceparent: Option<&str>,
    tracestate: Option<&str>,
) -> Option<Context>

作用:从 traceparent/tracestate 这两个文本头里解析出程序内部的追踪上下文。它是读取 W3C 追踪头的核心小工具。

数据流:进去的是可选的 traceparent 和 tracestate 字符串 → 如果没有 traceparent,直接返回空;否则把它们放进类似请求头的表里,用 TraceContextPropagator 解析,再检查结果是否有效 → 出来是可选的 Context。

调用关系:context_from_w3c_trace_context 和 load_traceparent_context 都把解析工作交给它。它负责把外部字符串边界转换成 OpenTelemetry 内部对象。

调用图:被 2 处调用(context_from_w3c_trace_context, load_traceparent_context);外部调用 2 个(new, new)。

load_traceparent_context115–129 ↗
fn load_traceparent_context() -> Option<Context>

作用:真正读取环境变量 TRACEPARENT/TRACESTATE,并尝试把它们变成父级追踪上下文。它还会在成功或失败时写调试日志。

数据流:进去时不需要参数,它读取系统环境变量 TRACEPARENT,顺带读取 TRACESTATE → 调用 context_from_trace_headers 解析;成功就记录 debug 日志并返回 Context,失败就记录 warn 警告并返回空 → 不修改环境变量。

调用关系:traceparent_context_from_env 会通过 OnceLock 懒加载它,也就是第一次需要时才做这件事。它把字符串解析交给 context_from_trace_headers。

调用图:调用 1 个内部函数(context_from_trace_headers);外部调用 3 个(debug!, var, warn!)。

tracestate_entries131–133 ↗
fn tracestate_entries() -> &'static RwLock<BTreeMap<String, BTreeMap<String, String>>>

作用:提供全局 tracestate 配置表的存放处。它保证这张表只初始化一次,并用读写锁保护它。

数据流:进去时不需要参数 → 如果全局 RwLock 还没创建,就创建一个空的 BTreeMap;如果已经有了,就直接返回引用 → 出来的是全局读写锁引用。

调用关系:set_tracestate_entries 用它拿写锁更新配置,span_w3c_trace_context 用它拿读锁读取配置。它是这份文件里 tracestate 配置共享的中心点。

调用图:被 2 处调用(set_tracestate_entries, span_w3c_trace_context)。

merge_tracestate_entries135–164 ↗
fn merge_tracestate_entries(
    tracestate: Option<&str>,
    configured_entries: &BTreeMap<String, BTreeMap<String, String>>,
) -> Option<String>

作用:把已有的 tracestate 和程序配置的 tracestate 合并起来。这样既保留别人传来的信息,又能补上本程序想带的字段。

数据流:进去的是已有 tracestate 字符串,以及全局配置表 → 先尝试按 W3C 规则解析已有字符串;解析失败就警告并当作空值;然后按配置逐个成员合并字段 → 出来是新的 tracestate 字符串;如果最终为空,就返回空。

调用关系:span_w3c_trace_context 在导出追踪头时调用它。它把每个成员内部字段的合并交给 merge_tracestate_member_fields。

调用图:调用 1 个内部函数(merge_tracestate_member_fields);被 1 处调用(span_w3c_trace_context);外部调用 1 个(warn!)。

validate_tracestate_entries167–190 ↗
fn validate_tracestate_entries(
    entries: &BTreeMap<String, BTreeMap<String, String>>,
) -> Result<(), Box<dyn std::error::Error>>

作用:检查整套 tracestate 配置是否合法。它的作用是提前拦住坏配置,避免程序向外传播别人无法解析的追踪信息。

数据流:进去的是完整配置表 → 对每个成员编码成最终头部值,再交给 OpenTelemetry 的 TraceState 校验整体格式 → 成功返回 Ok,失败返回带说明的错误。

调用关系:set_tracestate_entries 在安装配置前会调用它,配置转换流程也可能直接调用它。它是全局 tracestate 配置进入系统前的总闸门。

调用图:被 2 处调用(from, set_tracestate_entries);外部调用 1 个(from_key_value)。

validate_tracestate_member193–205 ↗
fn validate_tracestate_member(
    member_key: &str,
    fields: &BTreeMap<String, String>,
) -> Result<(), Box<dyn std::error::Error>>

作用:只检查一个 tracestate 成员是否合法。适合在用户刚配置某一项时,单独给出更精确的错误。

数据流:进去的是成员名和它下面的一组字段 → 先调用 encode_tracestate_member_fields 把字段编码成最终字符串,再用 TraceState 校验这个单独成员 → 成功返回 Ok,失败返回错误。

调用关系:它复用 encode_tracestate_member_fields 的字段级检查,再借助 OpenTelemetry 的 from_key_value 做标准格式检查。

调用图:调用 1 个内部函数(encode_tracestate_member_fields);外部调用 1 个(from_key_value)。

encode_tracestate_member_fields207–236 ↗
fn encode_tracestate_member_fields(
    member_key: &str,
    fields: &BTreeMap<String, String>,
) -> Result<(String, String), Box<dyn std::error::Error>>

作用:把配置里的多个字段编码成一个 tracestate 成员值。配置里是“字段名到值”的表,真正传播时要变成类似 key:value;key:value 的一段文本。

数据流:进去的是成员名和字段表 → 逐个检查字段名和值是否合法;合法就拼成 field_key:value,最后用分号连接;再检查整段文本是否适合放进头部 → 出来是成员名和编码后的值;如果发现非法字符就返回错误。

调用关系:validate_tracestate_member 会调用它。它把具体字符规则分给 is_configured_tracestate_field_key、is_configured_tracestate_field_value 和 is_header_safe_tracestate_member_value,出错时用 invalid_tracestate_config 生成错误。

调用图:调用 4 个内部函数(invalid_tracestate_config, is_configured_tracestate_field_key, is_configured_tracestate_field_value, is_header_safe_tracestate_member_value);被 1 处调用(validate_tracestate_member);外部调用 2 个(with_capacity, format!)。

is_configured_tracestate_field_key238–243 ↗
fn is_configured_tracestate_field_key(field_key: &str) -> bool

作用:检查配置里的字段名能不能用。它防止字段名里出现会破坏 tracestate 格式的字符。

数据流:进去的是字段名字符串 → 检查它不能为空,并且每个字节都是可见字符,同时不能包含冒号、分号、逗号、等号这些分隔符 → 出来是 true 或 false。

调用关系:encode_tracestate_member_fields 在编码每个字段前会调用它。它只判断字段名,不处理字段值。

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

is_configured_tracestate_field_value245–249 ↗
fn is_configured_tracestate_field_value(value: &str) -> bool

作用:检查配置里的字段值能不能安全放进 tracestate 成员里。重点是不能出现会把字段切断的分号。

数据流:进去的是字段值字符串 → 逐字节检查是否符合 tracestate 成员值允许的字符规则,并额外禁止分号 → 出来是 true 或 false。

调用关系:encode_tracestate_member_fields 在拼接 field_key:value 前会调用它。它依赖 is_tracestate_member_value_byte 定义基础字符范围。

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

is_header_safe_tracestate_member_value251–255 ↗
fn is_header_safe_tracestate_member_value(value: &str) -> bool

作用:检查已经拼好的 tracestate 成员值是否适合放进最终头部。它是最后一道安全检查。

数据流:进去的是完整成员值字符串 → 如果为空则允许;否则检查所有字节都符合成员值规则,并且最后一个字符不能是空格 → 出来是 true 或 false。

调用关系:encode_tracestate_member_fields 在所有字段拼接完成后调用它。它也使用 is_tracestate_member_value_byte 的基础字符规则。

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

is_tracestate_member_value_byte257–259 ↗
fn is_tracestate_member_value_byte(byte: u8) -> bool

作用:判断一个字节是不是 tracestate 成员值允许的基础字符。它把 W3C 头部值的底层字符规则集中在一个地方。

数据流:进去的是一个字节 → 检查它是否在空格到波浪号的可打印 ASCII 范围内,并且不是逗号或等号 → 出来是 true 或 false。

调用关系:它被字段值检查和头部安全检查间接依赖。虽然调用图只显示它内部使用 matches 宏,但它是这些字符判断函数背后的共同规则。

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

invalid_tracestate_config261–266 ↗
fn invalid_tracestate_config(message: String) -> Box<dyn std::error::Error>

作用:生成一个统一格式的“tracestate 配置无效”错误。这样上层拿到的错误类型一致,错误信息也更清楚。

数据流:进去的是错误说明字符串 → 包装成 std::io::Error,错误类型是 InvalidInput,再装箱成通用错误对象 → 出来的是 Box<dyn Error>。

调用关系:encode_tracestate_member_fields 发现字段名、字段值或最终头部值不合法时会调用它。它不判断对错,只负责把错误做成统一形状。

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

merge_tracestate_member_fields268–300 ↗
fn merge_tracestate_member_fields(
    existing: Option<&str>,
    configured_fields: &BTreeMap<String, String>,
) -> String

作用:合并同一个 tracestate 成员里的字段。它会更新配置里指定的字段,同时保留原来那些不相关的字段。

数据流:进去的是已有成员值,以及配置字段表 → 先把已有值按分号拆成一个个字段;如果字段名在配置里,就替换成配置值;如果不在配置里,就原样保留;最后补上配置里新增但原来没有的字段 → 出来是合并后的成员值字符串。

调用关系:merge_tracestate_entries 在处理每个 tracestate 成员时调用它。它解决的是成员内部的“小合并”,外层函数负责整个 tracestate 列表的“大合并”。

调用图:被 1 处调用(merge_tracestate_entries);外部调用 3 个(new, new, format!)。

tests::parses_valid_w3c_trace_context319–336 ↗
fn parses_valid_w3c_trace_context()

作用:测试合法的 W3C traceparent 能被正确解析。它确保外部传来的标准追踪信息不会被程序误判为无效。

数据流:进去的是测试里手写的 trace id 和 span id → 拼成 traceparent,调用 context_from_w3c_trace_context 解析 → 断言解析出的 trace id、span id 和远程标记都符合预期。

调用关系:它直接覆盖 context_from_w3c_trace_context 的成功路径,也间接验证 context_from_trace_headers 能正确使用 OpenTelemetry 的解析器。

调用图:调用 1 个内部函数(context_from_w3c_trace_context);外部调用 3 个(assert!, assert_eq!, format!)。

tests::invalid_traceparent_returns_none339–343 ↗
fn invalid_traceparent_returns_none()

作用:测试坏的 traceparent 会被拒绝。这样程序不会把乱七八糟的字符串当成有效追踪继续传播。

数据流:进去的是测试里的字符串 not-a-traceparent → 调用 context_from_trace_headers 尝试解析 → 断言结果是空。

调用关系:它覆盖 context_from_trace_headers 的失败路径,证明解析函数会做有效性检查,而不是只要有字符串就接受。

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

tests::missing_traceparent_returns_none346–354 ↗
fn missing_traceparent_returns_none()

作用:测试没有 traceparent 时不能只靠 tracestate 生成追踪上下文。因为 traceparent 才是 W3C 追踪链路的核心编号。

数据流:进去的是一个 traceparent 为空、tracestate 有值的 W3cTraceContext → 调用 context_from_w3c_trace_context → 断言返回空。

调用关系:它验证 context_from_w3c_trace_context 会把缺失 traceparent 的情况交给 context_from_trace_headers,并得到正确的空结果。

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

tests::current_span_trace_id_returns_hex_trace_id357–371 ↗
fn current_span_trace_id_returns_hex_trace_id()

作用:测试当前 Span 能返回一个真实的十六进制 trace id。它保证 current_span_trace_id 在安装了 OpenTelemetry 追踪层后能正常工作。

数据流:进去的是测试创建的 tracer、subscriber 和一个 test_span → 进入这个 Span 后调用 current_span_trace_id → 断言返回值长度是 32、全是十六进制字符,并且不是全零。

调用关系:它为 current_span_trace_id 搭好一套临时追踪环境,再检查函数输出。这个测试证明该函数依赖当前 tracing Span,并能从 OpenTelemetry 上下文里取到正确编号。

调用图:调用 1 个内部函数(current_span_trace_id);外部调用 7 个(builder, assert!, assert_eq!, assert_ne!, trace_span!, layer, registry)。

otel/src/events/mod.rs源码 ↗
othercompile time / cross-cutting

这个文件本身不做具体计算,也不处理数据。它的作用更像一本书的章节目录:声明 events 目录下有哪些代码文件属于同一个功能区域。这里的 session_telemetry 通常可以理解为“会话遥测”,也就是记录一次运行或一次交互中的观察数据;shared 则通常放这些事件代码都会用到的公共部分。没有这个文件,Rust 就不知道这些子文件属于 events 模块,其他地方也就没法按模块路径去使用它们。它很小,但对代码组织很重要:让事件相关代码集中在一个入口下,避免散落得到处都是。

otel/src/events/shared.rs源码 ↗
utilcross-cutting

这个文件像一张“统一报销单模板”:别人只要填自己那部分内容,它会自动补上固定信息。这里定义了几个宏,宏可以理解成“代码里的快捷模板”。log_event! 用来写普通日志事件,trace_event! 用来写适合追踪链路的事件,log_and_trace_event! 则一次同时写两份,但允许日志和追踪里放不同字段。它们都会把事件发给 tracing 这个记录事件的工具,并统一加上时间戳、会话 ID、应用版本、登录方式、来源、终端类型、模型名等元数据。特别要注意的是,日志事件会包含账号和邮箱,而追踪事件没有这两个字段,说明这里有意区分了“可记录到日志的内容”和“更安全的追踪内容”。timestamp 函数负责生成当前 UTC 时间,格式精确到毫秒,方便不同机器上的事件按时间对齐。

函数细节1
timestamp58–60 ↗
fn timestamp() -> String

作用:生成一段标准格式的当前时间字符串,给每条事件当“发生时间”。有人记录事件时会用它,这样所有事件的时间写法都一样。

数据流:它不需要外部传入参数;运行时读取当前的 UTC 时间,也就是全球统一时间;然后把时间转成 RFC3339 格式的字符串,并保留到毫秒,最后返回这段文字。它不会修改外部状态。

调用关系:它是几个事件记录宏背后的“小钟表”。log_event! 和 trace_event! 在写事件时会调用它,把生成的时间放进 event.timestamp 字段;它自己只把取当前时间这件事交给 chrono 库的 Utc::now 来完成。

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

传输遥测回调

这些文件定义回调 trait 和包装器,使客户端和 API 传输层能够向观察者报告请求尝试遥测。

codex-client/src/telemetry.rs源码 ↗
data_modelrequest handling / cross-cutting

这个文件本身不真正发送请求,也不真正写日志。它像是先规定了一张“回执单”的格式:每次客户端向 API 发请求后,都可以把第几次尝试、服务器返回的状态码、发生的传输错误、耗时多久这些信息交给实现者。这里的 trait(可以理解成一份接口约定)叫 RequestTelemetry,意思是“请求遥测”。遥测就是把运行时发生的情况记录下来,方便观察系统健康状况。没有这层约定,各处代码可能会用不同方式记录请求结果,后面统计和排错就会很乱。它还要求实现者能在线程之间安全使用,因为网络请求和监控记录可能发生在并发环境里。

codex-api/src/telemetry.rs源码 ↗
utilrequest handling / cross-cutting

这个文件像是在网络通信外面套了一层“计时器和记录员”。项目里会发普通 HTTP 请求,也会收流式事件,甚至用 WebSocket(一种保持连接、双方都能持续发消息的网络通道)。这些通信如果慢了、失败了、重试了,光看最终结果不够,所以这里定义了几种遥测接口,让外部代码可以在关键时刻收到通知。它还统一了 Response 和 StreamResponse 这两种返回值取 HTTP 状态码的方式,避免每处代码都自己写一遍。最核心的是 run_with_request_telemetry:它不自己发送请求,而是包住已有的重试工具 run_with_retry。每次尝试请求时,它先开始计时,请求结束后把第几次尝试、状态码、错误和耗时告诉 telemetry。这样普通请求和流式请求都能用同一套记录方式。

函数细节4
http_status49–54 ↗
fn http_status(err: &TransportError) -> Option<StatusCode>

作用:这个函数从网络传输错误里尝试拿出 HTTP 状态码。比如服务器返回了 500,它就能把 500 提取出来;如果错误不是 HTTP 返回造成的,就说明没有状态码。

数据流:进去的是一个 TransportError,也就是一次传输失败的原因。函数检查它是不是 HTTP 错误:如果是,就取出里面的 status;如果不是,比如连接断了或超时,就返回空。出来的是一个可能存在的 StatusCode。

调用关系:它是 run_with_request_telemetry 的小帮手。请求失败时,run_with_request_telemetry 会用它把错误里的状态码尽量提取出来,再交给遥测记录器,这样日志或指标里能看到更具体的失败原因。

Response::status57–59 ↗
fn status(&self) -> StatusCode

作用:这个函数让普通 HTTP 响应可以用统一方式拿到状态码。状态码就是 200、404、500 这类数字,用来表示请求结果。

数据流:进去的是一个 Response 对象本身。函数读取它保存的 status 字段,然后原样返回。它不修改任何东西,只是把状态码拿出来。

调用关系:它实现了 WithStatus 这个内部约定。run_with_request_telemetry 不需要关心拿到的是普通响应还是流式响应,只要它们都能提供 status,就能统一记录遥测。

StreamResponse::status63–65 ↗
fn status(&self) -> StatusCode

作用:这个函数让流式 HTTP 响应也可以用统一方式拿到状态码。流式响应是那种连接不断开、后面还会持续传数据的响应。

数据流:进去的是一个 StreamResponse 对象本身。函数读取它的 status 字段并返回,不改变响应内容,也不读取后续流数据。

调用关系:它和 Response::status 作用相同,都是为了满足 WithStatus 这个内部约定。这样 run_with_request_telemetry 可以同时服务普通请求和流式请求。

run_with_request_telemetry68–98 ↗
async fn run_with_request_telemetry(
    policy: RetryPolicy,
    telemetry: Option<Arc<dyn RequestTelemetry>>,
    make_request: impl FnMut() -> Request,
    send: F,
) -> Result<T, TransportError>

作用:这个函数负责“带重试地发请求,并且每次都记一笔账”。有人用它时,可以得到原本的请求结果,同时系统还能知道每次尝试花了多久、有没有报错、返回了什么状态码。

数据流:进去的是重试策略、可选的遥测记录器、一个生成请求的函数,以及一个真正发送请求的函数。它把这些交给 run_with_retry 来安排多次尝试;每一次尝试开始前计时,发送结束后看结果:成功就取响应状态码,失败就尽量从错误里取状态码和错误信息。然后它通知 telemetry。最后出来的是最终成功的响应,或者重试后仍失败的 TransportError。

调用关系:它位在实际发送请求和底层重试工具之间,像一个中间夹层。execute_with 和 stream_encoded_json_with 会调用它来发普通请求或流式请求;它自己把重试流程交给外部的 run_with_retry,同时在每次尝试后补上遥测通知。

调用图:被 2 处调用(execute_with, stream_encoded_json_with);外部调用 1 个(run_with_retry)。