1use std::fmt;
4use std::num::ParseIntError;
5use std::ops::ControlFlow;
6
7use chrono::{DateTime, Utc};
8use rand::Rng;
9use rand::distr::StandardUniform;
10use rand_pcg::Pcg32;
11use relay_protocol::Getter;
12use serde::Serialize;
13use uuid::Uuid;
14
15use crate::config::{RuleId, SamplingRule, SamplingValue};
16
17fn pseudo_random_from_seed(seed: Uuid) -> f64 {
21 let seed_number = seed.as_u128();
22 let mut generator = Pcg32::new((seed_number >> 64) as u64, seed_number as u64);
23 generator.sample(StandardUniform)
24}
25
26#[derive(Debug)]
28pub struct SamplingEvaluator {
29 now: DateTime<Utc>,
30 rule_ids: Vec<RuleId>,
31 factor: f64,
32 minimum_sample_rate: Option<f64>,
33}
34
35impl SamplingEvaluator {
36 pub fn new(now: DateTime<Utc>) -> Self {
38 Self {
39 now,
40 rule_ids: vec![],
41 factor: 1.0,
42 minimum_sample_rate: None,
43 }
44 }
45
46 pub fn match_rules<'a, I, G>(
58 mut self,
59 seed: Uuid,
60 instance: &G,
61 rules: I,
62 ) -> ControlFlow<SamplingMatch, Self>
63 where
64 G: Getter,
65 I: Iterator<Item = &'a SamplingRule>,
66 {
67 for rule in rules {
68 if !rule.time_range.contains(self.now) || !rule.condition.matches(instance) {
69 continue;
70 };
71
72 if let Some(sample_rate) = self.try_compute_sample_rate(rule) {
73 return ControlFlow::Break(SamplingMatch::new(sample_rate, seed, self.rule_ids));
74 };
75 }
76
77 ControlFlow::Continue(self)
78 }
79
80 fn try_compute_sample_rate(&mut self, rule: &SamplingRule) -> Option<f64> {
88 match rule.sampling_value {
89 SamplingValue::Factor { value } => {
90 self.factor *= rule.apply_decaying_fn(value, self.now)?;
91 self.rule_ids.push(rule.id);
92 None
93 }
94 SamplingValue::SampleRate { value } => {
95 let sample_rate = rule.apply_decaying_fn(value, self.now)?;
96 let minimum_sample_rate = self.minimum_sample_rate.unwrap_or(0.0);
97 let adjusted = (sample_rate.max(minimum_sample_rate) * self.factor).clamp(0.0, 1.0);
98
99 self.rule_ids.push(rule.id);
100 Some(adjusted)
101 }
102 SamplingValue::MinimumSampleRate { value } => {
103 if self.minimum_sample_rate.is_none() {
104 self.minimum_sample_rate = Some(rule.apply_decaying_fn(value, self.now)?);
105 self.rule_ids.push(rule.id);
106 }
107 None
108 }
109 }
110 }
111}
112
113fn sampling_match(sample_rate: f64, seed: Uuid) -> SamplingDecision {
114 if sample_rate <= 0.0 {
115 return SamplingDecision::Drop;
116 } else if sample_rate >= 1.0 {
117 return SamplingDecision::Keep;
118 }
119
120 let random_number = pseudo_random_from_seed(seed);
121 relay_log::trace!(
122 sample_rate,
123 random_number,
124 "applying dynamic sampling to matching event"
125 );
126
127 if random_number >= sample_rate {
128 relay_log::trace!("dropping event that matched the configuration");
129 SamplingDecision::Drop
130 } else {
131 relay_log::trace!("keeping event that matched the configuration");
132 SamplingDecision::Keep
133 }
134}
135
136#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
138pub enum SamplingDecision {
139 Keep,
141 Drop,
143}
144
145impl SamplingDecision {
146 pub fn is_keep(self) -> bool {
148 matches!(self, Self::Keep)
149 }
150
151 pub fn is_drop(self) -> bool {
153 matches!(self, Self::Drop)
154 }
155
156 pub fn as_str(self) -> &'static str {
158 match self {
159 Self::Keep => "keep",
160 Self::Drop => "drop",
161 }
162 }
163}
164
165impl fmt::Display for SamplingDecision {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 write!(f, "{}", self.as_str())
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
173pub struct SamplingMatch {
174 sample_rate: f64,
176 seed: Uuid,
182 matched_rules: MatchedRuleIds,
184 decision: SamplingDecision,
188}
189
190impl SamplingMatch {
191 fn new(sample_rate: f64, seed: Uuid, matched_rules: Vec<RuleId>) -> Self {
192 let matched_rules = MatchedRuleIds(matched_rules);
193 let decision = sampling_match(sample_rate, seed);
194
195 Self {
196 sample_rate,
197 seed,
198 matched_rules,
199 decision,
200 }
201 }
202
203 pub fn sample_rate(&self) -> f64 {
205 self.sample_rate
206 }
207
208 pub fn into_matched_rules(self) -> MatchedRuleIds {
213 self.matched_rules
214 }
215
216 pub fn decision(&self) -> SamplingDecision {
218 self.decision
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
224pub struct MatchedRuleIds(pub Vec<RuleId>);
225
226impl MatchedRuleIds {
227 pub fn parse(value: &str) -> Result<MatchedRuleIds, ParseIntError> {
235 let mut rule_ids = vec![];
236
237 for rule_id in value.split(',') {
238 rule_ids.push(RuleId(rule_id.parse()?));
239 }
240
241 Ok(MatchedRuleIds(rule_ids))
242 }
243}
244
245impl fmt::Display for MatchedRuleIds {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 for (i, rule_id) in self.0.iter().enumerate() {
248 if i > 0 {
249 write!(f, ",")?;
250 }
251 write!(f, "{rule_id}")?;
252 }
253
254 Ok(())
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use chrono::TimeZone;
261 use relay_base_schema::project::ProjectId;
262 use relay_protocol::RuleCondition;
263 use similar_asserts::assert_eq;
264 use std::str::FromStr;
265 use uuid::Uuid;
266
267 use crate::DynamicSamplingContext;
268 use crate::config::{DecayingFunction, RuleType, TimeRange};
269 use crate::dsc::TraceUserContext;
270
271 use super::*;
272
273 fn get_sampling_match(rules: &[SamplingRule], instance: &impl Getter) -> SamplingMatch {
275 match SamplingEvaluator::new(Utc::now()).match_rules(
276 Uuid::default(),
277 instance,
278 rules.iter(),
279 ) {
280 ControlFlow::Break(sampling_match) => sampling_match,
281 ControlFlow::Continue(_) => panic!("no match found"),
282 }
283 }
284
285 fn evaluation_is_match(res: ControlFlow<SamplingMatch, SamplingEvaluator>) -> bool {
286 matches!(res, ControlFlow::Break(_))
287 }
288
289 fn matches_rule_ids(rule_ids: &[u32], rules: &[SamplingRule], instance: &impl Getter) -> bool {
291 let matched_rule_ids = MatchedRuleIds(rule_ids.iter().map(|num| RuleId(*num)).collect());
292 let sampling_match = get_sampling_match(rules, instance);
293 matched_rule_ids == sampling_match.matched_rules
294 }
295
296 fn mocked_dsc_with_getter_values(
298 paths_and_values: Vec<(&str, &str)>,
299 ) -> DynamicSamplingContext {
300 let mut dsc = DynamicSamplingContext {
301 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
302 public_key: "12345678123456781234567812345678".parse().unwrap(),
303 project_id: Some(ProjectId::new(42)),
304 release: None,
305 environment: None,
306 transaction: None,
307 sample_rate: None,
308 user: TraceUserContext::default(),
309 replay_id: None,
310 sampled: None,
311 other: Default::default(),
312 };
313
314 for (path, value) in paths_and_values {
315 match path {
316 "trace.release" => dsc.release = Some(value.to_owned()),
317 "trace.environment" => dsc.environment = Some(value.to_owned()),
318 "trace.user.id" => value.clone_into(&mut dsc.user.user_id),
319 "trace.user.segment" => value.clone_into(&mut dsc.user.user_segment),
320 "trace.transaction" => dsc.transaction = Some(value.to_owned()),
321 "trace.replay_id" => dsc.replay_id = Some(Uuid::from_str(value).unwrap()),
322 _ => panic!("invalid path"),
323 }
324 }
325
326 dsc
327 }
328
329 fn is_match(now: DateTime<Utc>, rule: &SamplingRule, dsc: &DynamicSamplingContext) -> bool {
330 SamplingEvaluator::new(now)
331 .match_rules(Uuid::default(), dsc, std::iter::once(rule))
332 .is_break()
333 }
334
335 #[test]
336 fn test_sample_rate_compounding() {
337 let rules = simple_sampling_rules(vec![
338 (RuleCondition::all(), SamplingValue::Factor { value: 0.8 }),
339 (RuleCondition::all(), SamplingValue::Factor { value: 0.5 }),
340 (
341 RuleCondition::all(),
342 SamplingValue::SampleRate { value: 0.25 },
343 ),
344 ]);
345 let dsc = mocked_dsc_with_getter_values(vec![]);
346
347 assert_eq!(get_sampling_match(&rules, &dsc).sample_rate(), 0.1);
349 }
350
351 #[test]
352 fn test_minimum_sample_rate() {
353 let rules = simple_sampling_rules(vec![
354 (RuleCondition::all(), SamplingValue::Factor { value: 1.5 }),
355 (
356 RuleCondition::all(),
357 SamplingValue::MinimumSampleRate { value: 0.5 },
358 ),
359 (
361 RuleCondition::all(),
362 SamplingValue::MinimumSampleRate { value: 1.0 },
363 ),
364 (
365 RuleCondition::all(),
366 SamplingValue::SampleRate { value: 0.05 },
367 ),
368 ]);
369 let dsc = mocked_dsc_with_getter_values(vec![]);
370
371 assert_eq!(get_sampling_match(&rules, &dsc).sample_rate(), 0.75);
373 }
374
375 fn mocked_sampling_rule() -> SamplingRule {
376 SamplingRule {
377 condition: RuleCondition::all(),
378 sampling_value: SamplingValue::SampleRate { value: 1.0 },
379 ty: RuleType::Trace,
380 id: RuleId(0),
381 time_range: Default::default(),
382 decaying_fn: Default::default(),
383 }
384 }
385
386 fn simple_sampling_rules(vals: Vec<(RuleCondition, SamplingValue)>) -> Vec<SamplingRule> {
389 let mut vec = vec![];
390
391 for (i, val) in vals.into_iter().enumerate() {
392 let (condition, sampling_value) = val;
393 vec.push(SamplingRule {
394 condition,
395 sampling_value,
396 ty: RuleType::Trace,
397 id: RuleId(i as u32),
398 time_range: Default::default(),
399 decaying_fn: Default::default(),
400 });
401 }
402 vec
403 }
404
405 #[test]
407 fn test_expired_rules() {
408 let rule = SamplingRule {
409 condition: RuleCondition::all(),
410 sampling_value: SamplingValue::SampleRate { value: 1.0 },
411 ty: RuleType::Trace,
412 id: RuleId(0),
413 time_range: TimeRange {
414 start: Some(Utc.with_ymd_and_hms(1970, 10, 10, 0, 0, 0).unwrap()),
415 end: Some(Utc.with_ymd_and_hms(1970, 10, 12, 0, 0, 0).unwrap()),
416 },
417 decaying_fn: Default::default(),
418 };
419
420 let dsc = mocked_dsc_with_getter_values(vec![]);
421
422 let within_timerange = Utc.with_ymd_and_hms(1970, 10, 11, 0, 0, 0).unwrap();
424 let res = SamplingEvaluator::new(within_timerange).match_rules(
425 Uuid::default(),
426 &dsc,
427 [rule.clone()].iter(),
428 );
429 assert!(evaluation_is_match(res));
430
431 let before_timerange = Utc.with_ymd_and_hms(1969, 1, 1, 0, 0, 0).unwrap();
432 let res = SamplingEvaluator::new(before_timerange).match_rules(
433 Uuid::default(),
434 &dsc,
435 [rule.clone()].iter(),
436 );
437 assert!(!evaluation_is_match(res));
438
439 let after_timerange = Utc.with_ymd_and_hms(1971, 1, 1, 0, 0, 0).unwrap();
440 let res = SamplingEvaluator::new(after_timerange).match_rules(
441 Uuid::default(),
442 &dsc,
443 [rule].iter(),
444 );
445 assert!(!evaluation_is_match(res));
446 }
447
448 #[test]
450 fn test_condition_matching() {
451 let rules = simple_sampling_rules(vec![
452 (
453 RuleCondition::glob("trace.transaction", "*healthcheck*"),
454 SamplingValue::SampleRate { value: 1.0 },
455 ),
456 (
457 RuleCondition::glob("trace.environment", "*dev*"),
458 SamplingValue::SampleRate { value: 1.0 },
459 ),
460 (
461 RuleCondition::eq_ignore_case("trace.transaction", "raboof"),
462 SamplingValue::Factor { value: 1.0 },
463 ),
464 (
465 RuleCondition::glob("trace.release", "1.1.1")
466 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
467 SamplingValue::SampleRate { value: 1.0 },
468 ),
469 (
470 RuleCondition::eq_ignore_case("trace.release", "1.1.1")
471 & RuleCondition::eq_ignore_case("trace.environment", "prod"),
472 SamplingValue::Factor { value: 1.0 },
473 ),
474 (
475 RuleCondition::all(),
476 SamplingValue::SampleRate { value: 1.0 },
477 ),
478 ]);
479
480 let dsc = mocked_dsc_with_getter_values(vec![("trace.transaction", "foohealthcheckbar")]);
482 assert!(matches_rule_ids(&[0], &rules, &dsc));
483
484 let dsc = mocked_dsc_with_getter_values(vec![("trace.environment", "dev")]);
486 assert!(matches_rule_ids(&[1], &rules, &dsc));
487
488 let dsc = mocked_dsc_with_getter_values(vec![("trace.transaction", "raboof")]);
490 assert!(matches_rule_ids(&[2, 5], &rules, &dsc));
491
492 let dsc = mocked_dsc_with_getter_values(vec![
494 ("trace.transaction", "raboof"),
495 ("trace.release", "1.1.1"),
496 ("trace.user.segment", "vip"),
497 ]);
498 assert!(matches_rule_ids(&[2, 3], &rules, &dsc));
499
500 let dsc = mocked_dsc_with_getter_values(vec![
502 ("trace.transaction", "raboof"),
503 ("trace.release", "1.1.1"),
504 ("trace.environment", "prod"),
505 ]);
506 assert!(matches_rule_ids(&[2, 4, 5], &rules, &dsc));
507
508 let dsc = mocked_dsc_with_getter_values(vec![
510 ("trace.release", "1.1.1"),
511 ("trace.environment", "prod"),
512 ]);
513 assert!(matches_rule_ids(&[4, 5], &rules, &dsc));
514 }
515
516 #[test]
517 fn test_repeatable_seed() {
519 let val1 = pseudo_random_from_seed(Uuid::default());
520 let val2 = pseudo_random_from_seed(Uuid::default());
521 assert!(val1 + f64::EPSILON > val2 && val2 + f64::EPSILON > val1);
522 }
523
524 #[test]
525 fn matched_rule_ids_display() {
527 let matched_rule_ids = MatchedRuleIds(vec![RuleId(123), RuleId(456)]);
528 assert_eq!(matched_rule_ids.to_string(), "123,456");
529
530 let matched_rule_ids = MatchedRuleIds(vec![RuleId(123)]);
531 assert_eq!(matched_rule_ids.to_string(), "123");
532
533 let matched_rule_ids = MatchedRuleIds(vec![]);
534 assert_eq!(matched_rule_ids.to_string(), "")
535 }
536
537 #[test]
538 fn matched_rule_ids_parse() {
540 assert_eq!(
541 MatchedRuleIds::parse("123,456"),
542 Ok(MatchedRuleIds(vec![RuleId(123), RuleId(456)]))
543 );
544
545 assert_eq!(
546 MatchedRuleIds::parse("123"),
547 Ok(MatchedRuleIds(vec![RuleId(123)]))
548 );
549
550 assert!(MatchedRuleIds::parse("").is_err());
551
552 assert!(MatchedRuleIds::parse(",").is_err());
553
554 assert!(MatchedRuleIds::parse("123.456").is_err());
555
556 assert!(MatchedRuleIds::parse("a,b").is_err());
557 }
558
559 #[test]
560 fn test_get_sampling_match_result_with_no_match() {
562 let dsc = mocked_dsc_with_getter_values(vec![]);
563
564 let res = SamplingEvaluator::new(Utc::now()).match_rules(Uuid::default(), &dsc, [].iter());
565
566 assert!(!evaluation_is_match(res));
567 }
568
569 #[test]
574 fn test_sample_rate_valid_time_range() {
575 let dsc = mocked_dsc_with_getter_values(vec![]);
576 let time_range = TimeRange {
577 start: Some(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()),
578 end: Some(Utc.with_ymd_and_hms(1980, 1, 1, 0, 0, 0).unwrap()),
579 };
580
581 let before_time_range = Utc.with_ymd_and_hms(1969, 1, 1, 0, 0, 0).unwrap();
582 let during_time_range = Utc.with_ymd_and_hms(1975, 1, 1, 0, 0, 0).unwrap();
583 let after_time_range = Utc.with_ymd_and_hms(1981, 1, 1, 0, 0, 0).unwrap();
584
585 let rule = SamplingRule {
586 condition: RuleCondition::all(),
587 sampling_value: SamplingValue::SampleRate { value: 1.0 },
588 ty: RuleType::Trace,
589 id: RuleId(0),
590 time_range,
591 decaying_fn: DecayingFunction::Constant,
592 };
593
594 assert!(!is_match(before_time_range, &rule, &dsc));
596 assert!(is_match(during_time_range, &rule, &dsc));
597 assert!(!is_match(after_time_range, &rule, &dsc));
598
599 let mut rule_without_end = rule.clone();
601 rule_without_end.time_range.end = None;
602 assert!(!is_match(before_time_range, &rule_without_end, &dsc));
603 assert!(is_match(during_time_range, &rule_without_end, &dsc));
604 assert!(is_match(after_time_range, &rule_without_end, &dsc));
605
606 let mut rule_without_start = rule.clone();
608 rule_without_start.time_range.start = None;
609 assert!(is_match(before_time_range, &rule_without_start, &dsc));
610 assert!(is_match(during_time_range, &rule_without_start, &dsc));
611 assert!(!is_match(after_time_range, &rule_without_start, &dsc));
612
613 let mut rule_without_range = rule.clone();
615 rule_without_range.time_range = TimeRange::default();
616 assert!(is_match(before_time_range, &rule_without_range, &dsc));
617 assert!(is_match(during_time_range, &rule_without_range, &dsc));
618 assert!(is_match(after_time_range, &rule_without_range, &dsc));
619 }
620
621 #[test]
623 fn test_validate_match() {
624 let mut rule = mocked_sampling_rule();
625 let mut eval = SamplingEvaluator::new(Utc::now());
626
627 rule.sampling_value = SamplingValue::SampleRate { value: 1.0 };
628 assert_eq!(eval.try_compute_sample_rate(&rule), Some(1.0));
629
630 rule.sampling_value = SamplingValue::Factor { value: 1.0 };
631 assert_eq!(eval.try_compute_sample_rate(&rule), None);
632
633 rule.sampling_value = SamplingValue::MinimumSampleRate { value: 1.0 };
634 assert_eq!(eval.try_compute_sample_rate(&rule), None);
635 }
636}