core: trace executor skill discovery (#30318)

## Why

Make it easier to measure the performance of different parts of skill
loading.

## What

- Add spans for step-context capture, world-state construction, executor
catalog snapshot/root loading, and environment skill loading.
- Record the discovered environment skill count.
- Trace outbound exec-server requests with client kind and RPC method
fields.
- Update trace propagation tests to assert that requests keep the parent
trace id while creating their own child span.
This commit is contained in:
Adam Perry @ OpenAI
2026-07-06 18:44:08 +00:00
committed by GitHub
parent 84fe70c30e
commit a86d525e4d
6 changed files with 46 additions and 2 deletions
@@ -143,6 +143,12 @@ pub struct EnvironmentSkillLoadOutcome {
}
/// Discovers skills without converting environment-owned paths to host paths.
#[tracing::instrument(
name = "skills.environment.load",
level = "info",
skip_all,
fields(skill_count = tracing::field::Empty)
)]
pub async fn load_environment_skills_from_root(
file_system: &dyn ExecutorFileSystem,
root: &PathUri,
@@ -235,6 +241,7 @@ pub async fn load_environment_skills_from_root(
warnings: vec![format!("failed to walk skills root {root}: {error:#}")],
},
};
tracing::Span::current().record("skill_count", discovery.skills.len());
outcome.warnings.extend(discovery.warnings);
if discovery.skills.is_empty() {
return outcome;
+1
View File
@@ -2829,6 +2829,7 @@ impl Session {
/// This may refresh filesystem-derived state. Normal turns should call it only from
/// `run_turn` and pass the result down; standalone request or history boundaries may capture
/// their own step.
#[tracing::instrument(name = "step_context.capture", level = "info", skip_all)]
pub(crate) async fn capture_step_context(
self: &Arc<Self>,
turn_context: Arc<TurnContext>,
+1
View File
@@ -7,6 +7,7 @@ use crate::context::world_state::WorldState;
use codex_extension_api::WorldStateContributionInput;
impl Session {
#[tracing::instrument(name = "world_state.build", level = "info", skip_all)]
pub(crate) async fn build_world_state_for_step(
&self,
step_context: &StepContext,
+10 -1
View File
@@ -1394,7 +1394,16 @@ mod tests {
assert_eq!(session.process_id(), &process_id);
let trace = server.await.expect("server task").expect("trace context");
assert_eq!(trace, expected_trace);
let expected_traceparent = expected_trace
.traceparent
.as_deref()
.expect("parent traceparent");
let traceparent = trace.traceparent.as_deref().expect("request traceparent");
let expected_parts = expected_traceparent.split('-').collect::<Vec<_>>();
let parts = traceparent.split('-').collect::<Vec<_>>();
assert_eq!(parts[1], expected_parts[1]);
assert_ne!(parts[2], expected_parts[2]);
assert_eq!(trace.tracestate, expected_trace.tracestate);
}
async fn accept_websocket(listener: &TcpListener) -> WebSocketStream<TcpStream> {
+20 -1
View File
@@ -342,6 +342,16 @@ impl RpcClient {
drain_pending(&self.pending).await;
}
#[tracing::instrument(
name = "codex.exec_server.request",
level = "info",
skip_all,
fields(
otel.kind = "client",
otel.name = method,
method,
)
)]
pub(crate) async fn call<P, T>(&self, method: &str, params: &P) -> Result<T, RpcCallError>
where
P: Serialize,
@@ -803,6 +813,15 @@ mod tests {
.expect("RPC response");
assert_eq!(response, serde_json::json!({}));
let trace = server.await.expect("server task").expect("trace context");
assert_eq!(trace, expected_trace);
let expected_traceparent = expected_trace
.traceparent
.as_deref()
.expect("parent traceparent");
let traceparent = trace.traceparent.as_deref().expect("request traceparent");
let expected_parts = expected_traceparent.split('-').collect::<Vec<_>>();
let parts = traceparent.split('-').collect::<Vec<_>>();
assert_eq!(parts[1], expected_parts[1]);
assert_ne!(parts[2], expected_parts[2]);
assert_eq!(trace.tracestate, expected_trace.tracestate);
}
}
+7
View File
@@ -66,6 +66,12 @@ impl SkillsThreadState {
/// Environment availability only controls whether the root is projected into the current
/// step; it never invalidates the cache. There is intentionally no filesystem watcher or
/// content-based invalidation because selected environment roots are treated as stable.
#[tracing::instrument(
name = "skills.executor.catalog_snapshot",
level = "info",
skip_all,
fields(root_count = query.executor_roots.len())
)]
pub(crate) async fn executor_catalog_snapshot(
&self,
providers: &SkillProviders,
@@ -157,6 +163,7 @@ impl SkillsThreadState {
next_cache
}
#[tracing::instrument(name = "skills.executor.catalog_root", level = "info", skip_all)]
async fn executor_root_catalog(
&self,
providers: &SkillProviders,