Skip to main content

opentelemetry_sdk/trace/
sampler.rs

1use opentelemetry::{
2    trace::{Link, SpanKind, TraceContextExt, TraceId, TraceState},
3    Context, KeyValue,
4};
5
6#[cfg(feature = "jaeger_remote_sampler")]
7mod jaeger_remote;
8
9/// The result of sampling logic for a given span.
10#[derive(Clone, Debug, PartialEq)]
11pub struct SamplingResult {
12    /// The decision about whether or not to sample.
13    pub decision: SamplingDecision,
14
15    /// Extra attributes to be added to the span by the sampler
16    pub attributes: Vec<KeyValue>,
17
18    /// Trace state from parent context, may be modified by samplers.
19    pub trace_state: TraceState,
20}
21
22/// Decision about whether or not to sample
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum SamplingDecision {
25    /// Span will not be recorded and all events and attributes will be dropped.
26    Drop,
27
28    /// Span data wil be recorded, but not exported.
29    RecordOnly,
30
31    /// Span data will be recorded and exported.
32    RecordAndSample,
33}
34
35#[cfg(feature = "jaeger_remote_sampler")]
36pub use jaeger_remote::{JaegerRemoteSampler, JaegerRemoteSamplerBuilder};
37#[cfg(feature = "jaeger_remote_sampler")]
38use opentelemetry_http::HttpClient;
39
40/// The [`ShouldSample`] interface allows implementations to provide samplers
41/// which will return a sampling [`SamplingResult`] based on information that
42/// is typically available just before the [`Span`] was created.
43///
44/// # Sampling
45///
46/// Sampling is a mechanism to control the noise and overhead introduced by
47/// OpenTelemetry by reducing the number of samples of traces collected and
48/// sent to the backend.
49///
50/// Sampling may be implemented on different stages of a trace collection.
51/// [OpenTelemetry SDK] defines a [`ShouldSample`] interface that can be used at
52/// instrumentation points by libraries to check the sampling [`SamplingDecision`]
53/// early and optimize the amount of telemetry that needs to be collected.
54///
55/// All other sampling algorithms may be implemented on SDK layer in exporters,
56/// or even out of process in Agent or Collector.
57///
58/// The OpenTelemetry API has two properties responsible for the data collection:
59///
60/// * [`Span::is_recording()`]. If `true` the current [`Span`] records
61///   tracing events (attributes, events, status, etc.), otherwise all tracing
62///   events are dropped. Users can use this property to determine if expensive
63///   trace events can be avoided. [`SpanProcessor`]s will receive
64///   all spans with this flag set. However, [`SpanExporter`]s will
65///   not receive them unless the `Sampled` flag was set.
66/// * `Sampled` flag in [`SpanContext::trace_flags()`]. This flag is propagated
67///   via the [`SpanContext`] to child Spans. For more details see the [W3C
68///   specification](https://w3c.github.io/trace-context/). This flag indicates
69///   that the [`Span`] has been `sampled` and will be exported. [`SpanProcessor`]s
70///   and [`SpanExporter`]s will receive spans with the `Sampled` flag set for
71///   processing.
72///
73/// The flag combination `Sampled == false` and `is_recording == true` means
74/// that the current `Span` does record information, but most likely the child
75/// `Span` will not.
76///
77/// The flag combination `Sampled == true` and `is_recording == false` could
78/// cause gaps in the distributed trace, and because of this OpenTelemetry API
79/// MUST NOT allow this combination.
80///
81/// [OpenTelemetry SDK]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling
82/// [`SpanContext`]: opentelemetry::trace::SpanContext
83/// [`SpanContext::trace_flags()`]: opentelemetry::trace::SpanContext#method.trace_flags
84/// [`SpanExporter`]: crate::trace::SpanExporter
85/// [`SpanProcessor`]: crate::trace::SpanProcessor
86/// [`Span`]: opentelemetry::trace::Span
87/// [`Span::is_recording()`]: opentelemetry::trace::Span#tymethod.is_recording
88pub trait ShouldSample: CloneShouldSample + Send + Sync + std::fmt::Debug {
89    /// Returns the [`SamplingDecision`] for a [`Span`] to be created.
90    ///
91    /// The [`should_sample`] function can use any of the information provided to it in order to
92    /// make a decision about whether or not a [`Span`] should or should not be sampled. However,
93    /// there are performance implications on the creation of a span
94    ///
95    /// [`Span`]: opentelemetry::trace::Span
96    /// [`should_sample`]: ShouldSample::should_sample
97    #[allow(clippy::too_many_arguments)]
98    fn should_sample(
99        &self,
100        parent_context: Option<&Context>,
101        trace_id: TraceId,
102        name: &str,
103        span_kind: &SpanKind,
104        attributes: &[KeyValue],
105        links: &[Link],
106    ) -> SamplingResult;
107}
108
109impl<T: ShouldSample + 'static> From<T> for Box<dyn ShouldSample> {
110    #[inline]
111    fn from(value: T) -> Self {
112        Box::new(value)
113    }
114}
115
116/// This trait should not be used directly instead users should use [`ShouldSample`].
117pub trait CloneShouldSample {
118    fn box_clone(&self) -> Box<dyn ShouldSample>;
119}
120
121impl<T> CloneShouldSample for T
122where
123    T: ShouldSample + Clone + 'static,
124{
125    fn box_clone(&self) -> Box<dyn ShouldSample> {
126        Box::new(self.clone())
127    }
128}
129
130impl Clone for Box<dyn ShouldSample> {
131    fn clone(&self) -> Self {
132        self.box_clone()
133    }
134}
135
136/// Default Sampling options
137///
138/// The [built-in samplers] allow for simple decisions. For more complex scenarios consider
139/// implementing your own sampler using [`ShouldSample`] trait.
140///
141/// [built-in samplers]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-samplers
142#[derive(Clone, Debug)]
143#[non_exhaustive]
144pub enum Sampler {
145    /// Always sample the trace
146    AlwaysOn,
147    /// Never sample the trace
148    AlwaysOff,
149    /// Respects the parent span's sampling decision or delegates a delegate sampler for root spans.
150    ParentBased(Box<dyn ShouldSample>),
151    /// Sample a given fraction of traces. Fractions >= 1 will always sample. If the parent span is
152    /// sampled, then it's child spans will automatically be sampled. Fractions < 0 are treated as
153    /// zero, but spans may still be sampled if their parent is.
154    /// *Note:* If this is used then all Spans in a trace will become sampled assuming that the
155    /// first span is sampled as it is based on the `trace_id` not the `span_id`
156    TraceIdRatioBased(f64),
157    /// Jaeger remote sampler supports any remote service that implemented the jaeger remote sampler protocol.
158    /// The proto definition can be found [here](https://github.com/jaegertracing/jaeger-idl/blob/main/proto/api_v2/sampling.proto)
159    ///
160    /// Jaeger remote sampler allows remotely controlling the sampling configuration for the SDKs.
161    /// The sampling is typically configured at the collector and the SDKs actively poll for changes.
162    /// The sampler uses TraceIdRatioBased or rate-limited sampler under the hood.
163    /// These samplers can be configured per whole service (a.k.a default), or per span name in a
164    /// given service (a.k.a per operation).
165    #[cfg(feature = "jaeger_remote_sampler")]
166    JaegerRemote(JaegerRemoteSampler),
167}
168
169impl Sampler {
170    /// Create a jaeger remote sampler builder.
171    ///
172    /// ### Arguments
173    /// * `runtime` - A runtime to run the HTTP client.
174    /// * `http_client` - An HTTP client to query the sampling endpoint.
175    /// * `default_sampler` - A default sampler to make a sampling decision when the remote is unavailable or before the SDK receives the first response from remote.
176    /// * `service_name` - The name of the service. This is a required parameter to query the sampling endpoint.
177    ///
178    /// See [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/jaeger-remote-sampler/src/main.rs) for an example.
179    #[cfg(feature = "jaeger_remote_sampler")]
180    pub fn jaeger_remote<C, Sampler, R, Svc>(
181        runtime: R,
182        http_client: C,
183        default_sampler: Sampler,
184        service_name: Svc,
185    ) -> JaegerRemoteSamplerBuilder<C, Sampler, R>
186    where
187        C: HttpClient + 'static,
188        Sampler: ShouldSample,
189        R: crate::runtime::RuntimeChannel,
190        Svc: Into<String>,
191    {
192        JaegerRemoteSamplerBuilder::new(runtime, http_client, default_sampler, service_name)
193    }
194}
195
196impl ShouldSample for Sampler {
197    fn should_sample(
198        &self,
199        parent_context: Option<&Context>,
200        trace_id: TraceId,
201        name: &str,
202        span_kind: &SpanKind,
203        attributes: &[KeyValue],
204        links: &[Link],
205    ) -> SamplingResult {
206        let decision = match self {
207            // Always sample the trace
208            Sampler::AlwaysOn => SamplingDecision::RecordAndSample,
209            // Never sample the trace
210            Sampler::AlwaysOff => SamplingDecision::Drop,
211            // The parent decision if sampled; otherwise the decision of delegate_sampler
212            Sampler::ParentBased(delegate_sampler) => parent_context
213                .filter(|cx| cx.has_active_span())
214                .map_or_else(
215                    || {
216                        delegate_sampler
217                            .should_sample(
218                                parent_context,
219                                trace_id,
220                                name,
221                                span_kind,
222                                attributes,
223                                links,
224                            )
225                            .decision
226                    },
227                    |ctx| {
228                        let span = ctx.span();
229                        let parent_span_context = span.span_context();
230                        if parent_span_context.is_sampled() {
231                            SamplingDecision::RecordAndSample
232                        } else {
233                            SamplingDecision::Drop
234                        }
235                    },
236                ),
237            // Probabilistically sample the trace.
238            Sampler::TraceIdRatioBased(prob) => sample_based_on_probability(prob, trace_id),
239            #[cfg(feature = "jaeger_remote_sampler")]
240            Sampler::JaegerRemote(remote_sampler) => {
241                remote_sampler
242                    .should_sample(parent_context, trace_id, name, span_kind, attributes, links)
243                    .decision
244            }
245        };
246        SamplingResult {
247            decision,
248            // No extra attributes ever set by the SDK samplers.
249            attributes: Vec::new(),
250            // all sampler in SDK will not modify trace state.
251            trace_state: match parent_context {
252                Some(ctx) => ctx.span().span_context().trace_state().clone(),
253                None => TraceState::default(),
254            },
255        }
256    }
257}
258
259pub(crate) fn sample_based_on_probability(prob: &f64, trace_id: TraceId) -> SamplingDecision {
260    if *prob >= 1.0 {
261        SamplingDecision::RecordAndSample
262    } else {
263        let prob_upper_bound = (prob.max(0.0) * (1u64 << 63) as f64) as u64;
264        // TODO: update behavior when the spec definition resolves
265        // https://github.com/open-telemetry/opentelemetry-specification/issues/1413
266        let bytes = trace_id.to_bytes();
267        let (_, low) = bytes.split_at(8);
268        let trace_id_low = u64::from_be_bytes(low.try_into().unwrap());
269        let rnd_from_trace_id = trace_id_low >> 1;
270
271        if rnd_from_trace_id < prob_upper_bound {
272            SamplingDecision::RecordAndSample
273        } else {
274            SamplingDecision::Drop
275        }
276    }
277}
278
279#[cfg(all(test, feature = "testing", feature = "trace"))]
280mod tests {
281    use super::*;
282    use crate::testing::trace::TestSpan;
283    use opentelemetry::trace::{SpanContext, SpanId, TraceFlags};
284    use rand::random;
285
286    #[rustfmt::skip]
287    fn sampler_data() -> Vec<(&'static str, Sampler, f64, bool, bool)> {
288        vec![
289            // Span w/o a parent
290            ("never_sample", Sampler::AlwaysOff, 0.0, false, false),
291            ("always_sample", Sampler::AlwaysOn, 1.0, false, false),
292            ("ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, false, false),
293            ("ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, false, false),
294            ("ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, false, false),
295            ("ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, false, false),
296            ("ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, false, false),
297
298            // Spans w/o a parent delegate
299            ("delegate_to_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, false, false),
300            ("delegate_to_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, false, false),
301            ("delegate_to_ratio_-1", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(-1.0))), 0.0, false, false),
302            ("delegate_to_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.25, false, false),
303            ("delegate_to_ratio_.50", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.50))), 0.50, false, false),
304            ("delegate_to_ratio_.75", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.75))), 0.75, false, false),
305            ("delegate_to_ratio_2.0", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(2.0))), 1.0, false, false),
306
307            // Spans with a parent that is *not* sampled act like spans w/o a parent
308            ("unsampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, false),
309            ("unsampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, false),
310            ("unsampled_parent_with_ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, true, false),
311            ("unsampled_parent_with_ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, true, false),
312            ("unsampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, false),
313            ("unsampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 0.0, true, false),
314            ("unsampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, true, false),
315            ("unsampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.0, true, false),
316
317            // A ratio sampler with a parent that is sampled will ignore the parent
318            ("sampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, true),
319            ("sampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, true),
320            ("sampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, true),
321
322            // Spans with a parent that is sampled, will always sample, regardless of the delegate sampler
323            ("sampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, true, true),
324            ("sampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 1.0, true, true),
325            ("sampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 1.0, true, true),
326
327            // Spans with a sampled parent, but when using the NeverSample Sampler, aren't sampled
328            ("sampled_parent_span_with_never_sample", Sampler::AlwaysOff, 0.0, true, true),
329        ]
330    }
331
332    #[test]
333    fn sampling() {
334        let total = 10_000;
335        for (name, sampler, expectation, parent, sample_parent) in sampler_data() {
336            let mut sampled = 0;
337            for _ in 0..total {
338                let parent_context = if parent {
339                    let trace_flags = if sample_parent {
340                        TraceFlags::SAMPLED
341                    } else {
342                        TraceFlags::default()
343                    };
344                    let span_context = SpanContext::new(
345                        TraceId::from(1),
346                        SpanId::from(1),
347                        trace_flags,
348                        false,
349                        TraceState::default(),
350                    );
351
352                    Some(Context::current_with_span(TestSpan(span_context)))
353                } else {
354                    None
355                };
356
357                let trace_id = TraceId::from(random::<u128>());
358                if sampler
359                    .should_sample(
360                        parent_context.as_ref(),
361                        trace_id,
362                        name,
363                        &SpanKind::Internal,
364                        &[],
365                        &[],
366                    )
367                    .decision
368                    == SamplingDecision::RecordAndSample
369                {
370                    sampled += 1;
371                }
372            }
373            let mut tolerance = 0.0;
374            let got = sampled as f64 / total as f64;
375
376            if expectation > 0.0 && expectation < 1.0 {
377                // See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval
378                let z = 4.75342; // This should succeed 99.9999% of the time
379                tolerance = z * (got * (1.0 - got) / total as f64).sqrt();
380            }
381
382            let diff = (got - expectation).abs();
383            assert!(
384                diff <= tolerance,
385                "{name} got {got:?} (diff: {diff}), expected {expectation} (w/tolerance: {tolerance})"
386            );
387        }
388    }
389
390    #[test]
391    fn clone_a_parent_sampler() {
392        let sampler = Sampler::ParentBased(Box::new(Sampler::AlwaysOn));
393        #[allow(clippy::redundant_clone)]
394        let cloned_sampler = sampler.clone();
395
396        let cx = Context::current_with_value("some_value");
397
398        let result = sampler.should_sample(
399            Some(&cx),
400            TraceId::from(1),
401            "should sample",
402            &SpanKind::Internal,
403            &[],
404            &[],
405        );
406
407        let cloned_result = cloned_sampler.should_sample(
408            Some(&cx),
409            TraceId::from(1),
410            "should sample",
411            &SpanKind::Internal,
412            &[],
413            &[],
414        );
415
416        assert_eq!(result, cloned_result);
417    }
418
419    #[test]
420    fn parent_sampler() {
421        // name, delegate, context(with or without parent), expected decision
422        let test_cases = vec![
423            (
424                "should using delegate sampler",
425                Sampler::AlwaysOn,
426                Context::new(),
427                SamplingDecision::RecordAndSample,
428            ),
429            (
430                "should use parent result, always off",
431                Sampler::AlwaysOn,
432                Context::current_with_span(TestSpan(SpanContext::new(
433                    TraceId::from(1),
434                    SpanId::from(1),
435                    TraceFlags::default(), // not sampling
436                    false,
437                    TraceState::default(),
438                ))),
439                SamplingDecision::Drop,
440            ),
441            (
442                "should use parent result, always on",
443                Sampler::AlwaysOff,
444                Context::current_with_span(TestSpan(SpanContext::new(
445                    TraceId::from(1),
446                    SpanId::from(1),
447                    TraceFlags::SAMPLED, // not sampling
448                    false,
449                    TraceState::default(),
450                ))),
451                SamplingDecision::RecordAndSample,
452            ),
453        ];
454
455        for (name, delegate, parent_cx, expected) in test_cases {
456            let sampler = Sampler::ParentBased(Box::new(delegate));
457            let result = sampler.should_sample(
458                Some(&parent_cx),
459                TraceId::from(1),
460                name,
461                &SpanKind::Internal,
462                &[],
463                &[],
464            );
465
466            assert_eq!(result.decision, expected);
467        }
468    }
469}