1use async_trait::async_trait;
16use memory_accounting::{MemoryBounds, MemoryBoundsBuilder};
17use saluki_common::collections::FastHashMap;
18use saluki_config::GenericConfiguration;
19use saluki_core::{
20 components::{transforms::*, ComponentContext},
21 data_model::event::{
22 trace::{Span, Trace, TraceSampling},
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 apm::ApmConfig, sample_by_rate, DECISION_MAKER_MANUAL, DECISION_MAKER_PROBABILISTIC, OTEL_TRACE_ID_META_KEY,
43 SAMPLING_PRIORITY_METRIC_KEY, TAG_DECISION_MAKER,
44};
45use crate::common::otlp::config::TracesConfig;
46
47const PRIORITY_AUTO_DROP: i32 = 0;
49const PRIORITY_AUTO_KEEP: i32 = 1;
50const PRIORITY_USER_KEEP: i32 = 2;
51
52const ERROR_SAMPLE_RATE: f64 = 1.0; const KEY_SPAN_SAMPLING_MECHANISM: &str = "_dd.span_sampling.mechanism";
56const KEY_ANALYZED_SPANS: &str = "_dd.analyzed";
57
58fn normalize_sampling_rate(rate: f64) -> f64 {
61 if rate <= 0.0 || rate >= 1.0 {
62 1.0
63 } else {
64 rate
65 }
66}
67
68#[derive(Debug)]
70pub struct TraceSamplerConfiguration {
71 apm_config: ApmConfig,
72 otlp_sampling_rate: f64,
73}
74
75impl TraceSamplerConfiguration {
76 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
78 let apm_config = ApmConfig::from_configuration(config)?;
79 let otlp_traces: TracesConfig = config.try_get_typed("otlp_config.traces")?.unwrap_or_default();
80 let otlp_sampling_rate = normalize_sampling_rate(otlp_traces.probabilistic_sampler.sampling_percentage / 100.0);
81 Ok(Self {
82 apm_config,
83 otlp_sampling_rate,
84 })
85 }
86}
87
88#[async_trait]
89impl SynchronousTransformBuilder for TraceSamplerConfiguration {
90 async fn build(&self, _context: ComponentContext) -> Result<Box<dyn SynchronousTransform + Send>, GenericError> {
91 let sampler = TraceSampler {
94 sampling_rate: self.apm_config.probabilistic_sampler_sampling_percentage() / 100.0,
95 error_sampling_enabled: self.apm_config.error_sampling_enabled(),
96 error_tracking_standalone: self.apm_config.error_tracking_standalone_enabled(),
97 probabilistic_sampler_enabled: self.apm_config.probabilistic_sampler_enabled(),
98 otlp_sampling_rate: self.otlp_sampling_rate,
99 error_sampler: errors::ErrorsSampler::new(self.apm_config.errors_per_second(), ERROR_SAMPLE_RATE),
100 priority_sampler: priority_sampler::PrioritySampler::new(
101 self.apm_config.default_env().clone(),
102 ERROR_SAMPLE_RATE,
103 self.apm_config.target_traces_per_second(),
104 ),
105 no_priority_sampler: score_sampler::NoPrioritySampler::new(
106 self.apm_config.target_traces_per_second(),
107 ERROR_SAMPLE_RATE,
108 ),
109 rare_sampler: rare_sampler::RareSampler::new(
110 self.apm_config.rare_sampler_enabled(),
111 self.apm_config.rare_sampler_tps(),
112 std::time::Duration::from_secs_f64(self.apm_config.rare_sampler_cooldown_period_secs()),
113 self.apm_config.rare_sampler_cardinality(),
114 ),
115 };
116
117 Ok(Box::new(sampler))
118 }
119}
120
121impl MemoryBounds for TraceSamplerConfiguration {
122 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
123 builder.minimum().with_single_value::<TraceSampler>("component struct");
124 }
125}
126
127pub struct TraceSampler {
128 sampling_rate: f64,
129 error_tracking_standalone: bool,
130 error_sampling_enabled: bool,
131 probabilistic_sampler_enabled: bool,
132 otlp_sampling_rate: f64,
133 error_sampler: errors::ErrorsSampler,
134 priority_sampler: priority_sampler::PrioritySampler,
135 no_priority_sampler: score_sampler::NoPrioritySampler,
136 rare_sampler: rare_sampler::RareSampler,
137}
138
139impl TraceSampler {
140 fn get_root_span_index(&self, trace: &Trace) -> Option<usize> {
143 let spans = trace.spans();
145 if spans.is_empty() {
146 return None;
147 }
148 let length = spans.len();
149 let mut parent_id_to_child: FastHashMap<u64, usize> = FastHashMap::default();
155
156 for i in 0..length {
157 let j = length - 1 - i;
160 if spans[j].parent_id() == 0 {
161 return Some(j);
162 }
163 parent_id_to_child.insert(spans[j].parent_id(), j);
164 }
165
166 for span in spans.iter() {
167 parent_id_to_child.remove(&span.span_id());
168 }
169
170 if parent_id_to_child.len() != 1 {
172 debug!(
173 "Didn't reliably find the root span for traceID:{}",
174 &spans[0].trace_id()
175 );
176 }
177
178 if let Some((_, child_idx)) = parent_id_to_child.iter().next() {
181 return Some(*child_idx);
182 }
183
184 Some(length - 1)
186 }
187
188 fn get_user_priority(&self, trace: &Trace, root_span_idx: usize) -> Option<i32> {
190 if let Some(sampling) = trace.sampling() {
192 if let Some(priority) = sampling.priority {
193 return Some(priority);
194 }
195 }
196
197 if trace.spans().is_empty() {
198 return None;
199 }
200
201 if let Some(root) = trace.spans().get(root_span_idx) {
204 if let Some(&p) = root.metrics().get(SAMPLING_PRIORITY_METRIC_KEY) {
205 return Some(p as i32);
206 }
207 }
208 let spans = trace.spans();
209 spans
210 .iter()
211 .find_map(|span| span.metrics().get(SAMPLING_PRIORITY_METRIC_KEY).map(|&p| p as i32))
212 }
213
214 fn sample_probabilistic(&self, trace_id: u64) -> bool {
216 probabilistic::ProbabilisticSampler::sample(trace_id, self.sampling_rate)
217 }
218
219 fn is_otlp_trace(&self, trace: &Trace, root_span_idx: usize) -> bool {
220 trace
221 .spans()
222 .get(root_span_idx)
223 .map(|span| {
224 span.meta()
225 .contains_key(&MetaString::from_static(OTEL_TRACE_ID_META_KEY))
226 })
227 .unwrap_or(false)
228 }
229
230 fn trace_contains_error(&self, trace: &Trace, consider_exception_span_events: bool) -> bool {
232 trace.spans().iter().any(|span| {
233 span.error() != 0 || (consider_exception_span_events && self.span_contains_exception_span_event(span))
234 })
235 }
236
237 fn span_contains_exception_span_event(&self, span: &Span) -> bool {
241 if let Some(has_exception) = span.meta().get("_dd.span_events.has_exception") {
242 return has_exception == "true";
243 }
244 false
245 }
246
247 fn otlp_pre_sample(&mut self, trace: &mut Trace, root_span_idx: usize) -> Option<(i32, &'static str)> {
255 if self.probabilistic_sampler_enabled || !self.is_otlp_trace(trace, root_span_idx) {
256 return None;
257 }
258 let (priority, dm) = if let Some(user_priority) = self.get_user_priority(trace, root_span_idx) {
259 (user_priority, DECISION_MAKER_MANUAL)
260 } else {
261 let root_trace_id = trace.spans()[root_span_idx].trace_id();
262 if sample_by_rate(root_trace_id, self.otlp_sampling_rate) {
263 (PRIORITY_AUTO_KEEP, DECISION_MAKER_PROBABILISTIC)
264 } else {
265 (PRIORITY_AUTO_DROP, DECISION_MAKER_PROBABILISTIC)
266 }
267 };
268 if priority == PRIORITY_AUTO_KEEP {
269 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
270 root_span.metrics_mut().remove(PROB_RATE_KEY);
271 }
272 }
273 Some((priority, dm))
274 }
275
276 fn analyzed_span_sampling(&self, trace: &mut Trace) -> bool {
280 let retained = trace.retain_spans(|_, span| span.metrics().contains_key(KEY_ANALYZED_SPANS));
281 if retained > 0 {
282 let sampling = TraceSampling::new(false, Some(PRIORITY_USER_KEEP), None, Some(self.sampling_rate));
284 trace.set_sampling(Some(sampling));
285 true
286 } else {
287 false
288 }
289 }
290
291 fn has_analyzed_spans(&self, trace: &Trace) -> bool {
293 trace
294 .spans()
295 .iter()
296 .any(|span| span.metrics().contains_key(KEY_ANALYZED_SPANS))
297 }
298
299 fn single_span_sampling(&self, trace: &mut Trace) -> bool {
302 let retained = trace.retain_spans(|_, span| span.metrics().contains_key(KEY_SPAN_SAMPLING_MECHANISM));
303 if retained > 0 {
304 let sampling = TraceSampling::new(
306 false,
307 Some(PRIORITY_USER_KEEP),
308 None, Some(self.sampling_rate),
310 );
311 trace.set_sampling(Some(sampling));
312 true
313 } else {
314 false
315 }
316 }
317
318 fn run_samplers(&mut self, trace: &mut Trace) -> (bool, i32, &'static str, Option<usize>) {
323 if trace.spans().is_empty() {
326 return (false, PRIORITY_AUTO_DROP, "", None);
327 }
328
329 let now = std::time::SystemTime::now();
330 let Some(root_span_idx) = self.get_root_span_index(trace) else {
331 return (false, PRIORITY_AUTO_DROP, "", None);
332 };
333
334 if self.error_tracking_standalone {
337 let otlp_pre_sample = self.otlp_pre_sample(trace, root_span_idx);
338 if self.trace_contains_error(trace, true) {
339 let keep = self.error_sampler.sample_error(now, trace, root_span_idx);
340 let default_priority = if keep { PRIORITY_AUTO_KEEP } else { PRIORITY_AUTO_DROP };
341 let (priority, dm) = otlp_pre_sample.unwrap_or((default_priority, ""));
342 return (keep, priority, dm, Some(root_span_idx));
343 }
344 let (pre_priority, pre_dm) = otlp_pre_sample.unwrap_or((PRIORITY_AUTO_DROP, ""));
345 return (false, pre_priority, pre_dm, Some(root_span_idx));
346 }
347
348 let contains_error = self.trace_contains_error(trace, false);
349
350 let rare = self.rare_sampler.sample(trace, root_span_idx);
354
355 if self.probabilistic_sampler_enabled {
357 let mut prob_keep = false;
358 let mut decision_maker = "";
359
360 if rare {
361 prob_keep = true;
363 } else {
364 let root_trace_id = trace.spans()[root_span_idx].trace_id();
366 if self.sample_probabilistic(root_trace_id) {
367 decision_maker = DECISION_MAKER_PROBABILISTIC;
368 prob_keep = true;
369
370 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
371 let metrics = root_span.metrics_mut();
372 metrics.insert(MetaString::from(PROB_RATE_KEY), self.sampling_rate);
373 }
374 } else if self.error_sampling_enabled && contains_error {
375 prob_keep = self.error_sampler.sample_error(now, trace, root_span_idx);
376 }
377 }
378
379 let priority = if prob_keep {
380 PRIORITY_AUTO_KEEP
381 } else {
382 PRIORITY_AUTO_DROP
383 };
384
385 return (prob_keep, priority, decision_maker, Some(root_span_idx));
386 }
387
388 let user_priority = self.get_user_priority(trace, root_span_idx);
389 if let Some(priority) = user_priority {
390 if priority < PRIORITY_AUTO_DROP {
391 return (false, priority, "", Some(root_span_idx));
393 }
394
395 if rare {
396 return (true, priority, "", Some(root_span_idx));
397 }
398
399 if self.priority_sampler.sample(now, trace, root_span_idx, priority, 0.0) {
400 return (true, priority, "", Some(root_span_idx));
401 }
402 } else if self.is_otlp_trace(trace, root_span_idx) {
403 if rare {
405 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
406 }
407
408 let root_trace_id = trace.spans()[root_span_idx].trace_id();
410 if sample_by_rate(root_trace_id, self.otlp_sampling_rate) {
411 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
412 root_span.metrics_mut().remove(PROB_RATE_KEY);
413 }
414 return (
415 true,
416 PRIORITY_AUTO_KEEP,
417 DECISION_MAKER_PROBABILISTIC,
418 Some(root_span_idx),
419 );
420 }
421 } else {
422 if rare {
423 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
424 }
425 if self.no_priority_sampler.sample(now, trace, root_span_idx) {
426 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
427 }
428 }
429
430 if self.error_sampling_enabled && contains_error {
431 let keep = self.error_sampler.sample_error(now, trace, root_span_idx);
432 if keep {
433 return (true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx));
434 }
435 }
436
437 (false, PRIORITY_AUTO_DROP, "", Some(root_span_idx))
439 }
440
441 fn apply_sampling_metadata(
446 &self, trace: &mut Trace, keep: bool, priority: i32, decision_maker: &str, root_span_idx: usize,
447 ) {
448 let is_otlp = self.is_otlp_trace(trace, root_span_idx);
449 let root_span_value = match trace.spans_mut().get_mut(root_span_idx) {
450 Some(span) => span,
451 None => return,
452 };
453
454 let existing_decision_maker = if decision_maker.is_empty() {
456 root_span_value.meta().get(TAG_DECISION_MAKER).cloned()
457 } else {
458 None
459 };
460 let decision_maker_meta = if decision_maker.is_empty() {
461 existing_decision_maker
462 } else {
463 Some(MetaString::from(decision_maker))
464 };
465
466 let meta = root_span_value.meta_mut();
467 if priority > 0 && !(is_otlp && self.probabilistic_sampler_enabled) {
472 if let Some(dm) = decision_maker_meta.as_ref() {
473 meta.insert(MetaString::from(TAG_DECISION_MAKER), dm.clone());
474 }
475 }
476
477 let sampling = TraceSampling::new(
479 !keep,
480 Some(priority),
481 if priority > 0 { decision_maker_meta } else { None },
482 Some(if is_otlp {
483 self.otlp_sampling_rate
484 } else {
485 self.sampling_rate
486 }),
487 );
488 trace.set_sampling(Some(sampling));
489 }
490
491 fn process_trace(&mut self, trace: &mut Trace) -> bool {
492 let (keep, priority, decision_maker, root_span_idx) = self.run_samplers(trace);
497
498 if keep || self.error_tracking_standalone {
501 if let Some(root_idx) = root_span_idx {
502 self.apply_sampling_metadata(trace, keep, priority, decision_maker, root_idx);
503 }
504 return true;
505 }
506
507 let modified = self.single_span_sampling(trace);
510 if !modified {
511 if self.analyzed_span_sampling(trace) {
513 return true;
514 }
515 } else if self.has_analyzed_spans(trace) {
516 debug!(
518 "Detected both analytics events AND single span sampling in the same trace. Single span sampling wins because App Analytics is deprecated."
519 );
520 return true;
521 }
522
523 if modified {
525 return true;
526 }
527
528 debug!("Dropping trace with priority {}", priority);
530 false
531 }
532}
533
534impl SynchronousTransform for TraceSampler {
535 fn transform_buffer(&mut self, buffer: &mut EventsBuffer) {
536 buffer.remove_if(|event| match event {
537 Event::Trace(trace) => !self.process_trace(trace),
538 _ => false,
539 });
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use std::collections::HashMap;
546
547 use saluki_context::tags::TagSet;
548 use saluki_core::data_model::event::trace::{Span as DdSpan, Trace};
549 const PRIORITY_USER_DROP: i32 = -1;
550
551 use super::*;
552 fn create_test_sampler() -> TraceSampler {
553 TraceSampler {
554 sampling_rate: 1.0,
555 error_sampling_enabled: true,
556 error_tracking_standalone: false,
557 probabilistic_sampler_enabled: true,
558 otlp_sampling_rate: 1.0,
559 error_sampler: errors::ErrorsSampler::new(10.0, 1.0),
560 priority_sampler: priority_sampler::PrioritySampler::new(MetaString::from("agent-env"), 1.0, 10.0),
561 no_priority_sampler: score_sampler::NoPrioritySampler::new(10.0, 1.0),
562 rare_sampler: rare_sampler::RareSampler::new(false, 5.0, std::time::Duration::from_secs(300), 200),
563 }
564 }
565
566 fn create_test_span(trace_id: u64, span_id: u64, error: i32) -> DdSpan {
567 DdSpan::new(
568 MetaString::from("test-service"),
569 MetaString::from("test-operation"),
570 MetaString::from("test-resource"),
571 MetaString::from("test-type"),
572 trace_id,
573 span_id,
574 0, 0, 1000, error,
578 )
579 }
580
581 fn create_test_span_with_metrics(trace_id: u64, span_id: u64, metrics: HashMap<String, f64>) -> DdSpan {
582 let mut metrics_map = saluki_common::collections::FastHashMap::default();
583 for (k, v) in metrics {
584 metrics_map.insert(MetaString::from(k), v);
585 }
586 create_test_span(trace_id, span_id, 0).with_metrics(metrics_map)
587 }
588
589 #[allow(dead_code)]
590 fn create_test_span_with_meta(trace_id: u64, span_id: u64, meta: HashMap<String, String>) -> DdSpan {
591 let mut meta_map = saluki_common::collections::FastHashMap::default();
592 for (k, v) in meta {
593 meta_map.insert(MetaString::from(k), MetaString::from(v));
594 }
595 create_test_span(trace_id, span_id, 0).with_meta(meta_map)
596 }
597
598 fn create_test_trace(spans: Vec<DdSpan>) -> Trace {
599 let tags = TagSet::default();
600 Trace::new(spans, tags)
601 }
602
603 #[test]
604 fn test_user_priority_detection() {
605 let sampler = create_test_sampler();
606
607 let mut metrics = HashMap::new();
609 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
610 let span = create_test_span_with_metrics(12345, 1, metrics);
611 let trace = create_test_trace(vec![span]);
612 let root_idx = sampler.get_root_span_index(&trace).unwrap();
613
614 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(2));
615
616 let mut metrics = HashMap::new();
618 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), -1.0);
619 let span = create_test_span_with_metrics(12345, 1, metrics);
620 let trace = create_test_trace(vec![span]);
621 let root_idx = sampler.get_root_span_index(&trace).unwrap();
622
623 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(-1));
624
625 let span = create_test_span(12345, 1, 0);
627 let trace = create_test_trace(vec![span]);
628 let root_idx = sampler.get_root_span_index(&trace).unwrap();
629
630 assert_eq!(sampler.get_user_priority(&trace, root_idx), None);
631 }
632
633 #[test]
634 fn test_trace_level_priority_takes_precedence() {
635 let sampler = create_test_sampler();
636
637 let mut metrics_root = HashMap::new();
640 metrics_root.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 0.0);
641 let root_span = create_test_span_with_metrics(12345, 1, metrics_root);
642
643 let mut metrics_later = HashMap::new();
644 metrics_later.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 1.0);
645 let later_span = create_test_span_with_metrics(12345, 2, metrics_later).with_parent_id(1);
646
647 let mut trace = create_test_trace(vec![root_span, later_span]);
648 let root_idx = sampler.get_root_span_index(&trace).unwrap();
649
650 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(0));
652
653 trace.set_sampling(Some(TraceSampling::new(false, Some(2), None, None)));
655
656 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(2));
658
659 let span_no_priority = create_test_span(12345, 3, 0);
661 let mut trace_only_trace_level = create_test_trace(vec![span_no_priority]);
662 trace_only_trace_level.set_sampling(Some(TraceSampling::new(false, Some(1), None, None)));
663 let root_idx = sampler.get_root_span_index(&trace_only_trace_level).unwrap();
664
665 assert_eq!(sampler.get_user_priority(&trace_only_trace_level, root_idx), Some(1));
666 }
667
668 #[test]
669 fn test_manual_keep_with_trace_level_priority() {
670 let mut sampler = create_test_sampler();
671 sampler.probabilistic_sampler_enabled = false; let span = create_test_span(12345, 1, 0);
675 let mut trace = create_test_trace(vec![span]);
676 trace.set_sampling(Some(TraceSampling::new(false, Some(PRIORITY_USER_KEEP), None, None)));
677
678 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
679 assert!(keep);
680 assert_eq!(priority, PRIORITY_USER_KEEP);
681 assert_eq!(decision_maker, "");
682
683 let span = create_test_span(12345, 1, 0);
685 let mut trace = create_test_trace(vec![span]);
686 trace.set_sampling(Some(TraceSampling::new(false, Some(PRIORITY_USER_DROP), None, None)));
687
688 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
689 assert!(!keep); assert_eq!(priority, PRIORITY_USER_DROP);
691
692 let span = create_test_span(12345, 1, 0);
694 let mut trace = create_test_trace(vec![span]);
695 trace.set_sampling(Some(TraceSampling::new(false, Some(PRIORITY_AUTO_KEEP), None, None)));
696
697 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
698 assert!(keep);
699 assert_eq!(priority, PRIORITY_AUTO_KEEP);
700 assert_eq!(decision_maker, "");
701 }
702
703 #[test]
704 fn test_probabilistic_sampling_determinism() {
705 let sampler = create_test_sampler();
706
707 let trace_id = 0x1234567890ABCDEF_u64;
709 let result1 = sampler.sample_probabilistic(trace_id);
710 let result2 = sampler.sample_probabilistic(trace_id);
711 assert_eq!(result1, result2);
712 }
713
714 #[test]
715 fn test_error_detection() {
716 let sampler = create_test_sampler();
717
718 let span_with_error = create_test_span(12345, 1, 1);
720 let trace = create_test_trace(vec![span_with_error]);
721 assert!(sampler.trace_contains_error(&trace, false));
722
723 let span_without_error = create_test_span(12345, 1, 0);
725 let trace = create_test_trace(vec![span_without_error]);
726 assert!(!sampler.trace_contains_error(&trace, false));
727 }
728
729 #[test]
730 fn test_sampling_priority_order() {
731 let mut sampler = create_test_sampler();
733 sampler.sampling_rate = 0.5; sampler.probabilistic_sampler_enabled = true;
735
736 let span_with_error = create_test_span(u64::MAX - 1, 1, 1);
739 let mut trace = create_test_trace(vec![span_with_error]);
740
741 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
742 assert!(keep);
743 assert_eq!(priority, PRIORITY_AUTO_KEEP);
744 assert_eq!(decision_maker, ""); let mut sampler = create_test_sampler();
748 sampler.probabilistic_sampler_enabled = false; let mut metrics = HashMap::new();
751 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
752 let span = create_test_span_with_metrics(12345, 1, metrics);
753 let mut trace = create_test_trace(vec![span]);
754
755 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
756 assert!(keep);
757 assert_eq!(priority, 2); assert_eq!(decision_maker, "");
759 }
760
761 #[test]
762 fn test_empty_trace_handling() {
763 let mut sampler = create_test_sampler();
764 let mut trace = create_test_trace(vec![]);
765
766 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
767 assert!(!keep);
768 assert_eq!(priority, PRIORITY_AUTO_DROP);
769 }
770
771 #[test]
772 fn test_root_span_detection() {
773 let sampler = create_test_sampler();
774
775 let root_span = DdSpan::new(
777 MetaString::from("service"),
778 MetaString::from("operation"),
779 MetaString::from("resource"),
780 MetaString::from("type"),
781 12345,
782 1,
783 0, 0,
785 1000,
786 0,
787 );
788 let child_span = DdSpan::new(
789 MetaString::from("service"),
790 MetaString::from("child_op"),
791 MetaString::from("resource"),
792 MetaString::from("type"),
793 12345,
794 2,
795 1, 100,
797 500,
798 0,
799 );
800 let trace = create_test_trace(vec![child_span.clone(), root_span.clone()]);
802 let root_idx = sampler.get_root_span_index(&trace).unwrap();
803 assert_eq!(trace.spans()[root_idx].span_id(), 1);
804
805 let orphan_span = DdSpan::new(
807 MetaString::from("service"),
808 MetaString::from("orphan"),
809 MetaString::from("resource"),
810 MetaString::from("type"),
811 12345,
812 3,
813 999, 200,
815 300,
816 0,
817 );
818 let trace = create_test_trace(vec![orphan_span]);
819 let root_idx = sampler.get_root_span_index(&trace).unwrap();
820 assert_eq!(trace.spans()[root_idx].span_id(), 3);
821
822 let span1 = create_test_span(12345, 1, 0);
824 let span2 = create_test_span(12345, 2, 0);
825 let trace = create_test_trace(vec![span1, span2]);
826 let root_idx = sampler.get_root_span_index(&trace).unwrap();
828 assert_eq!(trace.spans()[root_idx].span_id(), 2);
829 }
830
831 #[test]
832 fn test_single_span_sampling() {
833 let mut sampler = create_test_sampler();
834
835 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
838
839 let mut metrics_map = saluki_common::collections::FastHashMap::default();
841 metrics_map.insert(MetaString::from(KEY_SPAN_SAMPLING_MECHANISM), 8.0); let sss_span = create_test_span(12345, 1, 0).with_metrics(metrics_map.clone());
843
844 let regular_span = create_test_span(12345, 2, 0);
846
847 let mut trace = create_test_trace(vec![sss_span.clone(), regular_span]);
848
849 let modified = sampler.single_span_sampling(&mut trace);
851 assert!(modified);
852 assert_eq!(trace.spans().len(), 1); assert_eq!(trace.spans()[0].span_id(), 1); assert!(trace.sampling().is_some());
857 assert_eq!(trace.sampling().as_ref().unwrap().priority, Some(PRIORITY_USER_KEEP));
858
859 let trace_without_sss = create_test_trace(vec![create_test_span(12345, 3, 0)]);
861 let mut trace_copy = trace_without_sss.clone();
862 let modified = sampler.single_span_sampling(&mut trace_copy);
863 assert!(!modified);
864 assert_eq!(trace_copy.spans().len(), trace_without_sss.spans().len());
865 }
866
867 #[test]
868 fn test_analytics_events() {
869 let sampler = create_test_sampler();
870
871 let mut metrics_map = saluki_common::collections::FastHashMap::default();
873 metrics_map.insert(MetaString::from(KEY_ANALYZED_SPANS), 1.0);
874 let analyzed_span = create_test_span(12345, 1, 0).with_metrics(metrics_map.clone());
875 let regular_span = create_test_span(12345, 2, 0);
876
877 let mut trace = create_test_trace(vec![analyzed_span.clone(), regular_span]);
878
879 let analyzed_span_ids: Vec<u64> = trace
880 .spans()
881 .iter()
882 .filter(|span| span.metrics().contains_key(KEY_ANALYZED_SPANS))
883 .map(|span| span.span_id())
884 .collect();
885 assert_eq!(analyzed_span_ids, vec![1]);
886
887 assert!(sampler.has_analyzed_spans(&trace));
888 let modified = sampler.analyzed_span_sampling(&mut trace);
889 assert!(modified);
890 assert_eq!(trace.spans().len(), 1);
891 assert_eq!(trace.spans()[0].span_id(), 1);
892 assert!(trace.sampling().is_some());
893
894 let trace_no_analytics = create_test_trace(vec![create_test_span(12345, 3, 0)]);
896 let mut trace_no_analytics_copy = trace_no_analytics.clone();
897 let analyzed_span_ids: Vec<u64> = trace_no_analytics
898 .spans()
899 .iter()
900 .filter(|span| span.metrics().contains_key(KEY_ANALYZED_SPANS))
901 .map(|span| span.span_id())
902 .collect();
903 assert!(analyzed_span_ids.is_empty());
904 assert!(!sampler.has_analyzed_spans(&trace_no_analytics));
905 let modified = sampler.analyzed_span_sampling(&mut trace_no_analytics_copy);
906 assert!(!modified);
907 assert_eq!(trace_no_analytics_copy.spans().len(), trace_no_analytics.spans().len());
908 }
909
910 #[test]
911 fn test_probabilistic_sampling_with_prob_rate_key() {
912 let mut sampler = create_test_sampler();
913 sampler.sampling_rate = 0.75; sampler.probabilistic_sampler_enabled = true;
915
916 let trace_id = 12345_u64;
918 let root_span = DdSpan::new(
919 MetaString::from("service"),
920 MetaString::from("operation"),
921 MetaString::from("resource"),
922 MetaString::from("type"),
923 trace_id,
924 1,
925 0, 0,
927 1000,
928 0,
929 );
930 let mut trace = create_test_trace(vec![root_span]);
931
932 let (keep, priority, decision_maker, root_span_idx) = sampler.run_samplers(&mut trace);
933
934 if keep && decision_maker == DECISION_MAKER_PROBABILISTIC {
935 assert_eq!(priority, PRIORITY_AUTO_KEEP);
937 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC); let root_idx = root_span_idx.unwrap_or(0);
941 let root_span = &trace.spans()[root_idx];
942 assert!(root_span.metrics().contains_key(PROB_RATE_KEY));
943 assert_eq!(*root_span.metrics().get(PROB_RATE_KEY).unwrap(), 0.75);
944
945 let mut trace_with_metadata = trace.clone();
947 sampler.apply_sampling_metadata(&mut trace_with_metadata, keep, priority, decision_maker, root_idx);
948
949 let modified_root = &trace_with_metadata.spans()[root_idx];
951 assert!(modified_root.meta().contains_key(TAG_DECISION_MAKER));
952 assert_eq!(
953 modified_root.meta().get(TAG_DECISION_MAKER).unwrap(),
954 &MetaString::from(DECISION_MAKER_PROBABILISTIC)
955 );
956 }
957 }
958
959 fn create_top_level_span(trace_id: u64, span_id: u64) -> DdSpan {
969 let mut metrics = saluki_common::collections::FastHashMap::default();
970 metrics.insert(MetaString::from("_top_level"), 1.0);
971 create_test_span(trace_id, span_id, 0).with_metrics(metrics)
972 }
973
974 fn create_sampler_with_rare_enabled() -> TraceSampler {
977 TraceSampler {
978 rare_sampler: rare_sampler::RareSampler::new(true, 1000.0, std::time::Duration::from_secs(300), 200),
979 ..create_test_sampler()
980 }
981 }
982
983 #[test]
987 fn rare_sampler_catches_unsampled_trace() {
988 let mut sampler = create_sampler_with_rare_enabled();
989 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
991
992 let span = create_top_level_span(111, 1);
993 let mut trace = create_test_trace(vec![span]);
994
995 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
996 assert!(keep, "rare sampler should catch first occurrence");
997 assert_eq!(priority, PRIORITY_AUTO_KEEP);
998 assert_eq!(decision_maker, "", "rare sampler does not set _dd.p.dm");
999 }
1000
1001 #[test]
1005 fn rare_sampler_sets_rare_metric_on_first_occurrence() {
1006 let mut sampler = create_sampler_with_rare_enabled();
1007 sampler.sampling_rate = 0.0;
1008 sampler.probabilistic_sampler_enabled = true;
1009
1010 let span = create_top_level_span(222, 1);
1011 let mut trace = create_test_trace(vec![span]);
1012
1013 let (keep, _, _, root_idx) = sampler.run_samplers(&mut trace);
1014 assert!(keep);
1015 let root = &trace.spans()[root_idx.unwrap()];
1016 assert_eq!(
1017 root.metrics().get(rare_sampler::RARE_KEY).copied(),
1018 Some(1.0),
1019 "_dd.rare should be 1 on first occurrence"
1020 );
1021 }
1022
1023 #[test]
1028 fn rare_sampler_does_not_resample_within_ttl() {
1029 let mut sampler = create_sampler_with_rare_enabled();
1030 sampler.sampling_rate = 0.0;
1031 sampler.probabilistic_sampler_enabled = true;
1032
1033 let span1 = create_top_level_span(333, 1);
1035 let mut trace1 = create_test_trace(vec![span1]);
1036 let (keep1, _, _, _) = sampler.run_samplers(&mut trace1);
1037 assert!(keep1, "first occurrence should be kept by rare sampler");
1038
1039 let span2 = create_top_level_span(333, 2);
1042 let mut trace2 = create_test_trace(vec![span2]);
1043 let (keep2, priority2, _, _) = sampler.run_samplers(&mut trace2);
1044 assert!(!keep2, "second occurrence within TTL should be dropped");
1045 assert_eq!(priority2, PRIORITY_AUTO_DROP);
1046 }
1047
1048 #[test]
1052 fn rare_sampler_disabled_does_not_catch_unsampled() {
1053 let mut sampler = create_test_sampler(); sampler.sampling_rate = 0.0;
1055 sampler.probabilistic_sampler_enabled = true;
1056
1057 let span = create_top_level_span(444, 1);
1058 let mut trace = create_test_trace(vec![span]);
1059
1060 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1061 assert!(!keep, "rare disabled should not catch the trace");
1062 assert_eq!(priority, PRIORITY_AUTO_DROP);
1063 }
1064
1065 #[test]
1068 fn rare_sampler_catches_priority_auto_drop_in_legacy_path() {
1069 let mut sampler = create_sampler_with_rare_enabled();
1070 sampler.probabilistic_sampler_enabled = false;
1071
1072 let mut metrics = saluki_common::collections::FastHashMap::default();
1073 metrics.insert(MetaString::from("_top_level"), 1.0);
1074 metrics.insert(
1075 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
1076 PRIORITY_AUTO_DROP as f64,
1077 );
1078 let span = create_test_span(555, 1, 0).with_metrics(metrics);
1079 let mut trace = create_test_trace(vec![span]);
1080
1081 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1082 assert!(keep, "rare sampler should catch PriorityAutoDrop on first occurrence");
1083 assert_eq!(priority, PRIORITY_AUTO_DROP, "tracer-set priority should be preserved");
1084 assert_eq!(decision_maker, "");
1085 }
1086
1087 #[test]
1090 fn rare_sampler_preserves_user_keep_priority_in_legacy_path() {
1091 let mut sampler = create_sampler_with_rare_enabled();
1092 sampler.probabilistic_sampler_enabled = false;
1093
1094 let mut metrics = saluki_common::collections::FastHashMap::default();
1095 metrics.insert(MetaString::from("_top_level"), 1.0);
1096 metrics.insert(MetaString::from(SAMPLING_PRIORITY_METRIC_KEY), 2.0); let span = create_test_span(556, 1, 0).with_metrics(metrics);
1098 let mut trace = create_test_trace(vec![span]);
1099
1100 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1101 assert!(keep);
1102 assert_eq!(priority, 2, "UserKeep priority must not be downgraded to AutoKeep");
1103 }
1104
1105 #[test]
1107 fn probabilistic_100_percent_keeps_trace_with_decision_maker() {
1108 let mut sampler = create_test_sampler(); sampler.sampling_rate = 1.0;
1110 sampler.probabilistic_sampler_enabled = true;
1111
1112 let span = create_top_level_span(666, 1);
1113 let mut trace = create_test_trace(vec![span]);
1114
1115 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1116 assert!(keep);
1117 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1118 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC);
1119 }
1120
1121 #[test]
1123 fn probabilistic_0_percent_drops_trace() {
1124 let mut sampler = create_test_sampler(); sampler.sampling_rate = 0.0;
1126 sampler.probabilistic_sampler_enabled = true;
1127 sampler.error_sampling_enabled = false;
1128
1129 let span = create_top_level_span(777, 1);
1130 let mut trace = create_test_trace(vec![span]);
1131
1132 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1133 assert!(!keep);
1134 assert_eq!(priority, PRIORITY_AUTO_DROP);
1135 }
1136
1137 #[test]
1140 fn rare_sampler_catches_otlp_no_priority_trace() {
1141 let mut sampler = create_sampler_with_rare_enabled();
1142 sampler.probabilistic_sampler_enabled = false;
1143 sampler.error_sampling_enabled = false;
1144 sampler.otlp_sampling_rate = 0.0;
1145
1146 let mut meta = saluki_common::collections::FastHashMap::default();
1147 meta.insert(
1148 MetaString::from_static(OTEL_TRACE_ID_META_KEY),
1149 MetaString::from("00000000000000000000000000000001"),
1150 );
1151 let span = create_top_level_span(888, 1).with_meta(meta);
1152 let mut trace = create_test_trace(vec![span]);
1153
1154 let (keep, priority, decision_maker, root_idx) = sampler.run_samplers(&mut trace);
1155 assert!(
1156 keep,
1157 "rare sampler should keep OTLP trace with no priority on first occurrence"
1158 );
1159 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1160 assert_eq!(decision_maker, "");
1161 assert_eq!(
1162 trace.spans()[root_idx.unwrap()]
1163 .metrics()
1164 .get(rare_sampler::RARE_KEY)
1165 .copied(),
1166 Some(1.0),
1167 "_dd.rare should be set to 1 on first occurrence"
1168 );
1169 }
1170
1171 #[test]
1176 fn rare_wins_over_probabilistic_no_decision_maker_tag() {
1177 let mut sampler = create_sampler_with_rare_enabled();
1178 sampler.sampling_rate = 1.0;
1179 sampler.probabilistic_sampler_enabled = true;
1180
1181 let span = create_top_level_span(901, 1);
1182 let mut trace = create_test_trace(vec![span]);
1183
1184 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1185 assert!(keep);
1186 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1187 assert_eq!(decision_maker, "", "rare takes precedence — _dd.p.dm must not be set");
1188 }
1189
1190 #[test]
1195 fn rare_catches_error_trace_before_error_sampler() {
1196 let mut sampler = create_sampler_with_rare_enabled();
1197 sampler.probabilistic_sampler_enabled = false;
1198 sampler.error_sampling_enabled = true;
1199
1200 let span = create_top_level_span(902, 1);
1201 let error_span = create_test_span(902, 2, 1); let mut trace = create_test_trace(vec![span, error_span]);
1203
1204 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1205 assert!(keep, "rare should catch the trace before the error sampler");
1206 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1207 assert_eq!(decision_maker, "");
1208 }
1209
1210 #[test]
1215 fn manual_drop_short_circuits_before_rare() {
1216 let mut sampler = create_sampler_with_rare_enabled();
1217 sampler.probabilistic_sampler_enabled = false;
1218
1219 let mut metrics = saluki_common::collections::FastHashMap::default();
1220 metrics.insert(MetaString::from("_top_level"), 1.0);
1221 metrics.insert(MetaString::from(SAMPLING_PRIORITY_METRIC_KEY), -1.0); let span = create_test_span(903, 1, 0).with_metrics(metrics);
1223 let mut trace = create_test_trace(vec![span]);
1224
1225 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1226 assert!(!keep, "UserDrop must be dropped even when rare would match");
1227 assert_eq!(priority, -1);
1228 }
1229
1230 fn create_sampler_with_ets() -> TraceSampler {
1234 TraceSampler {
1235 error_tracking_standalone: true,
1236 ..create_test_sampler()
1237 }
1238 }
1239
1240 #[test]
1242 fn ets_keeps_trace_with_error() {
1243 let mut sampler = create_sampler_with_ets();
1244
1245 let span = create_test_span(100, 1, 1); let mut trace = create_test_trace(vec![span]);
1247
1248 let (keep, priority, decision_maker, _) = sampler.run_samplers(&mut trace);
1249 assert!(keep, "ETS should keep traces with errors");
1250 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1251 assert_eq!(decision_maker, "", "ETS does not set a decision maker");
1252 }
1253
1254 #[test]
1256 fn ets_drops_trace_without_error() {
1257 let mut sampler = create_sampler_with_ets();
1258
1259 let span = create_test_span(101, 1, 0); let mut trace = create_test_trace(vec![span]);
1261
1262 let (keep, priority, _, _) = sampler.run_samplers(&mut trace);
1263 assert!(!keep, "ETS should drop traces without errors");
1264 assert_eq!(priority, PRIORITY_AUTO_DROP);
1265 }
1266
1267 #[test]
1269 fn ets_forwards_dropped_trace_with_dropped_flag() {
1270 let mut sampler = create_sampler_with_ets();
1271
1272 let mut metrics = saluki_common::collections::FastHashMap::default();
1274 metrics.insert(MetaString::from(KEY_SPAN_SAMPLING_MECHANISM), 8.0);
1275 let span = create_test_span(102, 1, 0).with_metrics(metrics);
1276 let mut trace = create_test_trace(vec![span]);
1277
1278 let forwarded = sampler.process_trace(&mut trace);
1279 assert!(forwarded, "ETS should forward non-error traces to intake");
1280 assert!(
1281 trace.sampling().is_some_and(|s| s.dropped_trace),
1282 "non-error ETS trace should have DroppedTrace=true"
1283 );
1284 }
1285
1286 #[test]
1288 fn ets_keeps_trace_with_exception_span_event() {
1289 let mut sampler = create_sampler_with_ets();
1290
1291 let mut meta = saluki_common::collections::FastHashMap::default();
1293 meta.insert(
1294 MetaString::from("_dd.span_events.has_exception"),
1295 MetaString::from("true"),
1296 );
1297 let span = create_test_span(104, 1, 0).with_meta(meta);
1298 let mut trace = create_test_trace(vec![span]);
1299
1300 let (keep, _, _, _) = sampler.run_samplers(&mut trace);
1301 assert!(keep, "ETS should treat exception span events as errors");
1302 }
1303
1304 #[test]
1306 fn ets_disabled_uses_normal_sampling() {
1307 let mut sampler = create_test_sampler(); sampler.sampling_rate = 1.0;
1309 sampler.probabilistic_sampler_enabled = true;
1310
1311 let span = create_test_span(105, 1, 0); let mut trace = create_test_trace(vec![span]);
1313
1314 let (keep, _, decision_maker, _) = sampler.run_samplers(&mut trace);
1315 assert!(keep, "normal probabilistic sampling should keep the trace");
1316 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC);
1317 }
1318
1319 fn create_otlp_test_span(trace_id: u64, span_id: u64, error: i32) -> DdSpan {
1325 let mut meta = saluki_common::collections::FastHashMap::default();
1326 meta.insert(
1327 MetaString::from_static(OTEL_TRACE_ID_META_KEY),
1328 MetaString::from("0000000000000000deadbeefcafebabe"),
1329 );
1330 create_test_span(trace_id, span_id, error).with_meta(meta)
1331 }
1332
1333 fn create_sampler_with_ets_legacy() -> TraceSampler {
1334 TraceSampler {
1335 error_tracking_standalone: true,
1336 probabilistic_sampler_enabled: false,
1337 otlp_sampling_rate: 1.0,
1338 ..create_test_sampler()
1339 }
1340 }
1341
1342 #[test]
1345 fn ets_otlp_non_error_gets_presample_priority_and_dm() {
1346 let mut sampler = create_sampler_with_ets_legacy();
1347
1348 let span = create_otlp_test_span(200, 1, 0); let mut trace = create_test_trace(vec![span]);
1350
1351 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1352 assert!(!keep, "ETS should drop non-error OTLP traces");
1353 assert_eq!(
1354 priority, PRIORITY_AUTO_KEEP,
1355 "OTLP pre-sampling sets priority=AutoKeep even for ETS-dropped traces"
1356 );
1357 assert_eq!(dm, DECISION_MAKER_PROBABILISTIC, "OTLP pre-sampling sets dm=-9");
1358 }
1359
1360 #[test]
1362 fn ets_otlp_error_gets_presample_priority_and_dm() {
1363 let mut sampler = create_sampler_with_ets_legacy();
1364
1365 let span = create_otlp_test_span(201, 1, 1); let mut trace = create_test_trace(vec![span]);
1367
1368 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1369 assert!(keep, "ETS should keep error OTLP traces");
1370 assert_eq!(priority, PRIORITY_AUTO_KEEP, "OTLP pre-sampling sets priority=AutoKeep");
1371 assert_eq!(dm, DECISION_MAKER_PROBABILISTIC, "OTLP pre-sampling sets dm=-9");
1372 }
1373
1374 #[test]
1377 fn ets_otlp_probabilistic_path_skips_presample() {
1378 let mut sampler = create_sampler_with_ets_legacy();
1379 sampler.probabilistic_sampler_enabled = true; let span = create_otlp_test_span(202, 1, 0); let mut trace = create_test_trace(vec![span]);
1383
1384 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1385 assert!(!keep, "ETS should drop non-error traces");
1386 assert_eq!(
1387 priority, PRIORITY_AUTO_DROP,
1388 "no pre-sampling when probabilistic path active"
1389 );
1390 assert_eq!(dm, "", "no dm when probabilistic path active");
1391 }
1392
1393 #[test]
1395 fn ets_non_otlp_unaffected_by_presample() {
1396 let mut sampler = create_sampler_with_ets_legacy();
1397
1398 let span = create_test_span(203, 1, 0); let mut trace = create_test_trace(vec![span]);
1400
1401 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1402 assert!(!keep, "ETS should drop non-error non-OTLP traces");
1403 assert_eq!(priority, PRIORITY_AUTO_DROP, "non-OTLP traces use default ETS priority");
1404 assert_eq!(dm, "", "non-OTLP traces get no dm");
1405 }
1406
1407 #[test]
1409 fn ets_otlp_user_priority_gets_manual_dm() {
1410 let mut sampler = create_sampler_with_ets_legacy();
1411
1412 let mut metrics = saluki_common::collections::FastHashMap::default();
1413 metrics.insert(MetaString::from(SAMPLING_PRIORITY_METRIC_KEY), 2.0); let span = create_otlp_test_span(204, 1, 0).with_metrics(metrics); let mut trace = create_test_trace(vec![span]);
1416
1417 let (keep, priority, dm, _) = sampler.run_samplers(&mut trace);
1418 assert!(!keep, "ETS drops non-error traces regardless of user priority");
1419 assert_eq!(priority, PRIORITY_USER_KEEP, "user priority is preserved");
1420 assert_eq!(dm, DECISION_MAKER_MANUAL, "user-set priority gets dm=-4");
1421 }
1422}