[codex] Cover OTLP HTTP log and trace event export (#27059)

## Why

The generic OTLP HTTP paths for log events and trace events need
end-to-end coverage before exec-server relies on them.

## What changed

- Adds loopback coverage for exporting `codex_otel.log_only` events to
`/v1/logs`.
- Verifies `codex_otel.trace_safe` events are present in the exported
trace payload.

This is a test-only PR. It does not change OTEL runtime behavior or
metric APIs.

## Related work

- #26091: counter descriptions
- #27057: gauge instruments
- #27058: second-based duration histograms

This PR is independent and can land directly on `main`.

## Validation

- `just test -p codex-otel`
- `just fix -p codex-otel`
- `just fmt`
This commit is contained in:
richardopenai
2026-06-15 09:59:26 -07:00
committed by GitHub
Unverified
parent 5e58b6d77f
commit bed60e3746
@@ -240,6 +240,105 @@ fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> {
Ok(())
}
#[test]
fn otlp_http_exporter_sends_logs_to_collector()
-> std::result::Result<(), Box<dyn std::error::Error>> {
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");
let (tx, rx) = mpsc::channel::<Vec<CapturedRequest>>();
let server = thread::spawn(move || {
let mut captured = Vec::new();
let deadline = Instant::now() + Duration::from_secs(3);
while Instant::now() < deadline {
match listener.accept() {
Ok((mut stream, _)) => {
let result = read_http_request(&mut stream);
let _ = write_http_response(&mut stream, "202 Accepted");
if let Ok((path, headers, body)) = result {
captured.push(CapturedRequest {
path,
content_type: headers.get("content-type").cloned(),
body,
});
}
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(_) => break,
}
}
let _ = tx.send(captured);
});
let otel = OtelProvider::from(&OtelSettings {
environment: "test".to_string(),
service_name: "codex-cli".to_string(),
service_version: env!("CARGO_PKG_VERSION").to_string(),
codex_home: PathBuf::from("."),
exporter: OtelExporter::OtlpHttp {
endpoint: format!("http://{addr}/v1/logs"),
headers: HashMap::new(),
protocol: OtelHttpProtocol::Json,
tls: None,
},
trace_exporter: OtelExporter::None,
metrics_exporter: OtelExporter::None,
runtime_metrics: false,
span_attributes: BTreeMap::new(),
tracestate: BTreeMap::new(),
})?
.expect("otel provider");
let logger_layer = otel.logger_layer().expect("logger layer");
let subscriber = tracing_subscriber::registry().with(logger_layer);
tracing::subscriber::with_default(subscriber, || {
tracing::callsite::rebuild_interest_cache();
tracing::event!(
target: "codex_otel.log_only",
tracing::Level::INFO,
event.name = "codex.test.log_exported",
"test OTEL log export"
);
});
otel.shutdown();
server.join().expect("server join");
let captured = rx.recv_timeout(Duration::from_secs(1)).expect("captured");
let request = captured
.iter()
.find(|req| req.path == "/v1/logs")
.unwrap_or_else(|| {
let paths = captured
.iter()
.map(|req| req.path.as_str())
.collect::<Vec<_>>()
.join(", ");
panic!("missing /v1/logs request; got {}: {paths}", captured.len());
});
let content_type = request
.content_type
.as_deref()
.unwrap_or("<missing content-type>");
assert!(
content_type.starts_with("application/json"),
"unexpected content-type: {content_type}"
);
let body = String::from_utf8_lossy(&request.body);
assert!(
body.contains("codex.test.log_exported"),
"expected exported log event not found; body prefix: {}",
&body.chars().take(2000).collect::<String>()
);
Ok(())
}
#[test]
fn otel_provider_rejects_header_unsafe_configured_tracestate() {
let result = OtelProvider::from(&OtelSettings {
@@ -357,6 +456,12 @@ fn otlp_http_exporter_sends_traces_to_collector()
let _guard = span.enter();
let propagated_trace =
current_span_w3c_trace_context().expect("current span should have trace context");
tracing::event!(
target: "codex_otel.trace_safe",
tracing::Level::INFO,
event.name = "codex.test.trace_event",
"test OTEL trace event"
);
tracing::info!("trace loopback event");
propagated_trace
});
@@ -409,6 +514,11 @@ fn otlp_http_exporter_sends_traces_to_collector()
"expected configured span attribute not found; body prefix: {}",
&body.chars().take(2000).collect::<String>()
);
assert!(
body.contains("codex.test.trace_event"),
"expected trace event not found; body prefix: {}",
&body.chars().take(2000).collect::<String>()
);
Ok(())
}