[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:
richardopenai
2026-06-10 21:36:38 +00:00
committed by GitHub
parent 22dd6ebc7d
commit 7e5e41daea
3 changed files with 91 additions and 0 deletions
+48
View File
@@ -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,