use opentelemetry::{
trace::{
Link, SamplingDecision, SamplingResult, SpanKind, TraceContextExt, TraceId, TraceState,
},
Context, KeyValue,
};
#[cfg(feature = "jaeger_remote_sampler")]
mod jaeger_remote;
#[cfg(feature = "jaeger_remote_sampler")]
pub use jaeger_remote::{JaegerRemoteSampler, JaegerRemoteSamplerBuilder};
#[cfg(feature = "jaeger_remote_sampler")]
use opentelemetry_http::HttpClient;
pub trait ShouldSample: CloneShouldSample + Send + Sync + std::fmt::Debug {
#[allow(clippy::too_many_arguments)]
fn should_sample(
&self,
parent_context: Option<&Context>,
trace_id: TraceId,
name: &str,
span_kind: &SpanKind,
attributes: &[KeyValue],
links: &[Link],
) -> SamplingResult;
}
pub trait CloneShouldSample {
fn box_clone(&self) -> Box<dyn ShouldSample>;
}
impl<T> CloneShouldSample for T
where
T: ShouldSample + Clone + 'static,
{
fn box_clone(&self) -> Box<dyn ShouldSample> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn ShouldSample> {
fn clone(&self) -> Self {
self.box_clone()
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Sampler {
AlwaysOn,
AlwaysOff,
ParentBased(Box<dyn ShouldSample>),
TraceIdRatioBased(f64),
#[cfg(feature = "jaeger_remote_sampler")]
JaegerRemote(JaegerRemoteSampler),
}
impl Sampler {
#[cfg(feature = "jaeger_remote_sampler")]
pub fn jaeger_remote<C, Sampler, R, Svc>(
runtime: R,
http_client: C,
default_sampler: Sampler,
service_name: Svc,
) -> JaegerRemoteSamplerBuilder<C, Sampler, R>
where
C: HttpClient + 'static,
Sampler: ShouldSample,
R: crate::runtime::RuntimeChannel,
Svc: Into<String>,
{
JaegerRemoteSamplerBuilder::new(runtime, http_client, default_sampler, service_name)
}
}
impl ShouldSample for Sampler {
fn should_sample(
&self,
parent_context: Option<&Context>,
trace_id: TraceId,
name: &str,
span_kind: &SpanKind,
attributes: &[KeyValue],
links: &[Link],
) -> SamplingResult {
let decision = match self {
Sampler::AlwaysOn => SamplingDecision::RecordAndSample,
Sampler::AlwaysOff => SamplingDecision::Drop,
Sampler::ParentBased(delegate_sampler) => parent_context
.filter(|cx| cx.has_active_span())
.map_or_else(
|| {
delegate_sampler
.should_sample(
parent_context,
trace_id,
name,
span_kind,
attributes,
links,
)
.decision
},
|ctx| {
let span = ctx.span();
let parent_span_context = span.span_context();
if parent_span_context.is_sampled() {
SamplingDecision::RecordAndSample
} else {
SamplingDecision::Drop
}
},
),
Sampler::TraceIdRatioBased(prob) => sample_based_on_probability(prob, trace_id),
#[cfg(feature = "jaeger_remote_sampler")]
Sampler::JaegerRemote(remote_sampler) => {
remote_sampler
.should_sample(parent_context, trace_id, name, span_kind, attributes, links)
.decision
}
};
SamplingResult {
decision,
attributes: Vec::new(),
trace_state: match parent_context {
Some(ctx) => ctx.span().span_context().trace_state().clone(),
None => TraceState::default(),
},
}
}
}
pub(crate) fn sample_based_on_probability(prob: &f64, trace_id: TraceId) -> SamplingDecision {
if *prob >= 1.0 {
SamplingDecision::RecordAndSample
} else {
let prob_upper_bound = (prob.max(0.0) * (1u64 << 63) as f64) as u64;
let bytes = trace_id.to_bytes();
let (_, low) = bytes.split_at(8);
let trace_id_low = u64::from_be_bytes(low.try_into().unwrap());
let rnd_from_trace_id = trace_id_low >> 1;
if rnd_from_trace_id < prob_upper_bound {
SamplingDecision::RecordAndSample
} else {
SamplingDecision::Drop
}
}
}
#[cfg(all(test, feature = "testing", feature = "trace"))]
mod tests {
use super::*;
use crate::testing::trace::TestSpan;
use opentelemetry::trace::{SpanContext, SpanId, TraceFlags};
use rand::random;
#[rustfmt::skip]
fn sampler_data() -> Vec<(&'static str, Sampler, f64, bool, bool)> {
vec![
("never_sample", Sampler::AlwaysOff, 0.0, false, false),
("always_sample", Sampler::AlwaysOn, 1.0, false, false),
("ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, false, false),
("ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, false, false),
("ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, false, false),
("ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, false, false),
("ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, false, false),
("delegate_to_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, false, false),
("delegate_to_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, false, false),
("delegate_to_ratio_-1", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(-1.0))), 0.0, false, false),
("delegate_to_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.25, false, false),
("delegate_to_ratio_.50", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.50))), 0.50, false, false),
("delegate_to_ratio_.75", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.75))), 0.75, false, false),
("delegate_to_ratio_2.0", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(2.0))), 1.0, false, false),
("unsampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, false),
("unsampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, false),
("unsampled_parent_with_ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, true, false),
("unsampled_parent_with_ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, true, false),
("unsampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, false),
("unsampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 0.0, true, false),
("unsampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, true, false),
("unsampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.0, true, false),
("sampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, true),
("sampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, true),
("sampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, true),
("sampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, true, true),
("sampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 1.0, true, true),
("sampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 1.0, true, true),
("sampled_parent_span_with_never_sample", Sampler::AlwaysOff, 0.0, true, true),
]
}
#[test]
fn sampling() {
let total = 10_000;
for (name, sampler, expectation, parent, sample_parent) in sampler_data() {
let mut sampled = 0;
for _ in 0..total {
let parent_context = if parent {
let trace_flags = if sample_parent {
TraceFlags::SAMPLED
} else {
TraceFlags::default()
};
let span_context = SpanContext::new(
TraceId::from(1),
SpanId::from(1),
trace_flags,
false,
TraceState::default(),
);
Some(Context::current_with_span(TestSpan(span_context)))
} else {
None
};
let trace_id = TraceId::from(random::<u128>());
if sampler
.should_sample(
parent_context.as_ref(),
trace_id,
name,
&SpanKind::Internal,
&[],
&[],
)
.decision
== SamplingDecision::RecordAndSample
{
sampled += 1;
}
}
let mut tolerance = 0.0;
let got = sampled as f64 / total as f64;
if expectation > 0.0 && expectation < 1.0 {
let z = 4.75342; tolerance = z * (got * (1.0 - got) / total as f64).sqrt();
}
let diff = (got - expectation).abs();
assert!(
diff <= tolerance,
"{name} got {got:?} (diff: {diff}), expected {expectation} (w/tolerance: {tolerance})"
);
}
}
#[test]
fn clone_a_parent_sampler() {
let sampler = Sampler::ParentBased(Box::new(Sampler::AlwaysOn));
#[allow(clippy::redundant_clone)]
let cloned_sampler = sampler.clone();
let cx = Context::current_with_value("some_value");
let result = sampler.should_sample(
Some(&cx),
TraceId::from(1),
"should sample",
&SpanKind::Internal,
&[],
&[],
);
let cloned_result = cloned_sampler.should_sample(
Some(&cx),
TraceId::from(1),
"should sample",
&SpanKind::Internal,
&[],
&[],
);
assert_eq!(result, cloned_result);
}
#[test]
fn parent_sampler() {
let test_cases = vec![
(
"should using delegate sampler",
Sampler::AlwaysOn,
Context::new(),
SamplingDecision::RecordAndSample,
),
(
"should use parent result, always off",
Sampler::AlwaysOn,
Context::current_with_span(TestSpan(SpanContext::new(
TraceId::from(1),
SpanId::from(1),
TraceFlags::default(), false,
TraceState::default(),
))),
SamplingDecision::Drop,
),
(
"should use parent result, always on",
Sampler::AlwaysOff,
Context::current_with_span(TestSpan(SpanContext::new(
TraceId::from(1),
SpanId::from(1),
TraceFlags::SAMPLED, false,
TraceState::default(),
))),
SamplingDecision::RecordAndSample,
),
];
for (name, delegate, parent_cx, expected) in test_cases {
let sampler = Sampler::ParentBased(Box::new(delegate));
let result = sampler.should_sample(
Some(&parent_cx),
TraceId::from(1),
name,
&SpanKind::Internal,
&[],
&[],
);
assert_eq!(result.decision, expected);
}
}
}