1use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34use std::sync::Arc;
35use std::time::{Duration, Instant};
36use tokio::sync::RwLock;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum SamplingDecision {
41 RecordAndSample,
43 RecordOnly,
45 Drop,
47}
48
49#[derive(Debug, Clone)]
51pub struct SamplingResult {
52 pub decision: SamplingDecision,
54 pub rate: f64,
56 pub reason: String,
58}
59
60impl SamplingResult {
61 pub fn should_sample(&self) -> bool {
63 self.decision == SamplingDecision::RecordAndSample
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct SamplingContext {
70 pub operation_name: String,
72 pub operation_type: String,
74 pub has_error: bool,
76 pub duration: Option<Duration>,
78 pub attributes: HashMap<String, String>,
80 pub priority: u8,
82}
83
84impl SamplingContext {
85 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 pub fn with_error(mut self, has_error: bool) -> Self {
99 self.has_error = has_error;
100 self
101 }
102
103 pub fn with_duration(mut self, duration: Duration) -> Self {
105 self.duration = Some(duration);
106 self
107 }
108
109 pub fn with_priority(mut self, priority: u8) -> Self {
111 self.priority = priority.min(10);
112 self
113 }
114
115 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
122pub trait Sampler: Send + Sync {
124 fn should_sample(&self, context: &SamplingContext) -> SamplingResult;
126
127 fn get_sampling_rate(&self) -> f64;
129
130 fn update(&mut self, _context: &SamplingContext, _sampled: bool) {}
132}
133
134#[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#[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#[derive(Debug, Clone)]
172pub struct ProbabilisticSampler {
173 rate: f64,
174}
175
176impl ProbabilisticSampler {
177 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
205pub 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 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 let should_sample = fastrand::f64() < 0.1; 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#[derive(Debug, Clone)]
255pub struct PrioritySampler {
256 priority_rates: HashMap<u8, f64>,
257 default_rate: f64,
258}
259
260impl PrioritySampler {
261 pub fn new() -> Self {
263 let mut priority_rates = HashMap::new();
264 priority_rates.insert(10, 1.0);
266 priority_rates.insert(9, 1.0);
267 priority_rates.insert(8, 1.0);
268 priority_rates.insert(7, 0.5);
270 priority_rates.insert(6, 0.5);
271 priority_rates.insert(5, 0.5);
272 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 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
322pub struct ErrorAwareSampler {
324 base_sampler: Arc<dyn Sampler>,
325}
326
327impl ErrorAwareSampler {
328 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
352pub struct TailSampler {
354 base_sampler: Arc<dyn Sampler>,
355 threshold: Duration,
356}
357
358impl TailSampler {
359 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
388pub 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 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 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 pub fn with_error_sampling(mut self, enabled: bool) -> Self {
430 self.error_sampling = enabled;
431 self
432 }
433
434 pub fn with_tail_threshold(mut self, threshold: Duration) -> Self {
436 self.tail_threshold = Some(threshold);
437 self
438 }
439
440 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 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 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 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 if context.has_error {
502 }
504 if sampled {
505 }
507 }
508}
509
510pub struct CompositeSampler {
512 samplers: Vec<(String, Arc<dyn Sampler>)>,
513 strategy: CompositeStrategy,
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
518pub enum CompositeStrategy {
519 Any,
521 All,
523 FirstMatch,
525}
526
527impl CompositeSampler {
528 pub fn new(strategy: CompositeStrategy) -> Self {
530 Self {
531 samplers: Vec::new(),
532 strategy,
533 }
534 }
535
536 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 let mut sampled = 0;
671 for _ in 0..1000 {
672 if sampler.should_sample(&ctx).should_sample() {
673 sampled += 1;
674 }
675 }
676
677 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 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 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 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 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}