[codex] Propagate traces through exec-server HTTP (#30117)

Fixes distributed trace continuity across exec-server JSON-RPC HTTP
egress by adding an executor client span and injecting its W3C context
through a reusable `codex-otel` helper.

This preserves the caller trace across core/tool → executor →
provider/MCP instead of dropping parentage at raw reqwest.

Note that this doesn't include the websocket path, which is needed to
really get the full story but at least we cover the basic http path with
this change.
This commit is contained in:
Tom
2026-06-25 16:22:22 -07:00
committed by GitHub
Unverified
parent 5eebeb8169
commit 8ce931ab76
3 changed files with 48 additions and 2 deletions
@@ -18,6 +18,7 @@ use reqwest::Url;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderName;
use reqwest::header::HeaderValue;
use tracing::Instrument;
use super::HttpResponseBodyStream;
use super::response_body_stream::send_body_delta;
@@ -146,15 +147,26 @@ impl ReqwestHttpRequestRunner {
}
}
let headers = Self::build_headers(params.headers)?;
let request_span = tracing::info_span!(
"codex.exec_server.http_request",
otel.kind = "client",
http.request.method = method.as_str(),
server.address = url.host_str().unwrap_or_default(),
server.port = u64::from(url.port_or_known_default().unwrap_or_default()),
http.response.status_code = tracing::field::Empty,
error.type = tracing::field::Empty,
);
let mut headers = Self::build_headers(params.headers)?;
codex_otel::inject_span_w3c_trace_headers(&request_span, &mut headers);
let mut request = self.client.request(method.clone(), url).headers(headers);
if let Some(body) = params.body {
request = request.body(body.into_inner());
}
let response = match request.send().await {
let response = match request.send().instrument(request_span.clone()).await {
Ok(response) => response,
Err(error) => {
request_span.record("error.type", "request");
let error_message = error.to_string();
log_send_error(&method, error);
return Err(internal_error(format!(
@@ -163,6 +175,7 @@ impl ReqwestHttpRequestRunner {
}
};
let status = response.status().as_u16();
request_span.record("http.response.status_code", u64::from(status));
let headers = Self::response_headers(response.headers());
if params.stream_response {
+1
View File
@@ -29,6 +29,7 @@ pub use crate::provider::OtelProvider;
pub use crate::trace_context::context_from_w3c_trace_context;
pub use crate::trace_context::current_span_trace_id;
pub use crate::trace_context::current_span_w3c_trace_context;
pub use crate::trace_context::inject_span_w3c_trace_headers;
pub use crate::trace_context::set_parent_from_context;
pub use crate::trace_context::set_parent_from_w3c_trace_context;
pub use crate::trace_context::span_w3c_trace_context;
+32
View File
@@ -49,6 +49,38 @@ pub fn span_w3c_trace_context(span: &Span) -> Option<W3cTraceContext> {
})
}
/// Injects the W3C trace context for `span` into HTTP headers.
///
/// Existing `traceparent` and `tracestate` values are replaced so callers can
/// safely reuse a request header map while keeping the supplied span as the
/// source of truth.
pub fn inject_span_w3c_trace_headers(span: &Span, headers: &mut http::HeaderMap) -> bool {
let Some(trace) = span_w3c_trace_context(span) else {
return false;
};
match trace.traceparent {
Some(traceparent) => {
if let Ok(value) = http::HeaderValue::from_str(&traceparent) {
headers.insert("traceparent", value);
}
}
None => {
headers.remove("traceparent");
}
}
match trace.tracestate {
Some(tracestate) => {
if let Ok(value) = http::HeaderValue::from_str(&tracestate) {
headers.insert("tracestate", value);
}
}
None => {
headers.remove("tracestate");
}
}
true
}
pub(crate) fn set_tracestate_entries(
entries: BTreeMap<String, BTreeMap<String, String>>,
) -> Result<(), Box<dyn std::error::Error>> {