Skip to main content

opentelemetry_traceable/instrumentation/
mod.rs

1//! An [`Instrumentation`] is an intance of a tracing configuration that defines
2//! which (of the traceable functions) are enabled for tracing. Each instrumentation
3//! maps to one tracer provider (and exporter), so that different exporting
4//! configuration can be assigned to different instrumentations.
5//!
6//! This allows producing multiple isolated traces that can each be exported using
7//! their specific configuration (endpoint, sampling strategy, etc).
8//!
9//! Every instrumentation takes up an available `slot`. There are a total of 64 slots
10//! available, therefore there is a maximum number of 64 instrumentations that can be
11//! configured at once.
12//!
13//! ```ignore
14//! let provider = /* some SdkTracerProvider */;
15//! let checkout = opentelemetry_traceable::instrumentation::Instrumentation::builder()
16//!     .name("checkout-debug")
17//!     .tracer(provider.tracer("checkout-debug"))
18//!     .build()
19//!     .expect("a free instrumentation slot");
20//! checkout.enable(&["my_crate::checkout::*"])?;
21//! // ... dropping `checkout` stops new spans for it and frees its slot for reuse.
22//! ```
23//!
24//! # How isolation works
25//!
26//! Each function marked by the `#[traceable]` attribute constitutes a traceable
27//! site. Each traceable site carries an "enabled" bitmask, one bit per slot.
28//! The macro loads it once per call: `0` means nothing is tracing,
29//! anything else routes through [`start_spans`], which builds one child span
30//! *per active slot* using that slot's own tracer and parent.
31//!
32//! # Two kinds of instrumentation: in-process and distributed
33//!
34//! ## In-process (the default)
35//!
36//! Every parent lives in one `opentelemetry::Context` extension, one entry per
37//! active slot. These instrumentations cannot interact with propagators, therefore
38//! their traces are contained in the current process (not distributed).
39//! There can be many in-process instrumentations configured at once.
40//!
41//! ## Distributed (at most one)
42//!
43//! [`InstrumentationBuilder::distributed`] uses the `Context` current-span
44//! slot instead of the multi-slot envelope. The "current" span becomes the parent
45//! of the new span.
46//! The parent can be from an incoming `traceparent` header, created by another library,
47//! or its own previously active span.
48//! The main advantage of a distributed instrumentation is that it is compatible with
49//! Context propagation. A propagator that injects from `Context::current()` results
50//! in the new span being parented under the propagated context, as expected.
51//!
52//! There is exactly one current-span slot per `Context`, so at most one
53//! distributed instrumentation may be live. If a second one is generated,
54//! [`InstrumentationBuilder::build`] refuses it with [`BuildError::DistributedAlreadyLive`].
55
56use std::sync::Arc;
57use std::sync::atomic::Ordering;
58
59use opentelemetry::trace::{SpanBuilder, Tracer};
60use opentelemetry::{Context, KeyValue};
61
62mod context;
63mod helpers;
64mod slots;
65mod tracer;
66
67use crate::instrumentation::context::{Envelope, InProcessParents};
68use crate::instrumentation::helpers::{BitOp, apply, slot_names};
69use crate::instrumentation::slots::{SLOTS, update_slots};
70use crate::instrumentation::tracer::DynTracer;
71use crate::registry::REGISTRY;
72use crate::selector::{self, Selection, UnknownKeys};
73use helpers::bit;
74
75/// Number of [`Instrumentation`]s that can be live *simultaneously*: one bit per
76/// slot in each site's mask.
77pub const MAX_INSTRUMENTATIONS: u32 = 64;
78const _: () = assert!(
79    MAX_INSTRUMENTATIONS as usize == u64::BITS as usize,
80    "MAX_INSTRUMENTATIONS must equal the number of bits in a site's mask"
81);
82
83fn span_builder(name: &'static str, attrs: &[KeyValue]) -> SpanBuilder {
84    if attrs.is_empty() {
85        SpanBuilder::from_name(name)
86    } else {
87        SpanBuilder::from_name(name).with_attributes(attrs.to_vec())
88    }
89}
90
91/// Called by the `#[traceable]` macro whenever the mask is non-zero.
92///
93/// Build one child span per active slot in `enabled_slots` and return the updated
94/// `Context`, or `None` if no span was created.
95///
96/// This leverages OpenTelemetry's `Context` threadl-local storage which uses
97/// RAII to guarantee the content of the context is always up to date and reflects
98/// the current state for the duration of a particular function call.
99///
100/// The distributed slot uses the `span` field of the context to store its current
101/// span, and it inherits the parent from the current `span` field of the context.
102///
103/// Every other slot uses the [`InProcessParents`] envelope of the `Context`,
104/// a generic slot's parent is bound to (and uses the tracer from) a dedicated slot
105/// in the envelope. Every slot follows a specific call path.
106#[doc(hidden)]
107pub fn start_spans(
108    enabled_slots: u64,
109    span_name: &'static str,
110    attrs: Vec<KeyValue>,
111) -> Option<Context> {
112    // we use `load_full` to avoid the arc-swap lock of `load` which would
113    // be blocking for a `store` (i.e. a config reload)
114    let slots = SLOTS.load_full();
115    let mut cx = Context::current();
116    let mut created_any = false;
117
118    // The distributed slot, if it has this site enabled.
119    let distributed = slots
120        .distributed
121        .filter(|slot| enabled_slots & bit(*slot) != 0);
122    if let Some(tracer) = distributed.and_then(|slot| slots.tracers[slot as usize].as_ref()) {
123        // here parent is whatever span is current (could be remote, another
124        // library's span, or this instrumentation's own previous span) the
125        // child lands in that same slot.
126        cx = tracer.start_in(span_builder(span_name, &attrs), &cx);
127        created_any = true;
128    }
129
130    // Everything else is in-process: parented within the envelope.
131    let in_process_mask = enabled_slots & !distributed.map_or(0, bit);
132    if in_process_mask != 0 {
133        let mut envelope: Envelope = cx
134            .get::<InProcessParents>()
135            .map(|s| s.0.clone())
136            .unwrap_or_default();
137        let mut env_changed = false;
138
139        let mut bits = in_process_mask;
140        while bits != 0 {
141            let slot = bits.trailing_zeros() as u8;
142            bits &= bits - 1;
143
144            let Some(tracer) = slots.tracers[slot as usize].as_ref() else {
145                // Instrumentation was dropped mid-flight; just skip its slot.
146                continue;
147            };
148            // Load the parent from the corresponding slot.
149            // In the most common scenario this linear search iterates a small number of
150            // elements. The hard cap is 64, and typically there will be few instrumentations
151            // configured.
152            let parent = envelope.iter().find(|(s, _)| *s == slot).map(|(_, c)| c);
153            let base = parent.cloned().unwrap_or_default();
154            let child = tracer.start_in(span_builder(span_name, &attrs), &base);
155            match envelope.iter_mut().find(|(s, _)| *s == slot) {
156                Some(entry) => entry.1 = child,
157                None => envelope.push((slot, child)),
158            }
159            env_changed = true;
160        }
161
162        if env_changed {
163            // One envelope carrying every active in-process slot's new tip.
164            cx = cx.with_value(InProcessParents(envelope));
165            created_any = true;
166        }
167    }
168
169    created_any.then_some(cx)
170}
171
172/// `Instrumentaiton` builder errors
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum BuildError {
175    /// `MAX_INSTRUMENTATIONS` reached.
176    SlotsExhausted,
177    /// A distributed instrumentation already exists.
178    DistributedAlreadyLive,
179}
180
181impl std::fmt::Display for BuildError {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            Self::SlotsExhausted => write!(
185                f,
186                "no free instrumentation slot ({MAX_INSTRUMENTATIONS} reached)"
187            ),
188            Self::DistributedAlreadyLive => {
189                write!(f, "a distributed instrumentation is already configured")
190            }
191        }
192    }
193}
194
195impl std::error::Error for BuildError {}
196
197/// A dynamic, isolated tracing instrumentation.
198/// Each instrumentation takes a dedicated slot.
199/// The slot number identifies which slot this instrumentation uses.
200/// The slot number is used to map to that slot in `SLOTS`, and in
201/// the OTel Context to identify which (active) span belongs to the slot.
202#[derive(Debug)]
203pub struct Instrumentation {
204    slot_number: u8,
205}
206
207/// Builder for an [`Instrumentation`].
208#[derive(Default)]
209pub struct InstrumentationBuilder {
210    tracer: Option<Arc<dyn DynTracer>>,
211    distributed: bool,
212}
213
214impl std::fmt::Debug for InstrumentationBuilder {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        f.debug_struct("InstrumentationBuilder")
217            .field("tracer", &self.tracer.as_ref().map(|_| "<tracer>"))
218            .field("distributed", &self.distributed)
219            .finish()
220    }
221}
222
223impl InstrumentationBuilder {
224    /// Set the [`Tracer`] this instrumentation's spans are created with.
225    #[must_use]
226    pub fn tracer<T>(mut self, tracer: T) -> Self
227    where
228        T: Tracer + Send + Sync + 'static,
229        T::Span: Send + Sync + 'static,
230    {
231        self.tracer = Some(Arc::new(tracer));
232        self
233    }
234
235    /// Make this a _distributed_ instrumentation instead of the default _in-process_.
236    /// A distributed instrumentation is compatible with distributed tracing (
237    /// it can inherit a parent span via Context propagation, e.g. traceparent header).
238    /// At most one _distributed_ instrumentation can be live at a time.
239    #[must_use]
240    pub fn distributed(mut self) -> Self {
241        self.distributed = true;
242        self
243    }
244
245    /// Build the instrumentation.
246    /// Allocates a slot and registers the tracer, returning a handle.
247    ///
248    /// # Errors
249    ///
250    /// [`BuildError::SlotsExhausted`] if [`MAX_INSTRUMENTATIONS`] are already
251    /// live, or [`BuildError::DistributedAlreadyLive`] if this is a distributed
252    /// instrumentation and one already exists.
253    ///
254    /// # Panics
255    ///
256    /// Panics if no tracer was set via [`tracer`](Self::tracer).
257    pub fn build(self) -> Result<Instrumentation, BuildError> {
258        let tracer = self
259            .tracer
260            .expect("InstrumentationBuilder::build requires a tracer");
261
262        let slot = update_slots(|slots| {
263            if self.distributed && slots.distributed.is_some() {
264                return Err(BuildError::DistributedAlreadyLive);
265            }
266            let slot = slots.alloc().ok_or(BuildError::SlotsExhausted)?;
267            slots.tracers[slot as usize] = Some(tracer);
268            if self.distributed {
269                slots.distributed = Some(slot);
270            }
271            Ok(slot)
272        })?;
273
274        Ok(Instrumentation { slot_number: slot })
275    }
276}
277
278impl Instrumentation {
279    /// Create a new `InstrumentationBuilder`.
280    #[must_use]
281    pub fn builder() -> InstrumentationBuilder {
282        InstrumentationBuilder::default()
283    }
284
285    /// Whether this is the distributed instrumentation (see
286    /// [`InstrumentationBuilder::distributed`]).
287    #[must_use]
288    pub fn is_distributed(&self) -> bool {
289        SLOTS.load().distributed == Some(self.slot_number)
290    }
291
292    /// Enable every known `#[traceable]` function for this instrumentation.
293    pub fn enable_all(&self) {
294        let b = bit(self.slot_number);
295        for site in REGISTRY.iter() {
296            site.enabled_slots.fetch_or(b, Ordering::Relaxed);
297        }
298    }
299
300    /// Disable every function for this instrumentation.
301    pub fn disable_all(&self) {
302        let b = bit(self.slot_number);
303        for site in REGISTRY.iter() {
304            site.enabled_slots.fetch_and(!b, Ordering::Relaxed);
305        }
306    }
307
308    /// Replace this instrumentation's enabled sites with the ones the
309    /// provided `selectors` resolve to.
310    ///
311    /// # Errors
312    ///
313    /// [`UnknownKeys`] does not resolve to any `#[traceable]` function. In
314    /// that case **nothing is applied** and this instrumentation keeps the
315    /// set it already had.
316    pub fn set_enabled<S: AsRef<str>>(&self, selectors: &[S]) -> Result<Selection, UnknownKeys> {
317        let selection = selector::resolve(selectors)?;
318        apply(&selection, self.slot_number, BitOp::Replace);
319        Ok(selection)
320    }
321
322    /// Enable everything that `selectors` resolves to, leaving the rest of the enabled
323    /// set unchanged (additive).
324    ///
325    /// # Errors
326    ///
327    /// As [`set_enabled`](Self::set_enabled).
328    pub fn enable<S: AsRef<str>>(&self, selectors: &[S]) -> Result<Selection, UnknownKeys> {
329        let selection = selector::resolve(selectors)?;
330        apply(&selection, self.slot_number, BitOp::Add);
331        Ok(selection)
332    }
333
334    /// Disable everything that `selectors` resolves to, leaving the rest of the enabled
335    /// set unchanged.
336    ///
337    /// # Errors
338    ///
339    /// As [`set_enabled`](Self::set_enabled).
340    pub fn disable<S: AsRef<str>>(&self, selectors: &[S]) -> Result<Selection, UnknownKeys> {
341        let selection = selector::resolve(selectors)?;
342        apply(&selection, self.slot_number, BitOp::Remove);
343        Ok(selection)
344    }
345
346    /// Registry keys currently enabled for this instrumentation.
347    pub fn enabled_names(&self) -> impl Iterator<Item = &'static str> {
348        slot_names(self.slot_number)
349    }
350}
351
352impl Drop for Instrumentation {
353    fn drop(&mut self) {
354        // Clear this slot's bit everywhere first (stops new spans), then drop
355        // the tracer, so no thread can see a set bit with a missing tracer.
356        let b = bit(self.slot_number);
357        for site in REGISTRY.iter() {
358            site.enabled_slots.fetch_and(!b, Ordering::Relaxed);
359        }
360
361        let slot = self.slot_number;
362        let _: Result<(), ()> = update_slots(|slots| {
363            slots.tracers[slot as usize] = None;
364            if slots.distributed == Some(slot) {
365                slots.distributed = None;
366            }
367            slots.freed.push(slot);
368            Ok(())
369        });
370    }
371}