diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 0dcb598595..a3dd15309d 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -106,6 +106,18 @@ through that shared tracing pipeline. OTEL resolution follows the same defaults as Codex core (`environment = "dev"`, `exporter = "none"`, `trace_exporter = exporter`, `metrics_exporter = "statsig"`), and `log_user_prompt` is accepted for compatibility but ignored by the proxy. +Standalone mode also honors top-level `[analytics].enabled`; when it is `false`, metrics export is +disabled (`metrics_exporter = "none"`), even if a metrics exporter is configured under `[otel]`. + +Example: + +```toml +[analytics] +enabled = false + +[otel] +metrics_exporter = "statsig" # ignored while analytics is disabled +``` To filter proxy logs locally, use: diff --git a/codex-rs/network-proxy/src/main.rs b/codex-rs/network-proxy/src/main.rs index 3b60cbe2ac..30d71ffd6c 100644 --- a/codex-rs/network-proxy/src/main.rs +++ b/codex-rs/network-proxy/src/main.rs @@ -30,6 +30,12 @@ struct StandaloneConfig { config_missing: bool, network: NetworkProxyConfig, otel: standalone_otel::StandaloneOtelConfigToml, + analytics_enabled: bool, +} + +#[derive(Debug, Deserialize, Default)] +struct StandaloneAnalyticsConfigToml { + enabled: Option, } #[derive(Debug, Deserialize, Default)] @@ -38,6 +44,8 @@ struct StandaloneConfigToml { network: NetworkProxyConfig, #[serde(default)] otel: standalone_otel::StandaloneOtelConfigToml, + #[serde(default)] + analytics: Option, } #[tokio::main] @@ -50,10 +58,16 @@ async fn main() -> Result<()> { config_missing, network, otel: otel_config, + analytics_enabled, } = config; let otel_provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - standalone_otel::build_provider(otel_config, codex_home, env!("CARGO_PKG_VERSION")) + standalone_otel::build_provider( + otel_config, + analytics_enabled, + codex_home, + env!("CARGO_PKG_VERSION"), + ) })) { Ok(Ok(otel)) => otel, Ok(Err(err)) => { @@ -122,16 +136,22 @@ async fn load_standalone_config() -> Result { } }; - let (network, otel, config_missing) = match raw { + let (network, otel, analytics_enabled, config_missing) = match raw { Some(raw) => { let parsed: StandaloneConfigToml = toml::from_str(&raw) .with_context(|| format!("failed to parse {}", config_path.display()))?; - (parsed.network, parsed.otel, false) + let StandaloneConfigToml { + network, + otel, + analytics, + } = parsed; + (network, otel, resolve_analytics_enabled(analytics), false) } None => ( NetworkProxyConfig::default(), standalone_otel::StandaloneOtelConfigToml::default(), true, + true, ), }; @@ -141,9 +161,14 @@ async fn load_standalone_config() -> Result { config_missing, network, otel, + analytics_enabled, }) } +fn resolve_analytics_enabled(analytics: Option) -> bool { + analytics.and_then(|a| a.enabled).unwrap_or(true) +} + struct StandaloneConfigReloader; #[async_trait] @@ -162,3 +187,35 @@ impl ConfigReloader for StandaloneConfigReloader { )) } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn standalone_config_analytics_defaults_enabled_when_omitted() { + let parsed: StandaloneConfigToml = toml::from_str( + r#" +[network] +enabled = true +"#, + ) + .unwrap(); + + assert_eq!(resolve_analytics_enabled(parsed.analytics), true); + } + + #[test] + fn standalone_config_analytics_respects_disabled_flag() { + let parsed: StandaloneConfigToml = toml::from_str( + r#" +[analytics] +enabled = false +"#, + ) + .unwrap(); + + assert_eq!(resolve_analytics_enabled(parsed.analytics), false); + } +} diff --git a/codex-rs/network-proxy/src/standalone_otel.rs b/codex-rs/network-proxy/src/standalone_otel.rs index 6883deddf0..acaa95520a 100644 --- a/codex-rs/network-proxy/src/standalone_otel.rs +++ b/codex-rs/network-proxy/src/standalone_otel.rs @@ -82,12 +82,17 @@ pub(crate) fn parse_otel_config(raw: &str) -> Result ResolvedStandaloneOtelConfig { let exporter = config.exporter.unwrap_or(StandaloneOtelExporterKind::None); let trace_exporter = config.trace_exporter.unwrap_or_else(|| exporter.clone()); - let metrics_exporter = config - .metrics_exporter - .unwrap_or(StandaloneOtelExporterKind::Statsig); + let metrics_exporter = if analytics_enabled { + config + .metrics_exporter + .unwrap_or(StandaloneOtelExporterKind::Statsig) + } else { + StandaloneOtelExporterKind::None + }; ResolvedStandaloneOtelConfig { environment: config @@ -101,10 +106,11 @@ pub(crate) fn resolve_otel_config( pub(crate) fn build_provider( config: StandaloneOtelConfigToml, + analytics_enabled: bool, codex_home: PathBuf, service_version: &str, ) -> Result, Box> { - let resolved = resolve_otel_config(config); + let resolved = resolve_otel_config(config, analytics_enabled); let settings = OtelSettings { environment: resolved.environment, service_name: STANDALONE_SERVICE_NAME.to_string(), @@ -175,7 +181,7 @@ enabled = true "#, ) .unwrap(); - let resolved = resolve_otel_config(parsed); + let resolved = resolve_otel_config(parsed, true); assert_eq!( resolved, @@ -198,7 +204,7 @@ exporter = { otlp-http = { endpoint = "https://collector.example/v1/logs", proto "#, ) .unwrap(); - let resolved = resolve_otel_config(parsed); + let resolved = resolve_otel_config(parsed, true); assert_eq!( resolved, @@ -230,7 +236,7 @@ trace_exporter = { otlp-grpc = { endpoint = "https://collector.example:4317" } } "#, ) .unwrap(); - let resolved = resolve_otel_config(parsed); + let resolved = resolve_otel_config(parsed, true); assert_eq!( resolved, @@ -256,7 +262,7 @@ log_user_prompt = true "#, ) .unwrap(); - let resolved = resolve_otel_config(parsed); + let resolved = resolve_otel_config(parsed, true); assert_eq!( resolved, @@ -278,7 +284,7 @@ future_field = "ignored" "#, ) .unwrap(); - let resolved = resolve_otel_config(parsed); + let resolved = resolve_otel_config(parsed, true); assert_eq!( resolved, @@ -290,4 +296,48 @@ future_field = "ignored" } ); } + + #[test] + fn parse_metrics_exporter_defaults_to_none_when_analytics_disabled() { + let parsed = parse_otel_config( + r#" +[otel] +environment = "staging" +"#, + ) + .unwrap(); + let resolved = resolve_otel_config(parsed, false); + + assert_eq!( + resolved, + ResolvedStandaloneOtelConfig { + environment: "staging".to_string(), + exporter: StandaloneOtelExporterKind::None, + trace_exporter: StandaloneOtelExporterKind::None, + metrics_exporter: StandaloneOtelExporterKind::None, + } + ); + } + + #[test] + fn parse_metrics_exporter_is_none_when_analytics_disabled_even_if_explicitly_set() { + let parsed = parse_otel_config( + r#" +[otel] +metrics_exporter = { otlp-grpc = { endpoint = "https://collector.example:4317" } } +"#, + ) + .unwrap(); + let resolved = resolve_otel_config(parsed, false); + + assert_eq!( + resolved, + ResolvedStandaloneOtelConfig { + environment: "dev".to_string(), + exporter: StandaloneOtelExporterKind::None, + trace_exporter: StandaloneOtelExporterKind::None, + metrics_exporter: StandaloneOtelExporterKind::None, + } + ); + } }