1use agent_data_plane_config::domains;
16use async_trait::async_trait;
17use saluki_common::collections::FastHashMap;
18use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
19use saluki_core::{
20 components::{transforms::*, ComponentContext},
21 data_model::event::{
22 trace::{AttributeValue, Span, Trace},
23 Event,
24 },
25 topology::EventsBuffer,
26};
27use saluki_error::GenericError;
28use stringtheory::MetaString;
29use tracing::debug;
30
31mod catalog;
32mod core_sampler;
33mod errors;
34mod priority_sampler;
35mod probabilistic;
36mod rare_sampler;
37mod score_sampler;
38mod signature;
39
40use self::probabilistic::PROB_RATE_KEY;
41use crate::common::datadog::{
42 sample_by_rate, DECISION_MAKER_MANUAL, DECISION_MAKER_PROBABILISTIC, OTEL_TRACE_ID_META_KEY,
43 SAMPLING_PRIORITY_METRIC_KEY, TAG_DECISION_MAKER,
44};
45
46const PRIORITY_AUTO_DROP: i32 = 0;
48const PRIORITY_AUTO_KEEP: i32 = 1;
49const PRIORITY_USER_KEEP: i32 = 2;
50
51const ERROR_SAMPLE_RATE: f64 = 1.0; const KEY_SPAN_SAMPLING_MECHANISM: &str = "_dd.span_sampling.mechanism";
55const KEY_ANALYZED_SPANS: &str = "_dd.analyzed";
56
57fn normalize_sampling_rate(rate: f64) -> f64 {
60 if rate <= 0.0 || rate >= 1.0 {
61 1.0
62 } else {
63 rate
64 }
65}
66
67#[derive(Debug)]
69pub struct TraceSamplerConfiguration {
70 probabilistic_sampler_enabled: bool,
71 sampling_percentage: f64,
72 error_sampling_enabled: bool,
73 error_tracking_standalone: bool,
74 errors_per_second: f64,
75 target_traces_per_second: f64,
76 default_env: MetaString,
77 rare_sampler_enabled: bool,
78 rare_sampler_tps: f64,
79 rare_sampler_cooldown_secs: f64,
80 rare_sampler_cardinality: usize,
81 otlp_sampling_rate: f64,
82}
83
84impl TraceSamplerConfiguration {
85 pub fn from_configuration(traces: &domains::traces::Domain, otlp_traces: &domains::otlp::Traces) -> Self {
90 let otlp_sampling_rate = normalize_sampling_rate(otlp_traces.probabilistic_sampler_sampling_percentage / 100.0);
91 Self {
92 probabilistic_sampler_enabled: traces.probabilistic_sampler.enabled,
93 sampling_percentage: traces.probabilistic_sampler.sampling_percentage,
94 error_sampling_enabled: traces.error_sampling_enabled,
95 error_tracking_standalone: traces.error_tracking_standalone_enabled,
96 errors_per_second: traces.errors_per_second,
97 target_traces_per_second: traces.target_traces_per_second,
98 default_env: MetaString::from(traces.default_env.clone()),
99 rare_sampler_enabled: traces.enable_rare_sampler,
100 rare_sampler_tps: traces.rare_sampler.tps,
101 rare_sampler_cooldown_secs: traces.rare_sampler.cooldown,
102 rare_sampler_cardinality: traces.rare_sampler.cardinality,
103 otlp_sampling_rate,
104 }
105 }
106}
107
108#[async_trait]
109impl SynchronousTransformBuilder for TraceSamplerConfiguration {
110 async fn build(&self, _context: ComponentContext) -> Result<Box<dyn SynchronousTransform + Send>, GenericError> {
111 let sampler = TraceSampler {
114 sampling_rate: self.sampling_percentage / 100.0,
115 error_sampling_enabled: self.error_sampling_enabled,
116 error_tracking_standalone: self.error_tracking_standalone,
117 probabilistic_sampler_enabled: self.probabilistic_sampler_enabled,
118 otlp_sampling_rate: self.otlp_sampling_rate,
119 error_sampler: errors::ErrorsSampler::new(self.errors_per_second, ERROR_SAMPLE_RATE),
120 priority_sampler: priority_sampler::PrioritySampler::new(
121 self.default_env.clone(),
122 ERROR_SAMPLE_RATE,
123 self.target_traces_per_second,
124 ),
125 no_priority_sampler: score_sampler::NoPrioritySampler::new(
126 self.target_traces_per_second,
127 ERROR_SAMPLE_RATE,
128 ),
129 rare_sampler: rare_sampler::RareSampler::new(
130 self.rare_sampler_enabled,
131 self.rare_sampler_tps,
132 std::time::Duration::from_secs_f64(self.rare_sampler_cooldown_secs),
133 self.rare_sampler_cardinality,
134 ),
135 };
136
137 Ok(Box::new(sampler))
138 }
139}
140
141impl MemoryBounds for TraceSamplerConfiguration {
142 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
143 builder.minimum().with_single_value::<TraceSampler>("component struct");
144 }
145}
146
147pub struct TraceSampler {
148 sampling_rate: f64,
149 error_tracking_standalone: bool,
150 error_sampling_enabled: bool,
151 probabilistic_sampler_enabled: bool,
152 otlp_sampling_rate: f64,
153 error_sampler: errors::ErrorsSampler,
154 priority_sampler: priority_sampler::PrioritySampler,
155 no_priority_sampler: score_sampler::NoPrioritySampler,
156 rare_sampler: rare_sampler::RareSampler,
157}
158
159impl TraceSampler {
160 fn get_root_span_index(&self, trace: &Trace) -> Option<usize> {
163 let spans = trace.spans();
165 if spans.is_empty() {
166 return None;
167 }
168 let length = spans.len();
169 let mut parent_id_to_child: FastHashMap<u64, usize> = FastHashMap::default();
175
176 for i in 0..length {
177 let j = length - 1 - i;
180 if spans[j].parent_id() == 0 {
181 return Some(j);
182 }
183 parent_id_to_child.insert(spans[j].parent_id(), j);
184 }
185
186 for span in spans.iter() {
187 parent_id_to_child.remove(&span.span_id());
188 }
189
190 if parent_id_to_child.len() != 1 {
192 debug!(
193 "Didn't reliably find the root span for traceID:{:016x}{:016x}",
194 trace.trace_id_high, trace.trace_id_low,
195 );
196 }
197
198 if let Some((_, child_idx)) = parent_id_to_child.iter().next() {
201 return Some(*child_idx);
202 }
203
204 Some(length - 1)
206 }
207
208 fn get_user_priority(&self, trace: &Trace, root_span_idx: usize) -> Option<i32> {
210 if let Some(priority) = trace.priority {
212 return Some(priority);
213 }
214
215 if trace.spans().is_empty() {
216 return None;
217 }
218
219 if let Some(root) = trace.spans().get(root_span_idx) {
222 if let Some(p) = root
223 .attributes
224 .get(SAMPLING_PRIORITY_METRIC_KEY)
225 .and_then(AttributeValue::as_num)
226 {
227 return Some(p as i32);
228 }
229 }
230 let spans = trace.spans();
231 spans.iter().find_map(|span| {
232 span.attributes
233 .get(SAMPLING_PRIORITY_METRIC_KEY)
234 .and_then(AttributeValue::as_num)
235 .map(|p| p as i32)
236 })
237 }
238
239 fn sample_probabilistic(&self, trace_id: u64) -> bool {
241 probabilistic::ProbabilisticSampler::sample(trace_id, self.sampling_rate)
242 }
243
244 fn is_otlp_trace(&self, trace: &Trace, root_span_idx: usize) -> bool {
245 trace
246 .spans()
247 .get(root_span_idx)
248 .map(|span| {
249 span.attributes
250 .contains_key(&MetaString::from_static(OTEL_TRACE_ID_META_KEY))
251 })
252 .unwrap_or(false)
253 }
254
255 fn trace_contains_error(&self, trace: &Trace, consider_exception_span_events: bool) -> bool {
257 trace.spans().iter().any(|span| {
258 span.error() != 0 || (consider_exception_span_events && self.span_contains_exception_span_event(span))
259 })
260 }
261
262 fn span_contains_exception_span_event(&self, span: &Span) -> bool {
266 if let Some(has_exception) = span
267 .attributes
268 .get("_dd.span_events.has_exception")
269 .and_then(AttributeValue::as_string)
270 {
271 return has_exception == "true";
272 }
273 false
274 }
275
276 fn otlp_pre_sample(&mut self, trace: &mut Trace, root_span_idx: usize) -> Option<(i32, &'static str)> {
284 if self.probabilistic_sampler_enabled || !self.is_otlp_trace(trace, root_span_idx) {
285 return None;
286 }
287 let (priority, dm) = if let Some(user_priority) = self.get_user_priority(trace, root_span_idx) {
288 (user_priority, DECISION_MAKER_MANUAL)
289 } else {
290 let root_trace_id = trace.trace_id_low;
291 if sample_by_rate(root_trace_id, self.otlp_sampling_rate) {
292 (PRIORITY_AUTO_KEEP, DECISION_MAKER_PROBABILISTIC)
293 } else {
294 (PRIORITY_AUTO_DROP, DECISION_MAKER_PROBABILISTIC)
295 }
296 };
297 if priority == PRIORITY_AUTO_KEEP {
298 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
299 root_span.attributes.remove(PROB_RATE_KEY);
300 }
301 }
302 Some((priority, dm))
303 }
304
305 fn analyzed_span_sampling(&self, trace: &mut Trace) -> bool {
309 let retained = trace.retain_spans(|_, span| span.attributes.contains_key(KEY_ANALYZED_SPANS));
310 if retained > 0 {
311 trace.dropped_trace = false;
312 trace.priority = Some(PRIORITY_USER_KEEP);
313 trace.otlp_sampling_rate = Some(self.sampling_rate);
314 true
315 } else {
316 false
317 }
318 }
319
320 fn has_analyzed_spans(&self, trace: &Trace) -> bool {
322 trace
323 .spans()
324 .iter()
325 .any(|span| span.attributes.contains_key(KEY_ANALYZED_SPANS))
326 }
327
328 fn single_span_sampling(&self, trace: &mut Trace) -> bool {
331 let retained = trace.retain_spans(|_, span| span.attributes.contains_key(KEY_SPAN_SAMPLING_MECHANISM));
332 if retained > 0 {
333 trace.dropped_trace = false;
334 trace.priority = Some(PRIORITY_USER_KEEP);
335 trace.otlp_sampling_rate = Some(self.sampling_rate);
336 true
337 } else {
338 false
339 }
340 }
341
342 fn run_samplers(&mut self, trace: &mut Trace) -> (bool, i32, &'static str, Option<usize>) {
347 if trace.spans().is_empty() {
350 return (false, PRIORITY_AUTO_DROP, "", None);
351 }
352
353 let now = std::time::SystemTime::now();
354 let Some(root_span_idx) = self.get_root_span_index(trace) else {
355 return (false, PRIORITY_AUTO_DROP, "", None);
356 };
357
358 if self.error_tracking_standalone {
361 let otlp_pre_sample = self.otlp_pre_sample(trace, root_span_idx);
362 if self.trace_contains_error(trace, true) {
363 let keep = self.error_sampler.sample_error(now, trace, root_span_idx);
364 let default_priority = if keep { PRIORITY_AUTO_KEEP } else { PRIORITY_AUTO_DROP };
365 let (priority, dm) = otlp_pre_sample.unwrap_or((default_priority, ""));
366 return (keep, priority, dm, Some(root_span_idx));
367 }
368 let (pre_priority, pre_dm) = otlp_pre_sample.unwrap_or((PRIORITY_AUTO_DROP, ""));
369 return (false, pre_priority, pre_dm, Some(root_span_idx));
370 }
371
372 let contains_error = self.trace_contains_error(trace, false);
373
374 let rare = self.rare_sampler.sample(trace, root_span_idx);
378
379 if self.probabilistic_sampler_enabled {
381 let mut prob_keep = false;
382 let mut decision_maker = "";
383
384 if rare {
385 prob_keep = true;
387 } else {
388 let root_trace_id = trace.trace_id_low;
390 if self.sample_probabilistic(root_trace_id) {
391 decision_maker = DECISION_MAKER_PROBABILISTIC;
392 prob_keep = true;
393
394 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
395 root_span.attributes.insert(
396 MetaString::from(PROB_RATE_KEY),
397 AttributeValue::Float(self.sampling_rate),
398 );
399 }
400 } else if self.error_sampling_enabled && contains_error {
401 prob_keep = self.error_sampler.sample_error(now, trace, root_span_idx);
402 }
403 }
404
405 let priority = if prob_keep {
406 PRIORITY_AUTO_KEEP
407 } else {
408 PRIORITY_AUTO_DROP
409 };
410
411 return (prob_keep, priority, decision_maker, Some(root_span_idx));
412 }
413
414 let user_priority = self.get_user_priority(trace, root_span_idx);
415 if let Some(priority) = user_priority {
416 if priority < PRIORITY_AUTO_DROP {
417 return (false, priority, "", Some(root_span_idx));
419 }
420
421 if rare {
422 return (true, priority, "", Some(root_span_idx));
423 }
424
425 if self.priority_sampler.sample(now, trace, root_span_idx, priority, 0.0) {
426 return (true, priority, "", Some(root_span_idx));
427 }
428 } else if self.is_otlp_trace(trace, root_span_idx) {
429 if rare {
431 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
432 }
433
434 let root_trace_id = trace.trace_id_low;
436 if sample_by_rate(root_trace_id, self.otlp_sampling_rate) {
437 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
438 root_span.attributes.remove(PROB_RATE_KEY);
439 }
440 return (
441 true,
442 PRIORITY_AUTO_KEEP,
443 DECISION_MAKER_PROBABILISTIC,
444 Some(root_span_idx),
445 );
446 }
447 } else {
448 if rare {
449 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
450 }
451 if self.no_priority_sampler.sample(now, trace, root_span_idx) {
452 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
453 }
454 }
455
456 if self.error_sampling_enabled && contains_error {
457 let keep = self.error_sampler.sample_error(now, trace, root_span_idx);
458 if keep {
459 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
460 }
461 }
462
463 (false, PRIORITY_AUTO_DROP, "", Some(root_span_idx))
465 }
466
467 fn apply_sampling_metadata(
472 &self, trace: &mut Trace, keep: bool, priority: i32, decision_maker: &str, root_span_idx: usize,
473 ) {
474 let is_otlp = self.is_otlp_trace(trace, root_span_idx);
475 let root_span_value = match trace.spans_mut().get_mut(root_span_idx) {
476 Some(span) => span,
477 None => return,
478 };
479
480 let existing_decision_maker = if decision_maker.is_empty() {
482 root_span_value
483 .attributes
484 .get(TAG_DECISION_MAKER)
485 .and_then(AttributeValue::as_string)
486 .cloned()
487 } else {
488 None
489 };
490 let decision_maker_meta = if decision_maker.is_empty() {
491 existing_decision_maker
492 } else {
493 Some(MetaString::from(decision_maker))
494 };
495
496 if priority > 0 && !(is_otlp && self.probabilistic_sampler_enabled) {
501 if let Some(dm) = decision_maker_meta.as_ref() {
502 root_span_value
503 .attributes
504 .insert(MetaString::from(TAG_DECISION_MAKER), AttributeValue::String(dm.clone()));
505 }
506 }
507
508 trace.dropped_trace = !keep;
510 trace.priority = Some(priority);
511 trace.decision_maker = if priority > 0 { decision_maker_meta } else { None };
512 trace.otlp_sampling_rate = Some(if is_otlp {
513 self.otlp_sampling_rate
514 } else {
515 self.sampling_rate
516 });
517 }
518
519 fn process_trace(&mut self, trace: &mut Trace) -> bool {
520 let (keep, priority, decision_maker, root_span_idx) = self.run_samplers(trace);
525
526 if keep || self.error_tracking_standalone {
529 if let Some(root_idx) = root_span_idx {
530 self.apply_sampling_metadata(trace, keep, priority, decision_maker, root_idx);
531 }
532 return true;
533 }
534
535 let modified = self.single_span_sampling(trace);
538 if !modified {
539 if self.analyzed_span_sampling(trace) {
541 return true;
542 }
543 } else if self.has_analyzed_spans(trace) {
544 debug!(
546 "Detected both analytics events AND single span sampling in the same trace. Single span sampling wins because App Analytics is deprecated."
547 );
548 return true;
549 }
550
551 if modified {
553 return true;
554 }
555
556 debug!("Dropping trace with priority {}", priority);
558 false
559 }
560}
561
562impl SynchronousTransform for TraceSampler {
563 fn transform_buffer(&mut self, buffer: &mut EventsBuffer) {
564 buffer.remove_if(|event| match event {
565 Event::Trace(trace) => !self.process_trace(trace),
566 _ => false,
567 });
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use std::collections::HashMap;
574
575 use saluki_core::data_model::event::trace::{AttributeValue, Span as DdSpan, Trace};
576 const PRIORITY_USER_DROP: i32 = -1;
577
578 use super::*;
579 fn create_test_sampler() -> TraceSampler {
580 TraceSampler {
581 sampling_rate: 1.0,
582 error_sampling_enabled: true,
583 error_tracking_standalone: false,
584 probabilistic_sampler_enabled: true,
585 otlp_sampling_rate: 1.0,
586 error_sampler: errors::ErrorsSampler::new(10.0, 1.0),
587 priority_sampler: priority_sampler::PrioritySampler::new(MetaString::from("agent-env"), 1.0, 10.0),
588 no_priority_sampler: score_sampler::NoPrioritySampler::new(10.0, 1.0),
589 rare_sampler: rare_sampler::RareSampler::new(false, 5.0, std::time::Duration::from_secs(300), 200),
590 }
591 }
592
593 fn create_test_span(span_id: u64, error: i32) -> DdSpan {
594 DdSpan::new(
595 MetaString::from("test-service"),
596 MetaString::from("test-operation"),
597 MetaString::from("test-resource"),
598 MetaString::from("test-type"),
599 span_id,
600 0, 0, 1000, error,
604 )
605 }
606
607 fn create_test_span_with_metrics(span_id: u64, metrics: HashMap<String, f64>) -> DdSpan {
608 let attrs: saluki_common::collections::FastHashMap<MetaString, AttributeValue> = metrics
609 .into_iter()
610 .map(|(k, v)| (MetaString::from(k), AttributeValue::Float(v)))
611 .collect();
612 create_test_span(span_id, 0).with_attributes(attrs)
613 }
614
615 #[allow(dead_code)]
616 fn create_test_span_with_meta(span_id: u64, meta: HashMap<String, String>) -> DdSpan {
617 let attrs: saluki_common::collections::FastHashMap<MetaString, AttributeValue> = meta
618 .into_iter()
619 .map(|(k, v)| (MetaString::from(k), AttributeValue::String(MetaString::from(v))))
620 .collect();
621 create_test_span(span_id, 0).with_attributes(attrs)
622 }
623
624 fn create_test_trace(spans: Vec<DdSpan>) -> Trace {
625 Trace::new(spans)
626 }
627
628 #[test]
629 fn user_priority_detection() {
630 let sampler = create_test_sampler();
631
632 let mut metrics = HashMap::new();
634 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
635 let span = create_test_span_with_metrics(1, metrics);
636 let trace = create_test_trace(vec![span]);
637 let root_idx = sampler.get_root_span_index(&trace).unwrap();
638
639 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(2));
640
641 let mut metrics = HashMap::new();
643 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), -1.0);
644 let span = create_test_span_with_metrics(1, metrics);
645 let trace = create_test_trace(vec![span]);
646 let root_idx = sampler.get_root_span_index(&trace).unwrap();
647
648 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(-1));
649
650 let span = create_test_span(1, 0);
652 let trace = create_test_trace(vec![span]);
653 let root_idx = sampler.get_root_span_index(&trace).unwrap();
654
655 assert_eq!(sampler.get_user_priority(&trace, root_idx), None);
656 }
657
658 #[test]
659 fn trace_level_priority_takes_precedence() {
660 let sampler = create_test_sampler();
661
662 let mut metrics_root = HashMap::new();
665 metrics_root.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 0.0);
666 let root_span = create_test_span_with_metrics(1, metrics_root);
667
668 let mut metrics_later = HashMap::new();
669 metrics_later.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 1.0);
670 let later_span = create_test_span_with_metrics(2, metrics_later).with_parent_id(1);
671
672 let mut trace = create_test_trace(vec![root_span, later_span]);
673 let root_idx = sampler.get_root_span_index(&trace).unwrap();
674
675 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(0));
677
678 trace.priority = Some(2);
680
681 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(2));
683
684 let span_no_priority = create_test_span(3, 0);
686 let mut trace_only_trace_level = create_test_trace(vec![span_no_priority]);
687 trace_only_trace_level.priority = Some(1);
688 let root_idx = sampler.get_root_span_index(&trace_only_trace_level).unwrap();
689
690 assert_eq!(sampler.get_user_priority(&trace_only_trace_level, root_idx), Some(1));
691 }
692
693 #[test]
694 fn manual_keep_with_trace_level_priority() {
695 let mut sampler = create_test_sampler();
696 sampler.probabilistic_sampler_enabled = false; let span = create_test_span(1, 0);
700 let mut trace = create_test_trace(vec![span]);
701 trace.priority = Some(PRIORITY_USER_KEEP);
702
703 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
704 assert!(keep);
705 assert_eq!(priority, PRIORITY_USER_KEEP);
706 assert_eq!(decision_maker, "");
707
708 let span = create_test_span(1, 0);
710 let mut trace = create_test_trace(vec![span]);
711 trace.priority = Some(PRIORITY_USER_DROP);
712
713 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
714 assert!(!keep); assert_eq!(priority, PRIORITY_USER_DROP);
716
717 let span = create_test_span(1, 0);
719 let mut trace = create_test_trace(vec![span]);
720 trace.priority = Some(PRIORITY_AUTO_KEEP);
721
722 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
723 assert!(keep);
724 assert_eq!(priority, PRIORITY_AUTO_KEEP);
725 assert_eq!(decision_maker, "");
726 }
727
728 #[test]
729 fn probabilistic_sampling_known_decisions() {
730 struct Case {
739 trace_id: u64,
740 rate: f64,
741 expected_keep: bool,
742 }
743
744 let cases = [
745 Case {
747 trace_id: 0x1234567890ABCDEF,
748 rate: 1.0,
749 expected_keep: true,
750 },
751 Case {
753 trace_id: 0x1234567890ABCDEF,
754 rate: 0.0,
755 expected_keep: false,
756 },
757 Case {
759 trace_id: 0x1234567890ABCDEF,
760 rate: 0.10,
761 expected_keep: false,
762 },
763 Case {
764 trace_id: 0x1234567890ABCDEF,
765 rate: 0.20,
766 expected_keep: true,
767 },
768 Case {
770 trace_id: 0,
771 rate: 0.50,
772 expected_keep: false,
773 },
774 Case {
775 trace_id: 0,
776 rate: 0.60,
777 expected_keep: true,
778 },
779 Case {
781 trace_id: u64::MAX,
782 rate: 0.50,
783 expected_keep: false,
784 },
785 Case {
786 trace_id: u64::MAX,
787 rate: 0.80,
788 expected_keep: true,
789 },
790 ];
791
792 for case in cases {
793 let mut sampler = create_test_sampler();
794 sampler.sampling_rate = case.rate;
795 assert_eq!(
796 sampler.sample_probabilistic(case.trace_id),
797 case.expected_keep,
798 "trace_id={:#018x} rate={}",
799 case.trace_id,
800 case.rate
801 );
802 }
803 }
804
805 #[test]
806 fn probabilistic_sampling_is_deterministic() {
807 let sampler = create_test_sampler();
811 let trace_id = 0x1234567890ABCDEF_u64;
812 assert_eq!(
813 sampler.sample_probabilistic(trace_id),
814 sampler.sample_probabilistic(trace_id)
815 );
816 }
817
818 #[test]
819 fn error_detection() {
820 let sampler = create_test_sampler();
821
822 let span_with_error = create_test_span(1, 1);
824 let trace = create_test_trace(vec![span_with_error]);
825 assert!(sampler.trace_contains_error(&trace, false));
826
827 let span_without_error = create_test_span(1, 0);
829 let trace = create_test_trace(vec![span_without_error]);
830 assert!(!sampler.trace_contains_error(&trace, false));
831 }
832
833 #[test]
834 fn sampling_priority_order() {
835 let mut sampler = create_test_sampler();
837 sampler.sampling_rate = 0.5; sampler.probabilistic_sampler_enabled = true;
839
840 let span_with_error = create_test_span(1, 1);
843 let mut trace = create_test_trace(vec![span_with_error]);
844 trace.trace_id_low = u64::MAX - 1;
845
846 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
847 assert!(keep);
848 assert_eq!(priority, PRIORITY_AUTO_KEEP);
849 assert_eq!(decision_maker, ""); let mut sampler = create_test_sampler();
853 sampler.probabilistic_sampler_enabled = false; let mut metrics = HashMap::new();
856 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
857 let span = create_test_span_with_metrics(1, metrics);
858 let mut trace = create_test_trace(vec![span]);
859
860 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
861 assert!(keep);
862 assert_eq!(priority, 2); assert_eq!(decision_maker, "");
864 }
865
866 #[test]
867 fn empty_trace_handling() {
868 let mut sampler = create_test_sampler();
869 let mut trace = create_test_trace(vec![]);
870
871 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
872 assert!(!keep);
873 assert_eq!(priority, PRIORITY_AUTO_DROP);
874 }
875
876 #[test]
877 fn root_span_detection() {
878 let sampler = create_test_sampler();
879
880 let root_span = DdSpan::new(
882 MetaString::from("service"),
883 MetaString::from("operation"),
884 MetaString::from("resource"),
885 MetaString::from("type"),
886 1,
887 0, 0,
889 1000,
890 0,
891 );
892 let child_span = DdSpan::new(
893 MetaString::from("service"),
894 MetaString::from("child_op"),
895 MetaString::from("resource"),
896 MetaString::from("type"),
897 2,
898 1, 100,
900 500,
901 0,
902 );
903 let trace = create_test_trace(vec![child_span.clone(), root_span.clone()]);
905 let root_idx = sampler.get_root_span_index(&trace).unwrap();
906 assert_eq!(trace.spans()[root_idx].span_id(), 1);
907
908 let orphan_span = DdSpan::new(
910 MetaString::from("service"),
911 MetaString::from("orphan"),
912 MetaString::from("resource"),
913 MetaString::from("type"),
914 3,
915 999, 200,
917 300,
918 0,
919 );
920 let trace = create_test_trace(vec![orphan_span]);
921 let root_idx = sampler.get_root_span_index(&trace).unwrap();
922 assert_eq!(trace.spans()[root_idx].span_id(), 3);
923
924 let span1 = create_test_span(1, 0);
926 let span2 = create_test_span(2, 0);
927 let trace = create_test_trace(vec![span1, span2]);
928 let root_idx = sampler.get_root_span_index(&trace).unwrap();
930 assert_eq!(trace.spans()[root_idx].span_id(), 2);
931 }
932
933 #[test]
934 fn single_span_sampling() {
935 let mut sampler = create_test_sampler();
936
937 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
940
941 let mut attrs_map = saluki_common::collections::FastHashMap::default();
943 attrs_map.insert(
944 MetaString::from(KEY_SPAN_SAMPLING_MECHANISM),
945 AttributeValue::Float(8.0),
946 );
947 let sss_span = create_test_span(1, 0).with_attributes(attrs_map.clone());
948
949 let regular_span = create_test_span(2, 0);
951
952 let mut trace = create_test_trace(vec![sss_span.clone(), regular_span]);
953
954 let modified = sampler.single_span_sampling(&mut trace);
956 assert!(modified);
957 assert_eq!(trace.spans().len(), 1); assert_eq!(trace.spans()[0].span_id(), 1); assert_eq!(trace.priority, Some(PRIORITY_USER_KEEP));
962
963 let trace_without_sss = create_test_trace(vec![create_test_span(3, 0)]);
965 let mut trace_copy = trace_without_sss.clone();
966 let modified = sampler.single_span_sampling(&mut trace_copy);
967 assert!(!modified);
968 assert_eq!(trace_copy.spans().len(), trace_without_sss.spans().len());
969 }
970
971 #[test]
972 fn analytics_events() {
973 let sampler = create_test_sampler();
974
975 let mut attrs_map = saluki_common::collections::FastHashMap::default();
977 attrs_map.insert(MetaString::from(KEY_ANALYZED_SPANS), AttributeValue::Float(1.0));
978 let analyzed_span = create_test_span(1, 0).with_attributes(attrs_map.clone());
979 let regular_span = create_test_span(2, 0);
980
981 let mut trace = create_test_trace(vec![analyzed_span.clone(), regular_span]);
982
983 let analyzed_span_ids: Vec<u64> = trace
984 .spans()
985 .iter()
986 .filter(|span| span.attributes.contains_key(KEY_ANALYZED_SPANS))
987 .map(|span| span.span_id())
988 .collect();
989 assert_eq!(analyzed_span_ids, vec![1]);
990
991 assert!(sampler.has_analyzed_spans(&trace));
992 let modified = sampler.analyzed_span_sampling(&mut trace);
993 assert!(modified);
994 assert_eq!(trace.spans().len(), 1);
995 assert_eq!(trace.spans()[0].span_id(), 1);
996 assert_eq!(trace.priority, Some(PRIORITY_USER_KEEP));
997
998 let trace_no_analytics = create_test_trace(vec![create_test_span(3, 0)]);
1000 let mut trace_no_analytics_copy = trace_no_analytics.clone();
1001 let analyzed_span_ids: Vec<u64> = trace_no_analytics
1002 .spans()
1003 .iter()
1004 .filter(|span| span.attributes.contains_key(KEY_ANALYZED_SPANS))
1005 .map(|span| span.span_id())
1006 .collect();
1007 assert!(analyzed_span_ids.is_empty());
1008 assert!(!sampler.has_analyzed_spans(&trace_no_analytics));
1009 let modified = sampler.analyzed_span_sampling(&mut trace_no_analytics_copy);
1010 assert!(!modified);
1011 assert_eq!(trace_no_analytics_copy.spans().len(), trace_no_analytics.spans().len());
1012 }
1013
1014 #[test]
1015 fn probabilistic_sampling_with_prob_rate_key() {
1016 let mut sampler = create_test_sampler();
1017 sampler.sampling_rate = 0.75; sampler.probabilistic_sampler_enabled = true;
1019
1020 let trace_id = 12345_u64;
1022 let root_span = DdSpan::new(
1023 MetaString::from("service"),
1024 MetaString::from("operation"),
1025 MetaString::from("resource"),
1026 MetaString::from("type"),
1027 1,
1028 0, 0,
1030 1000,
1031 0,
1032 );
1033 let mut trace = create_test_trace(vec![root_span]);
1034 trace.trace_id_low = trace_id;
1035
1036 let (keep, priority, decision_maker, root_span_idx) = sampler.run_samplers(&mut trace);
1037
1038 if keep && decision_maker == DECISION_MAKER_PROBABILISTIC {
1039 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1041 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC); let root_idx = root_span_idx.unwrap_or(0);
1045 let root_span = &trace.spans()[root_idx];
1046 assert!(root_span.attributes.contains_key(PROB_RATE_KEY));
1047 assert_eq!(
1048 root_span
1049 .attributes
1050 .get(PROB_RATE_KEY)
1051 .and_then(AttributeValue::as_float),
1052 Some(0.75)
1053 );
1054
1055 let mut trace_with_metadata = trace.clone();
1057 sampler.apply_sampling_metadata(&mut trace_with_metadata, keep, priority, decision_maker, root_idx);
1058
1059 let modified_root = &trace_with_metadata.spans()[root_idx];
1061 assert!(modified_root.attributes.contains_key(TAG_DECISION_MAKER));
1062 assert_eq!(
1063 modified_root
1064 .attributes
1065 .get(TAG_DECISION_MAKER)
1066 .and_then(AttributeValue::as_string),
1067 Some(&MetaString::from(DECISION_MAKER_PROBABILISTIC))
1068 );
1069 }
1070 }
1071
1072 fn create_top_level_span(span_id: u64) -> DdSpan {
1082 let mut attrs = saluki_common::collections::FastHashMap::default();
1083 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
1084 create_test_span(span_id, 0).with_attributes(attrs)
1085 }
1086
1087 fn create_sampler_with_rare_enabled() -> TraceSampler {
1090 TraceSampler {
1091 rare_sampler: rare_sampler::RareSampler::new(true, 1000.0, std::time::Duration::from_secs(300), 200),
1092 ..create_test_sampler()
1093 }
1094 }
1095
1096 #[test]
1100 fn rare_sampler_catches_unsampled_trace() {
1101 let mut sampler = create_sampler_with_rare_enabled();
1102 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
1104
1105 let span = create_top_level_span(1);
1106 let mut trace = create_test_trace(vec![span]);
1107
1108 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1109 assert!(keep, "rare sampler should catch first occurrence");
1110 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1111 assert_eq!(decision_maker, "", "rare sampler does not set _dd.p.dm");
1112 }
1113
1114 #[test]
1118 fn rare_sampler_sets_rare_metric_on_first_occurrence() {
1119 let mut sampler = create_sampler_with_rare_enabled();
1120 sampler.sampling_rate = 0.0;
1121 sampler.probabilistic_sampler_enabled = true;
1122
1123 let span = create_top_level_span(1);
1124 let mut trace = create_test_trace(vec![span]);
1125
1126 let (keep, _, _, root_idx) = sampler.run_samplers(&mut trace);
1127 assert!(keep);
1128 let root = &trace.spans()[root_idx.unwrap()];
1129 assert_eq!(
1130 root.attributes
1131 .get(rare_sampler::RARE_KEY)
1132 .and_then(AttributeValue::as_float),
1133 Some(1.0),
1134 "_dd.rare should be 1 on first occurrence"
1135 );
1136 }
1137
1138 #[test]
1143 fn rare_sampler_does_not_resample_within_ttl() {
1144 let mut sampler = create_sampler_with_rare_enabled();
1145 sampler.sampling_rate = 0.0;
1146 sampler.probabilistic_sampler_enabled = true;
1147
1148 let span1 = create_top_level_span(1);
1150 let mut trace1 = create_test_trace(vec![span1]);
1151 let (keep1, _, _, _) = sampler.run_samplers(&mut trace1);
1152 assert!(keep1, "first occurrence should be kept by rare sampler");
1153
1154 let span2 = create_top_level_span(2);
1157 let mut trace2 = create_test_trace(vec![span2]);
1158 let (keep2, priority2, _, _) = sampler.run_samplers(&mut trace2);
1159 assert!(!keep2, "second occurrence within TTL should be dropped");
1160 assert_eq!(priority2, PRIORITY_AUTO_DROP);
1161 }
1162
1163 #[test]
1167 fn rare_sampler_disabled_does_not_catch_unsampled() {
1168 let mut sampler = create_test_sampler(); sampler.sampling_rate = 0.0;
1170 sampler.probabilistic_sampler_enabled = true;
1171
1172 let span = create_top_level_span(1);
1173 let mut trace = create_test_trace(vec![span]);
1174
1175 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1176 assert!(!keep, "rare disabled should not catch the trace");
1177 assert_eq!(priority, PRIORITY_AUTO_DROP);
1178 }
1179
1180 #[test]
1183 fn rare_sampler_catches_priority_auto_drop_in_legacy_path() {
1184 let mut sampler = create_sampler_with_rare_enabled();
1185 sampler.probabilistic_sampler_enabled = false;
1186
1187 let mut attrs = saluki_common::collections::FastHashMap::default();
1188 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
1189 attrs.insert(
1190 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
1191 AttributeValue::Float(PRIORITY_AUTO_DROP as f64),
1192 );
1193 let span = create_test_span(1, 0).with_attributes(attrs);
1194 let mut trace = create_test_trace(vec![span]);
1195
1196 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1197 assert!(keep, "rare sampler should catch PriorityAutoDrop on first occurrence");
1198 assert_eq!(priority, PRIORITY_AUTO_DROP, "tracer-set priority should be preserved");
1199 assert_eq!(decision_maker, "");
1200 }
1201
1202 #[test]
1205 fn rare_sampler_preserves_user_keep_priority_in_legacy_path() {
1206 let mut sampler = create_sampler_with_rare_enabled();
1207 sampler.probabilistic_sampler_enabled = false;
1208
1209 let mut attrs = saluki_common::collections::FastHashMap::default();
1210 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
1211 attrs.insert(
1212 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
1213 AttributeValue::Float(2.0),
1214 ); let span = create_test_span(1, 0).with_attributes(attrs);
1216 let mut trace = create_test_trace(vec![span]);
1217
1218 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1219 assert!(keep);
1220 assert_eq!(priority, 2, "UserKeep priority must not be downgraded to AutoKeep");
1221 }
1222
1223 #[test]
1225 fn probabilistic_100_percent_keeps_trace_with_decision_maker() {
1226 let mut sampler = create_test_sampler(); sampler.sampling_rate = 1.0;
1228 sampler.probabilistic_sampler_enabled = true;
1229
1230 let span = create_top_level_span(1);
1231 let mut trace = create_test_trace(vec![span]);
1232
1233 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1234 assert!(keep);
1235 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1236 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC);
1237 }
1238
1239 #[test]
1241 fn probabilistic_0_percent_drops_trace() {
1242 let mut sampler = create_test_sampler(); sampler.sampling_rate = 0.0;
1244 sampler.probabilistic_sampler_enabled = true;
1245 sampler.error_sampling_enabled = false;
1246
1247 let span = create_top_level_span(1);
1248 let mut trace = create_test_trace(vec![span]);
1249
1250 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1251 assert!(!keep);
1252 assert_eq!(priority, PRIORITY_AUTO_DROP);
1253 }
1254
1255 #[test]
1258 fn rare_sampler_catches_otlp_no_priority_trace() {
1259 let mut sampler = create_sampler_with_rare_enabled();
1260 sampler.probabilistic_sampler_enabled = false;
1261 sampler.error_sampling_enabled = false;
1262 sampler.otlp_sampling_rate = 0.0;
1263
1264 let mut span = create_top_level_span(1);
1265 span.attributes.insert(
1266 MetaString::from_static(OTEL_TRACE_ID_META_KEY),
1267 AttributeValue::String(MetaString::from("00000000000000000000000000000001")),
1268 );
1269 let mut trace = create_test_trace(vec![span]);
1270
1271 let (keep, priority, decision_maker, root_idx) = sampler.run_samplers(&mut trace);
1272 assert!(
1273 keep,
1274 "rare sampler should keep OTLP trace with no priority on first occurrence"
1275 );
1276 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1277 assert_eq!(decision_maker, "");
1278 assert_eq!(
1279 trace.spans()[root_idx.unwrap()]
1280 .attributes
1281 .get(rare_sampler::RARE_KEY)
1282 .and_then(AttributeValue::as_float),
1283 Some(1.0),
1284 "_dd.rare should be set to 1 on first occurrence"
1285 );
1286 }
1287
1288 #[test]
1293 fn rare_wins_over_probabilistic_no_decision_maker_tag() {
1294 let mut sampler = create_sampler_with_rare_enabled();
1295 sampler.sampling_rate = 1.0;
1296 sampler.probabilistic_sampler_enabled = true;
1297
1298 let span = create_top_level_span(1);
1299 let mut trace = create_test_trace(vec![span]);
1300
1301 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1302 assert!(keep);
1303 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1304 assert_eq!(decision_maker, "", "rare takes precedence—_dd.p.dm must not be set");
1305 }
1306
1307 #[test]
1312 fn rare_catches_error_trace_before_error_sampler() {
1313 let mut sampler = create_sampler_with_rare_enabled();
1314 sampler.probabilistic_sampler_enabled = false;
1315 sampler.error_sampling_enabled = true;
1316
1317 let span = create_top_level_span(1);
1318 let error_span = create_test_span(2, 1); let mut trace = create_test_trace(vec![span, error_span]);
1320
1321 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1322 assert!(keep, "rare should catch the trace before the error sampler");
1323 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1324 assert_eq!(decision_maker, "");
1325 }
1326
1327 #[test]
1332 fn manual_drop_short_circuits_before_rare() {
1333 let mut sampler = create_sampler_with_rare_enabled();
1334 sampler.probabilistic_sampler_enabled = false;
1335
1336 let mut attrs = saluki_common::collections::FastHashMap::default();
1337 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
1338 attrs.insert(
1339 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
1340 AttributeValue::Float(-1.0),
1341 ); let span = create_test_span(1, 0).with_attributes(attrs);
1343 let mut trace = create_test_trace(vec![span]);
1344
1345 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1346 assert!(!keep, "UserDrop must be dropped even when rare would match");
1347 assert_eq!(priority, -1);
1348 }
1349
1350 fn create_sampler_with_ets() -> TraceSampler {
1354 TraceSampler {
1355 error_tracking_standalone: true,
1356 ..create_test_sampler()
1357 }
1358 }
1359
1360 #[test]
1362 fn ets_keeps_trace_with_error() {
1363 let mut sampler = create_sampler_with_ets();
1364
1365 let span = create_test_span(1, 1); let mut trace = create_test_trace(vec![span]);
1367
1368 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1369 assert!(keep, "ETS should keep traces with errors");
1370 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1371 assert_eq!(decision_maker, "", "ETS does not set a decision maker");
1372 }
1373
1374 #[test]
1376 fn ets_drops_trace_without_error() {
1377 let mut sampler = create_sampler_with_ets();
1378
1379 let span = create_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
1381
1382 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1383 assert!(!keep, "ETS should drop traces without errors");
1384 assert_eq!(priority, PRIORITY_AUTO_DROP);
1385 }
1386
1387 #[test]
1389 fn ets_forwards_dropped_trace_with_dropped_flag() {
1390 let mut sampler = create_sampler_with_ets();
1391
1392 let mut attrs = saluki_common::collections::FastHashMap::default();
1394 attrs.insert(
1395 MetaString::from(KEY_SPAN_SAMPLING_MECHANISM),
1396 AttributeValue::Float(8.0),
1397 );
1398 let span = create_test_span(1, 0).with_attributes(attrs);
1399 let mut trace = create_test_trace(vec![span]);
1400
1401 let forwarded = sampler.process_trace(&mut trace);
1402 assert!(forwarded, "ETS should forward non-error traces to intake");
1403 assert!(trace.dropped_trace, "non-error ETS trace should have DroppedTrace=true");
1404 }
1405
1406 #[test]
1408 fn ets_keeps_trace_with_exception_span_event() {
1409 let mut sampler = create_sampler_with_ets();
1410
1411 let mut attrs = saluki_common::collections::FastHashMap::default();
1413 attrs.insert(
1414 MetaString::from("_dd.span_events.has_exception"),
1415 AttributeValue::String(MetaString::from("true")),
1416 );
1417 let span = create_test_span(1, 0).with_attributes(attrs);
1418 let mut trace = create_test_trace(vec![span]);
1419
1420 let (keep, _, _, _) = sampler.run_samplers(&mut trace);
1421 assert!(keep, "ETS should treat exception span events as errors");
1422 }
1423
1424 #[test]
1426 fn ets_disabled_uses_normal_sampling() {
1427 let mut sampler = create_test_sampler(); sampler.sampling_rate = 1.0;
1429 sampler.probabilistic_sampler_enabled = true;
1430
1431 let span = create_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
1433
1434 let (keep, _, decision_maker, _) = sampler.run_samplers(&mut trace);
1435 assert!(keep, "normal probabilistic sampling should keep the trace");
1436 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC);
1437 }
1438
1439 fn create_otlp_test_span(span_id: u64, error: i32) -> DdSpan {
1445 let mut attrs = saluki_common::collections::FastHashMap::default();
1446 attrs.insert(
1447 MetaString::from_static(OTEL_TRACE_ID_META_KEY),
1448 AttributeValue::String(MetaString::from("0000000000000000deadbeefcafebabe")),
1449 );
1450 create_test_span(span_id, error).with_attributes(attrs)
1451 }
1452
1453 fn create_sampler_with_ets_legacy() -> TraceSampler {
1454 TraceSampler {
1455 error_tracking_standalone: true,
1456 probabilistic_sampler_enabled: false,
1457 otlp_sampling_rate: 1.0,
1458 ..create_test_sampler()
1459 }
1460 }
1461
1462 #[test]
1465 fn ets_otlp_non_error_gets_presample_priority_and_dm() {
1466 let mut sampler = create_sampler_with_ets_legacy();
1467
1468 let span = create_otlp_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
1470
1471 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1472 assert!(!keep, "ETS should drop non-error OTLP traces");
1473 assert_eq!(
1474 priority, PRIORITY_AUTO_KEEP,
1475 "OTLP pre-sampling sets priority=AutoKeep even for ETS-dropped traces"
1476 );
1477 assert_eq!(dm, DECISION_MAKER_PROBABILISTIC, "OTLP pre-sampling sets dm=-9");
1478 }
1479
1480 #[test]
1482 fn ets_otlp_error_gets_presample_priority_and_dm() {
1483 let mut sampler = create_sampler_with_ets_legacy();
1484
1485 let span = create_otlp_test_span(1, 1); let mut trace = create_test_trace(vec![span]);
1487
1488 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1489 assert!(keep, "ETS should keep error OTLP traces");
1490 assert_eq!(priority, PRIORITY_AUTO_KEEP, "OTLP pre-sampling sets priority=AutoKeep");
1491 assert_eq!(dm, DECISION_MAKER_PROBABILISTIC, "OTLP pre-sampling sets dm=-9");
1492 }
1493
1494 #[test]
1497 fn ets_otlp_probabilistic_path_skips_presample() {
1498 let mut sampler = create_sampler_with_ets_legacy();
1499 sampler.probabilistic_sampler_enabled = true; let span = create_otlp_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
1503
1504 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1505 assert!(!keep, "ETS should drop non-error traces");
1506 assert_eq!(
1507 priority, PRIORITY_AUTO_DROP,
1508 "no pre-sampling when probabilistic path active"
1509 );
1510 assert_eq!(dm, "", "no dm when probabilistic path active");
1511 }
1512
1513 #[test]
1515 fn ets_non_otlp_unaffected_by_presample() {
1516 let mut sampler = create_sampler_with_ets_legacy();
1517
1518 let span = create_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
1520
1521 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1522 assert!(!keep, "ETS should drop non-error non-OTLP traces");
1523 assert_eq!(priority, PRIORITY_AUTO_DROP, "non-OTLP traces use default ETS priority");
1524 assert_eq!(dm, "", "non-OTLP traces get no dm");
1525 }
1526
1527 #[test]
1529 fn ets_otlp_user_priority_gets_manual_dm() {
1530 let mut sampler = create_sampler_with_ets_legacy();
1531
1532 let mut span = create_otlp_test_span(1, 0);
1533 span.attributes.insert(
1534 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
1535 AttributeValue::Float(2.0),
1536 ); let mut trace = create_test_trace(vec![span]);
1538
1539 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1540 assert!(!keep, "ETS drops non-error traces regardless of user priority");
1541 assert_eq!(priority, PRIORITY_USER_KEEP, "user priority is preserved");
1542 assert_eq!(dm, DECISION_MAKER_MANUAL, "user-set priority gets dm=-4");
1543 }
1544}