mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Add reusable OTEL gauge instruments (#27057)
## Why Exec-server observability needs current-value measurements in addition to counters. The reusable OTEL client should expose that primitive without coupling it to exec-server runtime behavior. ## What changed - Adds integer gauge instruments, with optional descriptions. - Caches gauges by name and description so instrument metadata remains part of the declaration identity. - Covers gauge values, descriptions, merged attributes, and OTLP HTTP export. This PR only adds the gauge primitive. It does not add second-based duration histograms or exec-server adoption. ## Stack 1. #26091: counter descriptions 2. **#27057: gauge instruments** 3. #27058: second-based duration histograms Related independent coverage: #27059 tests OTLP HTTP log and trace event export. ## Validation - `just test -p codex-otel` - `just fix -p codex-otel` - `just fmt`
This commit is contained in:
committed by
GitHub
Unverified
parent
22dd6ebc7d
commit
7e5e41daea
@@ -12,6 +12,7 @@ use crate::metrics::validation::validate_tags;
|
||||
use codex_utils_string::sanitize_metric_tag_value;
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry::metrics::Counter;
|
||||
use opentelemetry::metrics::Gauge;
|
||||
use opentelemetry::metrics::Histogram;
|
||||
use opentelemetry::metrics::Meter;
|
||||
use opentelemetry::metrics::MeterProvider as _;
|
||||
@@ -89,6 +90,7 @@ struct MetricsClientInner {
|
||||
meter_provider: SdkMeterProvider,
|
||||
meter: Meter,
|
||||
counters: Mutex<HashMap<InstrumentKey, Counter<u64>>>,
|
||||
gauges: Mutex<HashMap<InstrumentKey, Gauge<i64>>>,
|
||||
histograms: Mutex<HashMap<String, Histogram<f64>>>,
|
||||
duration_histograms: Mutex<HashMap<String, Histogram<f64>>>,
|
||||
runtime_reader: Option<Arc<ManualReader>>,
|
||||
@@ -146,6 +148,35 @@ impl MetricsClientInner {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn gauge(
|
||||
&self,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
value: i64,
|
||||
tags: &[(&str, &str)],
|
||||
) -> Result<()> {
|
||||
validate_metric_name(name)?;
|
||||
let attributes = self.attributes(tags)?;
|
||||
|
||||
let mut gauges = self
|
||||
.gauges
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let key = InstrumentKey {
|
||||
name: name.to_string(),
|
||||
description: description.map(str::to_string),
|
||||
};
|
||||
let gauge = gauges.entry(key).or_insert_with(|| {
|
||||
let builder = self.meter.i64_gauge(name.to_string());
|
||||
match description {
|
||||
Some(description) => builder.with_description(description.to_string()).build(),
|
||||
None => builder.build(),
|
||||
}
|
||||
});
|
||||
gauge.record(value, &attributes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn duration_histogram(&self, name: &str, value: i64, tags: &[(&str, &str)]) -> Result<()> {
|
||||
validate_metric_name(name)?;
|
||||
let attributes = self.attributes(tags)?;
|
||||
@@ -253,6 +284,7 @@ impl MetricsClient {
|
||||
meter_provider,
|
||||
meter,
|
||||
counters: Mutex::new(HashMap::new()),
|
||||
gauges: Mutex::new(HashMap::new()),
|
||||
histograms: Mutex::new(HashMap::new()),
|
||||
duration_histograms: Mutex::new(HashMap::new()),
|
||||
runtime_reader,
|
||||
@@ -281,6 +313,22 @@ impl MetricsClient {
|
||||
self.0.histogram(name, value, tags)
|
||||
}
|
||||
|
||||
/// Send a single gauge measurement.
|
||||
pub fn gauge(&self, name: &str, value: i64, tags: &[(&str, &str)]) -> Result<()> {
|
||||
self.0.gauge(name, /*description*/ None, value, tags)
|
||||
}
|
||||
|
||||
/// Send a single gauge measurement with an instrument description.
|
||||
pub fn gauge_with_description(
|
||||
&self,
|
||||
name: &str,
|
||||
description: &str,
|
||||
value: i64,
|
||||
tags: &[(&str, &str)],
|
||||
) -> Result<()> {
|
||||
self.0.gauge(name, Some(description), value, tags)
|
||||
}
|
||||
|
||||
/// Record a duration in milliseconds using a histogram.
|
||||
pub fn record_duration(
|
||||
&self,
|
||||
|
||||
@@ -186,6 +186,12 @@ fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> {
|
||||
))?;
|
||||
|
||||
metrics.counter("codex.turns", /*inc*/ 1, &[("source", "test")])?;
|
||||
metrics.gauge_with_description(
|
||||
"codex.active",
|
||||
"Number of active Codex operations.",
|
||||
/*value*/ 1,
|
||||
&[("component", "test")],
|
||||
)?;
|
||||
metrics.shutdown()?;
|
||||
|
||||
server.join().expect("server join");
|
||||
@@ -220,6 +226,16 @@ fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> {
|
||||
"expected metric name not found; body prefix: {}",
|
||||
&body.chars().take(2000).collect::<String>()
|
||||
);
|
||||
assert!(
|
||||
body.contains("codex.active"),
|
||||
"expected gauge not found; body prefix: {}",
|
||||
&body.chars().take(2000).collect::<String>()
|
||||
);
|
||||
assert!(
|
||||
body.contains("component") && body.contains("test"),
|
||||
"expected gauge tag not found; body prefix: {}",
|
||||
&body.chars().take(2000).collect::<String>()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ fn send_builds_payload_with_tags_and_histograms() -> Result<()> {
|
||||
/*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);
|
||||
@@ -80,6 +86,27 @@ fn send_builds_payload_with_tags_and_histograms() -> Result<()> {
|
||||
]);
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user