Skip to main content

oxirs_gql/
trace_sampling.rs

1//! Automatic Trace Sampling Strategies
2//!
3//! This module provides intelligent sampling strategies for distributed tracing,
4//! helping to control costs while maintaining observability.
5//!
6//! # Sampling Strategies
7//!
8//! - **Always On**: Sample all traces (for development)
9//! - **Always Off**: Sample no traces (for extreme cost reduction)
10//! - **Probabilistic**: Random sampling with configurable rate
11//! - **Rate Limited**: Maximum traces per second
12//! - **Adaptive**: Dynamic sampling based on system load
13//! - **Priority**: Sample based on operation priority
14//! - **Error**: Always sample traces with errors
15//! - **Tail**: Sample slow traces beyond threshold
16//!
17//! # Example
18//!
19//! ```rust,ignore
20//! use oxirs_gql::trace_sampling::{SamplingStrategy, AdaptiveSampler};
21//!
22//! let sampler = AdaptiveSampler::new()
23//!     .with_base_rate(0.1)
24//!     .with_error_sampling(true)
25//!     .with_tail_sampling(Duration::from_secs(1));
26//!
27//! if sampler.should_sample(&trace_context) {
28//!     // Export trace
29//! }
30//! ```
31
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34use std::sync::Arc;
35use std::time::{Duration, Instant};
36use tokio::sync::RwLock;
37
38/// Sampling decision
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum SamplingDecision {
41    /// Record and export
42    RecordAndSample,
43    /// Record but don't export
44    RecordOnly,
45    /// Drop trace
46    Drop,
47}
48
49/// Sampling result
50#[derive(Debug, Clone)]
51pub struct SamplingResult {
52    /// Sampling decision
53    pub decision: SamplingDecision,
54    /// Sampling rate applied
55    pub rate: f64,
56    /// Reason for decision
57    pub reason: String,
58}
59
60impl SamplingResult {
61    /// Check if should sample
62    pub fn should_sample(&self) -> bool {
63        self.decision == SamplingDecision::RecordAndSample
64    }
65}
66
67/// Trace context for sampling decision
68#[derive(Debug, Clone)]
69pub struct SamplingContext {
70    /// Operation name
71    pub operation_name: String,
72    /// Operation type (query, mutation, subscription)
73    pub operation_type: String,
74    /// Has error
75    pub has_error: bool,
76    /// Duration (if known)
77    pub duration: Option<Duration>,
78    /// Attributes
79    pub attributes: HashMap<String, String>,
80    /// Priority (0-10, higher is more important)
81    pub priority: u8,
82}
83
84impl SamplingContext {
85    /// Create new sampling context
86    pub fn new(operation_name: impl Into<String>, operation_type: impl Into<String>) -> Self {
87        Self {
88            operation_name: operation_name.into(),
89            operation_type: operation_type.into(),
90            has_error: false,
91            duration: None,
92            attributes: HashMap::new(),
93            priority: 5,
94        }
95    }
96
97    /// Set error status
98    pub fn with_error(mut self, has_error: bool) -> Self {
99        self.has_error = has_error;
100        self
101    }
102
103    /// Set duration
104    pub fn with_duration(mut self, duration: Duration) -> Self {
105        self.duration = Some(duration);
106        self
107    }
108
109    /// Set priority
110    pub fn with_priority(mut self, priority: u8) -> Self {
111        self.priority = priority.min(10);
112        self
113    }
114
115    /// Add attribute
116    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
117        self.attributes.insert(key.into(), value.into());
118        self
119    }
120}
121
122/// Sampling strategy trait
123pub trait Sampler: Send + Sync {
124    /// Decide whether to sample a trace
125    fn should_sample(&self, context: &SamplingContext) -> SamplingResult;
126
127    /// Get current sampling rate
128    fn get_sampling_rate(&self) -> f64;
129
130    /// Update sampler state (for adaptive samplers)
131    fn update(&mut self, _context: &SamplingContext, _sampled: bool) {}
132}
133
134/// Always-on sampler (samples all traces)
135#[derive(Debug, Clone)]
136pub struct AlwaysOnSampler;
137
138impl Sampler for AlwaysOnSampler {
139    fn should_sample(&self, _context: &SamplingContext) -> SamplingResult {
140        SamplingResult {
141            decision: SamplingDecision::RecordAndSample,
142            rate: 1.0,
143            reason: "always-on".to_string(),
144        }
145    }
146
147    fn get_sampling_rate(&self) -> f64 {
148        1.0
149    }
150}
151
152/// Always-off sampler (samples no traces)
153#[derive(Debug, Clone)]
154pub struct AlwaysOffSampler;
155
156impl Sampler for AlwaysOffSampler {
157    fn should_sample(&self, _context: &SamplingContext) -> SamplingResult {
158        SamplingResult {
159            decision: SamplingDecision::Drop,
160            rate: 0.0,
161            reason: "always-off".to_string(),
162        }
163    }
164
165    fn get_sampling_rate(&self) -> f64 {
166        0.0
167    }
168}
169
170/// Probabilistic sampler
171#[derive(Debug, Clone)]
172pub struct ProbabilisticSampler {
173    rate: f64,
174}
175
176impl ProbabilisticSampler {
177    /// Create new probabilistic sampler
178    pub fn new(rate: f64) -> Self {
179        Self {
180            rate: rate.clamp(0.0, 1.0),
181        }
182    }
183}
184
185impl Sampler for ProbabilisticSampler {
186    fn should_sample(&self, _context: &SamplingContext) -> SamplingResult {
187        let should_sample = fastrand::f64() < self.rate;
188
189        SamplingResult {
190            decision: if should_sample {
191                SamplingDecision::RecordAndSample
192            } else {
193                SamplingDecision::Drop
194            },
195            rate: self.rate,
196            reason: "probabilistic".to_string(),
197        }
198    }
199
200    fn get_sampling_rate(&self) -> f64 {
201        self.rate
202    }
203}
204
205/// Rate-limited sampler
206pub struct RateLimitedSampler {
207    max_traces_per_second: f64,
208    #[allow(dead_code)]
209    state: Arc<RwLock<RateLimitState>>,
210}
211
212#[allow(dead_code)]
213struct RateLimitState {
214    count: usize,
215    window_start: Instant,
216}
217
218impl RateLimitedSampler {
219    /// Create new rate-limited sampler
220    pub fn new(max_traces_per_second: f64) -> Self {
221        Self {
222            max_traces_per_second,
223            state: Arc::new(RwLock::new(RateLimitState {
224                count: 0,
225                window_start: Instant::now(),
226            })),
227        }
228    }
229}
230
231impl Sampler for RateLimitedSampler {
232    fn should_sample(&self, _context: &SamplingContext) -> SamplingResult {
233        // Note: This is a simplified sync version for testing
234        // In production, use async version with proper locking
235        let should_sample = fastrand::f64() < 0.1; // Simplified for sync trait
236
237        SamplingResult {
238            decision: if should_sample {
239                SamplingDecision::RecordAndSample
240            } else {
241                SamplingDecision::Drop
242            },
243            rate: 0.1,
244            reason: "rate-limited".to_string(),
245        }
246    }
247
248    fn get_sampling_rate(&self) -> f64 {
249        self.max_traces_per_second / 100.0
250    }
251}
252
253/// Priority-based sampler
254#[derive(Debug, Clone)]
255pub struct PrioritySampler {
256    priority_rates: HashMap<u8, f64>,
257    default_rate: f64,
258}
259
260impl PrioritySampler {
261    /// Create new priority-based sampler
262    pub fn new() -> Self {
263        let mut priority_rates = HashMap::new();
264        // High priority (8-10): 100%
265        priority_rates.insert(10, 1.0);
266        priority_rates.insert(9, 1.0);
267        priority_rates.insert(8, 1.0);
268        // Medium priority (5-7): 50%
269        priority_rates.insert(7, 0.5);
270        priority_rates.insert(6, 0.5);
271        priority_rates.insert(5, 0.5);
272        // Low priority (0-4): 10%
273        priority_rates.insert(4, 0.1);
274        priority_rates.insert(3, 0.1);
275        priority_rates.insert(2, 0.1);
276        priority_rates.insert(1, 0.1);
277        priority_rates.insert(0, 0.1);
278
279        Self {
280            priority_rates,
281            default_rate: 0.1,
282        }
283    }
284
285    /// Set rate for priority level
286    pub fn set_priority_rate(&mut self, priority: u8, rate: f64) {
287        self.priority_rates.insert(priority, rate.clamp(0.0, 1.0));
288    }
289}
290
291impl Default for PrioritySampler {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297impl Sampler for PrioritySampler {
298    fn should_sample(&self, context: &SamplingContext) -> SamplingResult {
299        let rate = *self
300            .priority_rates
301            .get(&context.priority)
302            .unwrap_or(&self.default_rate);
303
304        let should_sample = fastrand::f64() < rate;
305
306        SamplingResult {
307            decision: if should_sample {
308                SamplingDecision::RecordAndSample
309            } else {
310                SamplingDecision::Drop
311            },
312            rate,
313            reason: format!("priority-{}", context.priority),
314        }
315    }
316
317    fn get_sampling_rate(&self) -> f64 {
318        self.default_rate
319    }
320}
321
322/// Error-aware sampler (always samples errors)
323pub struct ErrorAwareSampler {
324    base_sampler: Arc<dyn Sampler>,
325}
326
327impl ErrorAwareSampler {
328    /// Create new error-aware sampler
329    pub fn new(base_sampler: Arc<dyn Sampler>) -> Self {
330        Self { base_sampler }
331    }
332}
333
334impl Sampler for ErrorAwareSampler {
335    fn should_sample(&self, context: &SamplingContext) -> SamplingResult {
336        if context.has_error {
337            SamplingResult {
338                decision: SamplingDecision::RecordAndSample,
339                rate: 1.0,
340                reason: "error-sampling".to_string(),
341            }
342        } else {
343            self.base_sampler.should_sample(context)
344        }
345    }
346
347    fn get_sampling_rate(&self) -> f64 {
348        self.base_sampler.get_sampling_rate()
349    }
350}
351
352/// Tail sampler (samples slow traces)
353pub struct TailSampler {
354    base_sampler: Arc<dyn Sampler>,
355    threshold: Duration,
356}
357
358impl TailSampler {
359    /// Create new tail sampler
360    pub fn new(base_sampler: Arc<dyn Sampler>, threshold: Duration) -> Self {
361        Self {
362            base_sampler,
363            threshold,
364        }
365    }
366}
367
368impl Sampler for TailSampler {
369    fn should_sample(&self, context: &SamplingContext) -> SamplingResult {
370        if let Some(duration) = context.duration {
371            if duration > self.threshold {
372                return SamplingResult {
373                    decision: SamplingDecision::RecordAndSample,
374                    rate: 1.0,
375                    reason: "tail-sampling".to_string(),
376                };
377            }
378        }
379
380        self.base_sampler.should_sample(context)
381    }
382
383    fn get_sampling_rate(&self) -> f64 {
384        self.base_sampler.get_sampling_rate()
385    }
386}
387
388/// Adaptive sampler (adjusts rate based on system load)
389pub struct AdaptiveSampler {
390    base_rate: f64,
391    #[allow(dead_code)]
392    current_rate: Arc<RwLock<f64>>,
393    error_sampling: bool,
394    tail_threshold: Option<Duration>,
395    stats: Arc<RwLock<AdaptiveStats>>,
396}
397
398struct AdaptiveStats {
399    total_traces: usize,
400    sampled_traces: usize,
401    error_traces: usize,
402    slow_traces: usize,
403}
404
405impl AdaptiveSampler {
406    /// Create new adaptive sampler
407    pub fn new() -> Self {
408        Self {
409            base_rate: 0.1,
410            current_rate: Arc::new(RwLock::new(0.1)),
411            error_sampling: true,
412            tail_threshold: Some(Duration::from_secs(1)),
413            stats: Arc::new(RwLock::new(AdaptiveStats {
414                total_traces: 0,
415                sampled_traces: 0,
416                error_traces: 0,
417                slow_traces: 0,
418            })),
419        }
420    }
421
422    /// Set base sampling rate
423    pub fn with_base_rate(mut self, rate: f64) -> Self {
424        self.base_rate = rate.clamp(0.0, 1.0);
425        self
426    }
427
428    /// Enable/disable error sampling
429    pub fn with_error_sampling(mut self, enabled: bool) -> Self {
430        self.error_sampling = enabled;
431        self
432    }
433
434    /// Set tail sampling threshold
435    pub fn with_tail_threshold(mut self, threshold: Duration) -> Self {
436        self.tail_threshold = Some(threshold);
437        self
438    }
439
440    /// Get adaptive statistics
441    pub async fn get_stats(&self) -> (usize, usize, usize, usize) {
442        let stats = self.stats.read().await;
443        (
444            stats.total_traces,
445            stats.sampled_traces,
446            stats.error_traces,
447            stats.slow_traces,
448        )
449    }
450}
451
452impl Default for AdaptiveSampler {
453    fn default() -> Self {
454        Self::new()
455    }
456}
457
458impl Sampler for AdaptiveSampler {
459    fn should_sample(&self, context: &SamplingContext) -> SamplingResult {
460        // Always sample errors
461        if self.error_sampling && context.has_error {
462            return SamplingResult {
463                decision: SamplingDecision::RecordAndSample,
464                rate: 1.0,
465                reason: "error".to_string(),
466            };
467        }
468
469        // Always sample slow traces
470        if let (Some(duration), Some(threshold)) = (context.duration, self.tail_threshold) {
471            if duration > threshold {
472                return SamplingResult {
473                    decision: SamplingDecision::RecordAndSample,
474                    rate: 1.0,
475                    reason: "tail".to_string(),
476                };
477            }
478        }
479
480        // Use base rate for normal traces
481        let should_sample = fastrand::f64() < self.base_rate;
482
483        SamplingResult {
484            decision: if should_sample {
485                SamplingDecision::RecordAndSample
486            } else {
487                SamplingDecision::Drop
488            },
489            rate: self.base_rate,
490            reason: "adaptive".to_string(),
491        }
492    }
493
494    fn get_sampling_rate(&self) -> f64 {
495        self.base_rate
496    }
497
498    fn update(&mut self, context: &SamplingContext, sampled: bool) {
499        // Note: This is simplified for sync trait
500        // In production, use async update
501        if context.has_error {
502            // Track error
503        }
504        if sampled {
505            // Track sampling
506        }
507    }
508}
509
510/// Composite sampler (combines multiple strategies)
511pub struct CompositeSampler {
512    samplers: Vec<(String, Arc<dyn Sampler>)>,
513    strategy: CompositeStrategy,
514}
515
516/// Composite strategy
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
518pub enum CompositeStrategy {
519    /// Sample if ANY sampler says yes
520    Any,
521    /// Sample if ALL samplers say yes
522    All,
523    /// Use first sampler that says yes
524    FirstMatch,
525}
526
527impl CompositeSampler {
528    /// Create new composite sampler
529    pub fn new(strategy: CompositeStrategy) -> Self {
530        Self {
531            samplers: Vec::new(),
532            strategy,
533        }
534    }
535
536    /// Add sampler
537    pub fn add_sampler(mut self, name: impl Into<String>, sampler: Arc<dyn Sampler>) -> Self {
538        self.samplers.push((name.into(), sampler));
539        self
540    }
541}
542
543impl Sampler for CompositeSampler {
544    fn should_sample(&self, context: &SamplingContext) -> SamplingResult {
545        if self.samplers.is_empty() {
546            return SamplingResult {
547                decision: SamplingDecision::Drop,
548                rate: 0.0,
549                reason: "no-samplers".to_string(),
550            };
551        }
552
553        match self.strategy {
554            CompositeStrategy::Any => {
555                for (name, sampler) in &self.samplers {
556                    let result = sampler.should_sample(context);
557                    if result.should_sample() {
558                        return SamplingResult {
559                            decision: SamplingDecision::RecordAndSample,
560                            rate: result.rate,
561                            reason: format!("composite-any:{}", name),
562                        };
563                    }
564                }
565                SamplingResult {
566                    decision: SamplingDecision::Drop,
567                    rate: 0.0,
568                    reason: "composite-any:none".to_string(),
569                }
570            }
571            CompositeStrategy::All => {
572                let mut total_rate = 1.0;
573                for (_, sampler) in &self.samplers {
574                    let result = sampler.should_sample(context);
575                    if !result.should_sample() {
576                        return SamplingResult {
577                            decision: SamplingDecision::Drop,
578                            rate: 0.0,
579                            reason: "composite-all:rejected".to_string(),
580                        };
581                    }
582                    total_rate *= result.rate;
583                }
584                SamplingResult {
585                    decision: SamplingDecision::RecordAndSample,
586                    rate: total_rate,
587                    reason: "composite-all:accepted".to_string(),
588                }
589            }
590            CompositeStrategy::FirstMatch => {
591                let (name, sampler) = &self.samplers[0];
592                let result = sampler.should_sample(context);
593                SamplingResult {
594                    decision: result.decision,
595                    rate: result.rate,
596                    reason: format!("composite-first:{}", name),
597                }
598            }
599        }
600    }
601
602    fn get_sampling_rate(&self) -> f64 {
603        if self.samplers.is_empty() {
604            return 0.0;
605        }
606
607        match self.strategy {
608            CompositeStrategy::Any | CompositeStrategy::FirstMatch => {
609                self.samplers[0].1.get_sampling_rate()
610            }
611            CompositeStrategy::All => self
612                .samplers
613                .iter()
614                .map(|(_, s)| s.get_sampling_rate())
615                .product(),
616        }
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623
624    #[test]
625    fn test_sampling_context_creation() {
626        let ctx = SamplingContext::new("GetUser", "query")
627            .with_error(false)
628            .with_priority(7)
629            .with_attribute("client", "apollo");
630
631        assert_eq!(ctx.operation_name, "GetUser");
632        assert_eq!(ctx.operation_type, "query");
633        assert!(!ctx.has_error);
634        assert_eq!(ctx.priority, 7);
635        assert_eq!(ctx.attributes.get("client"), Some(&"apollo".to_string()));
636    }
637
638    #[test]
639    fn test_always_on_sampler() {
640        let sampler = AlwaysOnSampler;
641        let ctx = SamplingContext::new("test", "query");
642
643        let result = sampler.should_sample(&ctx);
644
645        assert_eq!(result.decision, SamplingDecision::RecordAndSample);
646        assert_eq!(result.rate, 1.0);
647        assert!(result.should_sample());
648    }
649
650    #[test]
651    fn test_always_off_sampler() {
652        let sampler = AlwaysOffSampler;
653        let ctx = SamplingContext::new("test", "query");
654
655        let result = sampler.should_sample(&ctx);
656
657        assert_eq!(result.decision, SamplingDecision::Drop);
658        assert_eq!(result.rate, 0.0);
659        assert!(!result.should_sample());
660    }
661
662    #[test]
663    fn test_probabilistic_sampler() {
664        let sampler = ProbabilisticSampler::new(0.5);
665        let ctx = SamplingContext::new("test", "query");
666
667        assert_eq!(sampler.get_sampling_rate(), 0.5);
668
669        // Run multiple times to check probabilistic behavior
670        let mut sampled = 0;
671        for _ in 0..1000 {
672            if sampler.should_sample(&ctx).should_sample() {
673                sampled += 1;
674            }
675        }
676
677        // Should be around 500, with some variance
678        assert!(sampled > 400 && sampled < 600);
679    }
680
681    #[test]
682    fn test_probabilistic_sampler_bounds() {
683        let sampler_low = ProbabilisticSampler::new(-0.1);
684        assert_eq!(sampler_low.get_sampling_rate(), 0.0);
685
686        let sampler_high = ProbabilisticSampler::new(1.5);
687        assert_eq!(sampler_high.get_sampling_rate(), 1.0);
688    }
689
690    #[test]
691    fn test_rate_limited_sampler() {
692        let sampler = RateLimitedSampler::new(10.0);
693        let ctx = SamplingContext::new("test", "query");
694
695        let result = sampler.should_sample(&ctx);
696        assert!(result.rate >= 0.0 && result.rate <= 1.0);
697    }
698
699    #[test]
700    fn test_priority_sampler_high_priority() {
701        let sampler = PrioritySampler::new();
702        let ctx = SamplingContext::new("test", "query").with_priority(10);
703
704        let mut sampled = 0;
705        for _ in 0..100 {
706            if sampler.should_sample(&ctx).should_sample() {
707                sampled += 1;
708            }
709        }
710
711        // High priority should sample most/all traces
712        assert!(sampled > 80);
713    }
714
715    #[test]
716    fn test_priority_sampler_low_priority() {
717        let sampler = PrioritySampler::new();
718        let ctx = SamplingContext::new("test", "query").with_priority(1);
719
720        let mut sampled = 0;
721        for _ in 0..1000 {
722            if sampler.should_sample(&ctx).should_sample() {
723                sampled += 1;
724            }
725        }
726
727        // Low priority should sample around 10%
728        assert!(sampled > 50 && sampled < 200);
729    }
730
731    #[test]
732    fn test_priority_sampler_custom_rate() {
733        let mut sampler = PrioritySampler::new();
734        sampler.set_priority_rate(5, 0.9);
735
736        let ctx = SamplingContext::new("test", "query").with_priority(5);
737
738        let mut sampled = 0;
739        for _ in 0..1000 {
740            if sampler.should_sample(&ctx).should_sample() {
741                sampled += 1;
742            }
743        }
744
745        // Should sample around 90%
746        assert!(sampled > 850);
747    }
748
749    #[test]
750    fn test_error_aware_sampler_with_error() {
751        let base = Arc::new(ProbabilisticSampler::new(0.1));
752        let sampler = ErrorAwareSampler::new(base);
753
754        let ctx = SamplingContext::new("test", "query").with_error(true);
755
756        let result = sampler.should_sample(&ctx);
757
758        assert!(result.should_sample());
759        assert_eq!(result.rate, 1.0);
760        assert_eq!(result.reason, "error-sampling");
761    }
762
763    #[test]
764    fn test_error_aware_sampler_without_error() {
765        let base = Arc::new(AlwaysOffSampler);
766        let sampler = ErrorAwareSampler::new(base);
767
768        let ctx = SamplingContext::new("test", "query").with_error(false);
769
770        let result = sampler.should_sample(&ctx);
771
772        assert!(!result.should_sample());
773    }
774
775    #[test]
776    fn test_tail_sampler_slow_trace() {
777        let base = Arc::new(AlwaysOffSampler);
778        let sampler = TailSampler::new(base, Duration::from_millis(100));
779
780        let ctx = SamplingContext::new("test", "query").with_duration(Duration::from_millis(200));
781
782        let result = sampler.should_sample(&ctx);
783
784        assert!(result.should_sample());
785        assert_eq!(result.reason, "tail-sampling");
786    }
787
788    #[test]
789    fn test_tail_sampler_fast_trace() {
790        let base = Arc::new(AlwaysOffSampler);
791        let sampler = TailSampler::new(base, Duration::from_millis(100));
792
793        let ctx = SamplingContext::new("test", "query").with_duration(Duration::from_millis(50));
794
795        let result = sampler.should_sample(&ctx);
796
797        assert!(!result.should_sample());
798    }
799
800    #[test]
801    fn test_adaptive_sampler_error() {
802        let sampler = AdaptiveSampler::new().with_error_sampling(true);
803
804        let ctx = SamplingContext::new("test", "query").with_error(true);
805
806        let result = sampler.should_sample(&ctx);
807
808        assert!(result.should_sample());
809        assert_eq!(result.reason, "error");
810    }
811
812    #[test]
813    fn test_adaptive_sampler_tail() {
814        let sampler = AdaptiveSampler::new().with_tail_threshold(Duration::from_millis(100));
815
816        let ctx = SamplingContext::new("test", "query").with_duration(Duration::from_millis(200));
817
818        let result = sampler.should_sample(&ctx);
819
820        assert!(result.should_sample());
821        assert_eq!(result.reason, "tail");
822    }
823
824    #[test]
825    fn test_adaptive_sampler_normal() {
826        let sampler = AdaptiveSampler::new().with_base_rate(0.2);
827
828        let ctx = SamplingContext::new("test", "query");
829
830        let mut sampled = 0;
831        for _ in 0..1000 {
832            if sampler.should_sample(&ctx).should_sample() {
833                sampled += 1;
834            }
835        }
836
837        // Should sample around 20%
838        assert!(sampled > 150 && sampled < 250);
839    }
840
841    #[test]
842    fn test_composite_sampler_any() {
843        let sampler = CompositeSampler::new(CompositeStrategy::Any)
844            .add_sampler("always-off", Arc::new(AlwaysOffSampler))
845            .add_sampler("always-on", Arc::new(AlwaysOnSampler));
846
847        let ctx = SamplingContext::new("test", "query");
848
849        let result = sampler.should_sample(&ctx);
850
851        assert!(result.should_sample());
852        assert!(result.reason.contains("composite-any"));
853    }
854
855    #[test]
856    fn test_composite_sampler_all() {
857        let sampler = CompositeSampler::new(CompositeStrategy::All)
858            .add_sampler("always-on-1", Arc::new(AlwaysOnSampler))
859            .add_sampler("always-on-2", Arc::new(AlwaysOnSampler));
860
861        let ctx = SamplingContext::new("test", "query");
862
863        let result = sampler.should_sample(&ctx);
864
865        assert!(result.should_sample());
866        assert!(result.reason.contains("composite-all"));
867    }
868
869    #[test]
870    fn test_composite_sampler_all_rejected() {
871        let sampler = CompositeSampler::new(CompositeStrategy::All)
872            .add_sampler("always-on", Arc::new(AlwaysOnSampler))
873            .add_sampler("always-off", Arc::new(AlwaysOffSampler));
874
875        let ctx = SamplingContext::new("test", "query");
876
877        let result = sampler.should_sample(&ctx);
878
879        assert!(!result.should_sample());
880    }
881
882    #[test]
883    fn test_composite_sampler_first_match() {
884        let sampler = CompositeSampler::new(CompositeStrategy::FirstMatch)
885            .add_sampler("always-on", Arc::new(AlwaysOnSampler))
886            .add_sampler("always-off", Arc::new(AlwaysOffSampler));
887
888        let ctx = SamplingContext::new("test", "query");
889
890        let result = sampler.should_sample(&ctx);
891
892        assert!(result.should_sample());
893        assert!(result.reason.contains("composite-first"));
894    }
895
896    #[test]
897    fn test_composite_sampler_empty() {
898        let sampler = CompositeSampler::new(CompositeStrategy::Any);
899
900        let ctx = SamplingContext::new("test", "query");
901
902        let result = sampler.should_sample(&ctx);
903
904        assert!(!result.should_sample());
905        assert_eq!(result.reason, "no-samplers");
906    }
907}