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}