From dd3bc75145a685176b44911084e571549042cc12 Mon Sep 17 00:00:00 2001 From: Mar Witek Date: Thu, 3 Sep 2026 16:47:13 +0200 Subject: [PATCH] feat(telemetry): attach user spans to contexts --- AGENTS.md | 2 ++ foundations/src/telemetry/tracing/mod.rs | 28 +++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd65a577..ca576ae0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,8 @@ - `UserSpan::deferred()` creates a stable request root that can be captured by telemetry contexts before `activate()` makes the sampling decision. +- `TelemetryContext::with_user_span()` attaches an explicit `UserSpan` without changing ambient + scope; the returned context and handle share the same underlying span. - A deferred root is an inactive span in a shared `RwLock`. Activation replaces it under the write lock; the first active span wins, while inactive results remain retryable. - Recording and children created before activation are inactive. Contexts that captured the root diff --git a/foundations/src/telemetry/tracing/mod.rs b/foundations/src/telemetry/tracing/mod.rs index 3456d5b6..e66281aa 100644 --- a/foundations/src/telemetry/tracing/mod.rs +++ b/foundations/src/telemetry/tracing/mod.rs @@ -461,6 +461,21 @@ impl UserSpan { pub fn finish(self) {} } +#[cfg(feature = "user-tracing")] +impl TelemetryContext { + /// Returns a copy of this context with `span` as its current user-tracing span. + /// + /// The returned context and the handle share the same underlying span, so activating a + /// deferred handle updates work already carrying the context. Existing logging and internal + /// tracing state is preserved. Use [`TelemetryContext::scope`] or [`TelemetryContext::apply`] + /// to make the returned context active. + pub fn with_user_span(&self, span: &UserSpan) -> Self { + let mut ctx = self.clone(); + ctx.user_span = Some(span.span.clone()); + ctx + } +} + /// A span recorded in both the internal and user traces, produced by [`dual_span`]. /// /// Scope ends when the handle is dropped. [`into_context`](Self::into_context) carries both the @@ -1831,9 +1846,12 @@ mod user_tracing_tests { #[test] fn deferred_handle_activates_context_captured_before_activation() { let ctx = TelemetryContext::test(); - let _scope = ctx.scope(); + let unrelated_ctx = TelemetryContext::test(); let root = UserSpan::deferred(); - let request_ctx = root.enter().into_context(); + let request_ctx = { + let _scope = unrelated_ctx.scope(); + ctx.with_user_span(&root) + }; let tag_factory_called = Cell::new(false); root.set_tags(|| { @@ -1844,11 +1862,10 @@ mod user_tracing_tests { assert!(!root.is_sampled()); assert!(root.w3c_traceparent().is_none()); - root.activate("request", routing(), None); - assert!(root.is_sampled()); - { let _request = request_ctx.scope(); + root.activate("request", routing(), None); + assert!(root.is_sampled()); user_tracing::add_span_tags!("after" => true); let _child = user_tracing::span("child"); } @@ -1869,6 +1886,7 @@ mod user_tracing_tests { .contains(&("after".to_string(), TagValue::Boolean(true))) ); assert!(!traces[0].0.tags.iter().any(|(name, _)| name == "before")); + assert!(unrelated_ctx.user_traces(Default::default()).is_empty()); } #[test]