1use crate::constants::pattern::NO_RULE;
5use crate::glob_matcher::GlobMatcher;
6use crate::rate_sampler::RateSampler;
7use crate::sampling_rule_config::SamplingRuleConfig;
8use crate::types::{AttributeLike, SpanProperties, TraceIdLike, ValueLike};
9use std::collections::HashMap;
10
11const HTTP_RESPONSE_STATUS_CODE: &str = "http.response.status_code";
13const HTTP_STATUS_CODE: &str = "http.status_code";
14
15fn matcher_from_rule(rule: &str) -> Option<GlobMatcher> {
16 (rule != NO_RULE).then(|| GlobMatcher::new(rule))
17}
18
19#[derive(Clone, Debug)]
21pub struct SamplingRule {
22 pub(crate) sample_rate: f64,
24
25 pub(crate) provenance: String,
27
28 rate_sampler: RateSampler,
30
31 pub(crate) name_matcher: Option<GlobMatcher>,
33 pub(crate) service_matcher: Option<GlobMatcher>,
34 pub(crate) resource_matcher: Option<GlobMatcher>,
35 pub(crate) tag_matchers: HashMap<String, GlobMatcher>,
36}
37
38impl SamplingRule {
39 pub fn from_configs(configs: Vec<SamplingRuleConfig>) -> Vec<Self> {
42 configs
43 .into_iter()
44 .map(|config| {
45 Self::new(
46 config.sample_rate,
47 config.service,
48 config.name,
49 config.resource,
50 Some(config.tags),
51 Some(config.provenance),
52 )
53 })
54 .collect()
55 }
56
57 pub fn new(
59 sample_rate: f64,
60 service: Option<String>,
61 name: Option<String>,
62 resource: Option<String>,
63 tags: Option<HashMap<String, String>>,
64 provenance: Option<String>,
65 ) -> Self {
66 let name_matcher = name.as_deref().and_then(matcher_from_rule);
68 let service_matcher = service.as_deref().and_then(matcher_from_rule);
69 let resource_matcher = resource.as_deref().and_then(matcher_from_rule);
70
71 let tag_map = tags.unwrap_or_default();
73 let mut tag_matchers = HashMap::with_capacity(tag_map.len());
74 for (key, value) in tag_map {
75 if let Some(matcher) = matcher_from_rule(&value) {
76 tag_matchers.insert(key, matcher);
77 }
78 }
79
80 SamplingRule {
81 sample_rate,
82 provenance: provenance.unwrap_or_else(|| "default".to_string()),
83 rate_sampler: RateSampler::new(sample_rate),
84 name_matcher,
85 service_matcher,
86 resource_matcher,
87 tag_matchers,
88 }
89 }
90
91 pub(crate) fn matches(&self, span: &impl SpanProperties) -> bool {
94 let name = span.operation_name();
96
97 if let Some(ref matcher) = self.name_matcher {
99 if !matcher.matches(name.as_ref()) {
100 return false;
101 }
102 }
103
104 if let Some(ref matcher) = self.service_matcher {
106 let service = span.service();
108
109 if !matcher.matches(&service) {
111 return false;
112 }
113 }
114
115 let resource_str = span.resource();
117
118 if let Some(ref matcher) = self.resource_matcher {
120 if !matcher.matches(resource_str.as_ref()) {
122 return false;
123 }
124 }
125
126 for (key, matcher) in &self.tag_matchers {
128 let rule_tag_key_str = key.as_str();
129
130 if rule_tag_key_str == HTTP_STATUS_CODE || rule_tag_key_str == HTTP_RESPONSE_STATUS_CODE
133 {
134 match self.match_http_status_code_rule(matcher, span) {
135 Some(true) => continue, Some(false) | None => return false, }
138 } else {
139 let direct_match = span
142 .attributes()
143 .find(|attr| attr.key() == rule_tag_key_str)
144 .and_then(|attr| self.match_attribute_value(attr.value(), matcher));
145
146 if direct_match.unwrap_or(false) {
147 continue;
148 }
149
150 if rule_tag_key_str.starts_with("http.") {
155 let tag_match = span.attributes().any(|attr| {
156 if let Some(alternate_key) = span.get_alternate_key(attr.key()) {
157 if alternate_key == rule_tag_key_str {
158 return self
159 .match_attribute_value(attr.value(), matcher)
160 .unwrap_or(false);
161 }
162 }
163 false
164 });
165
166 if !tag_match {
167 return false; }
169 } else {
171 return false;
174 }
175 }
176 }
177
178 true
179 }
180
181 fn match_http_status_code_rule(
185 &self,
186 matcher: &GlobMatcher,
187 span: &impl SpanProperties,
188 ) -> Option<bool> {
189 span.status_code().and_then(|status_code| {
190 let status_value = ValueI64(i64::from(status_code));
191 self.match_attribute_value(&status_value, matcher)
192 })
193 }
194
195 fn match_attribute_value(&self, value: &impl ValueLike, matcher: &GlobMatcher) -> Option<bool> {
197 if let Some(float_val) = value.as_float() {
199 let is_integer = float_val.is_finite() && float_val.fract() == 0.0;
203
204 if !is_integer {
206 return Some(matcher.pattern().chars().all(|c| c == '*'));
208 }
209
210 return Some(matcher.matches(&float_val.to_string()));
212 }
213
214 value
216 .as_str()
217 .map(|string_value| matcher.matches(&string_value))
218 }
219
220 pub fn sample(&self, trace_id: &impl TraceIdLike) -> bool {
222 self.rate_sampler.sample(trace_id)
224 }
225}
226
227#[allow(dead_code)]
233#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
234pub(crate) enum RuleProvenance {
235 Customer = 0,
236 Dynamic = 1,
237 Default = 2,
238}
239
240impl From<&str> for RuleProvenance {
241 fn from(s: &str) -> Self {
242 match s {
243 "customer" => RuleProvenance::Customer,
244 "dynamic" => RuleProvenance::Dynamic,
245 _ => RuleProvenance::Default,
246 }
247 }
248}
249
250struct ValueI64(i64);
252
253impl ValueLike for ValueI64 {
254 fn as_float(&self) -> Option<f64> {
255 Some(self.0 as f64)
256 }
257
258 fn as_str(&self) -> Option<std::borrow::Cow<'_, str>> {
259 Some(std::borrow::Cow::Owned(self.0.to_string()))
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::sampling_rule_config::SamplingRuleConfig;
267 use std::borrow::Cow;
268
269 struct TestSpan {
271 name: &'static str,
272 service: &'static str,
273 resource: &'static str,
274 status_code: Option<u32>,
275 attrs: Vec<TestAttr>,
277 alternates: Vec<(&'static str, &'static str)>,
279 }
280
281 struct TestAttr {
282 key: &'static str,
283 value: TestValue,
284 }
285
286 struct TestValue {
287 value: &'static str,
288 is_metric: bool,
289 }
290
291 impl crate::types::ValueLike for TestValue {
292 fn as_float(&self) -> Option<f64> {
293 if self.is_metric {
294 self.value.parse().ok()
295 } else {
296 None
297 }
298 }
299 fn as_str(&self) -> Option<Cow<'_, str>> {
300 Some(Cow::Borrowed(self.value))
301 }
302 }
303
304 impl crate::types::AttributeLike for TestAttr {
305 type Value = TestValue;
306 fn key(&self) -> &str {
307 self.key
308 }
309 fn value(&self) -> &TestValue {
310 &self.value
311 }
312 }
313
314 impl crate::types::SpanProperties for TestSpan {
315 type Attribute<'a>
316 = &'a TestAttr
317 where
318 Self: 'a;
319
320 fn operation_name(&self) -> Cow<'_, str> {
321 Cow::Borrowed(self.name)
322 }
323 fn service(&self) -> Cow<'_, str> {
324 Cow::Borrowed(self.service)
325 }
326 fn env(&self) -> Cow<'_, str> {
327 Cow::Borrowed("")
328 }
329 fn resource(&self) -> Cow<'_, str> {
330 Cow::Borrowed(self.resource)
331 }
332 fn status_code(&self) -> Option<u32> {
333 self.status_code
334 }
335 fn attributes(&self) -> impl Iterator<Item = &TestAttr> + '_ {
336 self.attrs.iter()
337 }
338 fn get_alternate_key<'b>(&self, key: &'b str) -> Option<Cow<'b, str>> {
339 self.alternates
340 .iter()
341 .find(|(k, _)| *k == key)
342 .map(|(_, alt)| Cow::Borrowed(*alt))
343 }
344 }
345
346 fn make_span(name: &'static str, service: &'static str, resource: &'static str) -> TestSpan {
347 TestSpan {
348 name,
349 service,
350 resource,
351 status_code: None,
352 attrs: vec![],
353 alternates: vec![],
354 }
355 }
356
357 #[test]
360 fn test_from_configs_empty() {
361 let rules = SamplingRule::from_configs(vec![]);
362 assert!(rules.is_empty());
363 }
364
365 #[test]
366 fn test_from_configs_single() {
367 let config = SamplingRuleConfig {
368 sample_rate: 0.5,
369 service: Some("svc".into()),
370 name: Some("op.*".into()),
371 resource: None,
372 tags: HashMap::new(),
373 provenance: "customer".into(),
374 };
375 let rules = SamplingRule::from_configs(vec![config]);
376 assert_eq!(rules.len(), 1);
377 assert_eq!(rules[0].sample_rate, 0.5);
378 assert_eq!(rules[0].provenance, "customer");
379 }
380
381 #[test]
382 fn test_from_configs_preserves_provenance() {
383 let configs = vec![
384 SamplingRuleConfig {
385 sample_rate: 1.0,
386 provenance: "customer".into(),
387 ..Default::default()
388 },
389 SamplingRuleConfig {
390 sample_rate: 0.5,
391 provenance: "dynamic".into(),
392 ..Default::default()
393 },
394 SamplingRuleConfig {
395 sample_rate: 0.1,
396 provenance: "default".into(),
397 ..Default::default()
398 },
399 ];
400 let rules = SamplingRule::from_configs(configs);
401 assert_eq!(rules[0].provenance, "customer");
402 assert_eq!(rules[1].provenance, "dynamic");
403 assert_eq!(rules[2].provenance, "default");
404 }
405
406 #[test]
409 fn test_matches_http_status_code_rule_matching() {
410 let rule = SamplingRule::new(
411 1.0,
412 None,
413 None,
414 None,
415 Some(HashMap::from([("http.status_code".into(), "200".into())])),
416 None,
417 );
418 let mut span = make_span("op", "svc", "res");
419 span.status_code = Some(200);
420 assert!(rule.matches(&span));
421 }
422
423 #[test]
424 fn test_matches_http_status_code_rule_not_matching() {
425 let rule = SamplingRule::new(
426 1.0,
427 None,
428 None,
429 None,
430 Some(HashMap::from([("http.status_code".into(), "200".into())])),
431 None,
432 );
433 let mut span = make_span("op", "svc", "res");
434 span.status_code = Some(404);
435 assert!(!rule.matches(&span));
436 }
437
438 #[test]
439 fn test_matches_http_status_code_absent_returns_false() {
440 let rule = SamplingRule::new(
441 1.0,
442 None,
443 None,
444 None,
445 Some(HashMap::from([("http.status_code".into(), "200".into())])),
446 None,
447 );
448 let span = make_span("op", "svc", "res"); assert!(!rule.matches(&span));
450 }
451
452 #[test]
453 fn test_matches_http_response_status_code_key() {
454 let rule = SamplingRule::new(
455 1.0,
456 None,
457 None,
458 None,
459 Some(HashMap::from([(
460 "http.response.status_code".into(),
461 "404".into(),
462 )])),
463 None,
464 );
465 let mut span = make_span("op", "svc", "res");
466 span.status_code = Some(404);
467 assert!(rule.matches(&span));
468 }
469
470 #[test]
471 fn test_matches_http_status_code_wildcard() {
472 let rule = SamplingRule::new(
473 1.0,
474 None,
475 None,
476 None,
477 Some(HashMap::from([("http.status_code".into(), "2*".into())])),
478 None,
479 );
480 let mut span = make_span("op", "svc", "res");
481 span.status_code = Some(201);
482 assert!(rule.matches(&span));
483 }
484
485 #[test]
488 fn test_matches_alternate_key_found() {
489 let rule = SamplingRule::new(
492 1.0,
493 None,
494 None,
495 None,
496 Some(HashMap::from([("http.method".into(), "POST".into())])),
497 None,
498 );
499 let mut span = make_span("op", "svc", "res");
500 span.attrs = vec![TestAttr {
501 key: "http.request.method",
502 value: TestValue {
503 value: "POST",
504 is_metric: false,
505 },
506 }];
507 span.alternates = vec![("http.request.method", "http.method")];
508 assert!(rule.matches(&span));
509 }
510
511 #[test]
512 fn test_matches_alternate_key_value_mismatch() {
513 let rule = SamplingRule::new(
514 1.0,
515 None,
516 None,
517 None,
518 Some(HashMap::from([("http.method".into(), "POST".into())])),
519 None,
520 );
521 let mut span = make_span("op", "svc", "res");
522 span.attrs = vec![TestAttr {
523 key: "http.request.method",
524 value: TestValue {
525 value: "GET",
526 is_metric: false,
527 },
528 }];
529 span.alternates = vec![("http.request.method", "http.method")];
530 assert!(!rule.matches(&span));
531 }
532
533 #[test]
534 fn test_matches_non_http_tag_no_alternate_fallback() {
535 let rule = SamplingRule::new(
537 1.0,
538 None,
539 None,
540 None,
541 Some(HashMap::from([("custom.tag".into(), "value".into())])),
542 None,
543 );
544 let mut span = make_span("op", "svc", "res");
545 span.attrs = vec![TestAttr {
546 key: "some.other.key",
547 value: TestValue {
548 value: "value",
549 is_metric: false,
550 },
551 }];
552 span.alternates = vec![("some.other.key", "custom.tag")];
553 assert!(!rule.matches(&span));
554 }
555
556 #[test]
559 fn test_match_attribute_value_non_integer_float_wildcard_matches() {
560 let rule = SamplingRule::new(
561 1.0,
562 None,
563 None,
564 None,
565 Some(HashMap::from([("score".into(), "*".into())])),
566 None,
567 );
568 let mut span = make_span("op", "svc", "res");
569 span.attrs = vec![TestAttr {
570 key: "score",
571 value: TestValue {
572 value: "3.14",
573 is_metric: true,
574 },
575 }];
576 assert!(rule.matches(&span));
577 }
578
579 #[test]
580 fn test_match_attribute_value_non_integer_float_non_wildcard_no_match() {
581 let rule = SamplingRule::new(
582 1.0,
583 None,
584 None,
585 None,
586 Some(HashMap::from([("score".into(), "3.14".into())])),
587 None,
588 );
589 let mut span = make_span("op", "svc", "res");
590 span.attrs = vec![TestAttr {
591 key: "score",
592 value: TestValue {
593 value: "3.14",
594 is_metric: true,
595 },
596 }];
597 assert!(!rule.matches(&span));
598 }
599
600 #[test]
601 fn test_resource_mismatch_returns_false() {
602 let rule = SamplingRule::new(
603 1.0,
604 None,
605 Some("specific-resource".into()),
606 None,
607 None,
608 None,
609 );
610 let span = make_span("op", "svc", "other-resource");
611 assert!(!rule.matches(&span));
612 }
613
614 #[test]
617 fn test_rule_provenance_from_str() {
618 assert_eq!(RuleProvenance::from("customer"), RuleProvenance::Customer);
619 assert_eq!(RuleProvenance::from("dynamic"), RuleProvenance::Dynamic);
620 assert_eq!(RuleProvenance::from("default"), RuleProvenance::Default);
621 assert_eq!(RuleProvenance::from("unknown"), RuleProvenance::Default);
622 assert_eq!(RuleProvenance::from(""), RuleProvenance::Default);
623 }
624
625 #[test]
626 fn test_rule_provenance_ordering() {
627 assert!(RuleProvenance::Customer < RuleProvenance::Dynamic);
628 assert!(RuleProvenance::Dynamic < RuleProvenance::Default);
629 }
630}