opentelemetry_spanprocessor_any/sdk/trace/
sampler.rs

1//! # OpenTelemetry ShouldSample Interface
2//!
3//! ## Sampling
4//!
5//! Sampling is a mechanism to control the noise and overhead introduced by
6//! OpenTelemetry by reducing the number of samples of traces collected and
7//! sent to the backend.
8//!
9//! Sampling may be implemented on different stages of a trace collection.
10//! OpenTelemetry SDK defines a `ShouldSample` interface that can be used at
11//! instrumentation points by libraries to check the sampling `SamplingDecision`
12//! early and optimize the amount of telemetry that needs to be collected.
13//!
14//! All other sampling algorithms may be implemented on SDK layer in exporters,
15//! or even out of process in Agent or Collector.
16//!
17//! The OpenTelemetry API has two properties responsible for the data collection:
18//!
19//! * `is_recording` method on a `Span`. If `true` the current `Span` records
20//!   tracing events (attributes, events, status, etc.), otherwise all tracing
21//!   events are dropped. Users can use this property to determine if expensive
22//!   trace events can be avoided. `SpanProcessor`s will receive
23//!   all spans with this flag set. However, `SpanExporter`s will
24//!   not receive them unless the `Sampled` flag was set.
25//! * `Sampled` flag in `trace_flags` on `SpanContext`. This flag is propagated
26//!   via the `SpanContext` to child Spans. For more details see the [W3C
27//!   specification](https://w3c.github.io/trace-context/). This flag indicates
28//!   that the `Span` has been `sampled` and will be exported. `SpanProcessor`s
29//!   and `SpanExporter`s will receive spans with the `Sampled` flag set for
30//!   processing.
31//!
32//! The flag combination `Sampled == false` and `is_recording` == true` means
33//! that the current `Span` does record information, but most likely the child
34//! `Span` will not.
35//!
36//! The flag combination `Sampled == true` and `is_recording == false` could
37//! cause gaps in the distributed trace, and because of this OpenTelemetry API
38//! MUST NOT allow this combination.
39
40use crate::{
41    sdk::InstrumentationLibrary,
42    trace::{Link, SpanKind, TraceContextExt, TraceId, TraceState},
43    Context, KeyValue,
44};
45#[cfg(feature = "serialize")]
46use serde::{Deserialize, Serialize};
47
48/// The `ShouldSample` interface allows implementations to provide samplers
49/// which will return a sampling `SamplingResult` based on information that
50/// is typically available just before the `Span` was created.
51pub trait ShouldSample: Send + Sync + std::fmt::Debug {
52    /// Returns the `SamplingDecision` for a `Span` to be created.
53    #[allow(clippy::too_many_arguments)]
54    fn should_sample(
55        &self,
56        parent_context: Option<&Context>,
57        trace_id: TraceId,
58        name: &str,
59        span_kind: &SpanKind,
60        attributes: &[KeyValue],
61        links: &[Link],
62        instrumentation_library: &InstrumentationLibrary,
63    ) -> SamplingResult;
64}
65
66/// The result of sampling logic for a given `Span`.
67#[derive(Clone, Debug, PartialEq)]
68pub struct SamplingResult {
69    /// `SamplingDecision` reached by this result
70    pub decision: SamplingDecision,
71    /// Extra attributes added by this result
72    pub attributes: Vec<KeyValue>,
73    /// Trace state from parent context, might be modified by sampler
74    pub trace_state: TraceState,
75}
76
77/// Decision about whether or not to sample
78#[derive(Clone, Debug, PartialEq)]
79pub enum SamplingDecision {
80    /// `is_recording() == false`, span will not be recorded and all events and
81    /// attributes will be dropped.
82    Drop,
83    /// `is_recording() == true`, but `Sampled` flag MUST NOT be set.
84    RecordOnly,
85    /// `is_recording() == true` AND `Sampled` flag` MUST be set.
86    RecordAndSample,
87}
88
89/// Sampling options
90#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
91#[derive(Clone, Debug, PartialEq)]
92pub enum Sampler {
93    /// Always sample the trace
94    AlwaysOn,
95    /// Never sample the trace
96    AlwaysOff,
97    /// Respects the parent span's sampling decision or delegates a delegate sampler for root spans.
98    ParentBased(Box<Sampler>),
99    /// Sample a given fraction of traces. Fractions >= 1 will always sample. If the parent span is
100    /// sampled, then it's child spans will automatically be sampled. Fractions < 0 are treated as
101    /// zero, but spans may still be sampled if their parent is.
102    TraceIdRatioBased(f64),
103}
104
105impl ShouldSample for Sampler {
106    fn should_sample(
107        &self,
108        parent_context: Option<&Context>,
109        trace_id: TraceId,
110        name: &str,
111        span_kind: &SpanKind,
112        attributes: &[KeyValue],
113        links: &[Link],
114        instrumentation_library: &InstrumentationLibrary,
115    ) -> SamplingResult {
116        let decision = match self {
117            // Always sample the trace
118            Sampler::AlwaysOn => SamplingDecision::RecordAndSample,
119            // Never sample the trace
120            Sampler::AlwaysOff => SamplingDecision::Drop,
121            // The parent decision if sampled; otherwise the decision of delegate_sampler
122            Sampler::ParentBased(delegate_sampler) => {
123                parent_context.filter(|cx| cx.has_active_span()).map_or(
124                    delegate_sampler
125                        .should_sample(
126                            parent_context,
127                            trace_id,
128                            name,
129                            span_kind,
130                            attributes,
131                            links,
132                            instrumentation_library,
133                        )
134                        .decision,
135                    |ctx| {
136                        let span = ctx.span();
137                        let parent_span_context = span.span_context();
138                        if parent_span_context.is_sampled() {
139                            SamplingDecision::RecordAndSample
140                        } else {
141                            SamplingDecision::Drop
142                        }
143                    },
144                )
145            }
146            // Probabilistically sample the trace.
147            Sampler::TraceIdRatioBased(prob) => {
148                if *prob >= 1.0 {
149                    SamplingDecision::RecordAndSample
150                } else {
151                    let prob_upper_bound = (prob.max(0.0) * (1u64 << 63) as f64) as u64;
152                    // TODO: update behavior when the spec definition resolves
153                    // https://github.com/open-telemetry/opentelemetry-specification/issues/1413
154                    let rnd_from_trace_id = (trace_id.0 as u64) >> 1;
155
156                    if rnd_from_trace_id < prob_upper_bound {
157                        SamplingDecision::RecordAndSample
158                    } else {
159                        SamplingDecision::Drop
160                    }
161                }
162            }
163        };
164
165        SamplingResult {
166            decision,
167            // No extra attributes ever set by the SDK samplers.
168            attributes: Vec::new(),
169            // all sampler in SDK will not modify trace state.
170            trace_state: match parent_context {
171                Some(ctx) => ctx.span().span_context().trace_state().clone(),
172                None => TraceState::default(),
173            },
174        }
175    }
176}
177
178#[cfg(all(test, feature = "testing", feature = "trace"))]
179mod tests {
180    use super::*;
181    use crate::sdk::trace::{Sampler, SamplingDecision, ShouldSample};
182    use crate::testing::trace::TestSpan;
183    use crate::trace::{SpanContext, SpanId, TraceFlags, TraceState};
184    use rand::Rng;
185
186    #[rustfmt::skip]
187    fn sampler_data() -> Vec<(&'static str, Sampler, f64, bool, bool)> {
188        vec![
189            // Span w/o a parent
190            ("never_sample", Sampler::AlwaysOff, 0.0, false, false),
191            ("always_sample", Sampler::AlwaysOn, 1.0, false, false),
192            ("ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, false, false),
193            ("ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, false, false),
194            ("ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, false, false),
195            ("ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, false, false),
196            ("ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, false, false),
197
198            // Spans w/o a parent delegate
199            ("delegate_to_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, false, false),
200            ("delegate_to_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, false, false),
201            ("delegate_to_ratio_-1", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(-1.0))), 0.0, false, false),
202            ("delegate_to_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.25, false, false),
203            ("delegate_to_ratio_.50", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.50))), 0.50, false, false),
204            ("delegate_to_ratio_.75", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.75))), 0.75, false, false),
205            ("delegate_to_ratio_2.0", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(2.0))), 1.0, false, false),
206
207            // Spans with a parent that is *not* sampled act like spans w/o a parent
208            ("unsampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, false),
209            ("unsampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, false),
210            ("unsampled_parent_with_ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, true, false),
211            ("unsampled_parent_with_ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, true, false),
212            ("unsampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, false),
213            ("unsampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 0.0, true, false),
214            ("unsampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, true, false),
215            ("unsampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.0, true, false),
216
217            // A ratio sampler with a parent that is sampled will ignore the parent
218            ("sampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, true),
219            ("sampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, true),
220            ("sampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, true),
221
222            // Spans with a parent that is sampled, will always sample, regardless of the delegate sampler
223            ("sampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, true, true),
224            ("sampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 1.0, true, true),
225            ("sampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 1.0, true, true),
226
227            // Spans with a sampled parent, but when using the NeverSample Sampler, aren't sampled
228            ("sampled_parent_span_with_never_sample", Sampler::AlwaysOff, 0.0, true, true),
229        ]
230    }
231
232    #[test]
233    fn sampling() {
234        let total = 10_000;
235        let mut rng = rand::thread_rng();
236        for (name, sampler, expectation, parent, sample_parent) in sampler_data() {
237            let mut sampled = 0;
238            for _ in 0..total {
239                let parent_context = if parent {
240                    let trace_flags = if sample_parent {
241                        TraceFlags::SAMPLED
242                    } else {
243                        TraceFlags::default()
244                    };
245                    let span_context = SpanContext::new(
246                        TraceId::from_u128(1),
247                        SpanId::from_u64(1),
248                        trace_flags,
249                        false,
250                        TraceState::default(),
251                    );
252
253                    Some(Context::current_with_span(TestSpan(span_context)))
254                } else {
255                    None
256                };
257
258                let trace_id = TraceId::from(rng.gen::<[u8; 16]>());
259                if sampler
260                    .should_sample(
261                        parent_context.as_ref(),
262                        trace_id,
263                        name,
264                        &SpanKind::Internal,
265                        &[],
266                        &[],
267                        &InstrumentationLibrary::default(),
268                    )
269                    .decision
270                    == SamplingDecision::RecordAndSample
271                {
272                    sampled += 1;
273                }
274            }
275            let mut tolerance = 0.0;
276            let got = sampled as f64 / total as f64;
277
278            if expectation > 0.0 && expectation < 1.0 {
279                // See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval
280                let z = 4.75342; // This should succeed 99.9999% of the time
281                tolerance = z * (got * (1.0 - got) / total as f64).sqrt();
282            }
283
284            let diff = (got - expectation).abs();
285            assert!(
286                diff <= tolerance,
287                "{} got {:?} (diff: {}), expected {} (w/tolerance: {})",
288                name,
289                got,
290                diff,
291                expectation,
292                tolerance
293            );
294        }
295    }
296
297    #[test]
298    fn filter_parent_sampler_for_active_spans() {
299        let sampler = Sampler::ParentBased(Box::new(Sampler::AlwaysOn));
300        let cx = Context::current_with_value("some_value");
301        let instrumentation_library = InstrumentationLibrary::default();
302        let result = sampler.should_sample(
303            Some(&cx),
304            TraceId::from_u128(1),
305            "should sample",
306            &SpanKind::Internal,
307            &[],
308            &[],
309            &instrumentation_library,
310        );
311
312        assert_eq!(result.decision, SamplingDecision::RecordAndSample);
313    }
314}