diff --git a/codex-rs/otel/src/metrics/client.rs b/codex-rs/otel/src/metrics/client.rs index 619df69698..057d69680f 100644 --- a/codex-rs/otel/src/metrics/client.rs +++ b/codex-rs/otel/src/metrics/client.rs @@ -107,7 +107,7 @@ pub(super) struct MetricsClientInner { histograms: Mutex>>, duration_histograms: Mutex>>, runtime_reader: Option>, - runtime_only_metrics: &'static [&'static str], + statsig_disabled_metrics: &'static [&'static str], default_tags: BTreeMap, } @@ -128,7 +128,7 @@ impl MetricsClientInner { } let attributes = self.attributes(tags)?; - if self.runtime_only_metrics.contains(&name) { + if self.statsig_disabled_metrics.contains(&name) { return Ok(()); } @@ -156,6 +156,10 @@ impl MetricsClientInner { validate_metric_name(name)?; let attributes = self.attributes(tags)?; + if self.statsig_disabled_metrics.contains(&name) { + return Ok(()); + } + let mut histograms = self .histograms .lock() @@ -177,6 +181,10 @@ impl MetricsClientInner { validate_metric_name(name)?; let attributes = self.attributes(tags)?; + if self.statsig_disabled_metrics.contains(&name) { + return Ok(()); + } + let mut gauges = self .gauges .lock() @@ -206,6 +214,11 @@ impl MetricsClientInner { ) -> Result<()> { validate_metric_name(name)?; let attributes = self.attributes(tags)?; + + if self.statsig_disabled_metrics.contains(&name) { + return Ok(()); + } + let _gauge = self .meter .i64_observable_gauge(name.to_string()) @@ -227,7 +240,7 @@ impl MetricsClientInner { validate_metric_name(name)?; let attributes = self.attributes(tags)?; - if self.runtime_only_metrics.contains(&name) { + if self.statsig_disabled_metrics.contains(&name) { return Ok(()); } @@ -305,7 +318,7 @@ impl MetricsClient { exporter, export_interval, runtime_reader, - runtime_only_metrics, + statsig_disabled_metrics, default_tags, } = config; @@ -351,7 +364,7 @@ impl MetricsClient { histograms: Mutex::new(HashMap::new()), duration_histograms: Mutex::new(HashMap::new()), runtime_reader, - runtime_only_metrics, + statsig_disabled_metrics, default_tags, }), active: None, diff --git a/codex-rs/otel/src/metrics/config.rs b/codex-rs/otel/src/metrics/config.rs index 4228a0a71d..9bc34bacc3 100644 --- a/codex-rs/otel/src/metrics/config.rs +++ b/codex-rs/otel/src/metrics/config.rs @@ -2,19 +2,32 @@ use crate::config::OtelExporter; use crate::metrics::Result; use crate::metrics::names::API_CALL_COUNT_METRIC; use crate::metrics::names::API_CALL_DURATION_METRIC; +use crate::metrics::names::RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC; +use crate::metrics::names::RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC; +use crate::metrics::names::RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC; use crate::metrics::names::TOOL_CALL_COUNT_METRIC; use crate::metrics::names::TOOL_CALL_DURATION_METRIC; +use crate::metrics::names::TURN_TOKEN_USAGE_METRIC; use crate::metrics::validation::validate_tag_key; use crate::metrics::validation::validate_tag_value; use opentelemetry_sdk::metrics::InMemoryMetricExporter; use std::collections::BTreeMap; use std::time::Duration; -const RUNTIME_ONLY_METRICS: &[&str] = &[ +const CONVERSATION_TURN_COUNT_METRIC: &str = "codex.conversation.turn.count"; + +// Metrics intentionally not sent through Codex's built-in Statsig route. +// Keep this as an exact-name list so custom OTLP exporters still receive them. +const STATSIG_DISABLED_METRICS: &[&str] = &[ API_CALL_COUNT_METRIC, API_CALL_DURATION_METRIC, + CONVERSATION_TURN_COUNT_METRIC, + RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC, + RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC, + RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC, TOOL_CALL_COUNT_METRIC, TOOL_CALL_DURATION_METRIC, + TURN_TOKEN_USAGE_METRIC, ]; #[derive(Clone, Debug)] @@ -31,7 +44,7 @@ pub struct MetricsConfig { pub(crate) exporter: MetricsExporter, pub(crate) export_interval: Option, pub(crate) runtime_reader: bool, - pub(crate) runtime_only_metrics: &'static [&'static str], + pub(crate) statsig_disabled_metrics: &'static [&'static str], pub(crate) default_tags: BTreeMap, } @@ -42,8 +55,8 @@ impl MetricsConfig { service_version: impl Into, exporter: OtelExporter, ) -> Self { - let runtime_only_metrics = if matches!(exporter, OtelExporter::Statsig) { - RUNTIME_ONLY_METRICS + let statsig_disabled_metrics = if matches!(exporter, OtelExporter::Statsig) { + STATSIG_DISABLED_METRICS } else { &[] }; @@ -54,7 +67,7 @@ impl MetricsConfig { exporter: MetricsExporter::Otlp(exporter), export_interval: None, runtime_reader: false, - runtime_only_metrics, + statsig_disabled_metrics, default_tags: BTreeMap::new(), } } @@ -73,7 +86,7 @@ impl MetricsConfig { exporter: MetricsExporter::InMemory(exporter), export_interval: None, runtime_reader: false, - runtime_only_metrics: &[], + statsig_disabled_metrics: &[], default_tags: BTreeMap::new(), } } diff --git a/codex-rs/otel/src/provider.rs b/codex-rs/otel/src/provider.rs index 5f0f3f4d8e..513f3ed2b5 100644 --- a/codex-rs/otel/src/provider.rs +++ b/codex-rs/otel/src/provider.rs @@ -551,8 +551,12 @@ mod tests { use crate::metrics::API_CALL_COUNT_METRIC; use crate::metrics::API_CALL_DURATION_METRIC; use crate::metrics::MetricsExporter; + use crate::metrics::RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC; + use crate::metrics::RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC; + use crate::metrics::RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC; use crate::metrics::TOOL_CALL_COUNT_METRIC; use crate::metrics::TOOL_CALL_DURATION_METRIC; + use crate::metrics::TURN_TOKEN_USAGE_METRIC; use opentelemetry_sdk::metrics::InMemoryMetricExporter; use pretty_assertions::assert_eq; use std::path::PathBuf; @@ -658,7 +662,7 @@ mod tests { } #[test] - fn statsig_runtime_only_metrics_are_not_exported() -> Result<(), Box> { + fn statsig_disabled_metrics_are_not_exported() -> Result<(), Box> { let exporter = InMemoryMetricExporter::default(); let mut config = MetricsConfig::otlp( "test", @@ -671,8 +675,25 @@ mod tests { metrics.counter(API_CALL_COUNT_METRIC, /*inc*/ 1, &[])?; metrics.record_duration(API_CALL_DURATION_METRIC, Duration::from_millis(100), &[])?; + metrics.counter("codex.conversation.turn.count", /*inc*/ 1, &[])?; + metrics.record_duration( + RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC, + Duration::from_millis(100), + &[], + )?; + metrics.record_duration( + RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC, + Duration::from_millis(100), + &[], + )?; + metrics.record_duration( + RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC, + Duration::from_millis(100), + &[], + )?; metrics.counter(TOOL_CALL_COUNT_METRIC, /*inc*/ 1, &[])?; metrics.record_duration(TOOL_CALL_DURATION_METRIC, Duration::from_millis(25), &[])?; + metrics.histogram(TURN_TOKEN_USAGE_METRIC, /*value*/ 100, &[])?; metrics.counter("codex.turns", /*inc*/ 1, &[])?; metrics.shutdown()?; diff --git a/codex-rs/otel/tests/suite/otlp_http_loopback.rs b/codex-rs/otel/tests/suite/otlp_http_loopback.rs index d9316a269d..3de87435c3 100644 --- a/codex-rs/otel/tests/suite/otlp_http_loopback.rs +++ b/codex-rs/otel/tests/suite/otlp_http_loopback.rs @@ -192,12 +192,33 @@ fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> { Duration::from_millis(100), &[("status", "200")], )?; + metrics.counter("codex.conversation.turn.count", /*inc*/ 1, &[])?; + metrics.record_duration( + "codex.responses_api_engine_iapi_ttft.duration_ms", + Duration::from_millis(100), + &[], + )?; + metrics.record_duration( + "codex.responses_api_engine_service_tbt.duration_ms", + Duration::from_millis(100), + &[], + )?; + metrics.record_duration( + "codex.responses_api_engine_service_ttft.duration_ms", + Duration::from_millis(100), + &[], + )?; metrics.counter("codex.tool.call", /*inc*/ 1, &[("tool", "test")])?; metrics.record_duration( "codex.tool.call.duration_ms", Duration::from_millis(42), &[("tool", "test")], )?; + metrics.histogram( + "codex.turn.token_usage", + /*value*/ 100, + &[("token_type", "total")], + )?; metrics.gauge_with_description( "codex.active", "Number of active Codex operations.", @@ -243,11 +264,36 @@ fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> { "expected API-request duration not found; body prefix: {}", &body.chars().take(2000).collect::() ); + assert!( + body.contains("\"codex.conversation.turn.count\""), + "expected conversation turn count not found; body prefix: {}", + &body.chars().take(2000).collect::() + ); + assert!( + body.contains("\"codex.responses_api_engine_iapi_ttft.duration_ms\""), + "expected engine IAPI TTFT duration not found; body prefix: {}", + &body.chars().take(2000).collect::() + ); + assert!( + body.contains("\"codex.responses_api_engine_service_tbt.duration_ms\""), + "expected engine service TBT duration not found; body prefix: {}", + &body.chars().take(2000).collect::() + ); + assert!( + body.contains("\"codex.responses_api_engine_service_ttft.duration_ms\""), + "expected engine service TTFT duration not found; body prefix: {}", + &body.chars().take(2000).collect::() + ); assert!( body.contains("\"codex.tool.call\""), "expected tool-call counter not found; body prefix: {}", &body.chars().take(2000).collect::() ); + assert!( + body.contains("\"codex.turn.token_usage\""), + "expected turn-token histogram not found; body prefix: {}", + &body.chars().take(2000).collect::() + ); assert!( body.contains("\"codex.tool.call.duration_ms\""), "expected tool-call duration not found; body prefix: {}",