Skip to main content

provide_telemetry/otel/
adopt.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6//! Adopting a provider a host application installed on the OTel globals.
7//!
8//! The Python and TypeScript facades detect this for themselves: they resolve
9//! their tracer off the global and can ask whether what they got is real. Go
10//! duck-types the global provider's `ForceFlush`/`Shutdown` pair to the same
11//! end. Rust cannot — `opentelemetry::global::tracer_provider()` returns an
12//! opaque `GlobalTracerProvider` whose inner provider is private, with no
13//! downcast and no `is_noop`, so there is no way to tell a live SDK provider
14//! from the crate's own no-op.
15//!
16//! So the host asserts it instead. After calling
17//! [`adopt_global_providers`], `trace()` routes through `global::tracer(..)` —
18//! which already resolves the host's provider — rather than falling back to the
19//! no-op span path, and the host's sampler becomes the sampling authority.
20//!
21//! Adoption never implies ownership: [`shutdown_telemetry`](crate::shutdown_telemetry)
22//! releases the assertion without touching the host's providers, and
23//! [`flush_telemetry`](crate::flush_telemetry) does not drain them.
24
25use std::sync::atomic::{AtomicBool, Ordering};
26
27static TRACES_ADOPTED: AtomicBool = AtomicBool::new(false);
28static METRICS_ADOPTED: AtomicBool = AtomicBool::new(false);
29
30/// Which globals the host is asserting are backed by a live provider.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub struct AdoptedProviders {
33    /// A live `TracerProvider` is installed on the OTel global.
34    pub traces: bool,
35    /// A live `MeterProvider` is installed on the OTel global.
36    pub metrics: bool,
37}
38
39impl AdoptedProviders {
40    /// Assert both signals — the common case for a host running a full SDK.
41    #[must_use]
42    pub fn all() -> Self {
43        Self {
44            traces: true,
45            metrics: true,
46        }
47    }
48}
49
50/// Tell the facade that the host has installed live providers on the OTel
51/// globals, so emission routes through them instead of the no-op path.
52///
53/// Call it after the host's own SDK setup and after `setup_telemetry(None)`, which
54/// does not clear the assertion. Passing a field as `false` releases that
55/// signal's assertion.
56pub fn adopt_global_providers(adopted: AdoptedProviders) {
57    TRACES_ADOPTED.store(adopted.traces, Ordering::Release);
58    METRICS_ADOPTED.store(adopted.metrics, Ordering::Release);
59}
60
61/// Return which globals are currently adopted.
62#[must_use]
63pub fn adopted_global_providers() -> AdoptedProviders {
64    AdoptedProviders {
65        traces: TRACES_ADOPTED.load(Ordering::Acquire),
66        metrics: METRICS_ADOPTED.load(Ordering::Acquire),
67    }
68}
69
70/// Drop every assertion without touching the host's providers. Called by
71/// `shutdown_telemetry`; the host owns its own SDK's lifecycle.
72pub(crate) fn release_adopted_providers() {
73    adopt_global_providers(AdoptedProviders::default());
74}
75
76/// True when facade spans should go through the global tracer provider.
77#[cfg(feature = "otel")]
78pub(crate) fn traces_adopted() -> bool {
79    TRACES_ADOPTED.load(Ordering::Acquire)
80}
81
82/// True when facade measurements should go through the global meter provider.
83#[cfg(feature = "otel")]
84pub(crate) fn metrics_adopted() -> bool {
85    METRICS_ADOPTED.load(Ordering::Acquire)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::testing::acquire_test_state_lock;
92
93    #[test]
94    fn nothing_is_adopted_by_default() {
95        let _guard = acquire_test_state_lock();
96        release_adopted_providers();
97        assert_eq!(adopted_global_providers(), AdoptedProviders::default());
98        assert!(!adopted_global_providers().traces);
99        assert!(!adopted_global_providers().metrics);
100    }
101
102    #[test]
103    fn adoption_is_per_signal() {
104        let _guard = acquire_test_state_lock();
105        adopt_global_providers(AdoptedProviders {
106            traces: true,
107            metrics: false,
108        });
109        assert!(adopted_global_providers().traces);
110        assert!(!adopted_global_providers().metrics);
111        release_adopted_providers();
112    }
113
114    #[test]
115    fn all_asserts_both_signals() {
116        let _guard = acquire_test_state_lock();
117        adopt_global_providers(AdoptedProviders::all());
118        assert!(adopted_global_providers().traces);
119        assert!(adopted_global_providers().metrics);
120        release_adopted_providers();
121    }
122
123    // Effective-provider routing only exists when the OTel SDK is compiled in;
124    // without it there is nothing to adopt and the flag is inert by design.
125    #[cfg(feature = "otel")]
126    #[test]
127    fn adoption_makes_the_traces_provider_effective() {
128        let _guard = acquire_test_state_lock();
129        // Establish the premise. Both predicates are gated on the signal not
130        // having been switched off by a *loaded* config, and other tests
131        // (metrics_tests.rs) install a config with metrics disabled and leave
132        // it there, so this must start from no config rather than inherit one.
133        crate::testing::reset_telemetry_state();
134        release_adopted_providers();
135        assert!(!crate::otel::traces_provider_effective());
136
137        adopt_global_providers(AdoptedProviders::all());
138        assert!(crate::otel::traces_provider_effective());
139        assert!(crate::otel::metrics_provider_effective());
140
141        release_adopted_providers();
142    }
143
144    /// Status is not export.
145    ///
146    /// The tests above prove the facade *reports* an adopted provider, and the
147    /// cross-language host_provider_adoption case proves all four agree on that
148    /// reporting. Neither shows a span leaving through it — this one does, by
149    /// recording what the host provider's exporter actually received. Go had
150    /// this covered; Rust asserted only the emitted-traces health counter, which
151    /// a provider that accepts spans and drops them would satisfy just as well.
152    #[cfg(feature = "otel")]
153    #[test]
154    fn an_adopted_provider_actually_receives_the_span() {
155        use opentelemetry_sdk::error::OTelSdkResult;
156        use opentelemetry_sdk::trace::{SdkTracerProvider, SpanData, SpanExporter};
157        use std::sync::{Arc, Mutex};
158
159        #[derive(Debug, Clone)]
160        struct RecordingExporter {
161            names: Arc<Mutex<Vec<String>>>,
162        }
163
164        impl SpanExporter for RecordingExporter {
165            async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
166                let mut names = self.names.lock().expect("exporter mutex");
167                names.extend(batch.iter().map(|span| span.name.to_string()));
168                Ok(())
169            }
170        }
171
172        let _guard = acquire_test_state_lock();
173        crate::testing::reset_telemetry_state();
174
175        let names = Arc::new(Mutex::new(Vec::new()));
176        let host_provider = SdkTracerProvider::builder()
177            .with_simple_exporter(RecordingExporter {
178                names: Arc::clone(&names),
179            })
180            .build();
181        opentelemetry::global::set_tracer_provider(host_provider.clone());
182        adopt_global_providers(AdoptedProviders::all());
183
184        crate::trace("adopted.export.span", || {});
185
186        host_provider.force_flush().expect("flush host provider");
187        let recorded = names.lock().expect("exporter mutex").clone();
188        assert!(
189            recorded.iter().any(|name| name == "adopted.export.span"),
190            "facade reported the host provider as in play but no span reached its exporter; got {recorded:?}"
191        );
192
193        release_adopted_providers();
194        crate::testing::reset_telemetry_state();
195    }
196
197    #[test]
198    fn shutdown_releases_the_assertion_without_owning_the_providers() {
199        let _guard = acquire_test_state_lock();
200        adopt_global_providers(AdoptedProviders::all());
201
202        crate::shutdown_telemetry(None).expect("shutdown should succeed");
203
204        // The host's providers are untouched; only our assertion is dropped.
205        assert_eq!(adopted_global_providers(), AdoptedProviders::default());
206    }
207
208    // Effective-provider routing only exists when the OTel SDK is compiled in;
209    // without it there is nothing to adopt and the flag is inert by design.
210    #[cfg(feature = "otel")]
211    #[test]
212    fn adopted_traces_bypass_facade_sampling() {
213        use crate::sampling::{set_sampling_policy, SamplingPolicy, Signal};
214
215        let _guard = acquire_test_state_lock();
216        crate::shutdown_telemetry(None).expect("pre-test shutdown should succeed");
217        crate::health::_reset_health_for_tests();
218        set_sampling_policy(
219            Signal::Traces,
220            SamplingPolicy {
221                default_rate: 0.0,
222                overrides: Default::default(),
223            },
224        )
225        .expect("policy should apply");
226
227        // Without adoption the facade sampler drops the span.
228        crate::trace("adopt.unsampled", || {});
229        assert_eq!(crate::health::get_health_snapshot().emitted_traces, 0);
230
231        // With it, the host SDK's sampler is the authority and we do not stack.
232        adopt_global_providers(AdoptedProviders::all());
233        crate::trace("adopt.sampled", || {});
234        assert_eq!(crate::health::get_health_snapshot().emitted_traces, 1);
235
236        release_adopted_providers();
237        crate::sampling::_reset_sampling_for_tests();
238    }
239}