1use std::collections::HashMap;
11use std::sync::{Arc, LazyLock};
12
13use agent_data_plane_config::domains;
14use async_trait::async_trait;
15use datadog_agent_apm::sampling::{RemoteSampling, SamplerSettings, TraceSamplingSubscription};
16use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
17use saluki_core::{
18 components::{transforms::*, BuildContext},
19 data_model::event::{
20 trace::{AttributeValue, Span, Trace, TraceIngestInfo},
21 Event, EventType,
22 },
23 topology::{EventsBuffer, OutputDefinition},
24};
25use saluki_error::GenericError;
26use stringtheory::MetaString;
27use tokio::select;
28use tracing::{debug, warn};
29
30mod catalog;
31mod core_sampler;
32mod errors;
33mod event_processor;
34mod priority_sampler;
35mod probabilistic;
36mod rare_sampler;
37mod score_sampler;
38mod signature;
39mod telemetry;
40
41use self::event_processor::EventProcessor;
42use self::probabilistic::PROB_RATE_KEY;
43use self::telemetry::{DecisionWindow, WINDOW};
44use crate::common::datadog::{
45 compute_top_level, get_root_span_index, get_trace_env, sample_by_rate, DECISION_MAKER_MANUAL,
46 DECISION_MAKER_PROBABILISTIC, SAMPLING_PRIORITY_METRIC_KEY, TAG_DECISION_MAKER, TAG_ORIGIN,
47};
48use crate::config::AgentEnv;
49
50const PRIORITY_AUTO_DROP: i32 = 0;
52const PRIORITY_AUTO_KEEP: i32 = 1;
53const PRIORITY_USER_KEEP: i32 = 2;
54
55const KEY_SPAN_SAMPLING_MECHANISM: &str = "_dd.span_sampling.mechanism";
57const KEY_ANALYZED_SPANS: &str = "_dd.analyzed";
58
59#[derive(Debug)]
61pub struct TraceSamplerConfiguration {
62 probabilistic_sampler_enabled: bool,
63 probabilistic_hash_seed: u32,
64 probabilistic_full_trace_id: bool,
65 sampling_percentage: f64,
66 error_sampling_enabled: bool,
67 error_tracking_standalone: bool,
68 errors_per_second: f64,
69 target_traces_per_second: f64,
70 extra_sample_rate: f64,
71 max_catalog_entries: usize,
72 agent_env: AgentEnv,
74 rare_sampler_enabled: bool,
75 rare_sampler_tps: f64,
76 rare_sampler_cooldown_secs: f64,
77 rare_sampler_cardinality: usize,
78 compute_top_level_by_span_kind: bool,
79 analyzed_spans_by_service: HashMap<String, HashMap<String, f64>>,
80 analyzed_rate_by_service: HashMap<String, f64>,
81 max_events_per_second: f64,
82
83 remote_sampling: TraceSamplingSubscription,
85}
86
87impl TraceSamplerConfiguration {
88 pub fn from_configuration(
94 traces: &domains::traces::Domain, otlp_traces: &domains::otlp::Traces, agent_env: AgentEnv,
95 ) -> Self {
96 if !traces.analyzed_rate_by_service.is_empty() {
97 warn!("analyzed_rate_by_service is deprecated, please use analyzed_spans instead");
98 }
99 Self {
100 probabilistic_sampler_enabled: traces.probabilistic_sampler.enabled,
101 probabilistic_hash_seed: traces.probabilistic_sampler.hash_seed,
102 probabilistic_full_trace_id: traces
103 .features
104 .contains(&domains::traces::ApmFeature::ProbabilisticSamplerFullTraceId),
105 sampling_percentage: traces.probabilistic_sampler.sampling_percentage,
106 error_sampling_enabled: traces.error_sampling_enabled,
107 error_tracking_standalone: traces.error_tracking_standalone_enabled,
108 errors_per_second: traces.errors_per_second,
109 target_traces_per_second: traces.target_traces_per_second,
110 extra_sample_rate: traces.extra_sample_rate,
111 max_catalog_entries: traces.max_catalog_entries,
112 agent_env,
113 rare_sampler_enabled: traces.enable_rare_sampler,
114 rare_sampler_tps: traces.rare_sampler.tps,
115 rare_sampler_cooldown_secs: traces.rare_sampler.cooldown,
116 rare_sampler_cardinality: traces.rare_sampler.cardinality,
117 compute_top_level_by_span_kind: otlp_traces.enable_compute_top_level_by_span_kind,
118 analyzed_spans_by_service: traces.analyzed_spans_by_service.clone(),
119 analyzed_rate_by_service: traces.analyzed_rate_by_service.clone(),
120 max_events_per_second: traces.max_events_per_second,
121 remote_sampling: TraceSamplingSubscription::default(),
122 }
123 }
124
125 pub fn with_remote_sampling(mut self, remote_sampling: TraceSamplingSubscription) -> Self {
130 self.remote_sampling = remote_sampling;
131 self
132 }
133
134 fn build_sampler(&self) -> TraceSampler {
135 let telemetry = DecisionWindow::new();
136 let event_processor = EventProcessor::new(
137 &self.analyzed_spans_by_service,
138 &self.analyzed_rate_by_service,
139 self.max_events_per_second,
140 );
141 TraceSampler {
142 sampling_rate: self.sampling_percentage / 100.0,
143 error_sampling_enabled: self.error_sampling_enabled,
144 error_tracking_standalone: self.error_tracking_standalone,
145 probabilistic_sampler_enabled: self.probabilistic_sampler_enabled,
146 probabilistic: probabilistic::ProbabilisticSampler::new(
147 self.probabilistic_hash_seed,
148 self.probabilistic_full_trace_id,
149 ),
150 error_sampler: errors::ErrorsSampler::new(self.errors_per_second, self.extra_sample_rate),
151 priority_sampler: priority_sampler::PrioritySampler::new(
152 self.agent_env.clone().into(),
153 self.extra_sample_rate,
154 self.target_traces_per_second,
155 self.max_catalog_entries,
156 ),
157 no_priority_sampler: score_sampler::NoPrioritySampler::new(
158 self.target_traces_per_second,
159 self.extra_sample_rate,
160 ),
161 rare_sampler: rare_sampler::RareSampler::new(
162 self.rare_sampler_enabled,
163 self.rare_sampler_tps,
164 std::time::Duration::from_secs_f64(self.rare_sampler_cooldown_secs),
165 self.rare_sampler_cardinality,
166 telemetry.counters().clone(),
167 ),
168 event_processor,
169 telemetry,
170 compute_top_level_by_span_kind: self.compute_top_level_by_span_kind,
171 static_settings: SamplerSettings {
172 target_traces_per_second: self.target_traces_per_second,
173 errors_per_second: self.errors_per_second,
174 rare_sampler_enabled: self.rare_sampler_enabled,
175 },
176 agent_env: self.agent_env.clone().into(),
177 remote_sampling: self.remote_sampling.clone(),
178 applied_sampling: None,
179 }
180 }
181}
182
183#[async_trait]
184impl TransformBuilder for TraceSamplerConfiguration {
185 fn input_event_type(&self) -> EventType {
186 EventType::Trace
187 }
188
189 fn outputs(&self) -> &[OutputDefinition<EventType>] {
190 static OUTPUTS: LazyLock<Vec<OutputDefinition<EventType>>> = LazyLock::new(|| {
191 vec![
192 OutputDefinition::default_output(EventType::Trace),
193 OutputDefinition::named_output("metrics", EventType::Metric),
194 ]
195 });
196 &OUTPUTS
197 }
198
199 async fn build(&self, _context: BuildContext) -> Result<Box<dyn Transform + Send>, GenericError> {
200 Ok(Box::new(self.build_sampler()))
201 }
202}
203
204impl MemoryBounds for TraceSamplerConfiguration {
205 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
206 builder.minimum().with_single_value::<TraceSampler>("component struct");
207
208 let catalog_capacity = if self.max_catalog_entries == 0 {
211 catalog::MAX_CATALOG_ENTRIES
212 } else {
213 self.max_catalog_entries
214 };
215 builder
216 .minimum()
217 .with_map::<signature::ServiceSignature, u32>("priority sampler catalog map", catalog_capacity)
218 .with_map::<signature::ServiceSignature, signature::Signature>(
219 "priority sampler catalog entries",
220 catalog_capacity,
221 );
222
223 builder
228 .minimum()
229 .with_map::<telemetry::DecisionKey, telemetry::DecisionCounts>("sampler decision window", catalog_capacity);
230
231 let analyzed_operations: usize = self.analyzed_spans_by_service.values().map(|ops| ops.len()).sum();
234 builder
235 .minimum()
236 .with_map::<String, HashMap<String, f64>>(
237 "analyzed spans service rates",
238 self.analyzed_spans_by_service.len(),
239 )
240 .with_map::<String, f64>("analyzed spans operation rates", analyzed_operations)
241 .with_map::<String, f64>("legacy analyzed service rates", self.analyzed_rate_by_service.len());
242 }
243}
244
245pub struct TraceSampler {
246 sampling_rate: f64,
247 error_tracking_standalone: bool,
248 error_sampling_enabled: bool,
249 probabilistic_sampler_enabled: bool,
250 probabilistic: probabilistic::ProbabilisticSampler,
251 compute_top_level_by_span_kind: bool,
252 error_sampler: errors::ErrorsSampler,
253 priority_sampler: priority_sampler::PrioritySampler,
254 no_priority_sampler: score_sampler::NoPrioritySampler,
255 rare_sampler: rare_sampler::RareSampler,
256 event_processor: EventProcessor,
257 telemetry: DecisionWindow,
258 static_settings: SamplerSettings,
260 agent_env: MetaString,
262 remote_sampling: TraceSamplingSubscription,
263 applied_sampling: Option<Arc<RemoteSampling>>,
265}
266
267struct SamplerOutcome {
269 keep: bool,
271 priority: i32,
273 decision_maker: &'static str,
275 root_span_idx: Option<usize>,
277 sampler: telemetry::SamplerName,
279 extract_events: bool,
281}
282
283impl SamplerOutcome {
284 const fn new(
285 keep: bool, priority: i32, decision_maker: &'static str, root_span_idx: Option<usize>,
286 sampler: telemetry::SamplerName, extract_events: bool,
287 ) -> Self {
288 Self {
289 keep,
290 priority,
291 decision_maker,
292 root_span_idx,
293 sampler,
294 extract_events,
295 }
296 }
297}
298
299impl TraceSampler {
300 fn apply_remote_sampling(&mut self) {
307 let Some(current) = self.remote_sampling.current() else {
308 return;
309 };
310 if self
311 .applied_sampling
312 .as_ref()
313 .is_some_and(|applied| Arc::ptr_eq(applied, ¤t))
314 {
315 return;
316 }
317 if let RemoteSampling::Remote { id, config } = &*current {
318 let settings = config.resolve(&self.agent_env, &self.static_settings);
319 debug!(config_id = %id, ?settings, "Applying remote sampling configuration.");
320 self.priority_sampler
322 .update_target_tps(settings.target_traces_per_second);
323 self.error_sampler.update_target_tps(settings.errors_per_second);
324 self.rare_sampler.set_enabled(settings.rare_sampler_enabled);
325 }
326 self.applied_sampling = Some(current);
327 }
328
329 fn process_buffer(&mut self, events: &mut EventsBuffer) {
331 self.apply_remote_sampling();
332 events.remove_if(|event| match event {
333 Event::Trace(trace) => !self.process_trace(trace),
334 _ => false,
335 });
336 }
337
338 fn get_root_span_index(&self, trace: &Trace) -> Option<usize> {
340 get_root_span_index(trace.spans())
342 }
343
344 fn get_user_priority(&self, trace: &Trace, root_span_idx: usize) -> Option<i32> {
346 if let Some(priority) = trace.priority {
348 return Some(priority);
349 }
350
351 if trace.spans().is_empty() {
352 return None;
353 }
354
355 if let Some(root) = trace.spans().get(root_span_idx) {
358 if let Some(p) = root
359 .attributes
360 .get(SAMPLING_PRIORITY_METRIC_KEY)
361 .and_then(AttributeValue::as_num)
362 {
363 return Some(p as i32);
364 }
365 }
366 let spans = trace.spans();
367 spans.iter().find_map(|span| {
368 span.attributes
369 .get(SAMPLING_PRIORITY_METRIC_KEY)
370 .and_then(AttributeValue::as_num)
371 .map(|p| p as i32)
372 })
373 }
374
375 fn sample_probabilistic(&self, trace_id_high: u64, trace_id_low: u64) -> bool {
377 self.probabilistic
378 .sample(trace_id_high, trace_id_low, self.sampling_rate)
379 }
380
381 fn trace_contains_error(&self, trace: &Trace, consider_exception_span_events: bool) -> bool {
383 trace.spans().iter().any(|span| {
384 span.error() != 0 || (consider_exception_span_events && self.span_contains_exception_span_event(span))
385 })
386 }
387
388 fn span_contains_exception_span_event(&self, span: &Span) -> bool {
392 if let Some(has_exception) = span
393 .attributes
394 .get("_dd.span_events.has_exception")
395 .and_then(AttributeValue::as_string)
396 {
397 return has_exception == "true";
398 }
399 false
400 }
401
402 fn otlp_pre_sample(&mut self, trace: &mut Trace, root_span_idx: usize) -> Option<(i32, &'static str)> {
410 let TraceIngestInfo::Otlp { receiver_sampling_rate } = trace.ingest_info() else {
411 return None;
412 };
413 if self.probabilistic_sampler_enabled {
414 return None;
415 }
416 let (priority, dm) = if let Some(user_priority) = self.get_user_priority(trace, root_span_idx) {
417 (user_priority, DECISION_MAKER_MANUAL)
418 } else {
419 let root_trace_id = trace.trace_id_low;
420 if sample_by_rate(root_trace_id, receiver_sampling_rate) {
421 (PRIORITY_AUTO_KEEP, DECISION_MAKER_PROBABILISTIC)
422 } else {
423 (PRIORITY_AUTO_DROP, DECISION_MAKER_PROBABILISTIC)
424 }
425 };
426 if priority == PRIORITY_AUTO_KEEP {
427 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
428 root_span.attributes.remove(PROB_RATE_KEY);
429 }
430 }
431 Some((priority, dm))
432 }
433
434 fn forward_analyzed_spans(&self, trace: &mut Trace, priority: i32) -> bool {
436 let retained = trace.retain_spans(|_, span| span.attributes.contains_key(KEY_ANALYZED_SPANS));
437 if retained > 0 {
438 trace.dropped_trace = true;
439 trace.priority = Some(priority);
440 true
441 } else {
442 false
443 }
444 }
445
446 fn single_span_sampling(&self, trace: &mut Trace) -> bool {
449 let retained = trace.retain_spans(|_, span| span.attributes.contains_key(KEY_SPAN_SAMPLING_MECHANISM));
450 if retained > 0 {
451 trace.dropped_trace = false;
452 trace.priority = Some(PRIORITY_USER_KEEP);
453 true
454 } else {
455 false
456 }
457 }
458
459 fn run_samplers(&mut self, trace: &mut Trace) -> SamplerOutcome {
461 let outcome = self.run_samplers_inner(trace);
462
463 if outcome.sampler != telemetry::SamplerName::Unknown {
467 let (service, env) = self.decision_dimensions(trace, outcome.root_span_idx);
468 self.telemetry
469 .record_decision(outcome.keep, outcome.sampler, outcome.priority, service, env);
470 }
471
472 outcome
473 }
474
475 fn decision_dimensions(&self, trace: &Trace, root_span_idx: Option<usize>) -> (MetaString, MetaString) {
477 match root_span_idx {
478 Some(root_span_idx) => {
479 let service = trace
480 .spans()
481 .get(root_span_idx)
482 .map(|span| MetaString::from(span.service()))
483 .unwrap_or_default();
484 let env = get_trace_env(trace, root_span_idx).cloned().unwrap_or_default();
485 (service, env)
486 }
487 None => (MetaString::empty(), MetaString::empty()),
488 }
489 }
490
491 fn run_samplers_inner(&mut self, trace: &mut Trace) -> SamplerOutcome {
492 let mut sampler_name = telemetry::SamplerName::Unknown;
495
496 if trace.spans().is_empty() {
499 return SamplerOutcome::new(false, PRIORITY_AUTO_DROP, "", None, sampler_name, false);
500 }
501
502 let Some(root_span_idx) = self.get_root_span_index(trace) else {
503 return SamplerOutcome::new(false, PRIORITY_AUTO_DROP, "", None, sampler_name, false);
504 };
505
506 if self.error_tracking_standalone {
509 sampler_name = telemetry::SamplerName::Error;
510 let otlp_pre_sample = self.otlp_pre_sample(trace, root_span_idx);
511 if self.trace_contains_error(trace, true) {
512 let now = std::time::SystemTime::now();
513 let keep = self.error_sampler.sample_error(now, trace, root_span_idx);
514 let default_priority = if keep { PRIORITY_AUTO_KEEP } else { PRIORITY_AUTO_DROP };
515 let (priority, dm) = otlp_pre_sample.unwrap_or((default_priority, ""));
516 return SamplerOutcome::new(keep, priority, dm, Some(root_span_idx), sampler_name, false);
517 }
518 let (pre_priority, pre_dm) = otlp_pre_sample.unwrap_or((PRIORITY_AUTO_DROP, ""));
519 return SamplerOutcome::new(false, pre_priority, pre_dm, Some(root_span_idx), sampler_name, false);
520 }
521
522 let rare = self.rare_sampler.sample(trace, root_span_idx);
526
527 if self.probabilistic_sampler_enabled {
529 sampler_name = telemetry::SamplerName::Probabilistic;
530 let mut prob_keep = false;
531 let mut decision_maker = "";
532
533 if rare {
534 sampler_name = telemetry::SamplerName::Rare;
536 prob_keep = true;
537 } else {
538 if self.sample_probabilistic(trace.trace_id_high, trace.trace_id_low) {
539 decision_maker = DECISION_MAKER_PROBABILISTIC;
540 prob_keep = true;
541
542 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
543 root_span.attributes.insert(
544 MetaString::from_static(PROB_RATE_KEY),
545 AttributeValue::Float(self.sampling_rate),
546 );
547 }
548 } else if self.error_sampling_enabled && self.trace_contains_error(trace, false) {
549 sampler_name = telemetry::SamplerName::Error;
550 let now = std::time::SystemTime::now();
551 prob_keep = self.error_sampler.sample_error(now, trace, root_span_idx);
552 }
553 }
554
555 let priority = if prob_keep {
556 PRIORITY_AUTO_KEEP
557 } else {
558 PRIORITY_AUTO_DROP
559 };
560
561 return SamplerOutcome::new(
562 prob_keep,
563 priority,
564 decision_maker,
565 Some(root_span_idx),
566 sampler_name,
567 true,
568 );
569 }
570
571 let now = std::time::SystemTime::now();
574 let user_priority = self.get_user_priority(trace, root_span_idx);
575 if let Some(priority) = user_priority {
576 sampler_name = telemetry::SamplerName::Priority;
577 if Self::trace_chunk_contains_probability_sampling(trace) {
578 sampler_name = telemetry::SamplerName::Probabilistic;
581 }
582 if priority < PRIORITY_AUTO_DROP {
583 return SamplerOutcome::new(false, priority, "", Some(root_span_idx), sampler_name, false);
585 }
586
587 if rare {
588 sampler_name = telemetry::SamplerName::Rare;
589 return SamplerOutcome::new(true, priority, "", Some(root_span_idx), sampler_name, true);
590 }
591
592 if self.priority_sampler.sample(now, trace, root_span_idx, priority, 0.0) {
593 return SamplerOutcome::new(true, priority, "", Some(root_span_idx), sampler_name, true);
594 }
595 } else if let TraceIngestInfo::Otlp { receiver_sampling_rate } = trace.ingest_info() {
596 if rare {
598 sampler_name = telemetry::SamplerName::Rare;
599 return SamplerOutcome::new(true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx), sampler_name, true);
600 }
601
602 sampler_name = telemetry::SamplerName::Probabilistic;
605 let root_trace_id = trace.trace_id_low;
607 if sample_by_rate(root_trace_id, receiver_sampling_rate) {
608 if let Some(root_span) = trace.spans_mut().get_mut(root_span_idx) {
609 root_span.attributes.remove(PROB_RATE_KEY);
610 }
611 return SamplerOutcome::new(
612 true,
613 PRIORITY_AUTO_KEEP,
614 DECISION_MAKER_PROBABILISTIC,
615 Some(root_span_idx),
616 sampler_name,
617 true,
618 );
619 }
620 } else {
621 sampler_name = telemetry::SamplerName::NoPriority;
622 if rare {
623 sampler_name = telemetry::SamplerName::Rare;
624 return SamplerOutcome::new(true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx), sampler_name, true);
625 }
626 if self.no_priority_sampler.sample(now, trace, root_span_idx) {
627 return SamplerOutcome::new(true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx), sampler_name, true);
628 }
629 }
630
631 if self.error_sampling_enabled && self.trace_contains_error(trace, false) {
632 sampler_name = telemetry::SamplerName::Error;
633 let keep = self.error_sampler.sample_error(now, trace, root_span_idx);
634 if keep {
635 return SamplerOutcome::new(true, PRIORITY_AUTO_KEEP, "", Some(root_span_idx), sampler_name, true);
636 }
637 }
638
639 SamplerOutcome::new(false, PRIORITY_AUTO_DROP, "", Some(root_span_idx), sampler_name, true)
641 }
642
643 fn trace_chunk_contains_probability_sampling(trace: &Trace) -> bool {
648 trace
649 .spans()
650 .iter()
651 .find_map(|span| {
652 span.attributes
653 .get(TAG_DECISION_MAKER)
654 .and_then(AttributeValue::as_string)
655 })
656 .is_some_and(|dm| dm == DECISION_MAKER_PROBABILISTIC)
657 }
658
659 fn backfill_trace_metadata(&self, trace: &mut Trace, root_span_idx: usize) {
666 if trace.origin.is_empty() {
667 if let Some(origin) = trace.spans()[root_span_idx]
668 .attributes
669 .get(TAG_ORIGIN)
670 .and_then(AttributeValue::as_string)
671 {
672 trace.origin = origin.clone();
673 }
674 }
675
676 if trace.decision_maker.is_none() {
677 let first_decision_maker = trace.spans().iter().find_map(|span| {
678 span.attributes
679 .get(TAG_DECISION_MAKER)
680 .and_then(AttributeValue::as_string)
681 });
682 if let Some(dm) = first_decision_maker {
683 trace.decision_maker = Some(dm.clone());
684 }
685 }
686
687 if !self.compute_top_level_by_span_kind {
688 compute_top_level(trace.spans_mut());
689 }
690 }
691
692 fn apply_sampling_metadata(
697 &self, trace: &mut Trace, keep: bool, priority: i32, decision_maker: &'static str, root_span_idx: usize,
698 ) {
699 let is_otlp = matches!(trace.ingest_info(), TraceIngestInfo::Otlp { .. });
700 let promoted_decision_maker = if decision_maker.is_empty() {
703 trace.decision_maker.clone()
704 } else {
705 None
706 };
707 let root_span_value = match trace.spans_mut().get_mut(root_span_idx) {
708 Some(span) => span,
709 None => return,
710 };
711
712 let decision_maker_meta = if decision_maker.is_empty() {
713 promoted_decision_maker.or_else(|| {
714 root_span_value
715 .attributes
716 .get(TAG_DECISION_MAKER)
717 .and_then(AttributeValue::as_string)
718 .cloned()
719 })
720 } else {
721 Some(MetaString::from_static(decision_maker))
722 };
723
724 if priority > 0 && !decision_maker.is_empty() && !(is_otlp && self.probabilistic_sampler_enabled) {
733 root_span_value.attributes.insert(
734 MetaString::from_static(TAG_DECISION_MAKER),
735 AttributeValue::String(MetaString::from_static(decision_maker)),
736 );
737 }
738
739 trace.dropped_trace = !keep;
741 trace.priority = Some(priority);
742 trace.decision_maker = if priority > 0 { decision_maker_meta } else { None };
743 }
744
745 fn process_trace(&mut self, trace: &mut Trace) -> bool {
746 let SamplerOutcome {
751 keep,
752 priority,
753 decision_maker,
754 root_span_idx,
755 extract_events,
756 ..
757 } = self.run_samplers(trace);
758
759 if let Some(root_idx) = root_span_idx {
763 self.backfill_trace_metadata(trace, root_idx);
764 }
765
766 let events = if extract_events {
770 self.event_processor.process(trace, root_span_idx, priority)
771 } else {
772 event_processor::EventCounts::default()
773 };
774
775 if keep || self.error_tracking_standalone {
778 if let Some(root_idx) = root_span_idx {
779 self.apply_sampling_metadata(trace, keep, priority, decision_maker, root_idx);
780 }
781 return true;
782 }
783
784 let modified = self.single_span_sampling(trace);
787 if !modified {
788 if self.forward_analyzed_spans(trace, priority) {
790 return true;
791 }
792 } else if events.sampled > 0 {
793 debug!(
795 "Detected both analytics events AND single span sampling in the same trace. Single span sampling wins because App Analytics is deprecated."
796 );
797 return true;
798 }
799
800 if modified {
802 return true;
803 }
804
805 debug!("Dropping trace with priority {}", priority);
807 false
808 }
809}
810
811#[async_trait]
812impl Transform for TraceSampler {
813 async fn run(mut self: Box<Self>, mut context: TransformContext) -> Result<(), GenericError> {
814 let mut health = context.take_health_handle();
815 health.mark_ready();
816 debug!("Trace sampler transform started.");
817
818 let mut window = tokio::time::interval_at(tokio::time::Instant::now() + WINDOW, WINDOW);
819
820 loop {
821 select! {
822 _ = health.live() => continue,
823 _ = window.tick() => {
824 let mut events = self.telemetry.take_window_events(
829 self.priority_sampler.tracked_signature_count(),
830 self.no_priority_sampler.tracked_signature_count(),
831 self.error_sampler.tracked_signature_count(),
832 );
833 events.extend(self.event_processor.take_window_events());
834 context.dispatcher().buffered_named("metrics")?.send_all(events).await?;
835 }
836 maybe_events = context.events().next() => match maybe_events {
837 Some(mut events) => {
838 self.process_buffer(&mut events);
839 context.dispatcher().buffered()?.send_all(events).await?;
840 }
841 None => {
842 let mut events = self.telemetry.take_window_events(
844 self.priority_sampler.tracked_signature_count(),
845 self.no_priority_sampler.tracked_signature_count(),
846 self.error_sampler.tracked_signature_count(),
847 );
848 events.extend(self.event_processor.take_window_events());
849 context.dispatcher().buffered_named("metrics")?.send_all(events).await?;
850 break;
851 }
852 }
853 }
854 }
855
856 debug!("Trace sampler transform stopped.");
857 Ok(())
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use std::collections::HashMap;
864
865 use datadog_agent_apm::sampling::test_util::TestSamplingPublisher;
866 use saluki_core::components::test_util::TestComponentDriver;
867 use saluki_core::data_model::event::trace::{AttributeValue, Span as DdSpan, Trace, TraceIngestInfo};
868
869 use super::*;
870 use crate::common::datadog::OTEL_TRACE_ID_META_KEY;
871
872 const PRIORITY_USER_DROP: i32 = -1;
873
874 const OTLP_TEST_RECEIVER_SAMPLING_RATE: f64 = 0.5;
877 const OTLP_RECEIVER_KEPT_TRACE_ID: u64 = 123;
878 const OTLP_RECEIVER_DROPPED_TRACE_ID: u64 = 1237892138897;
879
880 fn create_test_sampler() -> TraceSampler {
881 TraceSampler {
882 sampling_rate: 1.0,
883 error_sampling_enabled: true,
884 error_tracking_standalone: false,
885 probabilistic_sampler_enabled: true,
886 probabilistic: probabilistic::ProbabilisticSampler::new(0, false),
887 error_sampler: errors::ErrorsSampler::new(10.0, 1.0),
888 priority_sampler: priority_sampler::PrioritySampler::new(MetaString::from("agent-env"), 1.0, 10.0, 5000),
889 no_priority_sampler: score_sampler::NoPrioritySampler::new(10.0, 1.0),
890 rare_sampler: rare_sampler::RareSampler::new(
891 false,
892 5.0,
893 std::time::Duration::from_secs(300),
894 200,
895 telemetry::SamplerCounters::new(),
896 ),
897 event_processor: EventProcessor::for_tests(&HashMap::new(), &HashMap::new(), f64::INFINITY),
898 telemetry: telemetry::DecisionWindow::new(),
899 compute_top_level_by_span_kind: false,
900 static_settings: SamplerSettings {
901 target_traces_per_second: 10.0,
902 errors_per_second: 10.0,
903 rare_sampler_enabled: false,
904 },
905 agent_env: MetaString::from("agent-env"),
906 remote_sampling: TraceSamplingSubscription::default(),
907 applied_sampling: None,
908 }
909 }
910
911 fn create_test_span(span_id: u64, error: i32) -> DdSpan {
912 DdSpan::new(
913 MetaString::from("test-service"),
914 MetaString::from("test-operation"),
915 MetaString::from("test-resource"),
916 MetaString::from("test-type"),
917 span_id,
918 0, 0, 1000, error,
922 )
923 }
924
925 fn create_test_span_with_metrics(span_id: u64, metrics: HashMap<String, f64>) -> DdSpan {
926 let attrs: saluki_common::collections::FastHashMap<MetaString, AttributeValue> = metrics
927 .into_iter()
928 .map(|(k, v)| (MetaString::from(k), AttributeValue::Float(v)))
929 .collect();
930 create_test_span(span_id, 0).with_attributes(attrs)
931 }
932
933 #[allow(dead_code)]
934 fn create_test_span_with_meta(span_id: u64, meta: HashMap<String, String>) -> DdSpan {
935 let attrs: saluki_common::collections::FastHashMap<MetaString, AttributeValue> = meta
936 .into_iter()
937 .map(|(k, v)| (MetaString::from(k), AttributeValue::String(MetaString::from(v))))
938 .collect();
939 create_test_span(span_id, 0).with_attributes(attrs)
940 }
941
942 fn create_test_trace(spans: Vec<DdSpan>) -> Trace {
943 Trace::new(spans, TraceIngestInfo::Datadog)
944 }
945
946 fn create_otlp_test_trace(spans: Vec<DdSpan>, receiver_sampling_rate: f64, trace_id_low: u64) -> Trace {
947 let mut trace = Trace::new(spans, TraceIngestInfo::Otlp { receiver_sampling_rate });
948 trace.trace_id_low = trace_id_low;
949 trace
950 }
951
952 fn create_span_with_parent_and_service(
953 span_id: u64, parent_id: u64, service: &str, meta: &[(&str, &str)],
954 ) -> DdSpan {
955 let mut attrs = saluki_common::collections::FastHashMap::default();
956 for (k, v) in meta {
957 attrs.insert(MetaString::from(*k), AttributeValue::String(MetaString::from(*v)));
958 }
959 DdSpan::new(
960 MetaString::from(service),
961 MetaString::from("operation"),
962 MetaString::from("resource"),
963 MetaString::from("type"),
964 span_id,
965 parent_id,
966 0,
967 1000,
968 0,
969 )
970 .with_attributes(attrs)
971 }
972
973 #[test]
974 fn apply_sampling_metadata_preserves_backfilled_decision_maker() {
975 let sampler = create_test_sampler();
976 let root = create_test_span(1, 0);
977 let mut trace = create_test_trace(vec![root]);
978 trace.decision_maker = Some(MetaString::from("-8"));
979
980 sampler.apply_sampling_metadata(&mut trace, true, PRIORITY_AUTO_KEEP, "", 0);
981
982 assert_eq!(
983 trace.decision_maker.as_deref(),
984 Some("-8"),
985 "the chunk-level decision maker keeps the promoted value"
986 );
987 assert!(
988 !trace.spans()[0].attributes.contains_key(TAG_DECISION_MAKER),
989 "a promoted decision maker is never stamped onto the root span; span-level _dd.p.dm is written only when a sampler decides"
990 );
991 }
992
993 #[test]
994 fn apply_sampling_metadata_keeps_root_decision_maker_without_sampler_decision() {
995 let sampler = create_test_sampler();
996 let mut attrs = saluki_common::collections::FastHashMap::default();
997 attrs.insert(
998 MetaString::from(TAG_DECISION_MAKER),
999 AttributeValue::String(MetaString::from("-9")),
1000 );
1001 let root = create_test_span(1, 0).with_attributes(attrs);
1002 let mut trace = create_test_trace(vec![root]);
1003 trace.decision_maker = Some(MetaString::from("-8"));
1005
1006 sampler.apply_sampling_metadata(&mut trace, true, PRIORITY_AUTO_KEEP, "", 0);
1007
1008 assert_eq!(
1009 trace.decision_maker.as_deref(),
1010 Some("-8"),
1011 "the chunk-level decision maker keeps the promoted value"
1012 );
1013 assert_eq!(
1014 trace.spans()[0]
1015 .attributes
1016 .get(TAG_DECISION_MAKER)
1017 .and_then(AttributeValue::as_string)
1018 .map(|s| s.as_ref()),
1019 Some("-9"),
1020 "the root span's own decision maker is never overwritten by a promoted value"
1021 );
1022 }
1023
1024 #[test]
1025 fn backfill_promotes_origin_from_root_span() {
1026 let sampler = create_test_sampler();
1027 let child = create_span_with_parent_and_service(2, 1, "svc", &[]);
1029 let root = create_span_with_parent_and_service(1, 0, "svc", &[("_dd.origin", "lambda")]);
1030 let mut trace = create_test_trace(vec![child, root]);
1031
1032 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1033 sampler.backfill_trace_metadata(&mut trace, root_idx);
1034
1035 assert_eq!(trace.origin.as_ref(), "lambda");
1036 }
1037
1038 #[test]
1039 fn backfill_ignores_origin_from_non_root_span() {
1040 let sampler = create_test_sampler();
1041 let root = create_span_with_parent_and_service(1, 0, "svc", &[]);
1042 let child = create_span_with_parent_and_service(2, 1, "svc", &[("_dd.origin", "rum")]);
1043 let mut trace = create_test_trace(vec![root, child]);
1044
1045 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1046 sampler.backfill_trace_metadata(&mut trace, root_idx);
1047
1048 assert!(trace.origin.as_ref().is_empty());
1049 }
1050
1051 #[test]
1052 fn backfill_promotes_first_decision_maker() {
1053 let sampler = create_test_sampler();
1054 let root = create_span_with_parent_and_service(1, 0, "svc", &[]);
1055 let early = create_span_with_parent_and_service(2, 1, "svc", &[("_dd.p.dm", "-8")]);
1056 let late = create_span_with_parent_and_service(3, 1, "svc", &[("_dd.p.dm", "-9")]);
1057 let mut trace = create_test_trace(vec![root, early, late]);
1058
1059 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1060 sampler.backfill_trace_metadata(&mut trace, root_idx);
1061
1062 assert_eq!(
1063 trace.decision_maker.as_deref(),
1064 Some("-8"),
1065 "first span carrying the tag wins - not a root-based rule"
1066 );
1067 }
1068
1069 #[test]
1070 fn backfill_marks_top_level_fallback_when_span_kind_computation_disabled() {
1071 let sampler = create_test_sampler();
1072 let root = create_span_with_parent_and_service(1, 0, "svc-a", &[]);
1074 let child = create_span_with_parent_and_service(2, 1, "svc-a", &[]);
1075 let orphan = create_span_with_parent_and_service(3, 0xEE, "svc-a", &[]);
1076 let other_service = create_span_with_parent_and_service(5, 1, "svc-b", &[]);
1077 let local_entry = create_span_with_parent_and_service(6, 5, "svc-a", &[]);
1078 let mut trace = create_test_trace(vec![root, child, orphan, other_service, local_entry]);
1079
1080 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1081 sampler.backfill_trace_metadata(&mut trace, root_idx);
1082
1083 let spans = trace.spans();
1084 let top_level = |sid: u64| {
1085 spans
1086 .iter()
1087 .find(|s| s.span_id() == sid)
1088 .unwrap()
1089 .attributes
1090 .get(crate::common::datadog::TOP_LEVEL_KEY)
1091 .and_then(AttributeValue::as_num)
1092 };
1093 assert_eq!(top_level(1), Some(1.0), "root spans are marked");
1094 assert_eq!(
1095 top_level(3),
1096 Some(1.0),
1097 "orphans whose parent is missing from the chunk are marked"
1098 );
1099 assert_eq!(top_level(5), Some(1.0), "spans entering a different service are marked");
1100 assert_eq!(
1101 top_level(6),
1102 Some(1.0),
1103 "spans crossing back into the resource service are marked (local root)"
1104 );
1105 assert_eq!(
1106 top_level(2),
1107 None,
1108 "same-service children with their parent present are not marked"
1109 );
1110 }
1111
1112 #[test]
1113 fn backfill_skips_top_level_when_span_kind_computation_enabled() {
1114 let sampler = TraceSampler {
1115 compute_top_level_by_span_kind: true,
1116 ..create_test_sampler()
1117 };
1118 let root = create_span_with_parent_and_service(1, 0, "svc", &[]);
1119 let child = create_span_with_parent_and_service(2, 1, "svc", &[]);
1120 let orphan = create_span_with_parent_and_service(3, 0xEE, "svc", &[]);
1121 let mut trace = create_test_trace(vec![root, child, orphan]);
1122
1123 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1124 sampler.backfill_trace_metadata(&mut trace, root_idx);
1125
1126 for span in trace.spans() {
1127 assert!(
1128 !span.attributes.contains_key(crate::common::datadog::TOP_LEVEL_KEY),
1129 "no span should carry a top-level mark with the fallback disabled"
1130 );
1131 }
1132 }
1133
1134 #[test]
1135 fn process_trace_backfills_metadata_for_forwarded_traces() {
1136 let mut sampler = create_test_sampler();
1137 sampler.sampling_rate = 1.0; sampler.probabilistic_sampler_enabled = true;
1139
1140 let child = create_span_with_parent_and_service(2, 1, "svc", &[]);
1141 let root = create_span_with_parent_and_service(1, 0, "svc", &[("_dd.origin", "lambda")]);
1142 let mut trace = create_test_trace(vec![child, root]);
1143
1144 let forwarded = sampler.process_trace(&mut trace);
1145 assert!(forwarded);
1146 assert_eq!(
1147 trace.origin.as_ref(),
1148 "lambda",
1149 "origin is backfilled for forwarded traces"
1150 );
1151 assert!(
1152 trace
1153 .spans()
1154 .iter()
1155 .any(|s| s.attributes.contains_key(crate::common::datadog::TOP_LEVEL_KEY)),
1156 "top-level marks are set for forwarded traces"
1157 );
1158 }
1159
1160 #[test]
1161 fn apply_sampling_metadata_sampler_decision_overrides_backfill() {
1162 let sampler = create_test_sampler();
1163 let root = create_test_span(1, 0);
1164 let mut trace = create_test_trace(vec![root]);
1165 trace.decision_maker = Some(MetaString::from("-8"));
1166
1167 sampler.apply_sampling_metadata(&mut trace, true, PRIORITY_AUTO_KEEP, DECISION_MAKER_PROBABILISTIC, 0);
1168
1169 assert_eq!(trace.decision_maker.as_deref(), Some(DECISION_MAKER_PROBABILISTIC));
1170 assert_eq!(
1171 trace.spans()[0]
1172 .attributes
1173 .get(TAG_DECISION_MAKER)
1174 .and_then(AttributeValue::as_string)
1175 .map(|s| s.as_ref()),
1176 Some(DECISION_MAKER_PROBABILISTIC)
1177 );
1178 }
1179
1180 #[test]
1181 fn user_priority_detection() {
1182 let sampler = create_test_sampler();
1183
1184 let mut metrics = HashMap::new();
1186 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
1187 let span = create_test_span_with_metrics(1, metrics);
1188 let trace = create_test_trace(vec![span]);
1189 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1190
1191 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(2));
1192
1193 let mut metrics = HashMap::new();
1195 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), -1.0);
1196 let span = create_test_span_with_metrics(1, metrics);
1197 let trace = create_test_trace(vec![span]);
1198 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1199
1200 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(-1));
1201
1202 let span = create_test_span(1, 0);
1204 let trace = create_test_trace(vec![span]);
1205 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1206
1207 assert_eq!(sampler.get_user_priority(&trace, root_idx), None);
1208 }
1209
1210 #[test]
1211 fn trace_level_priority_takes_precedence() {
1212 let sampler = create_test_sampler();
1213
1214 let mut metrics_root = HashMap::new();
1217 metrics_root.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 0.0);
1218 let root_span = create_test_span_with_metrics(1, metrics_root);
1219
1220 let mut metrics_later = HashMap::new();
1221 metrics_later.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 1.0);
1222 let later_span = create_test_span_with_metrics(2, metrics_later).with_parent_id(1);
1223
1224 let mut trace = create_test_trace(vec![root_span, later_span]);
1225 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1226
1227 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(0));
1229
1230 trace.priority = Some(2);
1232
1233 assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(2));
1235
1236 let span_no_priority = create_test_span(3, 0);
1238 let mut trace_only_trace_level = create_test_trace(vec![span_no_priority]);
1239 trace_only_trace_level.priority = Some(1);
1240 let root_idx = sampler.get_root_span_index(&trace_only_trace_level).unwrap();
1241
1242 assert_eq!(sampler.get_user_priority(&trace_only_trace_level, root_idx), Some(1));
1243 }
1244
1245 #[test]
1246 fn manual_keep_with_trace_level_priority() {
1247 let mut sampler = create_test_sampler();
1248 sampler.probabilistic_sampler_enabled = false; let span = create_test_span(1, 0);
1252 let mut trace = create_test_trace(vec![span]);
1253 trace.priority = Some(PRIORITY_USER_KEEP);
1254
1255 let SamplerOutcome {
1256 keep,
1257 priority,
1258 decision_maker,
1259 ..
1260 } = sampler.run_samplers(&mut trace);
1261 assert!(keep);
1262 assert_eq!(priority, PRIORITY_USER_KEEP);
1263 assert_eq!(decision_maker, "");
1264
1265 let span = create_test_span(1, 0);
1267 let mut trace = create_test_trace(vec![span]);
1268 trace.priority = Some(PRIORITY_USER_DROP);
1269
1270 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
1271 assert!(!keep); assert_eq!(priority, PRIORITY_USER_DROP);
1273
1274 let span = create_test_span(1, 0);
1276 let mut trace = create_test_trace(vec![span]);
1277 trace.priority = Some(PRIORITY_AUTO_KEEP);
1278
1279 let SamplerOutcome {
1280 keep,
1281 priority,
1282 decision_maker,
1283 ..
1284 } = sampler.run_samplers(&mut trace);
1285 assert!(keep);
1286 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1287 assert_eq!(decision_maker, "");
1288 }
1289
1290 #[test]
1291 fn probabilistic_sampling_known_decisions() {
1292 struct Case {
1301 trace_id: u64,
1302 rate: f64,
1303 expected_keep: bool,
1304 }
1305
1306 let cases = [
1307 Case {
1309 trace_id: 0x1234567890ABCDEF,
1310 rate: 1.0,
1311 expected_keep: true,
1312 },
1313 Case {
1315 trace_id: 0x1234567890ABCDEF,
1316 rate: 0.0,
1317 expected_keep: false,
1318 },
1319 Case {
1321 trace_id: 0x1234567890ABCDEF,
1322 rate: 0.10,
1323 expected_keep: false,
1324 },
1325 Case {
1326 trace_id: 0x1234567890ABCDEF,
1327 rate: 0.20,
1328 expected_keep: true,
1329 },
1330 Case {
1332 trace_id: 0,
1333 rate: 0.50,
1334 expected_keep: false,
1335 },
1336 Case {
1337 trace_id: 0,
1338 rate: 0.60,
1339 expected_keep: true,
1340 },
1341 Case {
1343 trace_id: u64::MAX,
1344 rate: 0.50,
1345 expected_keep: false,
1346 },
1347 Case {
1348 trace_id: u64::MAX,
1349 rate: 0.80,
1350 expected_keep: true,
1351 },
1352 ];
1353
1354 for case in cases {
1355 let mut sampler = create_test_sampler();
1356 sampler.sampling_rate = case.rate;
1357 assert_eq!(
1358 sampler.sample_probabilistic(0, case.trace_id),
1359 case.expected_keep,
1360 "trace_id={:#018x} rate={}",
1361 case.trace_id,
1362 case.rate
1363 );
1364 }
1365 }
1366
1367 #[test]
1368 fn probabilistic_sampling_is_deterministic() {
1369 let sampler = create_test_sampler();
1373 let trace_id = 0x1234567890ABCDEF_u64;
1374 assert_eq!(
1375 sampler.sample_probabilistic(0, trace_id),
1376 sampler.sample_probabilistic(0, trace_id)
1377 );
1378 }
1379
1380 #[test]
1381 fn probabilistic_sampling_hash_seed_changes_decisions() {
1382 let mut seeded = create_test_sampler();
1385 seeded.probabilistic = probabilistic::ProbabilisticSampler::new(22, false);
1386
1387 seeded.sampling_rate = 0.40;
1389 assert!(!seeded.sample_probabilistic(0, 0x1234567890ABCDEF));
1390 seeded.sampling_rate = 0.50;
1391 assert!(seeded.sample_probabilistic(0, 0x1234567890ABCDEF));
1392
1393 seeded.sampling_rate = 0.20;
1396 assert!(!seeded.sample_probabilistic(0, 0x1234567890ABCDEF));
1397 let mut unseeded = create_test_sampler();
1398 unseeded.sampling_rate = 0.20;
1399 assert!(unseeded.sample_probabilistic(0, 0x1234567890ABCDEF));
1400 }
1401
1402 #[test]
1403 fn probabilistic_sampling_full_trace_id_mode_hashes_both_halves() {
1404 let high = 0xAABBCCDD00112233_u64;
1405 let low = 0x1234567890ABCDEF_u64;
1406
1407 let mut legacy = create_test_sampler();
1409 legacy.probabilistic = probabilistic::ProbabilisticSampler::new(22, false);
1410 legacy.sampling_rate = 0.45;
1411 assert_eq!(
1412 legacy.sample_probabilistic(0, low),
1413 legacy.sample_probabilistic(high, low)
1414 );
1415
1416 let mut full = create_test_sampler();
1419 full.probabilistic = probabilistic::ProbabilisticSampler::new(22, true);
1420 full.sampling_rate = 0.02;
1421 assert!(!full.sample_probabilistic(high, low));
1422 full.sampling_rate = 0.05;
1423 assert!(full.sample_probabilistic(high, low));
1424
1425 full.sampling_rate = 0.89;
1428 assert!(!full.sample_probabilistic(0, low));
1429 full.sampling_rate = 0.90;
1430 assert!(full.sample_probabilistic(0, low));
1431 }
1432
1433 #[test]
1434 fn from_configuration_reads_the_full_trace_id_feature() {
1435 let traces = domains::traces::Domain {
1436 features: vec![domains::traces::ApmFeature::ProbabilisticSamplerFullTraceId],
1437 ..Default::default()
1438 };
1439 let config =
1440 TraceSamplerConfiguration::from_configuration(&traces, &domains::otlp::Traces::default(), prod_env());
1441 assert!(config.probabilistic_full_trace_id);
1442
1443 let traces = domains::traces::Domain {
1445 features: vec![domains::traces::ApmFeature::Other("table_names".to_owned())],
1446 ..Default::default()
1447 };
1448 let config =
1449 TraceSamplerConfiguration::from_configuration(&traces, &domains::otlp::Traces::default(), prod_env());
1450 assert!(!config.probabilistic_full_trace_id);
1451 }
1452
1453 #[test]
1454 fn every_trace_is_counted_under_exactly_one_sampler() {
1455 let mut sampler = create_test_sampler();
1459
1460 for i in 0..10 {
1461 let span = create_test_span(i, 0);
1462 let mut trace = create_test_trace(vec![span]);
1463 sampler.run_samplers(&mut trace);
1464 }
1465
1466 let decisions = sampler.telemetry.snapshot_decisions();
1467 let total_seen: u64 = decisions.values().map(|counts| counts.seen).sum();
1468 let total_kept: u64 = decisions.values().map(|counts| counts.kept).sum();
1469 assert_eq!(total_seen, 10);
1470 assert_eq!(total_kept, 10);
1471 assert!(decisions
1472 .keys()
1473 .all(|key| key.sampler == telemetry::SamplerName::Probabilistic));
1474 assert!(decisions.keys().all(|key| key.service == "test-service"));
1475 }
1476
1477 #[test]
1478 fn priority_decisions_are_counted_under_the_priority_sampler_with_priority_tag() {
1479 let mut sampler = create_test_sampler();
1482 sampler.probabilistic_sampler_enabled = false;
1483
1484 let mut metrics = HashMap::new();
1485 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
1486 let span = create_test_span_with_metrics(1, metrics);
1487 let mut trace = create_test_trace(vec![span]);
1488 sampler.run_samplers(&mut trace);
1489
1490 let decisions = sampler.telemetry.snapshot_decisions();
1491 assert_eq!(decisions.len(), 1);
1492 let (key, counts) = decisions.iter().next().unwrap();
1493 assert_eq!(key.sampler, telemetry::SamplerName::Priority);
1494 assert_eq!(key.priority, Some(telemetry::Priority::ManualKeep));
1495 assert_eq!(counts.seen, 1);
1496 assert_eq!(counts.kept, 1);
1497 }
1498
1499 #[test]
1500 fn upstream_probability_sampling_reattributes_to_the_probabilistic_sampler() {
1501 let mut sampler = create_test_sampler();
1505 sampler.probabilistic_sampler_enabled = false;
1506
1507 let mut metrics = HashMap::new();
1508 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
1509 let mut span = create_test_span_with_metrics(1, metrics);
1510 span.attributes.insert(
1511 MetaString::from_static(TAG_DECISION_MAKER),
1512 AttributeValue::String(MetaString::from_static(DECISION_MAKER_PROBABILISTIC)),
1513 );
1514 let mut trace = create_test_trace(vec![span]);
1515 sampler.run_samplers(&mut trace);
1516
1517 let decisions = sampler.telemetry.snapshot_decisions();
1518 assert_eq!(decisions.len(), 1);
1519 let (key, _) = decisions.iter().next().unwrap();
1520 assert_eq!(key.sampler, telemetry::SamplerName::Probabilistic);
1521 assert_eq!(key.priority, None);
1522 }
1523
1524 #[test]
1525 fn empty_traces_record_no_decision() {
1526 let mut sampler = create_test_sampler();
1529 sampler.probabilistic_sampler_enabled = false;
1530
1531 let mut trace = create_test_trace(vec![]);
1532 sampler.run_samplers(&mut trace);
1533 assert!(sampler.telemetry.snapshot_decisions().is_empty());
1534 }
1535
1536 #[test]
1537 fn error_detection() {
1538 let sampler = create_test_sampler();
1539
1540 let span_with_error = create_test_span(1, 1);
1542 let trace = create_test_trace(vec![span_with_error]);
1543 assert!(sampler.trace_contains_error(&trace, false));
1544
1545 let span_without_error = create_test_span(1, 0);
1547 let trace = create_test_trace(vec![span_without_error]);
1548 assert!(!sampler.trace_contains_error(&trace, false));
1549 }
1550
1551 #[test]
1552 fn sampling_priority_order() {
1553 let mut sampler = create_test_sampler();
1555 sampler.sampling_rate = 0.5; sampler.probabilistic_sampler_enabled = true;
1557
1558 let span_with_error = create_test_span(1, 1);
1561 let mut trace = create_test_trace(vec![span_with_error]);
1562 trace.trace_id_low = u64::MAX - 1;
1563
1564 let SamplerOutcome {
1565 keep,
1566 priority,
1567 decision_maker,
1568 ..
1569 } = sampler.run_samplers(&mut trace);
1570 assert!(keep);
1571 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1572 assert_eq!(decision_maker, ""); let mut sampler = create_test_sampler();
1576 sampler.probabilistic_sampler_enabled = false; let mut metrics = HashMap::new();
1579 metrics.insert(SAMPLING_PRIORITY_METRIC_KEY.to_string(), 2.0);
1580 let span = create_test_span_with_metrics(1, metrics);
1581 let mut trace = create_test_trace(vec![span]);
1582
1583 let SamplerOutcome {
1584 keep,
1585 priority,
1586 decision_maker,
1587 ..
1588 } = sampler.run_samplers(&mut trace);
1589 assert!(keep);
1590 assert_eq!(priority, 2); assert_eq!(decision_maker, "");
1592 }
1593
1594 #[test]
1595 fn empty_trace_handling() {
1596 let mut sampler = create_test_sampler();
1597 let mut trace = create_test_trace(vec![]);
1598
1599 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
1600 assert!(!keep);
1601 assert_eq!(priority, PRIORITY_AUTO_DROP);
1602 }
1603
1604 #[test]
1605 fn root_span_detection() {
1606 let sampler = create_test_sampler();
1607
1608 let root_span = DdSpan::new(
1610 MetaString::from("service"),
1611 MetaString::from("operation"),
1612 MetaString::from("resource"),
1613 MetaString::from("type"),
1614 1,
1615 0, 0,
1617 1000,
1618 0,
1619 );
1620 let child_span = DdSpan::new(
1621 MetaString::from("service"),
1622 MetaString::from("child_op"),
1623 MetaString::from("resource"),
1624 MetaString::from("type"),
1625 2,
1626 1, 100,
1628 500,
1629 0,
1630 );
1631 let trace = create_test_trace(vec![child_span.clone(), root_span.clone()]);
1633 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1634 assert_eq!(trace.spans()[root_idx].span_id(), 1);
1635
1636 let orphan_span = DdSpan::new(
1638 MetaString::from("service"),
1639 MetaString::from("orphan"),
1640 MetaString::from("resource"),
1641 MetaString::from("type"),
1642 3,
1643 999, 200,
1645 300,
1646 0,
1647 );
1648 let trace = create_test_trace(vec![orphan_span]);
1649 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1650 assert_eq!(trace.spans()[root_idx].span_id(), 3);
1651
1652 let span1 = create_test_span(1, 0);
1654 let span2 = create_test_span(2, 0);
1655 let trace = create_test_trace(vec![span1, span2]);
1656 let root_idx = sampler.get_root_span_index(&trace).unwrap();
1658 assert_eq!(trace.spans()[root_idx].span_id(), 2);
1659 }
1660
1661 #[test]
1662 fn single_span_sampling() {
1663 let mut sampler = create_test_sampler();
1664
1665 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
1668
1669 let mut attrs_map = saluki_common::collections::FastHashMap::default();
1671 attrs_map.insert(
1672 MetaString::from(KEY_SPAN_SAMPLING_MECHANISM),
1673 AttributeValue::Float(8.0),
1674 );
1675 let sss_span = create_test_span(1, 0).with_attributes(attrs_map.clone());
1676
1677 let regular_span = create_test_span(2, 0);
1679
1680 let mut trace = create_test_trace(vec![sss_span.clone(), regular_span]);
1681
1682 let modified = sampler.single_span_sampling(&mut trace);
1684 assert!(modified);
1685 assert_eq!(trace.spans().len(), 1); assert_eq!(trace.spans()[0].span_id(), 1); assert_eq!(trace.priority, Some(PRIORITY_USER_KEEP));
1690
1691 let trace_without_sss = create_test_trace(vec![create_test_span(3, 0)]);
1693 let mut trace_copy = trace_without_sss.clone();
1694 let modified = sampler.single_span_sampling(&mut trace_copy);
1695 assert!(!modified);
1696 assert_eq!(trace_copy.spans().len(), trace_without_sss.spans().len());
1697 }
1698
1699 #[test]
1700 fn probabilistic_sampling_with_prob_rate_key() {
1701 let mut sampler = create_test_sampler();
1702 sampler.sampling_rate = 0.75; sampler.probabilistic_sampler_enabled = true;
1704
1705 let trace_id = 12345_u64;
1707 let root_span = DdSpan::new(
1708 MetaString::from("service"),
1709 MetaString::from("operation"),
1710 MetaString::from("resource"),
1711 MetaString::from("type"),
1712 1,
1713 0, 0,
1715 1000,
1716 0,
1717 );
1718 let mut trace = create_test_trace(vec![root_span]);
1719 trace.trace_id_low = trace_id;
1720
1721 let SamplerOutcome {
1722 keep,
1723 priority,
1724 decision_maker,
1725 root_span_idx,
1726 ..
1727 } = sampler.run_samplers(&mut trace);
1728
1729 if keep && decision_maker == DECISION_MAKER_PROBABILISTIC {
1730 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1732 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC); let root_idx = root_span_idx.unwrap_or(0);
1736 let root_span = &trace.spans()[root_idx];
1737 assert!(root_span.attributes.contains_key(PROB_RATE_KEY));
1738 assert_eq!(
1739 root_span
1740 .attributes
1741 .get(PROB_RATE_KEY)
1742 .and_then(AttributeValue::as_float),
1743 Some(0.75)
1744 );
1745
1746 let mut trace_with_metadata = trace.clone();
1748 sampler.apply_sampling_metadata(&mut trace_with_metadata, keep, priority, decision_maker, root_idx);
1749
1750 let modified_root = &trace_with_metadata.spans()[root_idx];
1752 assert!(modified_root.attributes.contains_key(TAG_DECISION_MAKER));
1753 assert_eq!(
1754 modified_root
1755 .attributes
1756 .get(TAG_DECISION_MAKER)
1757 .and_then(AttributeValue::as_string),
1758 Some(&MetaString::from(DECISION_MAKER_PROBABILISTIC))
1759 );
1760 }
1761 }
1762
1763 fn create_top_level_span(span_id: u64) -> DdSpan {
1773 let mut attrs = saluki_common::collections::FastHashMap::default();
1774 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
1775 create_test_span(span_id, 0).with_attributes(attrs)
1776 }
1777
1778 fn create_sampler_with_rare_enabled() -> TraceSampler {
1781 TraceSampler {
1782 rare_sampler: rare_sampler::RareSampler::new(
1783 true,
1784 1000.0,
1785 std::time::Duration::from_secs(300),
1786 200,
1787 telemetry::SamplerCounters::new(),
1788 ),
1789 ..create_test_sampler()
1790 }
1791 }
1792
1793 #[test]
1797 fn rare_sampler_catches_unsampled_trace() {
1798 let mut sampler = create_sampler_with_rare_enabled();
1799 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
1801
1802 let span = create_top_level_span(1);
1803 let mut trace = create_test_trace(vec![span]);
1804
1805 let SamplerOutcome {
1806 keep,
1807 priority,
1808 decision_maker,
1809 ..
1810 } = sampler.run_samplers(&mut trace);
1811 assert!(keep, "rare sampler should catch first occurrence");
1812 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1813 assert_eq!(decision_maker, "", "rare sampler does not set _dd.p.dm");
1814 }
1815
1816 #[test]
1820 fn rare_sampler_sets_rare_metric_on_first_occurrence() {
1821 let mut sampler = create_sampler_with_rare_enabled();
1822 sampler.sampling_rate = 0.0;
1823 sampler.probabilistic_sampler_enabled = true;
1824
1825 let span = create_top_level_span(1);
1826 let mut trace = create_test_trace(vec![span]);
1827
1828 let SamplerOutcome {
1829 keep,
1830 root_span_idx: root_idx,
1831 ..
1832 } = sampler.run_samplers(&mut trace);
1833 assert!(keep);
1834 let root = &trace.spans()[root_idx.unwrap()];
1835 assert_eq!(
1836 root.attributes
1837 .get(rare_sampler::RARE_KEY)
1838 .and_then(AttributeValue::as_float),
1839 Some(1.0),
1840 "_dd.rare should be 1 on first occurrence"
1841 );
1842 }
1843
1844 #[test]
1849 fn rare_sampler_does_not_resample_within_ttl() {
1850 let mut sampler = create_sampler_with_rare_enabled();
1851 sampler.sampling_rate = 0.0;
1852 sampler.probabilistic_sampler_enabled = true;
1853
1854 let span1 = create_top_level_span(1);
1856 let mut trace1 = create_test_trace(vec![span1]);
1857 let SamplerOutcome { keep: keep1, .. } = sampler.run_samplers(&mut trace1);
1858 assert!(keep1, "first occurrence should be kept by rare sampler");
1859
1860 let span2 = create_top_level_span(2);
1863 let mut trace2 = create_test_trace(vec![span2]);
1864 let SamplerOutcome {
1865 keep: keep2,
1866 priority: priority2,
1867 ..
1868 } = sampler.run_samplers(&mut trace2);
1869 assert!(!keep2, "second occurrence within TTL should be dropped");
1870 assert_eq!(priority2, PRIORITY_AUTO_DROP);
1871 }
1872
1873 #[test]
1877 fn rare_sampler_disabled_does_not_catch_unsampled() {
1878 let mut sampler = create_test_sampler(); sampler.sampling_rate = 0.0;
1880 sampler.probabilistic_sampler_enabled = true;
1881
1882 let span = create_top_level_span(1);
1883 let mut trace = create_test_trace(vec![span]);
1884
1885 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
1886 assert!(!keep, "rare disabled should not catch the trace");
1887 assert_eq!(priority, PRIORITY_AUTO_DROP);
1888 }
1889
1890 #[test]
1893 fn rare_sampler_catches_priority_auto_drop_in_legacy_path() {
1894 let mut sampler = create_sampler_with_rare_enabled();
1895 sampler.probabilistic_sampler_enabled = false;
1896
1897 let mut attrs = saluki_common::collections::FastHashMap::default();
1898 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
1899 attrs.insert(
1900 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
1901 AttributeValue::Float(PRIORITY_AUTO_DROP as f64),
1902 );
1903 let span = create_test_span(1, 0).with_attributes(attrs);
1904 let mut trace = create_test_trace(vec![span]);
1905
1906 let SamplerOutcome {
1907 keep,
1908 priority,
1909 decision_maker,
1910 ..
1911 } = sampler.run_samplers(&mut trace);
1912 assert!(keep, "rare sampler should catch PriorityAutoDrop on first occurrence");
1913 assert_eq!(priority, PRIORITY_AUTO_DROP, "tracer-set priority should be preserved");
1914 assert_eq!(decision_maker, "");
1915 }
1916
1917 #[test]
1920 fn rare_sampler_preserves_user_keep_priority_in_legacy_path() {
1921 let mut sampler = create_sampler_with_rare_enabled();
1922 sampler.probabilistic_sampler_enabled = false;
1923
1924 let mut attrs = saluki_common::collections::FastHashMap::default();
1925 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
1926 attrs.insert(
1927 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
1928 AttributeValue::Float(2.0),
1929 ); let span = create_test_span(1, 0).with_attributes(attrs);
1931 let mut trace = create_test_trace(vec![span]);
1932
1933 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
1934 assert!(keep);
1935 assert_eq!(priority, 2, "UserKeep priority must not be downgraded to AutoKeep");
1936 }
1937
1938 #[test]
1940 fn probabilistic_100_percent_keeps_trace_with_decision_maker() {
1941 let mut sampler = create_test_sampler(); sampler.sampling_rate = 1.0;
1943 sampler.probabilistic_sampler_enabled = true;
1944
1945 let span = create_top_level_span(1);
1946 let mut trace = create_test_trace(vec![span]);
1947
1948 let SamplerOutcome {
1949 keep,
1950 priority,
1951 decision_maker,
1952 ..
1953 } = sampler.run_samplers(&mut trace);
1954 assert!(keep);
1955 assert_eq!(priority, PRIORITY_AUTO_KEEP);
1956 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC);
1957 }
1958
1959 #[test]
1961 fn probabilistic_0_percent_drops_trace() {
1962 let mut sampler = create_test_sampler(); sampler.sampling_rate = 0.0;
1964 sampler.probabilistic_sampler_enabled = true;
1965 sampler.error_sampling_enabled = false;
1966
1967 let span = create_top_level_span(1);
1968 let mut trace = create_test_trace(vec![span]);
1969
1970 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
1971 assert!(!keep);
1972 assert_eq!(priority, PRIORITY_AUTO_DROP);
1973 }
1974
1975 #[test]
1978 fn otlp_rate_decisions_record_under_the_probabilistic_sampler() {
1979 let mut sampler = create_test_sampler();
1982 sampler.probabilistic_sampler_enabled = false;
1983 sampler.error_sampling_enabled = false;
1984
1985 let mut trace = create_otlp_test_trace(
1986 vec![create_top_level_span(1)],
1987 OTLP_TEST_RECEIVER_SAMPLING_RATE,
1988 OTLP_RECEIVER_DROPPED_TRACE_ID,
1989 );
1990
1991 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
1992 assert!(!keep);
1993 assert_eq!(priority, PRIORITY_AUTO_DROP);
1994
1995 let decisions = sampler.telemetry.snapshot_decisions();
1996 let (key, counts) = decisions.iter().next().unwrap();
1997 assert_eq!(key.sampler, telemetry::SamplerName::Probabilistic);
1998 assert_eq!(counts.seen, 1);
1999 assert_eq!(counts.kept, 0);
2000 }
2001
2002 #[test]
2003 fn rare_sampler_catches_otlp_no_priority_trace() {
2004 let mut sampler = create_sampler_with_rare_enabled();
2005 sampler.probabilistic_sampler_enabled = false;
2006 sampler.error_sampling_enabled = false;
2007
2008 let mut trace = create_otlp_test_trace(
2009 vec![create_top_level_span(1)],
2010 OTLP_TEST_RECEIVER_SAMPLING_RATE,
2011 OTLP_RECEIVER_DROPPED_TRACE_ID,
2012 );
2013
2014 let SamplerOutcome {
2015 keep,
2016 priority,
2017 decision_maker,
2018 root_span_idx: root_idx,
2019 ..
2020 } = sampler.run_samplers(&mut trace);
2021 assert!(
2022 keep,
2023 "rare sampler should keep OTLP trace with no priority on first occurrence"
2024 );
2025 assert_eq!(priority, PRIORITY_AUTO_KEEP);
2026 assert_eq!(decision_maker, "");
2027 assert_eq!(
2028 trace.spans()[root_idx.unwrap()]
2029 .attributes
2030 .get(rare_sampler::RARE_KEY)
2031 .and_then(AttributeValue::as_float),
2032 Some(1.0),
2033 "_dd.rare should be set to 1 on first occurrence"
2034 );
2035 }
2036
2037 #[test]
2042 fn rare_wins_over_probabilistic_no_decision_maker_tag() {
2043 let mut sampler = create_sampler_with_rare_enabled();
2044 sampler.sampling_rate = 1.0;
2045 sampler.probabilistic_sampler_enabled = true;
2046
2047 let span = create_top_level_span(1);
2048 let mut trace = create_test_trace(vec![span]);
2049
2050 let SamplerOutcome {
2051 keep,
2052 priority,
2053 decision_maker,
2054 ..
2055 } = sampler.run_samplers(&mut trace);
2056 assert!(keep);
2057 assert_eq!(priority, PRIORITY_AUTO_KEEP);
2058 assert_eq!(decision_maker, "", "rare takes precedence—_dd.p.dm must not be set");
2059 }
2060
2061 #[test]
2066 fn rare_catches_error_trace_before_error_sampler() {
2067 let mut sampler = create_sampler_with_rare_enabled();
2068 sampler.probabilistic_sampler_enabled = false;
2069 sampler.error_sampling_enabled = true;
2070
2071 let span = create_top_level_span(1);
2072 let error_span = create_test_span(2, 1); let mut trace = create_test_trace(vec![span, error_span]);
2074
2075 let SamplerOutcome {
2076 keep,
2077 priority,
2078 decision_maker,
2079 ..
2080 } = sampler.run_samplers(&mut trace);
2081 assert!(keep, "rare should catch the trace before the error sampler");
2082 assert_eq!(priority, PRIORITY_AUTO_KEEP);
2083 assert_eq!(decision_maker, "");
2084 }
2085
2086 #[test]
2091 fn manual_drop_short_circuits_before_rare() {
2092 let mut sampler = create_sampler_with_rare_enabled();
2093 sampler.probabilistic_sampler_enabled = false;
2094
2095 let mut attrs = saluki_common::collections::FastHashMap::default();
2096 attrs.insert(MetaString::from("_top_level"), AttributeValue::Float(1.0));
2097 attrs.insert(
2098 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
2099 AttributeValue::Float(-1.0),
2100 ); let span = create_test_span(1, 0).with_attributes(attrs);
2102 let mut trace = create_test_trace(vec![span]);
2103
2104 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
2105 assert!(!keep, "UserDrop must be dropped even when rare would match");
2106 assert_eq!(priority, -1);
2107 }
2108
2109 fn create_sampler_with_ets() -> TraceSampler {
2113 TraceSampler {
2114 error_tracking_standalone: true,
2115 ..create_test_sampler()
2116 }
2117 }
2118
2119 #[test]
2121 fn ets_keeps_trace_with_error() {
2122 let mut sampler = create_sampler_with_ets();
2123
2124 let span = create_test_span(1, 1); let mut trace = create_test_trace(vec![span]);
2126
2127 let SamplerOutcome {
2128 keep,
2129 priority,
2130 decision_maker,
2131 ..
2132 } = sampler.run_samplers(&mut trace);
2133 assert!(keep, "ETS should keep traces with errors");
2134 assert_eq!(priority, PRIORITY_AUTO_KEEP);
2135 assert_eq!(decision_maker, "", "ETS does not set a decision maker");
2136 }
2137
2138 #[test]
2140 fn ets_drops_trace_without_error() {
2141 let mut sampler = create_sampler_with_ets();
2142
2143 let span = create_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
2145
2146 let SamplerOutcome { keep, priority, .. } = sampler.run_samplers(&mut trace);
2147 assert!(!keep, "ETS should drop traces without errors");
2148 assert_eq!(priority, PRIORITY_AUTO_DROP);
2149 }
2150
2151 #[test]
2153 fn ets_forwards_dropped_trace_with_dropped_flag() {
2154 let mut sampler = create_sampler_with_ets();
2155
2156 let mut attrs = saluki_common::collections::FastHashMap::default();
2158 attrs.insert(
2159 MetaString::from(KEY_SPAN_SAMPLING_MECHANISM),
2160 AttributeValue::Float(8.0),
2161 );
2162 let span = create_test_span(1, 0).with_attributes(attrs);
2163 let mut trace = create_test_trace(vec![span]);
2164
2165 let forwarded = sampler.process_trace(&mut trace);
2166 assert!(forwarded, "ETS should forward non-error traces to intake");
2167 assert!(trace.dropped_trace, "non-error ETS trace should have DroppedTrace=true");
2168 }
2169
2170 #[test]
2172 fn ets_keeps_trace_with_exception_span_event() {
2173 let mut sampler = create_sampler_with_ets();
2174
2175 let mut attrs = saluki_common::collections::FastHashMap::default();
2177 attrs.insert(
2178 MetaString::from("_dd.span_events.has_exception"),
2179 AttributeValue::String(MetaString::from("true")),
2180 );
2181 let span = create_test_span(1, 0).with_attributes(attrs);
2182 let mut trace = create_test_trace(vec![span]);
2183
2184 let SamplerOutcome { keep, .. } = sampler.run_samplers(&mut trace);
2185 assert!(keep, "ETS should treat exception span events as errors");
2186 }
2187
2188 #[test]
2190 fn ets_disabled_uses_normal_sampling() {
2191 let mut sampler = create_test_sampler(); sampler.sampling_rate = 1.0;
2193 sampler.probabilistic_sampler_enabled = true;
2194
2195 let span = create_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
2197
2198 let SamplerOutcome {
2199 keep, decision_maker, ..
2200 } = sampler.run_samplers(&mut trace);
2201 assert!(keep, "normal probabilistic sampling should keep the trace");
2202 assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC);
2203 }
2204
2205 fn create_receiver_kept_otlp_test_trace(spans: Vec<DdSpan>) -> Trace {
2211 create_otlp_test_trace(spans, OTLP_TEST_RECEIVER_SAMPLING_RATE, OTLP_RECEIVER_KEPT_TRACE_ID)
2212 }
2213
2214 fn create_sampler_with_ets_legacy() -> TraceSampler {
2215 TraceSampler {
2216 error_tracking_standalone: true,
2217 probabilistic_sampler_enabled: false,
2218 ..create_test_sampler()
2219 }
2220 }
2221
2222 #[test]
2225 fn ets_otlp_non_error_gets_presample_priority_and_dm() {
2226 let mut sampler = create_sampler_with_ets_legacy();
2227
2228 let span = create_test_span(1, 0); let mut trace = create_receiver_kept_otlp_test_trace(vec![span]);
2230
2231 let SamplerOutcome {
2232 keep,
2233 priority,
2234 decision_maker: dm,
2235 ..
2236 } = sampler.run_samplers(&mut trace);
2237 assert!(!keep, "ETS should drop non-error OTLP traces");
2238 assert_eq!(
2239 priority, PRIORITY_AUTO_KEEP,
2240 "OTLP pre-sampling sets priority=AutoKeep even for ETS-dropped traces"
2241 );
2242 assert_eq!(dm, DECISION_MAKER_PROBABILISTIC, "OTLP pre-sampling sets dm=-9");
2243 }
2244
2245 #[test]
2247 fn ets_otlp_error_gets_presample_priority_and_dm() {
2248 let mut sampler = create_sampler_with_ets_legacy();
2249
2250 let span = create_test_span(1, 1); let mut trace = create_receiver_kept_otlp_test_trace(vec![span]);
2252
2253 let SamplerOutcome {
2254 keep,
2255 priority,
2256 decision_maker: dm,
2257 ..
2258 } = sampler.run_samplers(&mut trace);
2259 assert!(keep, "ETS should keep error OTLP traces");
2260 assert_eq!(priority, PRIORITY_AUTO_KEEP, "OTLP pre-sampling sets priority=AutoKeep");
2261 assert_eq!(dm, DECISION_MAKER_PROBABILISTIC, "OTLP pre-sampling sets dm=-9");
2262 }
2263
2264 #[test]
2266 fn ets_otlp_receiver_drop_gets_drop_priority_and_no_dm() {
2267 let mut sampler = create_sampler_with_ets_legacy();
2268
2269 let mut trace = create_otlp_test_trace(
2270 vec![create_test_span(1, 0)],
2271 OTLP_TEST_RECEIVER_SAMPLING_RATE,
2272 OTLP_RECEIVER_DROPPED_TRACE_ID,
2273 );
2274
2275 assert!(sampler.process_trace(&mut trace), "ETS forwards non-error traces");
2276 assert!(trace.dropped_trace);
2277 assert_eq!(trace.priority, Some(PRIORITY_AUTO_DROP));
2278 assert_eq!(trace.decision_maker, None);
2279 }
2280
2281 #[test]
2284 fn ets_otlp_probabilistic_path_skips_presample() {
2285 let mut sampler = create_sampler_with_ets_legacy();
2286 sampler.probabilistic_sampler_enabled = true; let span = create_test_span(1, 0); let mut trace = create_receiver_kept_otlp_test_trace(vec![span]);
2290
2291 let SamplerOutcome {
2292 keep,
2293 priority,
2294 decision_maker: dm,
2295 ..
2296 } = sampler.run_samplers(&mut trace);
2297 assert!(!keep, "ETS should drop non-error traces");
2298 assert_eq!(
2299 priority, PRIORITY_AUTO_DROP,
2300 "no pre-sampling when probabilistic path active"
2301 );
2302 assert_eq!(dm, "", "no dm when probabilistic path active");
2303 }
2304
2305 #[test]
2307 fn ets_non_otlp_unaffected_by_presample() {
2308 let mut sampler = create_sampler_with_ets_legacy();
2309
2310 let span = create_test_span(1, 0); let mut trace = create_test_trace(vec![span]);
2312
2313 let SamplerOutcome {
2314 keep,
2315 priority,
2316 decision_maker: dm,
2317 ..
2318 } = sampler.run_samplers(&mut trace);
2319 assert!(!keep, "ETS should drop non-error non-OTLP traces");
2320 assert_eq!(priority, PRIORITY_AUTO_DROP, "non-OTLP traces use default ETS priority");
2321 assert_eq!(dm, "", "non-OTLP traces get no dm");
2322 }
2323
2324 #[test]
2326 fn ets_otlp_user_priority_gets_manual_dm() {
2327 let mut sampler = create_sampler_with_ets_legacy();
2328
2329 let mut span = create_test_span(1, 0);
2330 span.attributes.insert(
2331 MetaString::from(SAMPLING_PRIORITY_METRIC_KEY),
2332 AttributeValue::Float(2.0),
2333 ); let mut trace = create_receiver_kept_otlp_test_trace(vec![span]);
2335
2336 let SamplerOutcome {
2337 keep,
2338 priority,
2339 decision_maker: dm,
2340 ..
2341 } = sampler.run_samplers(&mut trace);
2342 assert!(!keep, "ETS drops non-error traces regardless of user priority");
2343 assert_eq!(priority, PRIORITY_USER_KEEP, "user priority is preserved");
2344 assert_eq!(dm, DECISION_MAKER_MANUAL, "user-set priority gets dm=-4");
2345 }
2346
2347 #[test]
2354 fn otlp_receiver_sampling_does_not_depend_on_span_attributes_or_root() {
2355 let mut sampler = create_test_sampler();
2356 sampler.probabilistic_sampler_enabled = false;
2357
2358 let span_sets = [
2359 ("rooted", vec![create_test_span(1, 0)]),
2360 (
2361 "original root removed",
2362 vec![
2363 create_test_span(2, 0).with_parent_id(1),
2364 create_test_span(3, 0).with_parent_id(2),
2365 ],
2366 ),
2367 ];
2368 let cases = [
2369 (
2370 OTLP_RECEIVER_KEPT_TRACE_ID,
2371 true,
2372 PRIORITY_AUTO_KEEP,
2373 DECISION_MAKER_PROBABILISTIC,
2374 ),
2375 (OTLP_RECEIVER_DROPPED_TRACE_ID, false, PRIORITY_AUTO_DROP, ""),
2376 ];
2377 for (name, spans) in &span_sets {
2378 for (trace_id, want_keep, want_priority, want_dm) in cases {
2379 let mut trace = create_otlp_test_trace(spans.clone(), OTLP_TEST_RECEIVER_SAMPLING_RATE, trace_id);
2380
2381 let SamplerOutcome {
2382 keep,
2383 priority,
2384 decision_maker,
2385 ..
2386 } = sampler.run_samplers(&mut trace);
2387 assert_eq!(keep, want_keep, "{name}, trace ID {trace_id}");
2388 assert_eq!(priority, want_priority, "{name}, trace ID {trace_id}");
2389 assert_eq!(decision_maker, want_dm, "{name}, trace ID {trace_id}");
2390 }
2391 }
2392 }
2393
2394 #[test]
2395 fn native_trace_with_otel_trace_id_attribute_is_not_sampled_as_otlp() {
2396 let spoofed_trace = || {
2397 let mut span = create_top_level_span(1);
2398 span.attributes.insert(
2399 MetaString::from_static(OTEL_TRACE_ID_META_KEY),
2400 AttributeValue::String(MetaString::from("0000000000000000000000000000007b")),
2401 );
2402 let mut trace = create_test_trace(vec![span]);
2403 trace.trace_id_low = OTLP_RECEIVER_KEPT_TRACE_ID;
2404 trace
2405 };
2406
2407 let mut sampler = create_test_sampler();
2409 sampler.probabilistic_sampler_enabled = false;
2410 let SamplerOutcome { decision_maker, .. } = sampler.run_samplers(&mut spoofed_trace());
2411 assert_eq!(decision_maker, "");
2412 let decisions = sampler.telemetry.snapshot_decisions();
2413 let (key, _) = decisions.iter().next().unwrap();
2414 assert_eq!(key.sampler, telemetry::SamplerName::NoPriority);
2415
2416 let mut sampler = create_sampler_with_ets_legacy();
2418 let SamplerOutcome {
2419 keep,
2420 priority,
2421 decision_maker,
2422 ..
2423 } = sampler.run_samplers(&mut spoofed_trace());
2424 assert!(!keep);
2425 assert_eq!(priority, PRIORITY_AUTO_DROP);
2426 assert_eq!(decision_maker, "");
2427 }
2428
2429 #[test]
2430 fn probabilistic_decision_maker_placement_follows_ingest_info() {
2431 let mut sampler = create_test_sampler();
2434
2435 let mut otlp = create_receiver_kept_otlp_test_trace(vec![create_test_span(1, 0)]);
2436 assert!(sampler.process_trace(&mut otlp));
2437 assert_eq!(otlp.decision_maker.as_deref(), Some(DECISION_MAKER_PROBABILISTIC));
2438 assert!(!otlp.spans()[0].attributes.contains_key(TAG_DECISION_MAKER));
2439
2440 let mut native = create_test_trace(vec![create_test_span(1, 0)]);
2441 assert!(sampler.process_trace(&mut native));
2442 assert_eq!(native.decision_maker.as_deref(), Some(DECISION_MAKER_PROBABILISTIC));
2443 }
2444
2445 #[test]
2446 fn analytics_events_forward_dropped_traces_with_only_event_spans() {
2447 let rates = HashMap::from([("test-service".to_string(), 1.0)]);
2448 let mut sampler = create_test_sampler();
2449 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
2451 sampler.event_processor = EventProcessor::for_tests(&HashMap::new(), &rates, f64::INFINITY);
2452
2453 let root = create_test_span(1, 0);
2454 let child = create_span_with_parent_and_service(2, 1, "other-service", &[]);
2455 let mut trace = create_test_trace(vec![root, child]);
2456
2457 assert!(sampler.process_trace(&mut trace));
2458 assert_eq!(trace.spans().len(), 1);
2461 assert!(trace.dropped_trace);
2462 assert_eq!(trace.priority, Some(PRIORITY_AUTO_DROP));
2463 let event_span = &trace.spans()[0];
2464 assert!(event_span.attributes.contains_key(KEY_ANALYZED_SPANS));
2465 assert!(!event_span.attributes.contains_key("_dd1.sr.eausr"));
2467 }
2468
2469 #[test]
2470 fn analytics_events_dropped_trace_without_survivors_is_not_forwarded() {
2471 let mut sampler = create_test_sampler();
2472 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
2474
2475 let mut trace = create_test_trace(vec![create_test_span(1, 0)]);
2476 assert!(!sampler.process_trace(&mut trace));
2478 }
2479
2480 #[test]
2481 fn analytics_events_marks_ride_along_on_kept_traces() {
2482 let rates = HashMap::from([("test-service".to_string(), 1.0)]);
2483 let mut sampler = create_test_sampler();
2484 sampler.event_processor = EventProcessor::for_tests(&HashMap::new(), &rates, f64::INFINITY);
2485
2486 let root = create_test_span(1, 0);
2487 let child = create_span_with_parent_and_service(2, 1, "other-service", &[]);
2488 let mut trace = create_test_trace(vec![root, child]);
2489
2490 assert!(sampler.process_trace(&mut trace));
2491 assert_eq!(trace.spans().len(), 2);
2493 assert!(!trace.dropped_trace);
2494 assert!(trace.spans()[0].attributes.contains_key(KEY_ANALYZED_SPANS));
2495 assert!(!trace.spans()[1].attributes.contains_key(KEY_ANALYZED_SPANS));
2496 }
2497
2498 #[test]
2499 fn single_span_sampling_wins_over_analytics_events() {
2500 let rates = HashMap::from([("test-service".to_string(), 1.0)]);
2501 let mut sampler = create_test_sampler();
2502 sampler.sampling_rate = 0.0; sampler.probabilistic_sampler_enabled = true;
2504 sampler.event_processor = EventProcessor::for_tests(&HashMap::new(), &rates, f64::INFINITY);
2505
2506 let mut attrs = saluki_common::collections::FastHashMap::default();
2507 attrs.insert(
2508 MetaString::from(KEY_SPAN_SAMPLING_MECHANISM),
2509 AttributeValue::Float(8.0),
2510 );
2511 let sss_span = create_test_span(1, 0).with_attributes(attrs);
2512 let mut trace = create_test_trace(vec![sss_span]);
2513
2514 assert!(sampler.process_trace(&mut trace));
2515 assert_eq!(trace.spans().len(), 1);
2517 assert!(!trace.dropped_trace);
2518 assert_eq!(trace.priority, Some(PRIORITY_USER_KEEP));
2519 }
2520
2521 #[test]
2522 fn manual_drop_skips_event_extraction() {
2523 let rates = HashMap::from([("test-service".to_string(), 1.0)]);
2524 let mut sampler = create_test_sampler();
2525 sampler.probabilistic_sampler_enabled = false;
2527 sampler.event_processor = EventProcessor::for_tests(&HashMap::new(), &rates, f64::INFINITY);
2528
2529 let root = create_test_span(1, 0);
2531 let mut trace = create_test_trace(vec![root]);
2532 trace.priority = Some(PRIORITY_USER_DROP);
2533
2534 assert!(!sampler.process_trace(&mut trace));
2535 assert!(!trace.spans()[0].attributes.contains_key(KEY_ANALYZED_SPANS));
2536 }
2537
2538 fn create_remote_sampler(errors_per_second: f64) -> (TestSamplingPublisher, TraceSampler) {
2543 let (publisher, remote_sampling) = TestSamplingPublisher::new();
2544 let sampler = TraceSampler {
2545 sampling_rate: 0.0,
2546 error_sampler: errors::ErrorsSampler::new(errors_per_second, 1.0),
2547 rare_sampler: rare_sampler::RareSampler::new(
2548 false,
2549 1000.0,
2550 std::time::Duration::from_secs(300),
2551 200,
2552 telemetry::SamplerCounters::new(),
2553 ),
2554 static_settings: SamplerSettings {
2555 target_traces_per_second: 10.0,
2556 errors_per_second,
2557 rare_sampler_enabled: false,
2558 },
2559 remote_sampling,
2560 ..create_test_sampler()
2561 };
2562 (publisher, sampler)
2563 }
2564
2565 fn prod_env() -> AgentEnv {
2567 AgentEnv::resolve(None, Some("Prod"), &[], &[])
2568 }
2569
2570 fn rare_only_traces() -> domains::traces::Domain {
2572 domains::traces::Domain {
2573 target_traces_per_second: 7.0,
2574 errors_per_second: 9.0,
2575 enable_rare_sampler: true,
2576 probabilistic_sampler: domains::traces::ProbabilisticSampler {
2577 enabled: true,
2578 sampling_percentage: 0.0,
2579 ..Default::default()
2580 },
2581 ..Default::default()
2582 }
2583 }
2584
2585 fn transform_empty_buffer(sampler: &mut TraceSampler) {
2586 sampler.process_buffer(&mut EventsBuffer::default());
2587 }
2588
2589 fn live_tps(sampler: &TraceSampler) -> (f64, f64) {
2590 (
2591 sampler.priority_sampler.get_target_tps(),
2592 sampler.error_sampler.get_target_tps(),
2593 )
2594 }
2595
2596 fn rare_keeps_new_trace(sampler: &mut TraceSampler, span_id: u64) -> bool {
2597 let mut trace = create_test_trace(vec![
2598 create_top_level_span(span_id).with_service(MetaString::from(format!("rare-probe-{span_id}")))
2599 ]);
2600 sampler.run_samplers(&mut trace).keep
2601 }
2602
2603 #[test]
2605 fn remote_configuration_changes_live_sampler_settings() {
2606 let (publisher, mut sampler) = create_remote_sampler(10.0);
2607 publisher.assign([(
2608 "sampling",
2609 r#"{"all_envs":{"priority_sampler_target_TPS":41,"errors_sampler_target_TPS":42,
2610 "rare_sampler_enabled":true}}"#,
2611 )]);
2612 assert_eq!(live_tps(&sampler), (10.0, 10.0), "nothing applies before a buffer");
2613 assert!(!rare_keeps_new_trace(&mut sampler, 1));
2614
2615 transform_empty_buffer(&mut sampler);
2616 assert_eq!(live_tps(&sampler), (41.0, 42.0));
2617 assert!(rare_keeps_new_trace(&mut sampler, 2));
2618 assert_eq!(sampler.no_priority_sampler.test_target_tps(), 10.0);
2620 }
2621
2622 #[test]
2623 fn more_than_one_remote_configuration_changes_nothing() {
2624 let (publisher, mut sampler) = create_remote_sampler(10.0);
2625 publisher.assign([
2626 (
2627 "a",
2628 r#"{"all_envs":{"priority_sampler_target_TPS":41,"rare_sampler_enabled":true}}"#,
2629 ),
2630 ("b", r#"{"all_envs":{"errors_sampler_target_TPS":42}}"#),
2631 ]);
2632 transform_empty_buffer(&mut sampler);
2633 assert_eq!(live_tps(&sampler), (10.0, 10.0));
2634 assert!(!rare_keeps_new_trace(&mut sampler, 1));
2635 }
2636
2637 #[test]
2638 fn more_than_one_remote_configuration_after_one_keeps_remote_settings() {
2639 let (publisher, mut sampler) = create_remote_sampler(10.0);
2640 publisher.assign([(
2641 "a",
2642 r#"{"all_envs":{"priority_sampler_target_TPS":41,"errors_sampler_target_TPS":42,
2643 "rare_sampler_enabled":true}}"#,
2644 )]);
2645 transform_empty_buffer(&mut sampler);
2646 assert_eq!(live_tps(&sampler), (41.0, 42.0));
2647
2648 publisher.assign([
2649 ("a", r#"{"all_envs":{"priority_sampler_target_TPS":7}}"#),
2650 ("b", r#"{"all_envs":{"errors_sampler_target_TPS":8}}"#),
2651 ]);
2652 transform_empty_buffer(&mut sampler);
2653 assert_eq!(
2654 live_tps(&sampler),
2655 (41.0, 42.0),
2656 "the rejected assignment changes nothing"
2657 );
2658 assert!(rare_keeps_new_trace(&mut sampler, 1));
2659 }
2660
2661 #[test]
2663 fn remote_sampling_subscription_reaches_the_built_sampler() {
2664 let (publisher, subscription) = TestSamplingPublisher::new();
2665 let config = TraceSamplerConfiguration::from_configuration(
2666 &rare_only_traces(),
2667 &domains::otlp::Traces::default(),
2668 prod_env(),
2669 )
2670 .with_remote_sampling(subscription);
2671 let mut sampler = config.build_sampler();
2672 assert_eq!(live_tps(&sampler), (7.0, 9.0));
2673 assert!(rare_keeps_new_trace(&mut sampler, 1));
2674
2675 publisher.assign([(
2677 "sampling",
2678 r#"{"all_envs":{"priority_sampler_target_TPS":null,"errors_sampler_target_TPS":null,
2679 "rare_sampler_enabled":null},
2680 "by_env":[{"env":"prod","config":{"priority_sampler_target_TPS":41,"errors_sampler_target_TPS":42,
2681 "rare_sampler_enabled":false}}]}"#,
2682 )]);
2683 transform_empty_buffer(&mut sampler);
2684 assert_eq!(live_tps(&sampler), (41.0, 42.0), "the normalized env selects `prod`");
2685 assert!(!rare_keeps_new_trace(&mut sampler, 2));
2686
2687 publisher.assign([(
2688 "sampling",
2689 r#"{"all_envs":{"priority_sampler_target_TPS":null,"errors_sampler_target_TPS":null,
2690 "rare_sampler_enabled":null},"by_env":null}"#,
2691 )]);
2692 transform_empty_buffer(&mut sampler);
2693 assert_eq!(
2694 live_tps(&sampler),
2695 (7.0, 9.0),
2696 "each setting reverts to its static value"
2697 );
2698 assert!(rare_keeps_new_trace(&mut sampler, 3));
2699 }
2700
2701 #[test]
2703 fn remote_selection_matches_the_resolved_agent_env() {
2704 let payload = r#"{"all_envs":{"errors_sampler_target_TPS":30},
2705 "by_env":[{"env":"prod","config":{"priority_sampler_target_TPS":42}},
2706 {"env":"none","config":{"priority_sampler_target_TPS":41}}]}"#;
2707 let cases = [
2708 (AgentEnv::resolve(None, None, &[], &[]), (41.0, 30.0)),
2710 (prod_env(), (42.0, 30.0)),
2711 (AgentEnv::resolve(Some("!!!"), None, &[], &[]), (7.0, 30.0)),
2713 ];
2714 for (agent_env, expected) in cases {
2715 let (publisher, subscription) = TestSamplingPublisher::new();
2716 let env = agent_env.as_str().to_owned();
2717 let mut sampler = TraceSamplerConfiguration::from_configuration(
2718 &rare_only_traces(),
2719 &domains::otlp::Traces::default(),
2720 agent_env,
2721 )
2722 .with_remote_sampling(subscription)
2723 .build_sampler();
2724 publisher.assign([("sampling", payload)]);
2725 transform_empty_buffer(&mut sampler);
2726 assert_eq!(live_tps(&sampler), expected, "agent env {env:?}");
2727 }
2728 }
2729
2730 #[test]
2735 fn remote_selection_and_priority_sampling_share_agent_env() {
2736 let (publisher, subscription) = TestSamplingPublisher::new();
2737 let agent_env = AgentEnv::resolve(Some("Staging"), Some("prod"), &[], &[]);
2738 let mut sampler = TraceSamplerConfiguration::from_configuration(
2739 &rare_only_traces(),
2740 &domains::otlp::Traces::default(),
2741 agent_env,
2742 )
2743 .with_remote_sampling(subscription)
2744 .build_sampler();
2745 publisher.assign([(
2746 "sampling",
2747 r#"{"by_env":[{"env":"staging","config":{"priority_sampler_target_TPS":43}},
2748 {"env":"prod","config":{"priority_sampler_target_TPS":42}}]}"#,
2749 )]);
2750 transform_empty_buffer(&mut sampler);
2751 assert_eq!(live_tps(&sampler).0, 43.0, "remote selection uses the agent env");
2752
2753 let start = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
2756 for now in [start, start + std::time::Duration::from_secs(10)] {
2757 for trace_id in 0..100 {
2758 let mut trace = create_test_trace(vec![create_test_span(trace_id, 0)]);
2759 sampler
2760 .priority_sampler
2761 .sample(now, &mut trace, 0, PRIORITY_AUTO_KEEP, 0.0);
2762 }
2763 }
2764 let learned: Vec<_> = sampler
2765 .priority_sampler
2766 .test_signature_sample_rates()
2767 .0
2768 .into_keys()
2769 .collect();
2770 assert_eq!(
2771 learned,
2772 vec![signature::ServiceSignature::new("test-service", "staging").hash()],
2773 "priority sampling uses the agent env"
2774 );
2775 }
2776
2777 #[tokio::test]
2779 async fn run_loop_samples_with_the_published_remote_settings() {
2780 let (publisher, subscription) = TestSamplingPublisher::new();
2781 let config = TraceSamplerConfiguration::from_configuration(
2782 &rare_only_traces(),
2783 &domains::otlp::Traces::default(),
2784 prod_env(),
2785 )
2786 .with_remote_sampling(subscription);
2787 let (control, mut outputs) = TestComponentDriver::transform(config)
2788 .await
2789 .expect("the sampler should start");
2790 let new_service_trace = |span_id: u64| {
2791 Event::Trace(create_test_trace(vec![
2792 create_top_level_span(span_id).with_service(MetaString::from(format!("rare-probe-{span_id}")))
2793 ]))
2794 };
2795
2796 control.send_event(new_service_trace(1)).await;
2798 outputs
2799 .default_output()
2800 .next()
2801 .await
2802 .expect("the rare sampler should keep the first trace");
2803
2804 publisher.assign([("sampling", r#"{"all_envs":{"rare_sampler_enabled":false}}"#)]);
2805 control.send_event(new_service_trace(2)).await;
2806 control.shutdown().await.expect("the sampler should stop cleanly");
2807
2808 let kept = outputs.default_output().collect_events().await;
2809 assert!(
2810 kept.is_empty(),
2811 "the second trace should be dropped now that remote settings disable the rare sampler: {kept:?}"
2812 );
2813 }
2814
2815 #[test]
2816 fn remote_field_removed_later_falls_back_to_static() {
2817 let (publisher, mut sampler) = create_remote_sampler(10.0);
2818 publisher.assign([(
2819 "sampling",
2820 r#"{"all_envs":{"priority_sampler_target_TPS":41,"rare_sampler_enabled":true}}"#,
2821 )]);
2822 transform_empty_buffer(&mut sampler);
2823 assert_eq!(live_tps(&sampler), (41.0, 10.0));
2824
2825 publisher.assign([("sampling", r#"{"all_envs":{"errors_sampler_target_TPS":42}}"#)]);
2826 transform_empty_buffer(&mut sampler);
2827 assert_eq!(
2828 live_tps(&sampler),
2829 (10.0, 42.0),
2830 "the priority target reverts to static, not to 41"
2831 );
2832 assert!(
2833 !rare_keeps_new_trace(&mut sampler, 1),
2834 "the rare sampler reverts to static, not to enabled"
2835 );
2836 }
2837
2838 #[test]
2840 fn static_zero_errors_tps_stays_disabled_under_remote_configuration() {
2841 let (publisher, mut sampler) = create_remote_sampler(0.0);
2842 publisher.assign([("sampling", r#"{"all_envs":{"errors_sampler_target_TPS":5}}"#)]);
2843 transform_empty_buffer(&mut sampler);
2844 assert_eq!(sampler.error_sampler.get_target_tps(), 5.0);
2845
2846 let mut trace = create_test_trace(vec![create_test_span(1, 1)]);
2847 assert!(!sampler
2848 .error_sampler
2849 .sample_error(std::time::SystemTime::now(), &mut trace, 0));
2850 }
2851
2852 #[test]
2853 fn inert_remote_sampling_changes_nothing() {
2854 let mut sampler = TraceSampler {
2855 remote_sampling: TraceSamplingSubscription::default(),
2856 ..create_remote_sampler(10.0).1
2857 };
2858 sampler.priority_sampler.update_target_tps(41.0);
2860 sampler.error_sampler.update_target_tps(42.0);
2861 sampler.rare_sampler.set_enabled(true);
2862
2863 transform_empty_buffer(&mut sampler);
2864 assert_eq!(live_tps(&sampler), (41.0, 42.0));
2865 assert!(rare_keeps_new_trace(&mut sampler, 1));
2866 }
2867
2868 #[test]
2870 fn unassigned_after_remote_configuration_keeps_remote_settings() {
2871 let (publisher, mut sampler) = create_remote_sampler(10.0);
2872 publisher.assign([(
2873 "sampling",
2874 r#"{"all_envs":{"priority_sampler_target_TPS":41,"errors_sampler_target_TPS":42,
2875 "rare_sampler_enabled":true}}"#,
2876 )]);
2877 transform_empty_buffer(&mut sampler);
2878 let applied = sampler.remote_sampling.current().expect("a configuration is accepted");
2879
2880 publisher.assign(std::iter::empty::<(&str, &str)>());
2881 assert!(Arc::ptr_eq(
2882 &applied,
2883 &sampler.remote_sampling.current().expect("the configuration is kept")
2884 ));
2885 transform_empty_buffer(&mut sampler);
2886 assert_eq!(live_tps(&sampler), (41.0, 42.0));
2887 assert!(rare_keeps_new_trace(&mut sampler, 1));
2888 }
2889
2890 #[test]
2895 fn unread_publications_keep_the_newest_accepted_configuration() {
2896 const A: &str = r#"{"all_envs":{"priority_sampler_target_TPS":41,"errors_sampler_target_TPS":42,
2897 "rare_sampler_enabled":true}}"#;
2898 const B: &str = r#"{"all_envs":{"priority_sampler_target_TPS":43,"errors_sampler_target_TPS":44}}"#;
2899 type Case<'a> = (&'a str, Option<&'a str>, &'a [Option<&'a str>], (f64, f64), bool);
2902 let cases: [Case; 7] = [
2903 ("initial empty assignment", None, &[None], (10.0, 10.0), false),
2904 (
2905 "A then empty before the first buffer",
2906 None,
2907 &[Some(A), None],
2908 (41.0, 42.0),
2909 true,
2910 ),
2911 (
2912 "B then empty after A is applied",
2913 Some(A),
2914 &[Some(B), None],
2915 (43.0, 44.0),
2916 false,
2917 ),
2918 (
2919 "malformed after A is applied",
2920 Some(A),
2921 &[Some("not json")],
2922 (41.0, 42.0),
2923 true,
2924 ),
2925 (
2926 "array payload after A is applied",
2927 Some(A),
2928 &[Some("[]")],
2929 (41.0, 42.0),
2930 true,
2931 ),
2932 (
2933 "array settings after A is applied",
2934 Some(A),
2935 &[Some(r#"{"all_envs":[0,0,false]}"#)],
2936 (41.0, 42.0),
2937 true,
2938 ),
2939 (
2940 "accepted {} after A is applied",
2941 Some(A),
2942 &[Some("{}")],
2943 (10.0, 10.0),
2944 false,
2945 ),
2946 ];
2947
2948 for (name, applied, pending, tps, rare) in cases {
2949 let (publisher, mut sampler) = create_remote_sampler(10.0);
2950 let publish = |payload: &Option<&str>| match payload {
2951 Some(payload) => publisher.assign([("sampling", *payload)]),
2952 None => publisher.assign(std::iter::empty::<(&str, &str)>()),
2953 };
2954 if let Some(applied) = applied {
2955 publish(&Some(applied));
2956 transform_empty_buffer(&mut sampler);
2957 }
2958 pending.iter().for_each(publish);
2959 transform_empty_buffer(&mut sampler);
2960 assert_eq!(live_tps(&sampler), tps, "{name}");
2961 assert_eq!(rare_keeps_new_trace(&mut sampler, 1), rare, "{name}");
2962 }
2963 }
2964
2965 #[tokio::test]
2967 async fn run_loop_applies_a_configuration_withdrawn_before_the_first_buffer() {
2968 let (publisher, subscription) = TestSamplingPublisher::new();
2969 let config = TraceSamplerConfiguration::from_configuration(
2970 &rare_only_traces(),
2971 &domains::otlp::Traces::default(),
2972 prod_env(),
2973 )
2974 .with_remote_sampling(subscription);
2975 publisher.assign([("sampling", r#"{"all_envs":{"rare_sampler_enabled":false}}"#)]);
2976 publisher.assign(std::iter::empty::<(&str, &str)>());
2977
2978 let (control, mut outputs) = TestComponentDriver::transform(config)
2979 .await
2980 .expect("the sampler should start");
2981 control
2982 .send_event(Event::Trace(create_test_trace(vec![
2983 create_top_level_span(1).with_service(MetaString::from("rare-probe-1"))
2984 ])))
2985 .await;
2986 control.shutdown().await.expect("the sampler should stop cleanly");
2987
2988 let kept = outputs.default_output().collect_events().await;
2989 assert!(
2990 kept.is_empty(),
2991 "the remote configuration disables the rare sampler, which would otherwise keep the trace: {kept:?}"
2992 );
2993 }
2994
2995 #[test]
2997 fn remote_configuration_scales_learned_rates() {
2998 let (publisher, mut sampler) = create_remote_sampler(10.0);
2999
3000 let start = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
3003 for now in [start, start + std::time::Duration::from_secs(10)] {
3004 for trace_id in 0..100 {
3005 let mut trace = create_test_trace(vec![create_test_span(trace_id, 1)]);
3006 sampler
3007 .priority_sampler
3008 .sample(now, &mut trace, 0, PRIORITY_AUTO_KEEP, 0.0);
3009 sampler.error_sampler.sample_error(now, &mut trace, 0);
3010 }
3011 }
3012 let learned_rates = |sampler: &TraceSampler| {
3013 let priority: Vec<f64> = sampler
3014 .priority_sampler
3015 .test_signature_sample_rates()
3016 .0
3017 .into_values()
3018 .collect();
3019 let errors: Vec<f64> = sampler
3020 .error_sampler
3021 .test_signature_sample_rates()
3022 .0
3023 .into_values()
3024 .collect();
3025 (priority, errors)
3026 };
3027 assert_eq!(learned_rates(&sampler), (vec![0.5], vec![0.5]));
3028
3029 publisher.assign([(
3030 "sampling",
3031 r#"{"all_envs":{"priority_sampler_target_TPS":5,"errors_sampler_target_TPS":2}}"#,
3032 )]);
3033 transform_empty_buffer(&mut sampler);
3034
3035 assert_eq!(live_tps(&sampler), (5.0, 2.0));
3036 assert_eq!(
3037 learned_rates(&sampler),
3038 (vec![0.5 * (5.0 / 10.0)], vec![0.5 * (2.0 / 10.0)]),
3039 "each learned rate scales by the ratio of the new target to the old one"
3040 );
3041 }
3042
3043 #[test]
3045 fn reapplying_equal_remote_settings_keeps_sampler_state() {
3046 let (publisher, mut sampler) = create_remote_sampler(10.0);
3047 let payload = r#"{"all_envs":{"priority_sampler_target_TPS":5,"errors_sampler_target_TPS":4,
3048 "rare_sampler_enabled":true}}"#;
3049 publisher.assign([("sampling", payload)]);
3050 transform_empty_buffer(&mut sampler);
3051
3052 let start = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
3054 for now in [start, start + std::time::Duration::from_secs(10)] {
3055 for trace_id in 0..100 {
3056 let mut trace = create_test_trace(vec![create_test_span(trace_id, 1)]);
3057 sampler
3058 .priority_sampler
3059 .sample(now, &mut trace, 0, PRIORITY_AUTO_KEEP, 0.0);
3060 sampler.error_sampler.sample_error(now, &mut trace, 0);
3061 }
3062 }
3063 assert!(rare_keeps_new_trace(&mut sampler, 1));
3064
3065 #[derive(Debug, PartialEq)]
3066 struct LearnedState {
3067 priority_rates: (saluki_common::collections::FastHashMap<signature::Signature, f64>, f64),
3068 priority_size: i64,
3069 errors_rates: (saluki_common::collections::FastHashMap<signature::Signature, f64>, f64),
3070 errors_size: i64,
3071 tps: (f64, f64),
3072 }
3073 let state = |sampler: &TraceSampler| LearnedState {
3074 priority_rates: sampler.priority_sampler.test_signature_sample_rates(),
3075 priority_size: sampler.priority_sampler.test_size(),
3076 errors_rates: sampler.error_sampler.test_signature_sample_rates(),
3077 errors_size: sampler.error_sampler.test_size(),
3078 tps: live_tps(sampler),
3079 };
3080 let before = state(&sampler);
3081 assert_eq!(
3082 before.priority_rates.0.len(),
3083 1,
3084 "the priority sampler has learned a rate"
3085 );
3086 assert_eq!(before.errors_rates.0.len(), 1, "the errors sampler has learned a rate");
3087
3088 let applied = sampler.remote_sampling.current().expect("a configuration is accepted");
3089 publisher.assign([("sampling", payload)]);
3090 let republished = sampler.remote_sampling.current().expect("a configuration is accepted");
3091 assert!(
3092 !Arc::ptr_eq(&applied, &republished),
3093 "the equal snapshot is a new allocation"
3094 );
3095 transform_empty_buffer(&mut sampler);
3096
3097 assert_eq!(state(&sampler), before);
3098 let mut seen = create_test_trace(vec![
3099 create_top_level_span(1).with_service(MetaString::from("rare-probe-1"))
3100 ]);
3101 let SamplerOutcome { keep, .. } = sampler.run_samplers(&mut seen);
3102 assert!(!keep, "the rare sampler still remembers the signature it kept");
3103 }
3104}