From 8665cb39e9ed5c2d18c729892e51e89968bb85ac Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 24 Jun 2026 12:04:33 +0200 Subject: [PATCH 01/11] feat: add support for global context observer --- opentelemetry/Cargo.toml | 1 + opentelemetry/src/context.rs | 93 ++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/opentelemetry/Cargo.toml b/opentelemetry/Cargo.toml index 5efba0cf2a..79bde72fa5 100644 --- a/opentelemetry/Cargo.toml +++ b/opentelemetry/Cargo.toml @@ -40,6 +40,7 @@ testing = ["trace"] logs = [] internal-logs = ["tracing"] experimental_metrics_bound_instruments = ["metrics"] +experimental_context_observer = [] [dev-dependencies] opentelemetry_sdk = { path = "../opentelemetry-sdk"} # for documentation tests diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index a25fef450e..2ca7537636 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -19,6 +19,8 @@ use std::fmt; use std::hash::{BuildHasherDefault, Hasher}; use std::marker::PhantomData; use std::sync::Arc; +#[cfg(feature = "experimental_context_observer")] +use std::sync::OnceLock; #[cfg(feature = "futures")] mod future_ext; @@ -26,6 +28,9 @@ mod future_ext; #[cfg(feature = "futures")] pub use future_ext::{FutureExt, WithContext}; +#[cfg(feature = "experimental_context_observer")] +pub use context_observer::*; + thread_local! { static CURRENT_CONTEXT: RefCell = RefCell::new(ContextStack::default()); } @@ -98,8 +103,26 @@ pub struct Context { pub(crate) span: Option>, entries: Option>, suppress_telemetry: bool, + /// A context's observer view of this [Context]. + /// + /// The main application for observers is to share information from the current context with + /// external readers (e.g. a full host eBPF profiler). In that case, the observer will attach + /// and detach some data on context events using its own mechanism. The shared data has no + /// reason to match our internal representation ([Context]). + /// + /// [observer_state] makes it possible to store the observer's view of the context alongside a + /// [Context] value, such that it can be attached and detached on context events. + #[cfg(feature = "experimental_context_observer")] + observer_cx_view: OnceLock>, } +/// A context's observer view of a [Context]. This is basically an arbitrary data structure. +/// +/// The `Any` supertrait lets an observer recover the concrete type it stored in the +/// `observer_cx_view` slot (via trait upcasting to `&dyn Any` and `downcast_ref`). +#[cfg(feature = "experimental_context_observer")] +pub trait ObserverContextView: Any + Send + Sync {} + type EntryMap = HashMap, BuildHasherDefault>; impl Context { @@ -264,6 +287,8 @@ impl Context { #[cfg(feature = "trace")] span: self.span.clone(), suppress_telemetry: self.suppress_telemetry, + #[cfg(feature = "experimental_context_observer")] + observer_cx_view: OnceLock::new(), } } @@ -363,6 +388,8 @@ impl Context { #[cfg(feature = "trace")] span: self.span.clone(), suppress_telemetry: true, + #[cfg(feature = "experimental_context_observer")] + observer_cx_view: OnceLock::new(), } } @@ -426,12 +453,21 @@ impl Context { Self::map_current(|cx| cx.is_telemetry_suppressed()) } + /// Returns the observer's view of this context, if any. + #[cfg(feature = "experimental_context_observer")] + #[inline] + pub fn observer_cx_view(&self) -> &OnceLock> { + &self.observer_cx_view + } + #[cfg(feature = "trace")] pub(crate) fn current_with_synchronized_span(value: SynchronizedSpan) -> Self { Self::map_current(|cx| Context { span: Some(Arc::new(value)), entries: cx.entries.clone(), suppress_telemetry: cx.suppress_telemetry, + #[cfg(feature = "experimental_context_observer")] + observer_cx_view: OnceLock::new(), }) } @@ -441,6 +477,8 @@ impl Context { span: Some(Arc::new(value)), entries: self.entries.clone(), suppress_telemetry: self.suppress_telemetry, + #[cfg(feature = "experimental_context_observer")] + observer_cx_view: OnceLock::new(), } } } @@ -557,6 +595,11 @@ impl ContextStack { // top of the [`ContextStack`] as the `current_cx`. let next_id = self.stack.len() + 1; if next_id < ContextStack::MAX_POS.into() { + #[cfg(feature = "experimental_context_observer")] + if let Some(observer) = GlobalContextObserver::get() { + observer.on_context_enter(&self.current_cx, &cx); + } + let current_cx = std::mem::replace(&mut self.current_cx, cx); self.stack.push(Some(current_cx)); next_id as u16 @@ -602,6 +645,11 @@ impl ContextStack { // empty context is always at the bottom of the stack if the // [`ContextStack`] is not empty. if let Some(Some(next_cx)) = self.stack.pop() { + #[cfg(feature = "experimental_context_observer")] + if let Some(observer) = GlobalContextObserver::get() { + observer.on_context_exit(&self.current_cx, &next_cx); + } + // Extract and return only the span to avoid cloning the entire Context #[cfg(feature = "trace")] { @@ -654,6 +702,51 @@ impl Default for ContextStack { } } +#[cfg(feature = "experimental_context_observer")] +mod context_observer { + use super::*; + + /// An observer trait for monitoring context transitions. + /// + /// Implementors of this trait can observe when a context is entered or exited, + /// allowing for custom logic to be executed during context switches. + pub trait ContextObserver { + /// Called when a context is entered, allowing observers to react to the transition + /// from one context (`from`) to another (`to`). + fn on_context_enter(&self, from: &Context, to: &Context); + + /// Called when a context is exited, allowing observers to react to the transition + /// from one context (`from`) to another (`to`). + fn on_context_exit(&self, from: &Context, to: &Context); + } + + static GLOBAL_CONTEXT_OBSERVER: OnceLock> = + OnceLock::new(); + + /// A global observer for context transitions. + /// + /// This struct provides static methods to set and get a global context observer. + #[allow(missing_debug_implementations)] + pub struct GlobalContextObserver; + + impl GlobalContextObserver { + /// Sets the global context observer, logging a warning if it was already set. + pub fn set(observer: Arc) { + if GLOBAL_CONTEXT_OBSERVER.set(observer).is_err() { + otel_warn!( + name: "GlobalContextObserver.SetFailed", + message = "Global context observer was already set. Ignoring new observer." + ); + } + } + + /// Returns the global context observer. + pub(super) fn get<'a>() -> Option<&'a Arc> { + GLOBAL_CONTEXT_OBSERVER.get() + } + } +} + #[cfg(test)] mod tests { use super::*; From 62fc736785c5b1c008e3a4c690e0e1ace15b4f0f Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 7 Jul 2026 16:07:25 +0200 Subject: [PATCH 02/11] test: add test for context observer --- opentelemetry/src/context.rs | 130 +++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index 2ca7537636..03b4e4668d 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -1282,3 +1282,133 @@ mod tests { assert!(!Context::is_current_telemetry_suppressed()); } } + +#[cfg(all(test, feature = "experimental_context_observer"))] +mod observer_tests { + use super::*; + use std::cell::RefCell; + use std::sync::Arc; + + #[derive(Debug, PartialEq)] + struct V(u64); + + #[derive(Debug, PartialEq, Clone)] + enum Event { + Enter { from: Option, to: Option }, + Exit { from: Option, to: Option }, + } + + thread_local! { + static EVENTS: RefCell> = const { RefCell::new(Vec::new()) }; + } + + fn value_of(cx: &Context) -> Option { + cx.get::().map(|v| v.0) + } + + // Records every transition into a thread-local buffer. As the observer is a process-global + // singleton, recording per-thread keeps each test's assertions isolated from context + // activity happening on other test threads. + struct RecordingObserver; + + impl ContextObserver for RecordingObserver { + fn on_context_enter(&self, from: &Context, to: &Context) { + EVENTS.with(|e| { + e.borrow_mut().push(Event::Enter { + from: value_of(from), + to: value_of(to), + }) + }); + } + + fn on_context_exit(&self, from: &Context, to: &Context) { + EVENTS.with(|e| { + e.borrow_mut().push(Event::Exit { + from: value_of(from), + to: value_of(to), + }) + }); + } + } + + // Establishes a clean base context on the current thread and clears any previously + // recorded events, so the assertions below only see transitions this test triggers. + fn setup() -> ContextGuard { + // Idempotent across tests: only the first call installs the observer, but every test + // uses the same `RecordingObserver`, so a lost race is harmless. + GlobalContextObserver::set(Arc::new(RecordingObserver)); + let guard = Context::new().attach(); + EVENTS.with(|e| e.borrow_mut().clear()); + guard + } + + #[test] + fn global_observer_records_enter_and_exit() { + let _clean = setup(); + + { + let _g = Context::current().with_value(V(1)).attach(); + // Entering fires immediately: from the clean base (no value) to `V(1)`. + EVENTS.with(|e| { + assert_eq!( + *e.borrow(), + vec![Event::Enter { + from: None, + to: Some(1) + }] + ) + }); + } + + // Dropping the guard restores the base and fires the matching exit event. + EVENTS.with(|e| { + assert_eq!( + *e.borrow(), + vec![ + Event::Enter { + from: None, + to: Some(1) + }, + Event::Exit { + from: Some(1), + to: None + } + ] + ) + }); + } + + #[test] + fn global_observer_records_nested_transitions() { + let _clean = setup(); + + let g1 = Context::current().with_value(V(1)).attach(); + let g2 = Context::current().with_value(V(2)).attach(); + drop(g2); + drop(g1); + + EVENTS.with(|e| { + assert_eq!( + *e.borrow(), + vec![ + Event::Enter { + from: None, + to: Some(1) + }, + Event::Enter { + from: Some(1), + to: Some(2) + }, + Event::Exit { + from: Some(2), + to: Some(1) + }, + Event::Exit { + from: Some(1), + to: None + }, + ] + ) + }); + } +} From 54530166916b765f90e48f540d36c4b805f75121 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 7 Jul 2026 16:18:32 +0200 Subject: [PATCH 03/11] chore: add CHANGELOG entry --- opentelemetry/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/opentelemetry/CHANGELOG.md b/opentelemetry/CHANGELOG.md index 05511dac4d..deeaebafab 100644 --- a/opentelemetry/CHANGELOG.md +++ b/opentelemetry/CHANGELOG.md @@ -2,6 +2,15 @@ ## vNext +- **Added** experimental support for a global context event observer. A + `ContextObserver` can be registered via `GlobalContextObserver::set` to be + notified of context transitions through the `on_context_enter` and + `on_context_exit` callbacks. This feature is primarily intended to publish a + different view of the current context (the `ObserverContextView`) through + alternative channels that let external readers (e.g. an eBPF profiler) track + the current context. See the associated + [OTEP](https://github.com/open-telemetry/opentelemetry-specification/pull/4947). + Gated behind the `experimental_context_observer` feature flag. - `otel_info!`, `otel_warn!`, `otel_debug!`, and `otel_error!` macros now accept quoted-key fields (e.g. `"otel.component.type" = "value"`) for dotted attribute names. - **Added** `BoundGauge` and `BoundUpDownCounter` types (and the From 47182eb82d1b6c8dfd51a207e2348c165d1263c2 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 7 Jul 2026 16:22:44 +0200 Subject: [PATCH 04/11] chore: remove superfluous comment --- opentelemetry/src/context.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index 03b4e4668d..92d884a51d 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -116,10 +116,7 @@ pub struct Context { observer_cx_view: OnceLock>, } -/// A context's observer view of a [Context]. This is basically an arbitrary data structure. -/// -/// The `Any` supertrait lets an observer recover the concrete type it stored in the -/// `observer_cx_view` slot (via trait upcasting to `&dyn Any` and `downcast_ref`). +/// A context's observer view of a [Context]. This is an arbitrary data structure. #[cfg(feature = "experimental_context_observer")] pub trait ObserverContextView: Any + Send + Sync {} From fe6986fb4ad2104d48de5ed4bbcf0cff91bf5e9f Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 7 Jul 2026 17:12:08 +0200 Subject: [PATCH 05/11] chore: do not disable clippy warning --- opentelemetry/src/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index 92d884a51d..b2df235f43 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -723,7 +723,7 @@ mod context_observer { /// A global observer for context transitions. /// /// This struct provides static methods to set and get a global context observer. - #[allow(missing_debug_implementations)] + #[derive(Debug)] pub struct GlobalContextObserver; impl GlobalContextObserver { From 311db632de989637f5828287b6b2cdcfb792b6b1 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Jul 2026 11:30:25 +0200 Subject: [PATCH 06/11] feat: add as_any for pre-1.86 trait upcasting + related test and example --- opentelemetry/src/context.rs | 117 ++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index b2df235f43..8705bbe932 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -8,6 +8,44 @@ //! //! - [`Context`]: An immutable, execution-scoped collection of values. //! +//! # Observer Views +//! +//! When the `experimental_context_observer` feature is enabled, an arbitrary observer-owned view +//! can be attached to a [`Context`] via [`Context::observer_cx_view`]. The view is type-erased to +//! `Arc`, and its concrete type is recovered through +//! [`ObserverContextView::as_any`]: +//! +//! ``` +//! # #[cfg(feature = "experimental_context_observer")] +//! # { +//! use opentelemetry::Context; +//! use opentelemetry::context::ObserverContextView; +//! use std::any::Any; +//! use std::sync::Arc; +//! +//! // An observer's own view of the context, carrying arbitrary data. +//! struct MyView { +//! correlation_id: u64, +//! } +//! +//! impl ObserverContextView for MyView { +//! fn as_any(&self) -> &dyn Any { +//! self +//! } +//! } +//! +//! // Attach the view to a context. +//! let cx = Context::new(); +//! cx.observer_cx_view() +//! .set(Arc::new(MyView { correlation_id: 42 })) +//! .unwrap_or_else(|_| unreachable!("view was just created and is empty")); +//! +//! // Later, recover the concrete type from the stored view. +//! let view = cx.observer_cx_view().get().expect("view was set above"); +//! let my_view = view.as_any().downcast_ref::().expect("view is a `MyView`"); +//! assert_eq!(my_view.correlation_id, 42); +//! # } +//! ``` use crate::otel_warn; #[cfg(feature = "trace")] @@ -117,8 +155,51 @@ pub struct Context { } /// A context's observer view of a [Context]. This is an arbitrary data structure. +/// +/// A view is stored and retrieved as a type-erased `Arc` (see +/// [`Context::observer_cx_view`]). To recover the original concrete type from a stored view, use +/// [`ObserverContextView::as_any`] together with [`Any::downcast_ref`]: +/// +/// ``` +/// # #[cfg(feature = "experimental_context_observer")] +/// # { +/// use opentelemetry::context::ObserverContextView; +/// use std::any::Any; +/// use std::sync::Arc; +/// +/// struct MyView { +/// correlation_id: u64, +/// } +/// +/// impl ObserverContextView for MyView { +/// fn as_any(&self) -> &dyn Any { +/// self +/// } +/// } +/// +/// let view: Arc = Arc::new(MyView { correlation_id: 42 }); +/// let my_view = view.as_any().downcast_ref::().unwrap(); +/// assert_eq!(my_view.correlation_id, 42); +/// # } +/// ``` +/// +/// The [`as_any`](ObserverContextView::as_any) method is required because this crate's MSRV is +/// below the Rust version (1.86) that stabilized trait upcasting; without it, callers on the +/// minimum supported compiler couldn't upcast `&dyn ObserverContextView` to `&dyn Any` to perform +/// the downcast themselves. #[cfg(feature = "experimental_context_observer")] -pub trait ObserverContextView: Any + Send + Sync {} +pub trait ObserverContextView: Any + Send + Sync { + /// Returns this view as a `&dyn Any`, enabling downcasting back to the concrete type. + /// + /// Implementors should simply return `self`: + /// + /// ```ignore + /// fn as_any(&self) -> &dyn std::any::Any { + /// self + /// } + /// ``` + fn as_any(&self) -> &dyn Any; +} type EntryMap = HashMap, BuildHasherDefault>; @@ -1408,4 +1489,38 @@ mod observer_tests { ) }); } + + // An observer view carrying arbitrary data, unrelated to the internal `Context` + // representation. + struct MyView { + correlation_id: u64, + } + + impl ObserverContextView for MyView { + fn as_any(&self) -> &dyn Any { + self + } + } + + #[test] + fn observer_view_roundtrips_through_as_any() { + let cx = Context::new(); + + // Attaching the view stores it type-erased as `Arc`. + cx.observer_cx_view() + .set(Arc::new(MyView { correlation_id: 7 })) + .unwrap_or_else(|_| unreachable!("view was just created and is empty")); + + // `as_any` lets us recover the concrete type on our low MSRV, where trait upcasting to + // `dyn Any` is not available. + let view = cx.observer_cx_view().get().expect("view was set above"); + let my_view = view + .as_any() + .downcast_ref::() + .expect("view is a `MyView`"); + assert_eq!(my_view.correlation_id, 7); + + // Downcasting to an unrelated type fails gracefully rather than misbehaving. + assert!(view.as_any().downcast_ref::().is_none()); + } } From 0fbba10ad95650f0cbd6aeb10eedc0175675ec69 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Jul 2026 16:15:00 +0200 Subject: [PATCH 07/11] test: add observer+view test, improve the existing observer test shared code --- opentelemetry/src/context.rs | 128 +++++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 13 deletions(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index 8705bbe932..f99780bf73 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -786,8 +786,9 @@ mod context_observer { /// An observer trait for monitoring context transitions. /// - /// Implementors of this trait can observe when a context is entered or exited, - /// allowing for custom logic to be executed during context switches. + /// Implementors of this trait can observe when a context is entered or exited, allowing for + /// custom logic to be executed during context switches. Implementors can cache context-specific + /// computed state in [Context::observer_cx_view]. pub trait ContextObserver { /// Called when a context is entered, allowing observers to react to the transition /// from one context (`from`) to another (`to`). @@ -1364,7 +1365,7 @@ mod tests { #[cfg(all(test, feature = "experimental_context_observer"))] mod observer_tests { use super::*; - use std::cell::RefCell; + use std::cell::{Cell, RefCell}; use std::sync::Arc; #[derive(Debug, PartialEq)] @@ -1384,22 +1385,37 @@ mod observer_tests { cx.get::().map(|v| v.0) } - // Records every transition into a thread-local buffer. As the observer is a process-global - // singleton, recording per-thread keeps each test's assertions isolated from context - // activity happening on other test threads. + // When enabled, on each transition it records the event into a thread-local buffer, and on + // enter it also installs an observer view on the entered context and bumps the thread-local + // `VIEWS_CREATED` counter. See `observer_installs_and_reuses_view`. + // + // The observer is a process-global singleton, so its callbacks fire for context activity on + // *every* thread across the whole test binary once installed. To keep that cheap and isolated, + // all of its work is gated behind the per-thread `ENABLE_TEST_OBSERVER` switch: threads that + // didn't opt in (via `setup()`) get an inert observer. struct RecordingObserver; impl ContextObserver for RecordingObserver { fn on_context_enter(&self, from: &Context, to: &Context) { + if !ENABLE_TEST_OBSERVER.with(Cell::get) { + return; + } + EVENTS.with(|e| { e.borrow_mut().push(Event::Enter { from: value_of(from), to: value_of(to), }) }); + + install_or_reuse_view(to); } fn on_context_exit(&self, from: &Context, to: &Context) { + if !ENABLE_TEST_OBSERVER.with(Cell::get) { + return; + } + EVENTS.with(|e| { e.borrow_mut().push(Event::Exit { from: value_of(from), @@ -1409,20 +1425,70 @@ mod observer_tests { } } - // Establishes a clean base context on the current thread and clears any previously - // recorded events, so the assertions below only see transitions this test triggers. - fn setup() -> ContextGuard { + thread_local! { + // Master switch for the observer per thread. Enabled for the duration of a test that cares, + // and left false everywhere else, so we don't slow down tests unrelated to the observer. + // See `setup`. + static ENABLE_TEST_OBSERVER: Cell = const { Cell::new(false) }; + // Counts how many distinct views the observer created on the current thread. + static VIEWS_CREATED: Cell = const { Cell::new(0) }; + } + + // On context enter, install the observer's view if this context doesn't have one yet, or reuse + // the existing one otherwise. The view is the context's `V` value (or 0 for a value-less + // context). + fn install_or_reuse_view(cx: &Context) { + if cx.observer_cx_view().get().is_some() { + // Already initialized (e.g. a clone of an already-observed context): reuse it. + return; + } + + let correlation_id = value_of(cx).unwrap_or(0); + + cx.observer_cx_view() + .set(Arc::new(MyView { correlation_id })) + .unwrap_or_else(|_| unreachable!("view was empty, just checked above")); + VIEWS_CREATED.with(|c| c.set(c.get() + 1)); + } + + // Recovers the correlation id stored in a context's observer view, if any. + fn view_id(cx: &Context) -> Option { + cx.observer_cx_view() + .get() + .and_then(|v| v.as_any().downcast_ref::()) + .map(|v| v.correlation_id) + } + + // Holds the base context guard and disables the observer on this thread when dropped. The + // struct's own `Drop` runs before its fields, so the flag is cleared before the base context is + // popped, which is thus not observed (symmetrically to `setup`, which installs the base context + // before registering/enabling the observer). + struct ObserverTestGuard { + _cx: ContextGuard, + } + + impl Drop for ObserverTestGuard { + fn drop(&mut self) { + ENABLE_TEST_OBSERVER.with(|e| e.set(false)); + } + } + + // Enables the observer on this thread, establishes a clean base context, and clears any + // previously recorded events, so the assertions below only see transitions this test + // triggers. Dropping the returned guard disables the observer again. + fn setup_observer() -> ObserverTestGuard { // Idempotent across tests: only the first call installs the observer, but every test // uses the same `RecordingObserver`, so a lost race is harmless. GlobalContextObserver::set(Arc::new(RecordingObserver)); + ENABLE_TEST_OBSERVER.with(|e| e.set(true)); let guard = Context::new().attach(); EVENTS.with(|e| e.borrow_mut().clear()); - guard + ObserverTestGuard { _cx: guard } } #[test] fn global_observer_records_enter_and_exit() { - let _clean = setup(); + let _clean = setup_observer(); { let _g = Context::current().with_value(V(1)).attach(); @@ -1438,7 +1504,7 @@ mod observer_tests { }); } - // Dropping the guard restores the base and fires the matching exit event. + // Dropping the (context) guard restores the base and fires the matching exit event. EVENTS.with(|e| { assert_eq!( *e.borrow(), @@ -1458,7 +1524,7 @@ mod observer_tests { #[test] fn global_observer_records_nested_transitions() { - let _clean = setup(); + let _clean = setup_observer(); let g1 = Context::current().with_value(V(1)).attach(); let g2 = Context::current().with_value(V(2)).attach(); @@ -1523,4 +1589,40 @@ mod observer_tests { // Downcasting to an unrelated type fails gracefully rather than misbehaving. assert!(view.as_any().downcast_ref::().is_none()); } + + // A realistic use of the observer view: the global observer installs its own view on each + // context the first time that context is entered, and reuses it on re-entry. The + // thread-local counter checks that exactly one view is created per distinct context. + #[test] + fn observer_installs_and_reuses_view() { + let _clean = setup_observer(); + + // Reset the counter for the work below. + VIEWS_CREATED.with(|c| c.set(0)); + + // Entering two distinct contexts installs a fresh view for each, derived from its value. + let g1 = Context::current().with_value(V(1)).attach(); + assert_eq!(view_id(&Context::current()), Some(1)); + + let g2 = Context::current().with_value(V(2)).attach(); + assert_eq!(view_id(&Context::current()), Some(2)); + + assert_eq!(VIEWS_CREATED.with(Cell::get), 2); + + // Re-entering an already-observed context must reuse its view, not create a new one. + // `Context::current()` clones the current context, carrying along its already-set view. + let observed = Context::current(); + let g3 = observed.attach(); + + assert_eq!(view_id(&Context::current()), Some(2)); + assert_eq!( + VIEWS_CREATED.with(Cell::get), + 2, + "re-entering an observed context must not create a new view" + ); + + drop(g3); + drop(g2); + drop(g1); + } } From 08e24022c5044fe117e2e4013d4f9491f895f33e Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Jul 2026 16:51:15 +0200 Subject: [PATCH 08/11] doc: reduce doc duplication, move things to observer's specific module --- opentelemetry/src/context.rs | 209 ++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 99 deletions(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index f99780bf73..f7c0102d6b 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -8,44 +8,6 @@ //! //! - [`Context`]: An immutable, execution-scoped collection of values. //! -//! # Observer Views -//! -//! When the `experimental_context_observer` feature is enabled, an arbitrary observer-owned view -//! can be attached to a [`Context`] via [`Context::observer_cx_view`]. The view is type-erased to -//! `Arc`, and its concrete type is recovered through -//! [`ObserverContextView::as_any`]: -//! -//! ``` -//! # #[cfg(feature = "experimental_context_observer")] -//! # { -//! use opentelemetry::Context; -//! use opentelemetry::context::ObserverContextView; -//! use std::any::Any; -//! use std::sync::Arc; -//! -//! // An observer's own view of the context, carrying arbitrary data. -//! struct MyView { -//! correlation_id: u64, -//! } -//! -//! impl ObserverContextView for MyView { -//! fn as_any(&self) -> &dyn Any { -//! self -//! } -//! } -//! -//! // Attach the view to a context. -//! let cx = Context::new(); -//! cx.observer_cx_view() -//! .set(Arc::new(MyView { correlation_id: 42 })) -//! .unwrap_or_else(|_| unreachable!("view was just created and is empty")); -//! -//! // Later, recover the concrete type from the stored view. -//! let view = cx.observer_cx_view().get().expect("view was set above"); -//! let my_view = view.as_any().downcast_ref::().expect("view is a `MyView`"); -//! assert_eq!(my_view.correlation_id, 42); -//! # } -//! ``` use crate::otel_warn; #[cfg(feature = "trace")] @@ -151,54 +113,7 @@ pub struct Context { /// [observer_state] makes it possible to store the observer's view of the context alongside a /// [Context] value, such that it can be attached and detached on context events. #[cfg(feature = "experimental_context_observer")] - observer_cx_view: OnceLock>, -} - -/// A context's observer view of a [Context]. This is an arbitrary data structure. -/// -/// A view is stored and retrieved as a type-erased `Arc` (see -/// [`Context::observer_cx_view`]). To recover the original concrete type from a stored view, use -/// [`ObserverContextView::as_any`] together with [`Any::downcast_ref`]: -/// -/// ``` -/// # #[cfg(feature = "experimental_context_observer")] -/// # { -/// use opentelemetry::context::ObserverContextView; -/// use std::any::Any; -/// use std::sync::Arc; -/// -/// struct MyView { -/// correlation_id: u64, -/// } -/// -/// impl ObserverContextView for MyView { -/// fn as_any(&self) -> &dyn Any { -/// self -/// } -/// } -/// -/// let view: Arc = Arc::new(MyView { correlation_id: 42 }); -/// let my_view = view.as_any().downcast_ref::().unwrap(); -/// assert_eq!(my_view.correlation_id, 42); -/// # } -/// ``` -/// -/// The [`as_any`](ObserverContextView::as_any) method is required because this crate's MSRV is -/// below the Rust version (1.86) that stabilized trait upcasting; without it, callers on the -/// minimum supported compiler couldn't upcast `&dyn ObserverContextView` to `&dyn Any` to perform -/// the downcast themselves. -#[cfg(feature = "experimental_context_observer")] -pub trait ObserverContextView: Any + Send + Sync { - /// Returns this view as a `&dyn Any`, enabling downcasting back to the concrete type. - /// - /// Implementors should simply return `self`: - /// - /// ```ignore - /// fn as_any(&self) -> &dyn std::any::Any { - /// self - /// } - /// ``` - fn as_any(&self) -> &dyn Any; + observer_view: OnceLock>, } type EntryMap = HashMap, BuildHasherDefault>; @@ -366,7 +281,7 @@ impl Context { span: self.span.clone(), suppress_telemetry: self.suppress_telemetry, #[cfg(feature = "experimental_context_observer")] - observer_cx_view: OnceLock::new(), + observer_view: OnceLock::new(), } } @@ -467,7 +382,7 @@ impl Context { span: self.span.clone(), suppress_telemetry: true, #[cfg(feature = "experimental_context_observer")] - observer_cx_view: OnceLock::new(), + observer_view: OnceLock::new(), } } @@ -534,8 +449,8 @@ impl Context { /// Returns the observer's view of this context, if any. #[cfg(feature = "experimental_context_observer")] #[inline] - pub fn observer_cx_view(&self) -> &OnceLock> { - &self.observer_cx_view + pub fn observer_view(&self) -> &OnceLock> { + &self.observer_view } #[cfg(feature = "trace")] @@ -545,7 +460,7 @@ impl Context { entries: cx.entries.clone(), suppress_telemetry: cx.suppress_telemetry, #[cfg(feature = "experimental_context_observer")] - observer_cx_view: OnceLock::new(), + observer_view: OnceLock::new(), }) } @@ -556,7 +471,7 @@ impl Context { entries: self.entries.clone(), suppress_telemetry: self.suppress_telemetry, #[cfg(feature = "experimental_context_observer")] - observer_cx_view: OnceLock::new(), + observer_view: OnceLock::new(), } } } @@ -787,8 +702,60 @@ mod context_observer { /// An observer trait for monitoring context transitions. /// /// Implementors of this trait can observe when a context is entered or exited, allowing for - /// custom logic to be executed during context switches. Implementors can cache context-specific - /// computed state in [Context::observer_cx_view]. + /// custom logic to be executed during context switches. + /// + /// An observer-specific view can be attached to a [`Context`] via + /// [`Context::observer_view`]. A view is usually a different representation of the context + /// to be published through an alternative channel, but in essence it is arbitrary data computed + /// from the context. + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "experimental_context_observer")] + /// # { + /// use opentelemetry::Context; + /// use opentelemetry::context::{ContextObserver, ObserverContextView}; + /// use std::any::Any; + /// use std::sync::Arc; + /// + /// // An observer's own view of the context, carrying arbitrary data. + /// struct MyView { + /// correlation_id: u64, + /// } + /// + /// impl ObserverContextView for MyView { + /// fn as_any(&self) -> &dyn Any { + /// self + /// } + /// } + /// + /// struct Observer; + /// + /// # fn do_something(view: &MyView) { } + /// + /// impl ContextObserver for Observer { + /// fn on_context_enter(&self, from: &Context, to: &Context) { + /// let view = to.observer_view() + /// .get_or_init(|| Arc::new(MyView { correlation_id: 42 })); + /// do_something(view.as_any().downcast_ref::().unwrap()); + /// } + /// + /// fn on_context_exit(&self, from: &Context, to: &Context) { + /// let view = to + /// .observer_view() + /// .get() + /// .unwrap() + /// .as_any() + /// .downcast_ref::() + /// .unwrap(); + /// assert_eq!(view.correlation_id, 42); + /// } + /// } + /// + + /// # } + /// ``` pub trait ContextObserver { /// Called when a context is entered, allowing observers to react to the transition /// from one context (`from`) to another (`to`). @@ -824,6 +791,50 @@ mod context_observer { GLOBAL_CONTEXT_OBSERVER.get() } } + + /// A context's observer view of a [Context]. This is an arbitrary data structure. See + /// [ContextObserver]'s documentation for an example usage. + /// + /// ``` + /// # #[cfg(feature = "experimental_context_observer")] + /// # { + /// use opentelemetry::context::ObserverContextView; + /// use std::any::Any; + /// use std::sync::Arc; + /// + /// struct MyView { + /// correlation_id: u64, + /// } + /// + /// impl ObserverContextView for MyView { + /// fn as_any(&self) -> &dyn Any { + /// self + /// } + /// } + /// + /// let view: Arc = Arc::new(MyView { correlation_id: 42 }); + /// let my_view = view.as_any().downcast_ref::().unwrap(); + /// assert_eq!(my_view.correlation_id, 42); + /// # } + /// ``` + /// + /// The [`as_any`](ObserverContextView::as_any) method is required because this crate's MSRV is + /// below the Rust version (1.86) that stabilized trait upcasting; without it, callers on the + /// minimum supported compiler couldn't upcast `&dyn ObserverContextView` to `&dyn Any` to perform + /// the downcast themselves. + #[cfg(feature = "experimental_context_observer")] + pub trait ObserverContextView: Any + Send + Sync { + /// Returns this view as a `&dyn Any`, enabling downcasting back to the concrete type. + /// + /// Implementors should simply return `self`: + /// + /// ```ignore + /// fn as_any(&self) -> &dyn std::any::Any { + /// self + /// } + /// ``` + fn as_any(&self) -> &dyn Any; + } } #[cfg(test)] @@ -1438,14 +1449,14 @@ mod observer_tests { // the existing one otherwise. The view is the context's `V` value (or 0 for a value-less // context). fn install_or_reuse_view(cx: &Context) { - if cx.observer_cx_view().get().is_some() { + if cx.observer_view().get().is_some() { // Already initialized (e.g. a clone of an already-observed context): reuse it. return; } let correlation_id = value_of(cx).unwrap_or(0); - cx.observer_cx_view() + cx.observer_view() .set(Arc::new(MyView { correlation_id })) .unwrap_or_else(|_| unreachable!("view was empty, just checked above")); VIEWS_CREATED.with(|c| c.set(c.get() + 1)); @@ -1453,7 +1464,7 @@ mod observer_tests { // Recovers the correlation id stored in a context's observer view, if any. fn view_id(cx: &Context) -> Option { - cx.observer_cx_view() + cx.observer_view() .get() .and_then(|v| v.as_any().downcast_ref::()) .map(|v| v.correlation_id) @@ -1573,13 +1584,13 @@ mod observer_tests { let cx = Context::new(); // Attaching the view stores it type-erased as `Arc`. - cx.observer_cx_view() + cx.observer_view() .set(Arc::new(MyView { correlation_id: 7 })) .unwrap_or_else(|_| unreachable!("view was just created and is empty")); // `as_any` lets us recover the concrete type on our low MSRV, where trait upcasting to // `dyn Any` is not available. - let view = cx.observer_cx_view().get().expect("view was set above"); + let view = cx.observer_view().get().expect("view was set above"); let my_view = view .as_any() .downcast_ref::() From 85b3821201f3f67815c16243e3ba4eef33d3ef2b Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Jul 2026 17:05:38 +0200 Subject: [PATCH 09/11] doc: add warning about refcell already borrowed panic --- opentelemetry/src/context.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index f7c0102d6b..2a47c7e290 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -709,6 +709,23 @@ mod context_observer { /// to be published through an alternative channel, but in essence it is arbitrary data computed /// from the context. /// + /// # Refcell already borrowed panic + /// + /// [crate::context] maintains thread-local, internal state in a `RefCell`. This is + /// implementation detail leaks when the `experimental_context_observer` feature is enabled: + /// [Self::on_context_enter] and [Self::on_context_exit] are called from a point where the cell + /// is currently borrowed. If a [ContextObserver] implementation calls back into a + /// cell-borrowing function from the Context API, typically `Context::current()`, this will + /// result in a `Refcell already borrowed` panic. + /// + /// As general hygiene, with respect to the [Context] API, it is strongly advised to only use + /// simple getters and setters on the `from` and `to` context objects provided to the observer. + /// Do not call `Context::current()`, nor try to manually attach or detach context objects from + /// an observer. + /// + /// If you get Refcell-related panics within the [crate::context] module, this is the most + /// likely cause. + /// /// # Example /// /// ``` @@ -752,8 +769,6 @@ mod context_observer { /// assert_eq!(view.correlation_id, 42); /// } /// } - /// - /// # } /// ``` pub trait ContextObserver { From 2e3dcb797a46fec2366a4fe6a6e0174df9171d60 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Jul 2026 17:12:19 +0200 Subject: [PATCH 10/11] style: format --- opentelemetry/src/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index 2a47c7e290..dce8bdfae1 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -717,7 +717,7 @@ mod context_observer { /// is currently borrowed. If a [ContextObserver] implementation calls back into a /// cell-borrowing function from the Context API, typically `Context::current()`, this will /// result in a `Refcell already borrowed` panic. - /// + /// /// As general hygiene, with respect to the [Context] API, it is strongly advised to only use /// simple getters and setters on the `from` and `to` context objects provided to the observer. /// Do not call `Context::current()`, nor try to manually attach or detach context objects from From 03950067c64a106d6006054ab5d42eac9a96aada Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Jul 2026 17:41:43 +0200 Subject: [PATCH 11/11] doc: remove duplicate example, reflow as_any comments --- opentelemetry/src/context.rs | 40 ++++++++---------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index dce8bdfae1..d2a0adf2a2 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -711,8 +711,8 @@ mod context_observer { /// /// # Refcell already borrowed panic /// - /// [crate::context] maintains thread-local, internal state in a `RefCell`. This is - /// implementation detail leaks when the `experimental_context_observer` feature is enabled: + /// [crate::context] maintains thread-local, internal state in a `RefCell`. This implementation + /// detail leaks when the `experimental_context_observer` feature is enabled: /// [Self::on_context_enter] and [Self::on_context_exit] are called from a point where the cell /// is currently borrowed. If a [ContextObserver] implementation calls back into a /// cell-borrowing function from the Context API, typically `Context::current()`, this will @@ -808,39 +808,17 @@ mod context_observer { } /// A context's observer view of a [Context]. This is an arbitrary data structure. See - /// [ContextObserver]'s documentation for an example usage. - /// - /// ``` - /// # #[cfg(feature = "experimental_context_observer")] - /// # { - /// use opentelemetry::context::ObserverContextView; - /// use std::any::Any; - /// use std::sync::Arc; - /// - /// struct MyView { - /// correlation_id: u64, - /// } - /// - /// impl ObserverContextView for MyView { - /// fn as_any(&self) -> &dyn Any { - /// self - /// } - /// } - /// - /// let view: Arc = Arc::new(MyView { correlation_id: 42 }); - /// let my_view = view.as_any().downcast_ref::().unwrap(); - /// assert_eq!(my_view.correlation_id, 42); - /// # } - /// ``` - /// - /// The [`as_any`](ObserverContextView::as_any) method is required because this crate's MSRV is - /// below the Rust version (1.86) that stabilized trait upcasting; without it, callers on the - /// minimum supported compiler couldn't upcast `&dyn ObserverContextView` to `&dyn Any` to perform - /// the downcast themselves. + /// [ContextObserver]'s documentation for examples. #[cfg(feature = "experimental_context_observer")] pub trait ObserverContextView: Any + Send + Sync { /// Returns this view as a `&dyn Any`, enabling downcasting back to the concrete type. /// + /// This method is required because this crate's MSRV is below the Rust version (1.86) that + /// stabilized trait upcasting. Without it, callers below 1.86 couldn't upcast `&dyn + /// ObserverContextView` to `&dyn Any` to perform the downcast themselves. + /// + /// This is a work-around for the absence of trait upcasting before Rust 1.86. + /// /// Implementors should simply return `self`: /// /// ```ignore