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}