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
+32
View File
@@ -1273,3 +1273,35 @@ fn project_config_cannot_change_system_proxy_routing_or_credential_masking() {
}
}
}
#[test]
fn otel_headers_helpers_cannot_come_from_repository_config() {
let mut config: TomlValue = toml::from_str(
"[otel]\nheaders_helper = 'untrusted-command'\nheaders_helper_refresh_interval_ms = 1",
)
.unwrap();
let ignored =
sanitize_project_config(&mut config, CredentialBrokerProjectState::Unconfigured, &[]);
assert_eq!(ignored, ["otel"]);
assert!(config.get("otel").is_none());
}
#[test]
fn otel_headers_helper_interval_must_be_positive() {
assert!(
toml::from_str::<ConfigToml>(
"[otel]\nheaders_helper = 'helper'\nheaders_helper_refresh_interval_ms = 0"
)
.is_err()
);
let config: ConfigToml = toml::from_str(
"[otel]\nheaders_helper = 'helper'\nheaders_helper_refresh_interval_ms = 900000",
)
.unwrap();
let otel = config.otel.unwrap();
assert_eq!(otel.headers_helper.as_deref(), Some("helper"));
assert_eq!(
otel.headers_helper_refresh_interval_ms.unwrap().get(),
900000
);
}
+10
View File
@@ -29,6 +29,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt;
use std::num::NonZeroU64;
use codex_utils_redacted_string::RedactedString;
use schemars::JsonSchema;
@@ -637,6 +638,11 @@ pub enum OtelExporterKind {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct OtelConfigToml {
/// Command that prints a JSON object of HTTP headers for explicit OTLP HTTP exporters.
/// Runs from CODEX_HOME. Repository-local configuration cannot set OTel options.
pub headers_helper: Option<String>,
/// Interval between helper executions in milliseconds. Defaults to 29 minutes.
pub headers_helper_refresh_interval_ms: Option<NonZeroU64>,
/// Byte limit for tool-result log output; independent of model-visible output.
pub tool_result: Option<codex_protocol::config_types::ToolResultLogConfig>,
/// Log successful SSE events whose kind ends in `.delta`. Defaults to true.
@@ -674,6 +680,8 @@ pub struct OtelConfigToml {
/// Effective OTEL settings after defaults are applied.
#[derive(Debug, Clone, PartialEq)]
pub struct OtelConfig {
pub headers_helper: Option<String>,
pub headers_helper_refresh_interval_ms: Option<NonZeroU64>,
pub tool_result: codex_protocol::config_types::ToolResultLogConfig,
pub log_sse_deltas: bool,
pub log_user_prompt: bool,
@@ -690,6 +698,8 @@ pub struct OtelConfig {
impl Default for OtelConfig {
fn default() -> Self {
OtelConfig {
headers_helper: None,
headers_helper_refresh_interval_ms: None,
tool_result: Default::default(),
log_sse_deltas: true,
log_user_prompt: false,