Files
codex/codex-rs/ext/skills/tests/skills_extension/source_telemetry_tests.rs
T
bkotsopoulos b82d542aef Add turn, tool, and skill attribution to traces (#56524)
## What changed

- Emit `codex.turn_profile` events with finalized sampling request and retry counts when turns complete or abort, and preserve the caller's tracing span for detached extension interrupts.
- Add conversation and turn identifiers to turn and tool dispatch spans, identify tool sources, and include dispatch-to-result-ready duration on tool completion events.
- Emit `codex.skill_read.source_selected` before cache or provider access, recording the selected cloud or executor source, skill and plugin identity, resource kind, and continuation status.

## Testing

Add regression coverage for trace correlation, tool completion timing, turn profile counters on completion and interruption, detached interrupt span propagation, and skill source attribution including cached cloud reads.

GitOrigin-RevId: db78aa270bbcb8dc9e4558d202c109ad54141811
2026-10-23 16:55:32 +00:00

117 lines
4.1 KiB
Rust

//! Verifies provider attribution follows the selected route, including cached reads.
use super::*;
use pretty_assertions::assert_eq;
use tracing::field::Field;
use tracing::field::Visit;
use tracing::instrument::WithSubscriber;
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
use tracing_subscriber::prelude::*;
#[derive(Clone, Default)]
struct ReadEvents(Arc<Mutex<Vec<Value>>>);
#[derive(Default)]
struct Fields(serde_json::Map<String, Value>);
impl Visit for Fields {
fn record_str(&mut self, field: &Field, value: &str) {
self.0.insert(field.name().into(), json!(value));
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.0.insert(field.name().into(), json!(value));
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
self.record_str(field, &format!("{value:?}"));
}
}
impl<S: tracing::Subscriber> Layer<S> for ReadEvents {
fn on_event(&self, event: &tracing::Event<'_>, _context: Context<'_, S>) {
let mut fields = Fields::default();
event.record(&mut fields);
if fields.0.get("event.name") == Some(&json!("codex.skill_read.source_selected")) {
self.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(Value::Object(fields.0));
}
}
}
#[tokio::test]
async fn source_events_cover_cached_cloud_and_explicit_executor_reads() -> TestResult {
let cloud = cloud_skill("work").with_plugin_id(Some("cloud-plugin".into()));
let installed = installed_skill("matching", "work")?;
let cloud_provider = provider(vec![cloud.clone()]);
let cloud_reads = cloud_provider.read_requests.clone();
let executor = provider(vec![installed]);
let executor_reads = executor.read_requests.clone();
let harness = MergeHarness::new(executor, cloud_provider, default_config()).await?;
harness
.turn_store
.insert(selected_plugins(&[None, Some("env-1:matching")], &[]));
let tools = harness.tools(&harness.step_store).await;
harness.render().await?;
let events = ReadEvents::default();
let subscriber = tracing_subscriber::registry().with(events.clone().with_filter(
tracing_subscriber::filter::filter_fn(codex_otel::OtelProvider::trace_export_filter),
));
// Prevent parallel tests with no subscriber from disabling this callsite globally.
let _interest_cache_guard =
tracing::Dispatch::new(tracing::subscriber::NoSubscriber::default());
async {
for source in ["cloud", "cloud", "executor", "executor"] {
let selector = if source == "executor" {
json!({"kind": source, "environment_id": "env-1"})
} else {
json!({"kind": source})
};
call_tool(
&tools,
"read",
json!({"package": cloud.id.0, "source": selector}),
)
.await?;
}
assert!(
call_tool(&tools, "read", json!({"package": "missing"}))
.await
.is_err()
);
TestResult::Ok(())
}
.with_subscriber(subscriber)
.await?;
let expected = ["cloud", "cloud", "executor", "executor"].map(|source| {
json!({
"message": "Selected skill read source",
"event.name": "codex.skill_read.source_selected",
"conversation.id": "thread",
"turn_id": "turn-1",
"call_id": "skill-test",
"tool_namespace": "skills",
"tool_name": "read",
"skill_source": source,
"skill_name": "deploy:work",
"skill_plugin_id": if source == "cloud" { "cloud-plugin" } else { "matching" },
"skill_resource_kind": "main_prompt",
"is_continuation": false,
})
});
assert_eq!(*events.0.lock().unwrap(), expected);
// Both cloud reads are attributable even though the second needs no provider call.
assert_eq!(
(
read_request_keys(&cloud_reads).len(),
read_request_keys(&executor_reads).len()
),
(1, 2),
);
Ok(())
}