diff --git a/codex-rs/core/src/state/additional_context.rs b/codex-rs/core/src/state/additional_context.rs index c7408816ef..3247bc5baa 100644 --- a/codex-rs/core/src/state/additional_context.rs +++ b/codex-rs/core/src/state/additional_context.rs @@ -120,7 +120,7 @@ impl AdditionalContextStore { telemetry.histogram_with_boundaries( ADDITIONAL_CONTEXT_VALUE_BYTES_METRIC, i64::try_from(bytes).unwrap_or(i64::MAX), - ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS, + ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS.as_slice(), &size_tags, ); } diff --git a/codex-rs/otel/src/metrics/names.rs b/codex-rs/otel/src/metrics/names.rs index a3f333c38f..2a6401f406 100644 --- a/codex-rs/otel/src/metrics/names.rs +++ b/codex-rs/otel/src/metrics/names.rs @@ -75,23 +75,16 @@ pub const THREAD_STARTED_METRIC: &str = "codex.thread.started"; pub const ADDITIONAL_CONTEXT_COUNT_METRIC: &str = "codex.additional_context"; /// UTF-8 value bytes before/after truncation, excluding message wrappers; after includes the marker. pub const ADDITIONAL_CONTEXT_VALUE_BYTES_METRIC: &str = "codex.additional_context.value_bytes"; -pub const ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS: &[f64] = &[ - 0.0, - 256.0, - 512.0, - 1_000.0, - 2_000.0, - 4_000.0, - 8_000.0, - 16_000.0, - 32_000.0, - 64_000.0, - 128_000.0, - 256_000.0, - 512_000.0, - 1_024_000.0, - 4_096_000.0, -]; +/// Logarithmic byte buckets through 4 MiB, with a 4 KiB boundary near the truncation cap. +pub static ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS: LazyLock> = LazyLock::new(|| { + let mut buckets = context_log_buckets(/*max_exponent*/ 22.0) + .into_iter() + .chain([4096.0]) + .collect::>(); + buckets.sort_by(f64::total_cmp); + buckets.dedup(); + buckets +}); pub const THREAD_SKILLS_ENABLED_TOTAL_METRIC: &str = "codex.thread.skills.enabled_total"; pub const THREAD_SKILLS_KEPT_TOTAL_METRIC: &str = "codex.thread.skills.kept_total"; pub const THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC: &str = diff --git a/codex-rs/otel/tests/suite/send.rs b/codex-rs/otel/tests/suite/send.rs index e112ddcc03..9da64701d0 100644 --- a/codex-rs/otel/tests/suite/send.rs +++ b/codex-rs/otel/tests/suite/send.rs @@ -3,6 +3,8 @@ use crate::harness::build_metrics_with_defaults; use crate::harness::find_metric; use crate::harness::histogram_data; use crate::harness::latest_metrics; +use codex_otel::ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS; +use codex_otel::ADDITIONAL_CONTEXT_VALUE_BYTES_METRIC; use codex_otel::Result; use codex_otel::THREAD_SKILLS_COUNT_METRIC_BUCKETS; use codex_otel::THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_BUCKETS; @@ -135,6 +137,37 @@ fn histogram_uses_explicit_bucket_boundaries() -> Result<()> { Ok(()) } +#[test] +fn additional_context_histogram_bounds_truncated_observations() -> Result<()> { + let (metrics, exporter) = build_metrics_with_defaults(&[])?; + let samples = [4_024, 4_027, 4_043]; + for value in samples { + metrics.histogram_with_boundaries( + ADDITIONAL_CONTEXT_VALUE_BYTES_METRIC, + value, + ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS.as_slice(), + &[("stage", "after"), ("truncated", "true")], + )?; + } + metrics.shutdown()?; + + let (bounds, counts, sum, count) = histogram_data( + &latest_metrics(&exporter), + ADDITIONAL_CONTEXT_VALUE_BYTES_METRIC, + ); + assert_eq!((sum, count), (12_094.0, 3)); + assert_eq!(counts.iter().sum::(), count); + assert_eq!(counts.last(), Some(&0)); + for (upper_bound, bucket_count) in bounds.iter().zip(&counts) { + assert!( + *bucket_count == 0 || *upper_bound <= 4_096.0, + "post-truncation observations landed in a bucket ending at {upper_bound} bytes" + ); + } + + Ok(()) +} + #[test] fn context_histograms_preserve_zero_and_family_ranges() -> Result<()> { let (metrics, exporter) = build_metrics_with_defaults(&[])?;