Add exec-server RPC timing and process startup tracing (#47680)

## Why

Detached work can keep request spans open beyond response enqueueing. Record explicit phase timings to distinguish request handling from the full span lifetime.

## What changed

- Record server dispatch and response enqueue offsets from request receipt, plus a response enqueue event for HTTP responses that bypass the dispatcher.
- Emit client request enqueue and response receipt events, including success or server-error outcomes and reader-to-caller delay. Carry completion timestamps to the awaiting task so receipt events retain its tracing context.
- Add spans for process startup, sandbox preparation, shell snapshot preparation and capture, and process spawning. Record process identifiers only when nonempty and at most 64 bytes long.

GitOrigin-RevId: 237db898b3284fe8a6a36cb0055b3c7991d8e8a4
This commit is contained in:
Adam Perry @ OpenAI
2026-09-23 22:10:28 +00:00
committed by copyberry
parent 8de2d336b8
commit 6fa3bcaae8
11 changed files with 130 additions and 16 deletions
@@ -52,6 +52,7 @@ use crate::protocol::FsWalkParams;
use crate::protocol::FsWalkResponse;
use crate::protocol::FsWriteFileParams;
use crate::protocol::FsWriteFileResponse;
use crate::protocol::HTTP_REQUEST_METHOD;
use crate::protocol::HttpRequestParams;
use crate::protocol::InitializeParams;
use crate::protocol::InitializeResponse;
@@ -292,6 +293,15 @@ impl ExecServerHandler {
}
return Err(error);
}
// This response bypasses the dispatcher; record it before body-stream setup.
tracing::event!(
name: "codex.exec_server.response_enqueued",
target: "codex_otel.trace_safe",
tracing::Level::INFO,
event.name = "codex.exec_server.response_enqueued",
rpc.method = HTTP_REQUEST_METHOD,
outcome = "success",
);
if let Some(pending_stream) = pending_stream {
self.start_http_body_stream(pending_stream).await;
}
@@ -182,6 +182,10 @@ impl RequestDispatcher {
self.telemetry
.request_queue_completed(method, queued_at.elapsed());
request_span.record("otel.name", method);
request_span.record(
"rpc.dispatch_offset_ns",
i64::try_from(queued_at.elapsed().as_nanos()).unwrap_or(i64::MAX),
);
if self
.outgoing_tx
.send(RpcServerOutboundMessage::Error {
@@ -203,6 +207,10 @@ impl RequestDispatcher {
);
return RequestTaskResult::ConnectionClosed;
}
request_span.record(
"rpc.response_enqueue_offset_ns",
i64::try_from(queued_at.elapsed().as_nanos()).unwrap_or(i64::MAX),
);
request_span.record("result", "error");
self.telemetry.request_completed(
method,
@@ -221,6 +229,10 @@ impl RequestDispatcher {
let mut disconnected_rx = self.disconnected_rx.clone();
let telemetry = self.telemetry.clone();
let task = async move {
request_span.record(
"rpc.dispatch_offset_ns",
i64::try_from(queued_at.elapsed().as_nanos()).unwrap_or(i64::MAX),
);
telemetry.request_queue_completed(
method,
queued_at.elapsed().saturating_sub(route_setup_duration),
@@ -240,10 +252,19 @@ impl RequestDispatcher {
};
let result = request_result(&message);
let response_sent = match message {
Some(message) => tokio::select! {
result = outgoing_tx.send(message) => result.is_ok(),
_ = disconnected_rx.changed() => false,
},
Some(message) => {
let response_sent = tokio::select! {
result = outgoing_tx.send(message) => result.is_ok(),
_ = disconnected_rx.changed() => false,
};
if response_sent {
request_span.record(
"rpc.response_enqueue_offset_ns",
i64::try_from(queued_at.elapsed().as_nanos()).unwrap_or(i64::MAX),
);
}
response_sent
}
None => true,
};
if !response_sent {