Skip to main content

saluki_components/transforms/trace_sampler/
mod.rs

1//! Decides which incoming traces to forward before they reach the backend.
2//!
3//! The transform honors user sampling decisions and can keep traces through probabilistic,
4//! priority, error, and rare sampling. It also handles OTLP sampling metadata. When given an
5//! `APM_SAMPLING` subscription, it updates priority and error sampler targets and the rare sampler
6//! switch before processing each buffer. Without a subscription, it uses local configuration.
7//!
8//! TODO: Add trace metrics from `datadog-agent/pkg/trace/sampler/metrics.go`.
9
10use 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
50// Sampling priority constants (matching datadog-agent)
51const PRIORITY_AUTO_DROP: i32 = 0;
52const PRIORITY_AUTO_KEEP: i32 = 1;
53const PRIORITY_USER_KEEP: i32 = 2;
54
55// Single Span Sampling and Analytics Events keys
56const KEY_SPAN_SAMPLING_MECHANISM: &str = "_dd.span_sampling.mechanism";
57const KEY_ANALYZED_SPANS: &str = "_dd.analyzed";
58
59/// Configuration for the trace sampler transform.
60#[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    /// Environment the priority sampler uses for traces without one, and used to select a remote `by_env` entry.
73    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 updates, or an inert subscription when remote sampling is not configured.
84    remote_sampling: TraceSamplingSubscription,
85}
86
87impl TraceSamplerConfiguration {
88    /// Creates a new `TraceSamplerConfiguration` from the resolved traces domain.
89    ///
90    /// The OTLP trace settings live in their own domain, so they arrive as a separate slice rather
91    /// than through the traces domain. The priority sampler samples traces without an environment
92    /// under `agent_env`, and remote sampling selects its `by_env` entry with it.
93    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    /// Connects the trace sampler to `APM_SAMPLING` remote updates.
126    ///
127    /// Only priority and error sampling targets and rare sampling can change. For each field
128    /// left unset by the remote configuration, the sampler uses its local setting.
129    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        // The catalog holds a hash map slot plus one LRU slab entry per tracked signature. A
209        // configured 0 selects the default cap, matching the catalog's own resolution.
210        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        // Decision keys span the same (service, env) universe as the catalog, across a handful
224        // of samplers. The window resets every ten seconds, so steady-state growth follows active
225        // combinations; within a window, growth is capped at the telemetry's decision-key limit,
226        // with excess combinations rolled into one bucket per sampler.
227        builder
228            .minimum()
229            .with_map::<telemetry::DecisionKey, telemetry::DecisionCounts>("sampler decision window", catalog_capacity);
230
231        // Event extraction rates are bounded by their configured size, plus one nested operation
232        // map per configured service.
233        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    /// Local settings used when a remote configuration leaves a field unset.
259    static_settings: SamplerSettings,
260    /// Agent environment, used to select a remote `by_env` entry.
261    agent_env: MetaString,
262    remote_sampling: TraceSamplingSubscription,
263    /// Last update handled, so each buffer does not reapply the same settings.
264    applied_sampling: Option<Arc<RemoteSampling>>,
265}
266
267/// The outcome of evaluating a trace against the configured samplers.
268struct SamplerOutcome {
269    /// Whether the trace should be kept.
270    keep: bool,
271    /// The final sampling priority.
272    priority: i32,
273    /// The decision maker tag value for the deciding sampler, if any.
274    decision_maker: &'static str,
275    /// The index of the root span the samplers evaluated against, when one was found.
276    root_span_idx: Option<usize>,
277    /// The sampler that decided the trace.
278    sampler: telemetry::SamplerName,
279    /// Whether the samplers decided without short-circuiting, so event extraction should run.
280    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    /// Applies the latest accepted remote sampling configuration, if it has changed.
301    ///
302    /// Unset fields use local settings, not previous remote values. When the assignment becomes
303    /// empty, the subscription keeps reporting the last configuration, as the Datadog Agent does. A newly published
304    /// configuration can have the same values after a worker restart; applying it again preserves
305    /// learned sampling rates and the rare sampler's record of seen span signatures.
306    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, &current))
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            // The Datadog Agent does not update the no-priority sampler from this product.
321            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    /// Applies pending sampling settings, then samples every trace in the buffer with those settings.
330    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    /// Find the root span index of a trace.
339    fn get_root_span_index(&self, trace: &Trace) -> Option<usize> {
340        // The shared root finder, so every consumer anchors metadata to identical root spans.
341        get_root_span_index(trace.spans())
342    }
343
344    /// Check for user-set sampling priority in trace
345    fn get_user_priority(&self, trace: &Trace, root_span_idx: usize) -> Option<i32> {
346        // First check trace-level sampling priority (last-seen priority from OTLP ingest)
347        if let Some(priority) = trace.priority {
348            return Some(priority);
349        }
350
351        if trace.spans().is_empty() {
352            return None;
353        }
354
355        // Fall back to checking spans (for compatibility with non-OTLP traces)
356        // Prefer the root span (common case), but fall back to scanning all spans to be robust to ordering.
357        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    /// Returns `true` if the given trace ID should be probabilistically sampled.
376    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    /// Returns `true` if the trace contains a span with an error.
382    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    /// Returns `true` if the span has exception span events.
389    ///
390    /// This checks for the `_dd.span_events.has_exception` meta field set to `"true"`.
391    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    /// Computes the OTLP pre-sampling priority and decision maker for a trace, mirroring
403    /// `OTLPReceiver.createChunks` in DDA which runs before `runSamplersV1`.
404    ///
405    /// Returns `Some((priority, dm))` for OTLP traces when the probabilistic sampler is disabled,
406    /// or `None` if pre-sampling doesn't apply.
407    ///
408    /// See: https://github.com/DataDog/datadog-agent/blob/be33ac1490c4a34602cbc65a211406b73ad6d00b/pkg/trace/api/otlp.go#L561-L585
409    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    /// Retains only the analyzed-marker spans of a dropped trace and forwards it as events.
435    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    /// Apply Single Span Sampling to the trace
447    /// Returns true if the trace was modified
448    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    /// Evaluates the given trace against all configured samplers.
460    fn run_samplers(&mut self, trace: &mut Trace) -> SamplerOutcome {
461        let outcome = self.run_samplers_inner(trace);
462
463        // Unclaimed decisions carry no sampler identity, so recording them would inflate a
464        // series that exists only to expose attribution gaps; empty and rootless traces are the
465        // only unclaimed cases.
466        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    /// Reads the service and env dimensions a decision is recorded under.
476    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        // The name starts unclaimed; whichever sampler decides the trace claims it, so every
493        // decision is recorded under exactly one sampler.
494        let mut sampler_name = telemetry::SamplerName::Unknown;
495
496        // logic taken from: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/agent/agent.go#L1066
497        // Empty trace check
498        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        // ETS: only sample traces containing errors (including exception span events); skip all other samplers.
507        // logic taken from: https://github.com/DataDog/datadog-agent/blob/be33ac1490c4a34602cbc65a211406b73ad6d00/pkg/trace/agent/agent.go#L1068
508        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        // Run the rare sampler early, before all other samplers. This mirrors the Go agent behavior
523        // where the rare sampler runs first to catch traces that would otherwise be dropped entirely.
524        // logic taken from: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/agent/agent.go#L1078
525        let rare = self.rare_sampler.sample(trace, root_span_idx);
526
527        // Modern path: ProbabilisticSamplerEnabled = true
528        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                // Rare sampler wins over probabilistic sampling.
535                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        // Read once here, where the samplers below consume it; every path above returns without
572        // needing it.
573        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                // Traces already sampled probabilistically upstream count under the probabilistic
579                // sampler; the priority sampler still decides them.
580                sampler_name = telemetry::SamplerName::Probabilistic;
581            }
582            if priority < PRIORITY_AUTO_DROP {
583                // Manual drop: short-circuit and skip other samplers.
584                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            // Rare check mirrors agent behavior: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/agent/agent.go#L1129-L1140
597            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            // The OTLP rate decision is probabilistic: its keeps and its drops both record
603            // under the probabilistic sampler, never the no-priority bucket.
604            sampler_name = telemetry::SamplerName::Probabilistic;
605            // some sampling happens upstream in the otlp receiver in the agent: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/api/otlp.go#L572
606            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        // Default: drop the trace
640        SamplerOutcome::new(false, PRIORITY_AUTO_DROP, "", Some(root_span_idx), sampler_name, true)
641    }
642
643    /// Returns whether the trace was already sampled probabilistically upstream.
644    ///
645    /// Tracer head sampling and receiver-side sampling both stamp the decision maker, so the
646    /// first span carrying it identifies the trace.
647    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    /// Fills in trace-level metadata from the spans.
660    ///
661    /// Origin comes from the root span's `_dd.origin` tag, and the decision maker is the first
662    /// span in chunk order carrying `_dd.p.dm` - both only when not already set. This runs here,
663    /// downstream of span filtering, so the metadata anchors to the final span population rather
664    /// than the one that arrived at decode time.
665    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    /// Apply sampling metadata to the trace in-place.
693    ///
694    /// The `root_span_id` parameter identifies which span should receive the sampling metadata.
695    /// This avoids recalculating the root span since it was already found in `run_samplers`.
696    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        // Chunk-level decision maker: the sampler's value when it decided, otherwise the first
701        // carrier promoted during backfill (falling back to the root's own tag).
702        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        // The span-level decision maker is stamped only when a sampler made the call: promotion
725        // targets trace chunk metadata, and each span's own tag is left in place because
726        // downstream systems rely on the span keeping its original value.
727        //
728        // With the APM-level probabilistic sampler on OTLP traces, `_dd.p.dm` is written to
729        // trace chunk tags only (not span meta); on the legacy sampling path it is written to
730        // both. The span meta write is skipped only when both conditions hold; the DM value
731        // still flows through trace fields to the encoder.
732        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        // Now set sampling metadata directly on the trace.
740        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        // keep is a boolean that indicates if the trace should be kept or dropped
747        // priority is the sampling priority
748        // decision_maker is the tag that indicates the decision maker (probabilistic, error, etc.)
749        // root_span_idx is the index of the root span of the trace
750        let SamplerOutcome {
751            keep,
752            priority,
753            decision_maker,
754            root_span_idx,
755            extract_events,
756            ..
757        } = self.run_samplers(trace);
758
759        // Backfill trace-derived metadata before any forwarding path (keep, ETS, or
760        // single-span/analytics sampling), so every forwarded trace carries metadata derived
761        // from its final span population.
762        if let Some(root_idx) = root_span_idx {
763            self.backfill_trace_metadata(trace, root_idx);
764        }
765
766        // Extract events from every trace the samplers did not short-circuit; surviving spans
767        // carry the analyzed marker and their rate metadata whether the trace is kept or dropped,
768        // and are forwarded as standalone events only when the trace is dropped.
769        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        // Apply sampling metadata and forward if kept, or if ETS (dropped non-error traces are
776        // forwarded with DroppedTrace=true, suppressing SSS/analytics).
777        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        // logic taken from here: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/agent/agent.go#L980-L990
785        // try single span sampling (keeps spans marked for sampling when trace would be dropped)
786        let modified = self.single_span_sampling(trace);
787        if !modified {
788            // Fall back to extracted events if no SSS spans
789            if self.forward_analyzed_spans(trace, priority) {
790                return true;
791            }
792        } else if events.sampled > 0 {
793            // Warn about both SSS and analytics events
794            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 we modified the trace with SSS, send it
801        if modified {
802            return true;
803        }
804
805        // Neither SSS nor analytics events found, drop the trace
806        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                    // Report the sampling telemetry through the dedicated metrics output, so the
825                    // metrics flow into the metrics pipeline and on to the backend. The signature
826                    // counts are read here, at report time, so a quiet window cannot republish
827                    // stale gauges.
828                    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                        // The input stream has ended, so report the final window before stopping.
843                        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    // Deterministic receiver sampling decisions at a rate of 0.5, from the Go trace agent's `TestCreateChunks`:
875    // https://github.com/DataDog/datadog-agent/blob/c6f7e0bc2d5744601a10f231751948ac7a4ee781/pkg/trace/api/otlp_test.go#L740-L810
876    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,    // parent_id
919            0,    // start
920            1000, // duration
921            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        // Promoted from an earlier child span during backfill.
1004        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        // The root is reported last here.
1028        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        // Roots, orphans, and service boundaries get marked; interior same-service spans don't.
1073        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; // keeps the trace, so it is forwarded
1138        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        // Test trace with user-set priority = 2 (UserKeep)
1185        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        // Test trace with user-set priority = -1 (UserDrop)
1194        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        // Test trace without user priority
1203        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        // Test trace-level priority overrides span priorities (last-seen priority)
1215        // Create spans with different priorities - root has 0, later span has 2
1216        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        // Without trace-level priority, should get priority from root (0)
1228        assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(0));
1229
1230        // Now set trace-level priority to 2 (simulating last-seen priority from OTLP translator)
1231        trace.priority = Some(2);
1232
1233        // Trace-level priority should take precedence
1234        assert_eq!(sampler.get_user_priority(&trace, root_idx), Some(2));
1235
1236        // Test that trace-level priority is used even when no span has priority
1237        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; // Use legacy path that checks user priority
1249
1250        // Test that manual keep (priority = 2) works via trace-level priority
1251        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        // Test manual drop (priority = -1) via trace-level priority
1266        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); // Should not keep when user drops
1272        assert_eq!(priority, PRIORITY_USER_DROP);
1273
1274        // Test that priority = 1 (auto keep) via trace-level is also respected
1275        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        // The bucketed probabilistic sampler is fully deterministic: it hashes the trace ID into one of 0x4000
1293        // buckets and keeps the trace when `bucket < (rate * 0x4000)`. These cases pin the exact keep/drop decision
1294        // for known trace IDs at known rates, so a regression in the hash, the bucket mask, or the comparison is
1295        // caught (a determinism-only check would not catch any of those).
1296        //
1297        // Expected values were computed directly from the FNV-1a bucket math in `ProbabilisticSampler::sample`.
1298        // For reference, the trace IDs below hash to these buckets (out of 0x4000 = 16384):
1299        // 0x1234567890ABCDEF -> 1764, 0x0 -> 9301, u64::MAX -> 12365.
1300        struct Case {
1301            trace_id: u64,
1302            rate: f64,
1303            expected_keep: bool,
1304        }
1305
1306        let cases = [
1307            // rate 1.0 keeps every trace (the maximum bucket, 16383, is always below 16384).
1308            Case {
1309                trace_id: 0x1234567890ABCDEF,
1310                rate: 1.0,
1311                expected_keep: true,
1312            },
1313            // rate 0.0 drops every trace (no bucket is below 0).
1314            Case {
1315                trace_id: 0x1234567890ABCDEF,
1316                rate: 0.0,
1317                expected_keep: false,
1318            },
1319            // Same trace ID (bucket 1764) flips from drop to keep as the rate crosses its bucket ratio (~0.108).
1320            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            // Trace ID 0 (bucket 9301) straddles rate 0.5 (scaled bucket 8192) vs 0.6 (scaled bucket 9830).
1331            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            // u64::MAX (bucket 12365) straddles rate 0.5 vs 0.8.
1342            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        // Determinism is a documented property of `ProbabilisticSampler::sample` (same trace ID + rate always yields
1370        // the same decision). This is intentionally a determinism-only check; correctness is covered by
1371        // `test_probabilistic_sampling_known_decisions`.
1372        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        // A seed re-buckets every trace ID: 0x1234567890ABCDEF moves from bucket 1764 with seed 0 to
1383        // bucket 7338 with seed 22, so rates that straddle only one of the buckets flip decisions.
1384        let mut seeded = create_test_sampler();
1385        seeded.probabilistic = probabilistic::ProbabilisticSampler::new(22, false);
1386
1387        // Bucket 7338 (7338/16384 = 0.4479): dropped below 0.45, kept above it.
1388        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        // At rate 0.20 the zero-seed sampler keeps this trace (bucket 1764, pinned in
1394        // `probabilistic_sampling_known_decisions`) while the seeded sampler drops it.
1395        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        // Legacy mode buckets on the low half alone, so the high half cannot move the decision.
1408        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        // Full mode hashes the 16-byte big-endian ID, high half first: (high, low) lands in bucket
1417        // 454 (kept at 0.05, dropped at 0.02), moving the decision relative to legacy mode.
1418        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        // A zero high half is still a different input than legacy mode: the halves swap byte
1426        // ranges, bucket 14666 versus 7338 (kept at 0.90, dropped at 0.89).
1427        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        // Unrecognized flags are carried, not rejected, and enable nothing.
1444        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        // With the probabilistic sampler enabled at rate 1.0, every trace is decided (and kept) by
1456        // it: the sum of seen across all decision keys must equal the number of traces, with no
1457        // double counting across samplers.
1458        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        // A trace with a user-set sampling priority is decided by the priority sampler, and its
1480        // decision key carries that priority; no other sampler claims it.
1481        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        // A trace already sampled probabilistically upstream carries the probabilistic decision
1502        // maker; its decision counts under the probabilistic sampler, without the priority
1503        // sampler's sampling_priority or target_env tags.
1504        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        // Unclaimed decisions carry no sampler identity, so they never inflate the unknown series;
1527        // a real attribution gap remains the only thing that could produce one.
1528        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        // Test trace with error field set
1541        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        // Test trace without error
1546        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        // Test modern path: error sampler overrides probabilistic drop
1554        let mut sampler = create_test_sampler();
1555        sampler.sampling_rate = 0.5; // 50% sampling rate
1556        sampler.probabilistic_sampler_enabled = true;
1557
1558        // Create trace with error that would be dropped by probabilistic
1559        // Using a trace ID that we know will be dropped at 50% rate
1560        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, ""); // Error sampler doesn't set decision_maker
1573
1574        // Test legacy path: user priority is respected
1575        let mut sampler = create_test_sampler();
1576        sampler.probabilistic_sampler_enabled = false; // Use legacy path
1577
1578        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); // UserKeep
1591        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        // Test 1: Root span with parent_id = 0 (common case)
1609        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, // parent_id = 0 indicates root
1616            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, // parent_id = 1 (points to root)
1627            100,
1628            500,
1629            0,
1630        );
1631        // Put root span second to test that we find it even when not first
1632        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        // Test 2: Orphaned span (parent not in trace)
1637        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, // parent_id = 999 (doesn't exist in trace)
1644            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        // Test 3: Multiple root candidates: should return the last one found (index 1)
1653        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        // Both have parent_id = 0, should return the last one found (span_id = 2)
1657        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        // Test 1: Trace with SSS tags should be kept even when probabilistic would drop it
1666        sampler.sampling_rate = 0.0; // 0% sampling rate - should drop everything
1667        sampler.probabilistic_sampler_enabled = true;
1668
1669        // Create span with SSS metric
1670        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        // Create regular span without SSS
1678        let regular_span = create_test_span(2, 0);
1679
1680        let mut trace = create_test_trace(vec![sss_span.clone(), regular_span]);
1681
1682        // Apply SSS
1683        let modified = sampler.single_span_sampling(&mut trace);
1684        assert!(modified);
1685        assert_eq!(trace.spans().len(), 1); // Only SSS span kept
1686        assert_eq!(trace.spans()[0].span_id(), 1); // It's the SSS span
1687
1688        // Check that trace has been marked as kept with high priority
1689        assert_eq!(trace.priority, Some(PRIORITY_USER_KEEP));
1690
1691        // Test 2: Trace without SSS tags should not be modified
1692        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; // 75% sampling rate
1703        sampler.probabilistic_sampler_enabled = true;
1704
1705        // Use a trace ID that we know will be sampled
1706        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, // parent_id = 0 indicates root
1714            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            // If sampled probabilistically, check that probRateKey was already added
1731            assert_eq!(priority, PRIORITY_AUTO_KEEP);
1732            assert_eq!(decision_maker, DECISION_MAKER_PROBABILISTIC); // probabilistic sampling marker
1733
1734            // Check that the root span already has the probRateKey (it should have been added in run_samplers)
1735            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            // Test that apply_sampling_metadata still works correctly for other metadata
1747            let mut trace_with_metadata = trace.clone();
1748            sampler.apply_sampling_metadata(&mut trace_with_metadata, keep, priority, decision_maker, root_idx);
1749
1750            // Check that decision maker tag was added
1751            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    // ── Rare-sampler interaction tests ──────────────────────────────────────────
1764    // Adapted from datadog-agent/pkg/trace/agent/agent_test.go TestSampling cases:
1765    // "rare-sampler-catch-unsampled", "rare-sampler-catch-sampled",
1766    // "rare-sampler-disabled", and related probabilistic path interactions.
1767
1768    /// Create a top-level span eligible for rare sampling.
1769    ///
1770    /// The rare sampler only considers spans that have `_top_level=1` or `_dd.measured=1`.
1771    /// This helper sets `_top_level=1` so that the rare sampler can consider the span.
1772    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    /// Create a `TraceSampler` with the rare sampler enabled and a very high TPS limit so it
1779    /// freely samples first occurrences, plus a long TTL so second occurrences stay within TTL.
1780    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    /// Adapted from Go "rare-sampler-catch-unsampled":
1794    ///
1795    /// Rare is enabled + probabilistic would drop → rare catches it (first occurrence).
1796    #[test]
1797    fn rare_sampler_catches_unsampled_trace() {
1798        let mut sampler = create_sampler_with_rare_enabled();
1799        sampler.sampling_rate = 0.0; // probabilistic drops everything
1800        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    /// Adapted from Go "rare-sampler-catch-sampled" (first trace):
1817    ///
1818    /// Rare is enabled, first occurrence—trace is kept and `_dd.rare` is set on the span.
1819    #[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    /// Adapted from Go "rare-sampler-catch-sampled" (second trace same signature):
1845    ///
1846    /// Within the TTL, the same signature is no longer "rare" and rare doesn't re-sample it.
1847    /// With probabilistic at 0%, the trace should be dropped.
1848    #[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        // First trace: rare catches it.
1855        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        // Second trace: same signature (same service/operation/resource on the top-level span),
1861        // still within TTL → rare won't catch it; probabilistic at 0% drops it.
1862        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    /// Adapted from Go "rare-sampler-disabled":
1874    ///
1875    /// Rare is disabled + probabilistic at 0% → trace is dropped.
1876    #[test]
1877    fn rare_sampler_disabled_does_not_catch_unsampled() {
1878        let mut sampler = create_test_sampler(); // rare disabled by default
1879        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    /// Rare + non-probabilistic path (priority path): rare catches `PriorityAutoDrop` on first
1891    /// occurrence, preserving the tracer-set priority rather than upgrading to AutoKeep.
1892    #[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    /// Rare + non-probabilistic path (priority path): UserKeep priority is preserved, not
1918    /// downgraded to AutoKeep. Mirrors Go agent behavior at agent.go#L1129-1131.
1919    #[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        ); // UserKeep
1930        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    /// Probabilistic path with 100% rate and rare disabled: keep with `_dd.p.dm = "-9"`.
1939    #[test]
1940    fn probabilistic_100_percent_keeps_trace_with_decision_maker() {
1941        let mut sampler = create_test_sampler(); // rare disabled
1942        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    /// Probabilistic path with 0% rate and rare disabled: drop.
1960    #[test]
1961    fn probabilistic_0_percent_drops_trace() {
1962        let mut sampler = create_test_sampler(); // rare disabled
1963        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    /// Rare sampler should catch OTLP traces without a sampling priority on their first occurrence,
1976    /// matching the Go agent behavior: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/agent/agent.go#L1129-L1140
1977    #[test]
1978    fn otlp_rate_decisions_record_under_the_probabilistic_sampler() {
1979        // Keeps and drops from the OTLP rate decision both belong to the probabilistic sampler;
1980        // the drop must not fall through to the no-priority bucket.
1981        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    /// Adapted from Go "probabilistic-rare-100":
2038    ///
2039    /// Rare fires before probabilistic is consulted, so even at 100% sampling rate the decision
2040    /// maker tag isn't set—the trace is attributed to rare, not probabilistic.
2041    #[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    /// Adapted from Go "error-sampled-prio-unsampled":
2062    ///
2063    /// Rare fires before the error sampler is reached. A no-priority error trace on its first
2064    /// occurrence is kept by rare, not by the error sampler.
2065    #[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); // error=1
2073        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    /// Adapted from Go manual-drop short-circuit behavior:
2087    ///
2088    /// UserDrop (-1) priority is checked before rare runs in the priority path. A UserDrop trace
2089    /// must be dropped even when rare is enabled and would otherwise match.
2090    #[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        ); // UserDrop
2101        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    // ── Error Tracking Standalone tests ─────────────────────────────────────────
2110    // Adapted from datadog-agent/pkg/trace/agent/agent.go runSamplers ETS block.
2111
2112    fn create_sampler_with_ets() -> TraceSampler {
2113        TraceSampler {
2114            error_tracking_standalone: true,
2115            ..create_test_sampler()
2116        }
2117    }
2118
2119    /// ETS enabled + trace with error → kept by error sampler.
2120    #[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); // error=1
2125        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    /// ETS enabled + trace without error → dropped; rare/probabilistic/priority not consulted.
2139    #[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); // error=0
2144        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    /// ETS enabled + non-error trace → forwarded with DroppedTrace=true; SSS/analytics suppressed.
2152    #[test]
2153    fn ets_forwards_dropped_trace_with_dropped_flag() {
2154        let mut sampler = create_sampler_with_ets();
2155
2156        // Span with SSS metric — would trigger single span sampling in non-ETS mode.
2157        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    /// ETS enabled + trace with exception span event → kept (exception events count as errors in ETS).
2171    #[test]
2172    fn ets_keeps_trace_with_exception_span_event() {
2173        let mut sampler = create_sampler_with_ets();
2174
2175        // Span with error=0 but exception span event metadata.
2176        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    /// ETS disabled → normal sampling path (probabilistic) is used.
2189    #[test]
2190    fn ets_disabled_uses_normal_sampling() {
2191        let mut sampler = create_test_sampler(); // ETS disabled
2192        sampler.sampling_rate = 1.0;
2193        sampler.probabilistic_sampler_enabled = true;
2194
2195        let span = create_test_span(1, 0); // no error
2196        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    // ── ETS + OTLP pre-sampling tests ────────────────────────────────────────────
2206    // These mirror DDA's OTLPReceiver.createChunks behavior which pre-assigns
2207    // priority/dm before runSamplersV1, so ETS sees those values even when it
2208    // short-circuits. See: pkg/trace/api/otlp.go#L561-L585.
2209
2210    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    /// ETS + OTLP non-error trace (legacy sampler path): pre-sampling sets priority=AutoKeep and dm=-9.
2223    /// Mirrors DDA `OTLPReceiver` assigning priority=1 + dm=-9 before ETS returns early.
2224    #[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); // no error
2229        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    /// ETS + OTLP error trace (legacy sampler path): pre-sampling sets priority=AutoKeep and dm=-9.
2246    #[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); // error=1
2251        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    /// ETS + OTLP trace dropped by the receiver rate (legacy sampler path): a drop is not stamped with a decision maker.
2265    #[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    /// ETS + OTLP + probabilistic_sampler_enabled=true: `OTLPReceiver` defers, no pre-sampling.
2282    /// DDA's `OTLPReceiver` sets PriorityNone and skips when ProbabilisticSamplerEnabled.
2283    #[test]
2284    fn ets_otlp_probabilistic_path_skips_presample() {
2285        let mut sampler = create_sampler_with_ets_legacy();
2286        sampler.probabilistic_sampler_enabled = true; // override to prob path
2287
2288        let span = create_test_span(1, 0); // no error
2289        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    /// ETS + non-OTLP trace (legacy sampler path): behavior unchanged—no pre-sampling.
2306    #[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); // no error, no OTLP meta
2311        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    /// ETS + OTLP trace with user-set priority: dm="-4" (manual sampling), matching DDA.
2325    #[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        ); // UserKeep
2334        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    // ── Ingest information tests ────────────────────────────────────────────────
2348    // Regression tests: the sampler once classified a trace as OTLP by the `otel.trace_id` attribute on its root
2349    // span. Removing that attribute, or filtering out the original root, sent OTLP traces down the native path, and
2350    // a native trace carrying the attribute was sampled at the OTLP receiver rate. Classification now uses the
2351    // ingest information fixed when the trace is constructed.
2352
2353    #[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        // Legacy path: the no-priority sampler decides, not the OTLP receiver rate.
2408        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        // Error tracking standalone: no OTLP pre-sampling priority or decision maker.
2417        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        // The APM probabilistic sampler is enabled at a rate of 1.0, so both traces are kept by it. The OTLP trace
2432        // carries no `otel.trace_id` attribute but still gets its decision maker on the chunk only.
2433        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; // probabilistic drops everything
2450        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        // Only the extracted event spans are forwarded; the trace stays marked dropped so the
2459        // backend indexes them as standalone events, carrying the sampling priority.
2460        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        // Extraction rates of 1.0 are not written; the backend assumes 1.0.
2466        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; // probabilistic drops everything
2473        sampler.probabilistic_sampler_enabled = true;
2474
2475        let mut trace = create_test_trace(vec![create_test_span(1, 0)]);
2476        // No extractor nominates the span, so the trace is dropped without events.
2477        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        // The full span payload is forwarded, with the analyzed mark riding along on the root.
2492        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; // probabilistic drops everything
2503        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        // The trace is forwarded as kept by single span sampling, not as dropped events.
2516        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        // The legacy path is the one that consults user priority.
2526        sampler.probabilistic_sampler_enabled = false;
2527        sampler.event_processor = EventProcessor::for_tests(&HashMap::new(), &rates, f64::INFINITY);
2528
2529        // Manual drop short-circuits the samplers, and the trace is not promoted to events.
2530        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    // Remote sampling tests based on the Datadog Agent's remote configuration tests:
2539    // https://github.com/DataDog/datadog-agent/blob/17ecddf4e3e/pkg/trace/remoteconfighandler/remote_config_handler_test.go
2540
2541    // With probabilistic sampling set to 0, only the rare sampler can keep test traces.
2542    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    /// The agent environment resolved from `env: Prod`.
2566    fn prod_env() -> AgentEnv {
2567        AgentEnv::resolve(None, Some("Prod"), &[], &[])
2568    }
2569
2570    /// Traces settings where probabilistic sampling drops every trace, so only the rare sampler keeps one.
2571    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    /// Covers the Datadog Agent's priority, error, and rare sampler update cases.
2604    #[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        // Remote settings do not change the no-priority sampler.
2619        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    /// Remote sampling uses the resolved agent environment to select a `by_env` entry.
2662    #[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        // The Datadog Agent's tests send unset fields as `null`.
2676        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    /// Remote selection matches the resolved agent environment exactly.
2702    #[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            // Nothing configured resolves to `none`.
2709            (AgentEnv::resolve(None, None, &[], &[]), (41.0, 30.0)),
2710            (prod_env(), (42.0, 30.0)),
2711            // `!!!` normalizes to empty, which matches no entry: `none` is not applied after normalization.
2712            (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    /// Remote selection and the priority sampler's fallback for traces without an env use the same agent environment.
2731    ///
2732    /// Regression: remote selection followed `env` while the priority sampler used a separate default, so the two
2733    /// could disagree.
2734    #[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        // Traces without an env learn their rate under the agent env. Counts in one bucket become rates when the next
2754        // begins.
2755        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    /// The run loop applies a published update before sampling the next buffer.
2778    #[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        // The rare sampler keeps the first trace. Waiting for it makes sure the update is published after it.
2797        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    /// Starting with a zero target disables error sampling; a remote update cannot re-enable it.
2839    #[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        // An inert subscription must not reset these overrides.
2859        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    /// Removing the remote assignment does not undo previously applied settings.
2869    #[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    // The subscription keeps only the latest state, and the sampler reads it once per buffer. Publications the sampler
2891    // has not yet read must not erase the newest accepted configuration, since the Datadog Agent applies each update
2892    // as it arrives, ignores an empty one, and rejects arrays where its JSON decoder expects an object:
2893    // https://github.com/DataDog/datadog-agent/blob/17ecddf4e3e83ccbb0e68aeb99461d1a1d902927/pkg/trace/remoteconfighandler/remote_config_handler.go#L234-L295
2894    #[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        // Each case is a name, the payload applied before the publications under test, those publications (`None`
2900        // for an empty assignment), and the expected targets and rare sampler state.
2901        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    /// The run loop applies a configuration whose withdrawal was published before the sampler's first buffer.
2966    #[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    /// Changing the target scales rates already learned for trace signatures.
2996    #[test]
2997    fn remote_configuration_scales_learned_rates() {
2998        let (publisher, mut sampler) = create_remote_sampler(10.0);
2999
3000        // 100 traces in one 5-second bucket are 20 traces per second, so a target of 10 learns a rate of 0.5 once the
3001        // next bucket begins.
3002        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    /// Republishing the same settings must not clear learned rates or rare-trace history.
3044    #[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        // Learn a rate for one signature in each sampler: counts in one bucket become rates when the next begins.
3053        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}