Files
codex/codex-rs/otel/tests/suite/buffered_operations_tests.rs
T
Celia Chen 0ed77c274b Add bounded buffering for global operation metrics (#47619)
## What changed

Add `record_global_operation` to record an operation count and duration in milliseconds, buffering up to 256 pairs until a metrics provider is installed. Validate both metric names and tags before accepting a pair, limit each operation to 16 tags and 1024 bytes of metadata, and drop whole pairs when the buffer is full.

Drain pending operations on installation and serialize recording with provider lifecycle changes so a pair stays with one installation. Resume buffering when the active provider shuts down without disturbing a newer installation. Disabling metrics discards pending operations and suppresses recording until metrics are enabled again.

## Testing

Add unit and integration coverage for buffer limits, validation, concurrent installation and replacement, redirected global handles, shutdown buffering, and accepted or rejected opt-out configuration changes.

GitOrigin-RevId: 3b094e69bf975016ff1fb8f99b3611c2ccddccf3
2026-09-23 18:39:47 +00:00

126 lines
4.1 KiB
Rust

//! Exercises global operation buffering across shutdown and rejected configuration changes.
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_otel::MetricsClient;
use codex_otel::MetricsConfig;
use codex_otel::OtelExporter;
use codex_otel::OtelHttpProtocol;
use codex_otel::OtelProvider;
use codex_otel::OtelSettings;
use codex_otel::install_global_metrics;
use codex_otel::record_global_operation;
use opentelemetry_sdk::metrics::data::AggregatedMetrics;
use opentelemetry_sdk::metrics::data::MetricData;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::time::Duration;
fn install() -> MetricsClient {
install_global_metrics(
MetricsClient::new(
MetricsConfig::in_memory("test", "test", "1", Default::default()).with_runtime_reader(),
)
.expect("build in-memory metrics"),
)
}
fn observe() {
record_global_operation(
"test.operations",
"test.duration",
Duration::from_millis(/*millis*/ 7),
&[],
)
.expect("record an operation");
}
fn totals(metrics: &MetricsClient) -> (u64, u64, f64) {
let snapshot = metrics.snapshot().expect("collect runtime metrics");
let mut totals = (0, 0, 0.0);
for metric in snapshot
.scope_metrics()
.flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics)
{
match metric.data() {
AggregatedMetrics::U64(MetricData::Sum(sum)) => {
totals.0 += sum
.data_points()
.map(opentelemetry_sdk::metrics::data::SumDataPoint::value)
.sum::<u64>();
}
AggregatedMetrics::F64(MetricData::Histogram(histogram)) => {
for point in histogram.data_points() {
totals.1 += point.count();
totals.2 += point.sum();
}
}
_ => panic!("unexpected metric"),
}
}
totals
}
#[test]
fn shutdown_buffers_until_replacement_and_preserves_newer_installations() {
let first = install();
first.shutdown().unwrap();
observe();
let second = install();
assert_eq!(totals(&second), (1, 1, 7.0));
let third = install();
second.shutdown().unwrap();
observe();
assert_eq!(totals(&third), (1, 1, 7.0));
third.shutdown().unwrap();
}
#[test]
fn rejected_opt_out_preserves_recording_and_accepted_opt_out_survives_shutdown() {
let metrics = install();
let mut settings = OtelSettings {
http_client_factory: HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
environment: "test".to_string(),
service_name: "test".to_string(),
service_version: "1".to_string(),
codex_home: ".".into(),
exporter: OtelExporter::None,
trace_exporter: OtelExporter::OtlpHttp {
endpoint: "http://127.0.0.1:1/v1/traces".to_string(),
headers: Default::default(),
protocol: OtelHttpProtocol::Json,
tls: None,
},
metrics_exporter: OtelExporter::None,
runtime_metrics: false,
span_attributes: BTreeMap::from([(String::new(), "invalid".to_string())]),
tracestate: BTreeMap::new(),
};
assert_eq!(
OtelProvider::try_new(&settings).err().unwrap().to_string(),
"configured span attribute key must not be empty"
);
observe();
assert_eq!(totals(&metrics), (1, 1, 7.0));
metrics.shutdown().unwrap();
observe();
assert!(OtelProvider::try_new(&settings).is_err());
let replacement = install();
assert_eq!(totals(&replacement), (1, 1, 7.0));
replacement.shutdown().unwrap();
settings.span_attributes.clear();
for trace_exporter in [settings.trace_exporter.clone(), OtelExporter::None] {
let metrics = install();
settings.trace_exporter = trace_exporter;
let _provider = OtelProvider::try_new(&settings).unwrap();
metrics.shutdown().unwrap();
observe();
let replacement = install();
assert_eq!(totals(&replacement), (0, 0, 0.0));
replacement.shutdown().unwrap();
}
}