@@ -45,8 +45,8 @@ impl DatadogSampler {
4545 }
4646 }
4747
48- // used for tests
49- #[ allow ( dead_code ) ]
48+ // Test-only helper that bypasses the agent-response parsing path.
49+ #[ cfg ( test ) ]
5050 pub ( crate ) fn update_service_rates ( & self , rates : impl IntoIterator < Item = ( String , f64 ) > ) {
5151 self . service_samplers . update_rates ( rates) ;
5252 }
@@ -64,9 +64,11 @@ impl DatadogSampler {
6464 } )
6565 }
6666
67- /// Creates a callback for updating sampling rules from remote configuration
67+ /// Creates a callback for updating sampling rules from remote configuration.
68+ ///
6869 /// # Returns
69- /// A boxed function that takes a slice of SamplingRuleConfig and updates the sampling rules
70+ ///
71+ /// A boxed function that takes a slice of `SamplingRuleConfig` and updates the sampling rules.
7072 pub fn on_rules_update ( & self ) -> SamplingRulesCallback {
7173 let rules_sampler = self . rules . clone ( ) ;
7274 Box :: new ( move |rule_configs : & [ SamplingRuleConfig ] | {
@@ -78,12 +80,9 @@ impl DatadogSampler {
7880
7981 /// Computes a key for service-based sampling
8082 fn service_key ( & self , span : & impl SpanProperties ) -> String {
81- // Get service from span
82- let service = span. service ( ) . into_owned ( ) ;
83- // Get env from span
84- let env = span. env ( ) ;
85-
86- format ! ( "service:{service},env:{env}" )
83+ // `Cow<str>` implements `Display`, so no `into_owned()` allocation is needed here;
84+ // `format!` will borrow directly from the span.
85+ format ! ( "service:{},env:{}" , span. service( ) , span. env( ) )
8786 }
8887
8988 /// Finds the highest precedence rule that matches the span
@@ -98,40 +97,46 @@ impl DatadogSampler {
9897 used_agent_sampler : bool ,
9998 ) -> SamplingMechanism {
10099 if let Some ( rule) = rule {
100+ // Provenance is set when rules come from remote configuration
101+ // (see `on_rules_update`); locally configured rules use the default value.
101102 match rule. provenance . as_str ( ) {
102- // Provenance will not be set for rules until we implement remote configuration
103103 "customer" => mechanism:: REMOTE_USER_TRACE_SAMPLING_RULE ,
104104 "dynamic" => mechanism:: REMOTE_DYNAMIC_TRACE_SAMPLING_RULE ,
105105 _ => mechanism:: LOCAL_USER_TRACE_SAMPLING_RULE ,
106106 }
107107 } else if used_agent_sampler {
108- // If using service-based sampling from the agent
109108 mechanism:: AGENT_RATE_BY_SERVICE
110109 } else {
111- // Should not happen, but just in case
110+ // Should not happen in practice: agent rates default to covering all services.
112111 mechanism:: DEFAULT
113112 }
114113 }
115114
116- /// Sample an incoming span based on the parent context and attributes
115+ /// Sample an incoming span based on the parent context and attributes.
116+ ///
117+ /// If a parent sampling decision is present it is inherited; otherwise the root-span
118+ /// sampling pipeline is run via [`Self::sample_root`].
117119 pub fn sample ( & self , data : & impl SamplingData ) -> DdSamplingResult {
118120 if let Some ( is_parent_sampled) = data. is_parent_sampled ( ) {
119121 let priority = match is_parent_sampled {
120122 false => priority:: AUTO_REJECT ,
121123 true => priority:: AUTO_KEEP ,
122124 } ;
123- // If a parent exists, inherit its sampling decision and trace state
124125 return DdSamplingResult {
125126 priority,
126127 trace_root_info : None ,
127128 } ;
128129 }
129130
130- // Apply rules-based sampling
131131 data. with_span_properties ( self , |sampler, span| sampler. sample_root ( data, span) )
132132 }
133133
134- /// Sample the root span of a trace
134+ /// Sample the root span of a trace.
135+ ///
136+ /// Order of precedence:
137+ /// 1. A matching local/remote sampling rule (with rate limiting on keep).
138+ /// 2. Agent-provided per-service sampling rate.
139+ /// 3. Default 100% keep.
135140 fn sample_root (
136141 & self ,
137142 data : & impl SamplingData ,
@@ -143,42 +148,32 @@ impl DatadogSampler {
143148 let mut rl_effective_rate: Option < f64 > = None ;
144149 let trace_id = data. trace_id ( ) ;
145150
146- // Find a matching rule
147151 let matching_rule = self . find_matching_rule ( span) ;
148152
149- // Apply sampling logic
150153 if let Some ( rule) = & matching_rule {
151- // Get the sample rate from the rule
152154 sample_rate = rule. sample_rate ;
153155
154- // First check if the span should be sampled according to the rule
155156 if !rule. sample ( trace_id) {
156157 is_keep = false ;
157- // If the span should be sampled, then apply rate limiting
158158 } else if !self . rate_limiter . is_allowed ( ) {
159+ // Rule kept the span, but the rate limiter dropped it.
159160 is_keep = false ;
160161 rl_effective_rate = Some ( self . rate_limiter . effective_rate ( ) ) ;
161162 }
162163 } else {
163- // Try service-based sampling from Agent
164164 let service_key = self . service_key ( span) ;
165165 if let Some ( sampler) = self . service_samplers . get ( & service_key) {
166- // Use the service-based sampler
167166 used_agent_sampler = true ;
168- sample_rate = sampler. sample_rate ( ) ; // Get rate for reporting
169-
170- // Check if the service sampler decides to drop
167+ sample_rate = sampler. sample_rate ( ) ;
171168 if !sampler. sample ( trace_id) {
172169 is_keep = false ;
173170 }
174171 } else {
175- // Default sample rate, should never happen in practice if agent provides rates
172+ // No agent rate for this service yet; keep with rate 1.0 until rates arrive.
176173 sample_rate = 1.0 ;
177- // Keep the default decision (RecordAndSample)
178174 }
179175 }
180176
181- // Determine the sampling mechanism
182177 let mechanism = self . get_sampling_mechanism ( matching_rule. as_ref ( ) , used_agent_sampler) ;
183178
184179 DdSamplingResult {
@@ -227,9 +222,7 @@ fn format_sampling_rate(rate: f64) -> Option<String> {
227222 let s = format ! ( "{:.prec$}" , rounded, prec = decimal_places) ;
228223 // Strip trailing zeros after decimal point
229224 Some ( if s. contains ( '.' ) {
230- let s = s. trim_end_matches ( '0' ) ;
231- let s = s. trim_end_matches ( '.' ) ;
232- s. to_string ( )
225+ s. trim_end_matches ( '0' ) . trim_end_matches ( '.' ) . to_string ( )
233226 } else {
234227 s
235228 } )
@@ -381,15 +374,15 @@ mod tests {
381374 }
382375
383376 impl ValueLike for TestValue {
384- fn extract_float ( & self ) -> Option < f64 > {
377+ fn as_float ( & self ) -> Option < f64 > {
385378 match self {
386379 TestValue :: I64 ( i) => Some ( * i as f64 ) ,
387380 TestValue :: F64 ( f) => Some ( * f) ,
388381 _ => None ,
389382 }
390383 }
391384
392- fn extract_string ( & self ) -> Option < Cow < ' _ , str > > {
385+ fn as_str ( & self ) -> Option < Cow < ' _ , str > > {
393386 match self {
394387 TestValue :: String ( s) => Some ( Cow :: Borrowed ( s. as_str ( ) ) ) ,
395388 TestValue :: I64 ( i) => Some ( Cow :: Owned ( i. to_string ( ) ) ) ,
@@ -476,23 +469,23 @@ mod tests {
476469 self . attributes
477470 . iter ( )
478471 . find ( |attr| attr. key ( ) == SERVICE_NAME )
479- . and_then ( |attr| attr. value ( ) . extract_string ( ) )
472+ . and_then ( |attr| attr. value ( ) . as_str ( ) )
480473 . unwrap_or ( Cow :: Borrowed ( "" ) )
481474 }
482475
483476 fn env ( & self ) -> Cow < ' _ , str > {
484477 self . attributes
485478 . iter ( )
486479 . find ( |attr| attr. key ( ) == "datadog.env" || attr. key ( ) == ENV_TAG )
487- . and_then ( |attr| attr. value ( ) . extract_string ( ) )
480+ . and_then ( |attr| attr. value ( ) . as_str ( ) )
488481 . unwrap_or ( Cow :: Borrowed ( "" ) )
489482 }
490483
491484 fn resource ( & self ) -> Cow < ' _ , str > {
492485 self . attributes
493486 . iter ( )
494487 . find ( |attr| attr. key ( ) == RESOURCE_TAG )
495- . and_then ( |attr| attr. value ( ) . extract_string ( ) )
488+ . and_then ( |attr| attr. value ( ) . as_str ( ) )
496489 . unwrap_or ( Cow :: Borrowed ( self . name ) )
497490 }
498491
@@ -617,6 +610,20 @@ mod tests {
617610 ]
618611 }
619612
613+ // Helper function to create attributes with service plus arbitrary extra string tags.
614+ fn create_attributes_with_extra (
615+ service : & ' static str ,
616+ resource : & ' static str ,
617+ env : & ' static str ,
618+ extra : & [ ( & ' static str , & ' static str ) ] ,
619+ ) -> Vec < TestAttribute > {
620+ let mut attrs = create_attributes_with_service ( service. to_string ( ) , resource, env) ;
621+ for ( k, v) in extra {
622+ attrs. push ( TestAttribute :: new ( * k, * v) ) ;
623+ }
624+ attrs
625+ }
626+
620627 // Helper function to create SamplingData for testing
621628 fn create_sampling_data < ' a > (
622629 is_parent_sampled : Option < bool > ,
@@ -710,18 +717,56 @@ mod tests {
710717
711718 #[ test]
712719 fn test_sampling_rule_matches ( ) {
713- // Create a rule with specific service and name patterns
714- let _rule = SamplingRule :: new (
720+ // Rule constrained on service, operation name, and a required tag value.
721+ // `TestSpan::operation_name()` returns "http.client.request" when the span
722+ // carries an `http.request.method` attribute (see `get_operation_name`).
723+ let rule = SamplingRule :: new (
715724 0.5 ,
716725 Some ( "web-*" . to_string ( ) ) ,
717- Some ( "http.*" . to_string ( ) ) ,
726+ Some ( "http.client. *" . to_string ( ) ) ,
718727 None ,
719728 Some ( HashMap :: from ( [ (
720729 "custom_key" . to_string ( ) ,
721730 "custom_value" . to_string ( ) ,
722731 ) ] ) ) ,
723732 None ,
724733 ) ;
734+
735+ // Matching span.
736+ let attrs = create_attributes_with_extra (
737+ "web-foo" ,
738+ "resource" ,
739+ "production" ,
740+ & [ ( HTTP_REQUEST_METHOD , "GET" ) , ( "custom_key" , "custom_value" ) ] ,
741+ ) ;
742+ let span = TestSpan :: new ( "span-name" , attrs. as_slice ( ) ) ;
743+ assert ! ( rule. matches( & span) , "rule should match qualifying span" ) ;
744+
745+ // Non-matching service.
746+ let attrs_bad_service = create_attributes_with_extra (
747+ "api-foo" ,
748+ "resource" ,
749+ "production" ,
750+ & [ ( HTTP_REQUEST_METHOD , "GET" ) , ( "custom_key" , "custom_value" ) ] ,
751+ ) ;
752+ let span_bad_service = TestSpan :: new ( "span-name" , attrs_bad_service. as_slice ( ) ) ;
753+ assert ! (
754+ !rule. matches( & span_bad_service) ,
755+ "rule should not match different service"
756+ ) ;
757+
758+ // Missing required tag.
759+ let attrs_no_tag = create_attributes_with_extra (
760+ "web-foo" ,
761+ "resource" ,
762+ "production" ,
763+ & [ ( HTTP_REQUEST_METHOD , "GET" ) ] ,
764+ ) ;
765+ let span_no_tag = TestSpan :: new ( "span-name" , attrs_no_tag. as_slice ( ) ) ;
766+ assert ! (
767+ !rule. matches( & span_no_tag) ,
768+ "rule should not match without required tag"
769+ ) ;
725770 }
726771
727772 #[ test]
0 commit comments