mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Honor the OTLP metrics temporality preference (#51220)
## Why Some metrics backends require cumulative metrics, while configured OTLP exporters previously always used delta temporality. ## What changed Read `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` for configured HTTP and gRPC metrics exporters. Accept `delta`, `cumulative`, and `lowmemory` case-insensitively, retaining delta for unset, empty, or unrecognized values. Document the setting and preserve existing temporality for the built-in Statsig exporter and runtime snapshots. ## Testing Update the HTTP loopback test to set the preference in an isolated subprocess and assert that the exported counter uses cumulative aggregation temporality. GitOrigin-RevId: ca212eea20054b947005d323186c8b9a819ace9d
This commit is contained in:
@@ -180,6 +180,12 @@ Modes:
|
||||
- OTLP: exports metrics via the OpenTelemetry OTLP exporter (HTTP or gRPC).
|
||||
- In-memory: records via `opentelemetry_sdk::metrics::InMemoryMetricExporter` for tests/assertions; call `shutdown()` to flush.
|
||||
|
||||
Configured OTLP metrics exporters (HTTP or gRPC) default to delta temporality. Set
|
||||
`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative` for backends that require
|
||||
cumulative metrics. The values `delta`, `cumulative`, and `lowmemory` are case-insensitive;
|
||||
unset, empty, or unrecognized values retain the delta default. This setting does not change
|
||||
the built-in Statsig exporter or on-demand runtime snapshots.
|
||||
|
||||
`codex-otel` also provides `OtelExporter::Statsig`, a shorthand for exporting OTLP/HTTP JSON metrics
|
||||
to Statsig using Codex-internal defaults.
|
||||
|
||||
|
||||
@@ -363,10 +363,16 @@ impl MetricsClient {
|
||||
build_provider(resource, exporter, export_interval, runtime_reader.clone())
|
||||
}
|
||||
MetricsExporter::Otlp(exporter) => {
|
||||
let temporality = otlp_metrics_temporality(
|
||||
&exporter,
|
||||
std::env::var("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE")
|
||||
.ok()
|
||||
.as_deref(),
|
||||
);
|
||||
let exporter = crate::network_policy::PolicyExporter {
|
||||
exporter: build_otlp_metric_exporter(
|
||||
exporter,
|
||||
Temporality::Delta,
|
||||
temporality,
|
||||
&http_client_factory,
|
||||
)?,
|
||||
policy: http_client_factory.network_policy().clone(),
|
||||
@@ -589,6 +595,19 @@ where
|
||||
(provider, meter)
|
||||
}
|
||||
|
||||
fn otlp_metrics_temporality(exporter: &OtelExporter, preference: Option<&str>) -> Temporality {
|
||||
// Select before resolving Statsig to OTLP so its built-in route stays Delta.
|
||||
match exporter {
|
||||
OtelExporter::None | OtelExporter::Statsig => Temporality::Delta,
|
||||
OtelExporter::OtlpGrpc { .. } | OtelExporter::OtlpHttp { .. } => match preference {
|
||||
Some(value) if value.eq_ignore_ascii_case("cumulative") => Temporality::Cumulative,
|
||||
Some(value) if value.eq_ignore_ascii_case("lowmemory") => Temporality::LowMemory,
|
||||
// Preserve Codex's existing default for unset, empty, or unknown values.
|
||||
_ => Temporality::Delta,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn build_otlp_metric_exporter(
|
||||
exporter: OtelExporter,
|
||||
temporality: Temporality,
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_otel::Result;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read as _;
|
||||
@@ -143,7 +144,34 @@ fn write_http_response(stream: &mut TcpStream, status: &str) -> std::io::Result<
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> {
|
||||
fn otlp_http_exporter_honors_cumulative_temporality_env() -> Result<()> {
|
||||
// Isolate the environment setting from other tests, including under Bazel/libtest.
|
||||
const CHILD_ENV: &str = "CODEX_OTEL_TEMPORALITY_TEST_CHILD";
|
||||
if std::env::var_os(CHILD_ENV).is_none() {
|
||||
let current_thread = thread::current();
|
||||
let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
|
||||
.args([
|
||||
"--exact",
|
||||
current_thread.name().expect("test name"),
|
||||
"--nocapture",
|
||||
])
|
||||
.env(CHILD_ENV, "1")
|
||||
.env(
|
||||
"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE",
|
||||
"cumulative",
|
||||
)
|
||||
.env("OTEL_METRIC_EXPORT_INTERVAL", "60000")
|
||||
.output()
|
||||
.expect("run test with cumulative temporality configured");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
|
||||
let addr = listener.local_addr().expect("local_addr");
|
||||
listener.set_nonblocking(true).expect("set_nonblocking");
|
||||
@@ -252,6 +280,18 @@ fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> {
|
||||
"unexpected content-type: {content_type}"
|
||||
);
|
||||
|
||||
let payload: serde_json::Value = serde_json::from_slice(&request.body).expect("OTLP JSON");
|
||||
let counter = payload["resourceMetrics"][0]["scopeMetrics"][0]["metrics"]
|
||||
.as_array()
|
||||
.expect("exported metrics")
|
||||
.iter()
|
||||
.find(|metric| metric["name"] == "codex.turns")
|
||||
.expect("exported counter");
|
||||
assert_eq!(
|
||||
counter["sum"]["aggregationTemporality"],
|
||||
serde_json::json!(2)
|
||||
);
|
||||
|
||||
let body = String::from_utf8_lossy(&request.body);
|
||||
assert!(
|
||||
body.contains("codex.turns"),
|
||||
|
||||
Reference in New Issue
Block a user