mirror of
https://github.com/openai/codex.git
synced 2026-09-11 20:36:49 +00:00
## Why SQLite log persistence needs visibility into batch size, write latency, failures, and entries dropped before they reach the writer. Exporter diagnostics must not feed back into the SQLite log sink and keep metric exports active indefinitely. ## What changed - Record write count, duration, batch bytes, entry count, largest-entry size, and queue drops with bounded outcome tags. - Add explicit byte histogram boundaries for log batch metrics. - Filter OTLP transport noise and the benign unsolicited HTTP/2 PING warning from SQLite log persistence to prevent export cycles while retaining other HTTP/2 warnings. ## Testing - Cover metric values, tags, queue-drop reasons, and histogram boundaries. - Verify successful and failed OTLP HTTP and gRPC exports become idle instead of generating recurring SQLite log-write metrics. GitOrigin-RevId: a63224ae15945fe84adc977a8b7d55bb47a4de53
264 lines
9.5 KiB
Rust
264 lines
9.5 KiB
Rust
use crate::harness::attributes_to_map;
|
|
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::Result;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::BTreeMap;
|
|
|
|
// Ensures counters/histograms render with default + per-call tags.
|
|
#[test]
|
|
fn send_builds_payload_with_tags_and_histograms() -> Result<()> {
|
|
let (metrics, exporter) =
|
|
build_metrics_with_defaults(&[("service", "codex-cli"), ("env", "prod")])?;
|
|
|
|
metrics.counter_with_description(
|
|
"codex.turns",
|
|
"Total number of Codex turns.",
|
|
/*inc*/ 1,
|
|
&[("model", "gpt-5.1"), ("env", "dev")],
|
|
)?;
|
|
metrics.histogram(
|
|
"codex.tool_latency",
|
|
/*value*/ 25,
|
|
&[("tool", "shell")],
|
|
)?;
|
|
metrics.gauge_with_description(
|
|
"codex.active",
|
|
"Number of active Codex operations.",
|
|
/*value*/ 2,
|
|
&[("component", "test")],
|
|
)?;
|
|
metrics.shutdown()?;
|
|
|
|
let resource_metrics = latest_metrics(&exporter);
|
|
|
|
let counter = find_metric(&resource_metrics, "codex.turns").expect("counter metric missing");
|
|
assert_eq!(counter.description(), "Total number of Codex turns.");
|
|
let counter_attributes = match counter.data() {
|
|
opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
|
|
opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
|
|
let points: Vec<_> = sum.data_points().collect();
|
|
assert_eq!(points.len(), 1);
|
|
assert_eq!(points[0].value(), 1);
|
|
attributes_to_map(points[0].attributes())
|
|
}
|
|
_ => panic!("unexpected counter aggregation"),
|
|
},
|
|
_ => panic!("unexpected counter data type"),
|
|
};
|
|
|
|
let expected_counter_attributes = BTreeMap::from([
|
|
("service".to_string(), "codex-cli".to_string()),
|
|
("env".to_string(), "dev".to_string()),
|
|
("model".to_string(), "gpt-5.1".to_string()),
|
|
]);
|
|
assert_eq!(counter_attributes, expected_counter_attributes);
|
|
|
|
let (bounds, bucket_counts, sum, count) =
|
|
histogram_data(&resource_metrics, "codex.tool_latency");
|
|
assert!(!bounds.is_empty());
|
|
assert_eq!(bucket_counts.iter().sum::<u64>(), 1);
|
|
assert_eq!(sum, 25.0);
|
|
assert_eq!(count, 1);
|
|
|
|
let histogram_attrs = attributes_to_map(
|
|
find_metric(&resource_metrics, "codex.tool_latency")
|
|
.and_then(|metric| match metric.data() {
|
|
opentelemetry_sdk::metrics::data::AggregatedMetrics::F64(
|
|
opentelemetry_sdk::metrics::data::MetricData::Histogram(histogram),
|
|
) => histogram
|
|
.data_points()
|
|
.next()
|
|
.map(opentelemetry_sdk::metrics::data::HistogramDataPoint::attributes),
|
|
_ => None,
|
|
})
|
|
.expect("codex.tool_latency histogram attributes should exist"),
|
|
);
|
|
let expected_histogram_attributes = BTreeMap::from([
|
|
("service".to_string(), "codex-cli".to_string()),
|
|
("env".to_string(), "prod".to_string()),
|
|
("tool".to_string(), "shell".to_string()),
|
|
]);
|
|
assert_eq!(histogram_attrs, expected_histogram_attributes);
|
|
|
|
let gauge = find_metric(&resource_metrics, "codex.active").expect("gauge metric missing");
|
|
assert_eq!(gauge.description(), "Number of active Codex operations.");
|
|
let gauge_point = match gauge.data() {
|
|
opentelemetry_sdk::metrics::data::AggregatedMetrics::I64(data) => match data {
|
|
opentelemetry_sdk::metrics::data::MetricData::Gauge(gauge) => {
|
|
gauge.data_points().next().expect("gauge point")
|
|
}
|
|
_ => panic!("unexpected gauge aggregation"),
|
|
},
|
|
_ => panic!("unexpected gauge metric data type"),
|
|
};
|
|
assert_eq!(gauge_point.value(), 2);
|
|
assert_eq!(
|
|
attributes_to_map(gauge_point.attributes()),
|
|
BTreeMap::from([
|
|
("component".to_string(), "test".to_string()),
|
|
("env".to_string(), "prod".to_string()),
|
|
("service".to_string(), "codex-cli".to_string()),
|
|
])
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn histogram_uses_explicit_bucket_boundaries() -> Result<()> {
|
|
let (metrics, exporter) = build_metrics_with_defaults(&[])?;
|
|
|
|
metrics.histogram_with_boundaries(
|
|
"codex.payload_bytes",
|
|
/*value*/ 1024,
|
|
&[256.0, 1024.0, 4096.0],
|
|
&[],
|
|
)?;
|
|
metrics.shutdown()?;
|
|
|
|
assert_eq!(
|
|
histogram_data(&latest_metrics(&exporter), "codex.payload_bytes"),
|
|
(vec![256.0, 1024.0, 4096.0], vec![0, 1, 0, 0], 1024.0, 1)
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Ensures defaults merge per line and overrides take precedence.
|
|
#[test]
|
|
fn send_merges_default_tags_per_line() -> Result<()> {
|
|
let (metrics, exporter) = build_metrics_with_defaults(&[
|
|
("service", "codex-cli"),
|
|
("env", "prod"),
|
|
("region", "us"),
|
|
])?;
|
|
|
|
metrics.counter(
|
|
"codex.alpha",
|
|
/*inc*/ 1,
|
|
&[("env", "dev"), ("component", "alpha")],
|
|
)?;
|
|
metrics.counter(
|
|
"codex.beta",
|
|
/*inc*/ 2,
|
|
&[("service", "worker"), ("component", "beta")],
|
|
)?;
|
|
metrics.shutdown()?;
|
|
|
|
let resource_metrics = latest_metrics(&exporter);
|
|
let alpha_metric =
|
|
find_metric(&resource_metrics, "codex.alpha").expect("codex.alpha metric missing");
|
|
let alpha_point = match alpha_metric.data() {
|
|
opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
|
|
opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
|
|
let points: Vec<_> = sum.data_points().collect();
|
|
assert_eq!(points.len(), 1);
|
|
points[0]
|
|
}
|
|
_ => panic!("unexpected counter aggregation"),
|
|
},
|
|
_ => panic!("unexpected counter data type"),
|
|
};
|
|
assert_eq!(alpha_point.value(), 1);
|
|
let alpha_attrs = attributes_to_map(alpha_point.attributes());
|
|
let expected_alpha_attrs = BTreeMap::from([
|
|
("component".to_string(), "alpha".to_string()),
|
|
("env".to_string(), "dev".to_string()),
|
|
("region".to_string(), "us".to_string()),
|
|
("service".to_string(), "codex-cli".to_string()),
|
|
]);
|
|
assert_eq!(alpha_attrs, expected_alpha_attrs);
|
|
|
|
let beta_metric =
|
|
find_metric(&resource_metrics, "codex.beta").expect("codex.beta metric missing");
|
|
let beta_point = match beta_metric.data() {
|
|
opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
|
|
opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
|
|
let points: Vec<_> = sum.data_points().collect();
|
|
assert_eq!(points.len(), 1);
|
|
points[0]
|
|
}
|
|
_ => panic!("unexpected counter aggregation"),
|
|
},
|
|
_ => panic!("unexpected counter data type"),
|
|
};
|
|
assert_eq!(beta_point.value(), 2);
|
|
let beta_attrs = attributes_to_map(beta_point.attributes());
|
|
let expected_beta_attrs = BTreeMap::from([
|
|
("component".to_string(), "beta".to_string()),
|
|
("env".to_string(), "prod".to_string()),
|
|
("region".to_string(), "us".to_string()),
|
|
("service".to_string(), "worker".to_string()),
|
|
]);
|
|
assert_eq!(beta_attrs, expected_beta_attrs);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Verifies enqueued metrics are delivered by the background worker.
|
|
#[test]
|
|
fn client_sends_enqueued_metric() -> Result<()> {
|
|
let (metrics, exporter) = build_metrics_with_defaults(&[])?;
|
|
|
|
metrics.counter("codex.turns", /*inc*/ 1, &[("model", "gpt-5.1")])?;
|
|
metrics.shutdown()?;
|
|
|
|
let resource_metrics = latest_metrics(&exporter);
|
|
let counter = find_metric(&resource_metrics, "codex.turns").expect("counter metric missing");
|
|
let points = match counter.data() {
|
|
opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
|
|
opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
|
|
sum.data_points().collect::<Vec<_>>()
|
|
}
|
|
_ => panic!("unexpected counter aggregation"),
|
|
},
|
|
_ => panic!("unexpected counter data type"),
|
|
};
|
|
assert_eq!(points.len(), 1);
|
|
let point = points[0];
|
|
assert_eq!(point.value(), 1);
|
|
let attrs = attributes_to_map(point.attributes());
|
|
assert_eq!(attrs.get("model").map(String::as_str), Some("gpt-5.1"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Ensures shutdown flushes successfully with in-memory exporters.
|
|
#[test]
|
|
fn shutdown_flushes_in_memory_exporter() -> Result<()> {
|
|
let (metrics, exporter) = build_metrics_with_defaults(&[])?;
|
|
|
|
metrics.counter("codex.turns", /*inc*/ 1, &[])?;
|
|
metrics.shutdown()?;
|
|
|
|
let resource_metrics = latest_metrics(&exporter);
|
|
let counter = find_metric(&resource_metrics, "codex.turns").expect("counter metric missing");
|
|
let points = match counter.data() {
|
|
opentelemetry_sdk::metrics::data::AggregatedMetrics::U64(data) => match data {
|
|
opentelemetry_sdk::metrics::data::MetricData::Sum(sum) => {
|
|
sum.data_points().collect::<Vec<_>>()
|
|
}
|
|
_ => panic!("unexpected counter aggregation"),
|
|
},
|
|
_ => panic!("unexpected counter data type"),
|
|
};
|
|
assert_eq!(points.len(), 1);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Ensures shutting down without recording metrics does not export anything.
|
|
#[test]
|
|
fn shutdown_without_metrics_exports_nothing() -> Result<()> {
|
|
let (metrics, exporter) = build_metrics_with_defaults(&[])?;
|
|
|
|
metrics.shutdown()?;
|
|
|
|
let finished = exporter.get_finished_metrics().unwrap();
|
|
assert!(finished.is_empty(), "expected no metrics exported");
|
|
Ok(())
|
|
}
|