Add refreshable headers for OTLP HTTP exporters (#61028)

## Why

Allow collector credentials to refresh without rebuilding OpenTelemetry providers.

## What changed

Add `otel.headers_helper`, a command run from `CODEX_HOME` that returns a JSON object of header strings. Share its results across explicitly configured HTTP log, trace, and metric exporters. Refresh every 29 minutes by default, configurable through `otel.headers_helper_refresh_interval_ms`.

Helper headers override matching static headers. Failed refreshes block affected exports until the helper recovers, and HTTP redirects are disabled when helper headers are used. Repository-local configuration cannot set the helper; exporters set by admin requirements, gRPC exporters, and built-in analytics do not use it. Helper execution respects network policy.

## Testing

Add tests for shared execution, credential refresh and recovery, blocked exports after failure, network restrictions, and configuration validation. Collector tests cover header precedence across all three signals using JSON and binary HTTP encodings, including exclusion of an admin-managed exporter.

GitOrigin-RevId: 89018ac09c2c99523d3f92bf77048e7ed9dae5f2
This commit is contained in:
Abhinav
2026-11-12 05:21:30 +00:00
committed by copyberry
parent 88c5856258
commit c0cc9fc820
27 changed files with 789 additions and 13 deletions
+16
View File
@@ -10542,6 +10542,10 @@ async fn load_config_applies_otel_trace_metadata() -> std::io::Result<()> {
let mut fixture = create_test_fixture()?;
fixture.cfg = toml::from_str(
r#"
[otel]
headers_helper = "company-auth telemetry-headers"
headers_helper_refresh_interval_ms = 900000
[otel.span_attributes]
"example.trace_attr" = "enabled"
@@ -10562,6 +10566,18 @@ beta = "two"
)
.await?;
assert_eq!(
config.otel.headers_helper.as_deref(),
Some("company-auth telemetry-headers")
);
assert_eq!(
config
.otel
.headers_helper_refresh_interval_ms
.unwrap()
.get(),
900000
);
assert_eq!(
config.otel.span_attributes,
HashMap::from([("example.trace_attr".to_string(), "enabled".to_string())])
+2
View File
@@ -27,6 +27,8 @@ pub(crate) fn resolve_config(
let tracestate = resolve_tracestate(config.tracestate, startup_warnings);
OtelConfig {
headers_helper: config.headers_helper,
headers_helper_refresh_interval_ms: config.headers_helper_refresh_interval_ms,
tool_result: config.tool_result.unwrap_or_default(),
log_sse_deltas: config.log_sse_deltas.unwrap_or(true),
log_user_prompt,
+27
View File
@@ -4,11 +4,16 @@ use codex_config::types::OtelHttpProtocol as Protocol;
use codex_features::Feature;
use codex_login::default_client::originator;
use codex_otel::OtelExporter;
use codex_otel::OtelHeadersHelper;
use codex_otel::OtelHttpProtocol;
use codex_otel::OtelProvider;
use codex_otel::OtelSettings;
use codex_otel::OtelTlsConfig as OtelTlsSettings;
use std::error::Error;
use std::num::NonZeroU64;
use std::time::Duration;
const DEFAULT_HEADERS_HELPER_REFRESH_INTERVAL_MS: u64 = 29 * 60 * 1_000;
/// Build an OpenTelemetry provider from the app Config.
///
@@ -82,7 +87,29 @@ pub fn build_provider(
let service_name = service_name_override.unwrap_or(originator.value.as_str());
let runtime_metrics = config.features.enabled(Feature::RuntimeMetrics);
let managed = config
.config_layer_stack
.requirements_toml()
.otel
.as_deref();
let headers_helper = config
.otel
.headers_helper
.as_ref()
.map(|command| OtelHeadersHelper {
command: command.clone(),
apply_to_logs: managed.is_none_or(|otel| otel.exporter.is_none()),
apply_to_traces: managed.is_none_or(|otel| otel.trace_exporter.is_none()),
apply_to_metrics: managed.is_none_or(|otel| otel.metrics_exporter.is_none()),
refresh_interval: Duration::from_millis(
config
.otel
.headers_helper_refresh_interval_ms
.map_or(DEFAULT_HEADERS_HELPER_REFRESH_INTERVAL_MS, NonZeroU64::get),
),
});
OtelProvider::try_new(&OtelSettings {
headers_helper,
http_client_factory: config.http_client_factory(),
service_name: service_name.to_string(),
service_version: service_version.to_string(),