diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index af465c1924..357f499f7d 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -743,6 +743,7 @@ dependencies = [ "maplit", "mcp-types", "openssl-sys", + "opentelemetry_sdk", "os_info", "portable-pty", "predicates", @@ -3247,6 +3248,7 @@ dependencies = [ "bytes", "http", "opentelemetry", + "reqwest", ] [[package]] @@ -3263,6 +3265,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", + "reqwest", "thiserror 1.0.69", "tokio", "tonic", diff --git a/codex-rs/codex-telemetry/Cargo.toml b/codex-rs/codex-telemetry/Cargo.toml index d4579e2738..a16730ea90 100644 --- a/codex-rs/codex-telemetry/Cargo.toml +++ b/codex-rs/codex-telemetry/Cargo.toml @@ -32,7 +32,8 @@ tracing-subscriber = { version = "0.3", features = ["registry", "fmt"], optional tracing-opentelemetry = { version = "0.27", optional = true } opentelemetry = { version = "0.26", features = ["trace"], optional = true } opentelemetry_sdk = { version = "0.26", features = ["trace", "rt-tokio"], optional = true } -opentelemetry-otlp = { version = "0.26", features = ["tonic", "http-proto"], optional = true } +# Enable HTTP exporter protocol and the reqwest HTTP client backend +opentelemetry-otlp = { version = "0.26", features = ["tonic", "http-proto", "reqwest-client"], optional = true } opentelemetry-proto = { version = "0.26", features = ["gen-tonic"], optional = true } prost = { version = "0.13", optional = true } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"], optional = true } diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 9fa5b11c94..31269f936d 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -64,6 +64,7 @@ uuid = { version = "1", features = ["serde", "v4"] } which = "6" wildmatch = "2.5.0" codex-telemetry = { path = "../codex-telemetry", features = ["otel"] } +opentelemetry_sdk = { version = "0.26", features = ["trace"] } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index c84d84e0ff..e9088d9fe0 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -159,8 +159,8 @@ pub struct Config { /// Responses API. pub model_reasoning_effort: Option, - /// If not "none", the value to use for `reasoning.summary` when making a - /// request using the Responses API. + /// If true, a short textual description of the agent's internal reasoning + /// will be included in model responses. pub model_reasoning_summary: ReasoningSummary, /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). @@ -194,6 +194,9 @@ pub struct Config { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: bool, + + /// Telemetry configuration (exporter type, endpoint, headers, etc.). + pub telemetry: crate::config_types::TelemetryConfig, } impl Config { @@ -705,6 +708,9 @@ pub struct ConfigToml { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: Option, + + /// Telemetry configuration. + pub telemetry: Option, } impl From for UserSavedConfig { @@ -1053,6 +1059,24 @@ impl Config { .as_ref() .map(|t| t.notifications.clone()) .unwrap_or_default(), + telemetry: { + use crate::config_types::TelemetryConfig; + use crate::config_types::TelemetryConfigToml; + use crate::config_types::TelemetryExporterKind; + let t: TelemetryConfigToml = cfg.telemetry.unwrap_or_default(); + let enabled = t.enabled.unwrap_or(true); + let exporter = t.exporter.unwrap_or(TelemetryExporterKind::OtlpFile); + let endpoint = t.endpoint; + let headers = t.headers.unwrap_or_default(); + let rotate_mb = t.rotate_mb.or(Some(100)); + TelemetryConfig { + enabled, + exporter, + endpoint, + headers, + rotate_mb, + } + }, }; Ok(config) } @@ -1631,6 +1655,13 @@ model_verbosity = "high" active_profile: Some("o3".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + telemetry: crate::config_types::TelemetryConfig { + enabled: true, + exporter: crate::config_types::TelemetryExporterKind::OtlpFile, + endpoint: None, + headers: HashMap::new(), + rotate_mb: Some(100), + }, }, o3_profile_config ); @@ -1689,6 +1720,13 @@ model_verbosity = "high" active_profile: Some("gpt3".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + telemetry: crate::config_types::TelemetryConfig { + enabled: true, + exporter: crate::config_types::TelemetryExporterKind::OtlpFile, + endpoint: None, + headers: HashMap::new(), + rotate_mb: Some(100), + }, }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -1762,6 +1800,13 @@ model_verbosity = "high" active_profile: Some("zdr".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + telemetry: crate::config_types::TelemetryConfig { + enabled: true, + exporter: crate::config_types::TelemetryExporterKind::OtlpFile, + endpoint: None, + headers: HashMap::new(), + rotate_mb: Some(100), + }, }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); @@ -1821,6 +1866,13 @@ model_verbosity = "high" active_profile: Some("gpt5".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + telemetry: crate::config_types::TelemetryConfig { + enabled: true, + exporter: crate::config_types::TelemetryExporterKind::OtlpFile, + endpoint: None, + headers: HashMap::new(), + rotate_mb: Some(100), + }, }; assert_eq!(expected_gpt5_profile_config, gpt5_profile_config); diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 3737649871..99cf9b8228 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -76,6 +76,48 @@ pub enum HistoryPersistence { None, } +// ===== Telemetry configuration ===== + +/// Which telemetry exporter to use. +#[derive(Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum TelemetryExporterKind { + None, + OtlpFile, + OtlpHttp, + OtlpGrpc, +} + +/// Telemetry settings loaded from config.toml. Fields are optional so we can apply defaults. +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +pub struct TelemetryConfigToml { + /// Enable or disable telemetry entirely. Defaults to true. + pub enabled: Option, + + /// Exporter to use. Defaults to `otlp-file`. + pub exporter: Option, + + /// Endpoint for HTTP/GRPC exporters. Example: "http://localhost:4318". + pub endpoint: Option, + + /// Optional headers for HTTP/GRPC exporters. + #[serde(default)] + pub headers: Option>, + + /// Rotation size (MiB) for file exporter. + pub rotate_mb: Option, +} + +/// Effective telemetry settings after defaults are applied. +#[derive(Debug, Clone, PartialEq)] +pub struct TelemetryConfig { + pub enabled: bool, + pub exporter: TelemetryExporterKind, + pub endpoint: Option, + pub headers: HashMap, + pub rotate_mb: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(untagged)] pub enum Notifications { diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index f73bef4051..02d8b9ae4b 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -100,3 +100,5 @@ pub use codex_protocol::models::LocalShellExecAction; pub use codex_protocol::models::LocalShellStatus; pub use codex_protocol::models::ReasoningItemContent; pub use codex_protocol::models::ResponseItem; + +pub mod telemetry_init; diff --git a/codex-rs/core/src/telemetry_init.rs b/codex-rs/core/src/telemetry_init.rs new file mode 100644 index 0000000000..1d615c2c51 --- /dev/null +++ b/codex-rs/core/src/telemetry_init.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use opentelemetry_sdk::trace::Tracer; + +use crate::config::Config; +use crate::config_types::TelemetryExporterKind as Kind; +use codex_telemetry as telemetry; + +/// Build an OpenTelemetry tracer and guard from the app Config. +/// +/// Returns `None` when telemetry is disabled. +pub fn build_otel_layer_from_config( + config: &Config, + service_name: &str, + service_version: &str, +) -> Option<(telemetry::Guard, Tracer)> { + let exporter = match config.telemetry.exporter { + Kind::None => telemetry::Exporter::None, + Kind::OtlpFile => telemetry::Exporter::OtlpFile { + path: PathBuf::new(), + rotate_mb: config.telemetry.rotate_mb, + }, + Kind::OtlpHttp => telemetry::Exporter::OtlpHttp { + endpoint: config + .telemetry + .endpoint + .clone() + .unwrap_or_else(|| "http://localhost:4318".to_string()), + headers: config + .telemetry + .headers + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + }, + Kind::OtlpGrpc => telemetry::Exporter::OtlpGrpc { + endpoint: config + .telemetry + .endpoint + .clone() + .unwrap_or_else(|| "http://localhost:4317".to_string()), + headers: config + .telemetry + .headers + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + }, + }; + + telemetry::build_layer(&telemetry::Settings { + enabled: config.telemetry.enabled, + exporter, + service_name: service_name.to_string(), + service_version: service_version.to_string(), + codex_home: Some(config.codex_home.clone()), + }) +} diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index efde3c00db..d4097d735c 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -169,16 +169,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?; // Build OTEL layer and compose into subscriber. - let telemetry = telemetry::build_layer(&telemetry::Settings { - enabled: true, - exporter: telemetry::Exporter::OtlpFile { - path: PathBuf::new(), - rotate_mb: Some(100), - }, - service_name: "codex".to_string(), - service_version: env!("CARGO_PKG_VERSION").to_string(), - codex_home: Some(config.codex_home.clone()), - }); + let telemetry = codex_core::telemetry_init::build_otel_layer_from_config( + &config, + "codex", + env!("CARGO_PKG_VERSION"), + ); let _telemetry_guard = if let Some((guard, tracer)) = telemetry { let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer); // Build env_filter separately and attach via with_filter. diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 3173d4b328..024d2e1a17 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -165,6 +165,13 @@ pub async fn run_main( } }; + // Build OTEL layer and compose into subscriber. + let telemetry = codex_core::telemetry_init::build_otel_layer_from_config( + &config, + "codex", + env!("CARGO_PKG_VERSION"), + ); + // we load config.toml here to determine project state. #[allow(clippy::print_stderr)] let config_toml = {