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#[derive(Clone, Debug, PartialEq)]
11pub struct SamplingResult {
12 pub decision: SamplingDecision,
14
15 pub attributes: Vec<KeyValue>,
17
18 pub trace_state: TraceState,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum SamplingDecision {
25 Drop,
27
28 RecordOnly,
30
31 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
40pub trait ShouldSample: CloneShouldSample + Send + Sync + std::fmt::Debug {
89 #[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
116pub 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#[derive(Clone, Debug)]
143#[non_exhaustive]
144pub enum Sampler {
145 AlwaysOn,
147 AlwaysOff,
149 ParentBased(Box<dyn ShouldSample>),
151 TraceIdRatioBased(f64),
157 #[cfg(feature = "jaeger_remote_sampler")]
166 JaegerRemote(JaegerRemoteSampler),
167}
168
169impl Sampler {
170 #[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 pub fn parent_based(root: impl Into<Box<dyn ShouldSample>>) -> ParentBasedSampler {
205 ParentBasedSampler::new(root)
206 }
207}
208
209#[derive(Clone, Debug)]
226pub struct ParentBasedSampler {
227 root: Box<dyn ShouldSample>,
228 remote_parent_sampled: Option<Box<dyn ShouldSample>>,
229 remote_parent_not_sampled: Option<Box<dyn ShouldSample>>,
230 local_parent_sampled: Option<Box<dyn ShouldSample>>,
231 local_parent_not_sampled: Option<Box<dyn ShouldSample>>,
232}
233
234impl ParentBasedSampler {
235 pub fn new(root: impl Into<Box<dyn ShouldSample>>) -> Self {
246 Self {
247 root: root.into(),
248 remote_parent_sampled: None,
249 remote_parent_not_sampled: None,
250 local_parent_sampled: None,
251 local_parent_not_sampled: None,
252 }
253 }
254 pub fn with_remote_parent_sampled(mut self, sampler: impl Into<Box<dyn ShouldSample>>) -> Self {
257 self.remote_parent_sampled = Some(sampler.into());
258 self
259 }
260
261 pub fn with_remote_parent_not_sampled(
264 mut self,
265 sampler: impl Into<Box<dyn ShouldSample>>,
266 ) -> Self {
267 self.remote_parent_not_sampled = Some(sampler.into());
268 self
269 }
270
271 pub fn with_local_parent_sampled(mut self, sampler: impl Into<Box<dyn ShouldSample>>) -> Self {
274 self.local_parent_sampled = Some(sampler.into());
275 self
276 }
277
278 pub fn with_local_parent_not_sampled(
281 mut self,
282 sampler: impl Into<Box<dyn ShouldSample>>,
283 ) -> Self {
284 self.local_parent_not_sampled = Some(sampler.into());
285 self
286 }
287}
288
289impl ShouldSample for ParentBasedSampler {
290 fn should_sample(
291 &self,
292 parent_context: Option<&Context>,
293 trace_id: TraceId,
294 name: &str,
295 span_kind: &SpanKind,
296 attributes: &[KeyValue],
297 links: &[Link],
298 ) -> SamplingResult {
299 let delegate = |sampler: &dyn ShouldSample| {
300 sampler.should_sample(parent_context, trace_id, name, span_kind, attributes, links)
301 };
302
303 match parent_context.filter(|cx| cx.span().span_context().is_valid()) {
304 None => delegate(self.root.as_ref()),
305 Some(cx) => {
306 let span = cx.span();
307 let parent_span_context = span.span_context();
308 match (
309 parent_span_context.is_remote(),
310 parent_span_context.is_sampled(),
311 ) {
312 (true, true) => match &self.remote_parent_sampled {
313 Some(s) => delegate(s.as_ref()),
314 None => delegate(&Sampler::AlwaysOn),
315 },
316 (true, false) => match &self.remote_parent_not_sampled {
317 Some(s) => delegate(s.as_ref()),
318 None => delegate(&Sampler::AlwaysOff),
319 },
320 (false, true) => match &self.local_parent_sampled {
321 Some(s) => delegate(s.as_ref()),
322 None => delegate(&Sampler::AlwaysOn),
323 },
324 (false, false) => match &self.local_parent_not_sampled {
325 Some(s) => delegate(s.as_ref()),
326 None => delegate(&Sampler::AlwaysOff),
327 },
328 }
329 }
330 }
331 }
332}
333
334impl ShouldSample for Sampler {
335 fn should_sample(
336 &self,
337 parent_context: Option<&Context>,
338 trace_id: TraceId,
339 name: &str,
340 span_kind: &SpanKind,
341 attributes: &[KeyValue],
342 links: &[Link],
343 ) -> SamplingResult {
344 let decision = match self {
345 Sampler::AlwaysOn => SamplingDecision::RecordAndSample,
347 Sampler::AlwaysOff => SamplingDecision::Drop,
349 Sampler::ParentBased(delegate_sampler) => parent_context
354 .filter(|cx| cx.has_active_span())
355 .map_or_else(
356 || {
357 delegate_sampler
358 .should_sample(
359 parent_context,
360 trace_id,
361 name,
362 span_kind,
363 attributes,
364 links,
365 )
366 .decision
367 },
368 |ctx| {
369 let span = ctx.span();
370 let parent_span_context = span.span_context();
371 if parent_span_context.is_sampled() {
372 SamplingDecision::RecordAndSample
373 } else {
374 SamplingDecision::Drop
375 }
376 },
377 ),
378 Sampler::TraceIdRatioBased(prob) => sample_based_on_probability(prob, trace_id),
380 #[cfg(feature = "jaeger_remote_sampler")]
381 Sampler::JaegerRemote(remote_sampler) => {
382 remote_sampler
383 .should_sample(parent_context, trace_id, name, span_kind, attributes, links)
384 .decision
385 }
386 };
387 SamplingResult {
388 decision,
389 attributes: Vec::new(),
391 trace_state: match parent_context {
393 Some(ctx) => ctx.span().span_context().trace_state().clone(),
394 None => TraceState::default(),
395 },
396 }
397 }
398}
399
400pub(crate) fn sample_based_on_probability(prob: &f64, trace_id: TraceId) -> SamplingDecision {
401 if *prob >= 1.0 {
402 SamplingDecision::RecordAndSample
403 } else {
404 let prob_upper_bound = (prob.max(0.0) * (1u64 << 63) as f64) as u64;
405 let bytes = trace_id.to_bytes();
408 let (_, low) = bytes.split_at(8);
409 let trace_id_low = u64::from_be_bytes(low.try_into().unwrap());
410 let rnd_from_trace_id = trace_id_low >> 1;
411
412 if rnd_from_trace_id < prob_upper_bound {
413 SamplingDecision::RecordAndSample
414 } else {
415 SamplingDecision::Drop
416 }
417 }
418}
419
420#[cfg(all(test, feature = "testing", feature = "trace"))]
421mod tests {
422 use super::*;
423 use crate::testing::trace::TestSpan;
424 use opentelemetry::trace::{SpanContext, SpanId, TraceFlags};
425 use rand::random;
426
427 #[rustfmt::skip]
428 fn sampler_data() -> Vec<(&'static str, Sampler, f64, bool, bool)> {
429 vec![
430 ("never_sample", Sampler::AlwaysOff, 0.0, false, false),
432 ("always_sample", Sampler::AlwaysOn, 1.0, false, false),
433 ("ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, false, false),
434 ("ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, false, false),
435 ("ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, false, false),
436 ("ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, false, false),
437 ("ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, false, false),
438
439 ("delegate_to_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, false, false),
441 ("delegate_to_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, false, false),
442 ("delegate_to_ratio_-1", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(-1.0))), 0.0, false, false),
443 ("delegate_to_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.25, false, false),
444 ("delegate_to_ratio_.50", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.50))), 0.50, false, false),
445 ("delegate_to_ratio_.75", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.75))), 0.75, false, false),
446 ("delegate_to_ratio_2.0", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(2.0))), 1.0, false, false),
447
448 ("unsampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, false),
450 ("unsampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, false),
451 ("unsampled_parent_with_ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, true, false),
452 ("unsampled_parent_with_ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, true, false),
453 ("unsampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, false),
454 ("unsampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 0.0, true, false),
455 ("unsampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, true, false),
456 ("unsampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.0, true, false),
457
458 ("sampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, true),
460 ("sampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, true),
461 ("sampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, true),
462
463 ("sampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, true, true),
465 ("sampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 1.0, true, true),
466 ("sampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 1.0, true, true),
467
468 ("sampled_parent_span_with_never_sample", Sampler::AlwaysOff, 0.0, true, true),
470 ]
471 }
472
473 #[test]
474 fn sampling() {
475 let total = 10_000;
476 for (name, sampler, expectation, parent, sample_parent) in sampler_data() {
477 let mut sampled = 0;
478 for _ in 0..total {
479 let parent_context = if parent {
480 let trace_flags = if sample_parent {
481 TraceFlags::SAMPLED
482 } else {
483 TraceFlags::default()
484 };
485 let span_context = SpanContext::new(
486 TraceId::from(1),
487 SpanId::from(1),
488 trace_flags,
489 false,
490 TraceState::default(),
491 );
492
493 Some(Context::current_with_span(TestSpan(span_context)))
494 } else {
495 None
496 };
497
498 let trace_id = TraceId::from(random::<u128>());
499 if sampler
500 .should_sample(
501 parent_context.as_ref(),
502 trace_id,
503 name,
504 &SpanKind::Internal,
505 &[],
506 &[],
507 )
508 .decision
509 == SamplingDecision::RecordAndSample
510 {
511 sampled += 1;
512 }
513 }
514 let mut tolerance = 0.0;
515 let got = sampled as f64 / total as f64;
516
517 if expectation > 0.0 && expectation < 1.0 {
518 let z = 4.75342; tolerance = z * (got * (1.0 - got) / total as f64).sqrt();
521 }
522
523 let diff = (got - expectation).abs();
524 assert!(
525 diff <= tolerance,
526 "{name} got {got:?} (diff: {diff}), expected {expectation} (w/tolerance: {tolerance})"
527 );
528 }
529 }
530
531 #[test]
532 fn clone_a_parent_sampler() {
533 let sampler = Sampler::ParentBased(Box::new(Sampler::AlwaysOn));
534 #[allow(clippy::redundant_clone)]
535 let cloned_sampler = sampler.clone();
536
537 let cx = Context::current_with_value("some_value");
538
539 let result = sampler.should_sample(
540 Some(&cx),
541 TraceId::from(1),
542 "should sample",
543 &SpanKind::Internal,
544 &[],
545 &[],
546 );
547
548 let cloned_result = cloned_sampler.should_sample(
549 Some(&cx),
550 TraceId::from(1),
551 "should sample",
552 &SpanKind::Internal,
553 &[],
554 &[],
555 );
556
557 assert_eq!(result, cloned_result);
558 }
559
560 #[test]
561 fn parent_sampler() {
562 let test_cases = vec![
564 (
565 "should using delegate sampler",
566 Sampler::AlwaysOn,
567 Context::new(),
568 SamplingDecision::RecordAndSample,
569 ),
570 (
571 "should use parent result, always off",
572 Sampler::AlwaysOn,
573 Context::current_with_span(TestSpan(SpanContext::new(
574 TraceId::from(1),
575 SpanId::from(1),
576 TraceFlags::default(), false,
578 TraceState::default(),
579 ))),
580 SamplingDecision::Drop,
581 ),
582 (
583 "should use parent result, always on",
584 Sampler::AlwaysOff,
585 Context::current_with_span(TestSpan(SpanContext::new(
586 TraceId::from(1),
587 SpanId::from(1),
588 TraceFlags::SAMPLED, false,
590 TraceState::default(),
591 ))),
592 SamplingDecision::RecordAndSample,
593 ),
594 ];
595
596 for (name, delegate, parent_cx, expected) in test_cases {
597 let sampler = Sampler::ParentBased(Box::new(delegate));
598 let result = sampler.should_sample(
599 Some(&parent_cx),
600 TraceId::from(1),
601 name,
602 &SpanKind::Internal,
603 &[],
604 &[],
605 );
606
607 assert_eq!(result.decision, expected);
608 }
609 }
610
611 #[derive(Clone, Debug)]
615 struct TaggedSampler(&'static str);
616
617 impl ShouldSample for TaggedSampler {
618 fn should_sample(
619 &self,
620 parent_context: Option<&Context>,
621 _trace_id: TraceId,
622 _name: &str,
623 _span_kind: &SpanKind,
624 _attributes: &[KeyValue],
625 _links: &[Link],
626 ) -> SamplingResult {
627 let parent_span_id = parent_context
628 .map(|cx| cx.span().span_context().span_id())
629 .unwrap_or(SpanId::INVALID);
630 SamplingResult {
631 decision: SamplingDecision::RecordAndSample,
632 attributes: vec![
633 KeyValue::new("branch", self.0),
634 KeyValue::new("parent_span_id", parent_span_id.to_string()),
635 ],
636 trace_state: TraceState::from_key_value([("branch", self.0)]).unwrap(),
637 }
638 }
639 }
640
641 fn tagged_attr(result: &SamplingResult, key: &str) -> String {
642 result
643 .attributes
644 .iter()
645 .find(|kv| kv.key.as_str() == key)
646 .map(|kv| kv.value.as_str().into_owned())
647 .expect("delegate result must be forwarded unchanged")
648 }
649
650 fn parent_cx(is_remote: bool, sampled: bool) -> Context {
651 let trace_flags = if sampled {
652 TraceFlags::SAMPLED
653 } else {
654 TraceFlags::default()
655 };
656 Context::current_with_span(TestSpan(SpanContext::new(
657 TraceId::from(1),
658 SpanId::from(1),
659 trace_flags,
660 is_remote,
661 TraceState::default(),
662 )))
663 }
664
665 #[test]
666 fn parent_based_sampler_defaults_match_spec() {
667 let sampler = Sampler::parent_based(Sampler::AlwaysOff);
670
671 assert_eq!(
672 sampler
673 .should_sample(
674 None,
675 TraceId::from(1),
676 "root",
677 &SpanKind::Internal,
678 &[],
679 &[]
680 )
681 .decision,
682 SamplingDecision::Drop,
683 "no parent uses the root sampler"
684 );
685 for is_remote in [true, false] {
686 let cx = parent_cx(is_remote, true);
687 assert_eq!(
688 sampler
689 .should_sample(
690 Some(&cx),
691 TraceId::from(1),
692 "s",
693 &SpanKind::Internal,
694 &[],
695 &[]
696 )
697 .decision,
698 SamplingDecision::RecordAndSample,
699 "sampled parent (remote={is_remote}) defaults to AlwaysOn"
700 );
701 let cx = parent_cx(is_remote, false);
702 assert_eq!(
703 sampler
704 .should_sample(
705 Some(&cx),
706 TraceId::from(1),
707 "s",
708 &SpanKind::Internal,
709 &[],
710 &[]
711 )
712 .decision,
713 SamplingDecision::Drop,
714 "unsampled parent (remote={is_remote}) defaults to AlwaysOff"
715 );
716 }
717 }
718
719 #[test]
720 fn parent_based_sampler_respects_branch_overrides() {
721 let sampler = Sampler::parent_based(TaggedSampler("root"))
724 .with_remote_parent_sampled(TaggedSampler("remote_sampled"))
725 .with_remote_parent_not_sampled(TaggedSampler("remote_not_sampled"))
726 .with_local_parent_sampled(TaggedSampler("local_sampled"))
727 .with_local_parent_not_sampled(TaggedSampler("local_not_sampled"));
728
729 let cases = [
730 (true, true, "remote_sampled"),
731 (true, false, "remote_not_sampled"),
732 (false, true, "local_sampled"),
733 (false, false, "local_not_sampled"),
734 ];
735 for (is_remote, sampled, expected) in cases {
736 let cx = parent_cx(is_remote, sampled);
737 let result = sampler.should_sample(
738 Some(&cx),
739 TraceId::from(1),
740 "s",
741 &SpanKind::Internal,
742 &[],
743 &[],
744 );
745 assert_eq!(
746 tagged_attr(&result, "branch"),
747 expected,
748 "remote={is_remote}, sampled={sampled} should honor the overridden branch"
749 );
750 assert_eq!(
751 result.trace_state.get("branch"),
752 Some(expected),
753 "the delegate's modified trace state must be forwarded"
754 );
755 assert_eq!(
756 tagged_attr(&result, "parent_span_id"),
757 SpanId::from(1).to_string(),
758 "the delegate must receive the original parent context"
759 );
760 }
761
762 let result = sampler.should_sample(
764 None,
765 TraceId::from(1),
766 "root",
767 &SpanKind::Internal,
768 &[],
769 &[],
770 );
771 assert_eq!(tagged_attr(&result, "branch"), "root");
772 assert_eq!(result.trace_state.get("branch"), Some("root"));
773 }
774
775 #[test]
776 fn parent_based_sampler_invalid_parent_falls_back_to_root() {
777 let sampler = Sampler::parent_based(Sampler::AlwaysOn);
779
780 let cx_invalid = Context::current_with_span(TestSpan(SpanContext::empty_context()));
782 assert!(cx_invalid.has_active_span());
783 assert!(!cx_invalid.span().span_context().is_valid());
784
785 let decision = sampler
786 .should_sample(
787 Some(&cx_invalid),
788 TraceId::from(1),
789 "s",
790 &SpanKind::Internal,
791 &[],
792 &[],
793 )
794 .decision;
795 assert_eq!(
796 decision,
797 SamplingDecision::RecordAndSample,
798 "invalid parent span must fall back to the root sampler (AlwaysOn)"
799 );
800
801 let cx_empty = Context::new();
803 assert!(!cx_empty.has_active_span());
804
805 let decision = sampler
806 .should_sample(
807 Some(&cx_empty),
808 TraceId::from(1),
809 "s",
810 &SpanKind::Internal,
811 &[],
812 &[],
813 )
814 .decision;
815 assert_eq!(
816 decision,
817 SamplingDecision::RecordAndSample,
818 "context without active span must fall back to the root sampler (AlwaysOn)"
819 );
820
821 let cx = Context::current_with_span(TestSpan(SpanContext::new(
824 TraceId::INVALID,
825 SpanId::from(7),
826 TraceFlags::SAMPLED,
827 true,
828 TraceState::default(),
829 )));
830 let result = Sampler::parent_based(TaggedSampler("root")).should_sample(
831 Some(&cx),
832 TraceId::from(1),
833 "s",
834 &SpanKind::Internal,
835 &[],
836 &[],
837 );
838 assert_eq!(tagged_attr(&result, "branch"), "root");
839 assert_eq!(
840 tagged_attr(&result, "parent_span_id"),
841 SpanId::from(7).to_string(),
842 "the root delegate must receive the original parent context"
843 );
844 }
845
846 #[test]
847 fn parent_based_sampler_new() {
848 let sampler = ParentBasedSampler::new(Sampler::AlwaysOn);
849 assert_eq!(
850 sampler
851 .should_sample(
852 None,
853 TraceId::from(1),
854 "root",
855 &SpanKind::Internal,
856 &[],
857 &[]
858 )
859 .decision,
860 SamplingDecision::RecordAndSample
861 );
862 }
863}