Skip to content

Commit 8a9a8ab

Browse files
committed
chore(sampling): pr review feedback
1 parent 4f6bd33 commit 8a9a8ab

14 files changed

Lines changed: 281 additions & 157 deletions

.codecov.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ component_management:
5555
name: libdd-profiling-ffi # this is a display name, and can be changed freely
5656
paths:
5757
- libdd-profiling-ffi
58+
- component_id: sampling # this is an identifier that should not be changed
59+
name: libdd-sampling # this is a display name, and can be changed freely
60+
paths:
61+
- libdd-sampling
5862
- component_id: sidecar # this is an identifier that should not be changed
5963
name: datadog-sidecar # this is a display name, and can be changed freely
6064
paths:

libdd-sampling/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ required-features = ["v04_span"]
2626
serde = { version = "1.0", features = ["derive"] }
2727
serde_json = "1.0"
2828
lru = "0.16.3"
29+
libdd-common = { path = "../libdd-common", version = "4.0.0" }
2930
libdd-trace-utils = { path = "../libdd-trace-utils", version = "3.0.1", optional = true }
3031

3132
[features]
@@ -34,4 +35,3 @@ v04_span = ["dep:libdd-trace-utils"]
3435
[dev-dependencies]
3536
criterion = "0.5"
3637
libdd-common = { path = "../libdd-common", version = "4.0.0", features = ["bench-utils"] }
37-
libdd-trace-utils = { path = "../libdd-trace-utils", version = "3.0.1" }

libdd-sampling/src/agent_service_sampler.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use std::{
55
collections::HashMap,
6-
sync::{Arc, RwLock},
6+
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
77
};
88

99
use crate::rate_sampler::RateSampler;
@@ -20,32 +20,48 @@ pub struct ServicesSampler {
2020
}
2121

2222
impl ServicesSampler {
23+
fn read_or_panic(&self) -> RwLockReadGuard<'_, HashMap<String, RateSampler>> {
24+
#[allow(clippy::panic)]
25+
match self.inner.read() {
26+
Ok(guard) => guard,
27+
Err(_) => panic!("ServicesSampler read lock poisoned"),
28+
}
29+
}
30+
31+
fn write_or_panic(&self) -> RwLockWriteGuard<'_, HashMap<String, RateSampler>> {
32+
#[allow(clippy::panic)]
33+
match self.inner.write() {
34+
Ok(guard) => guard,
35+
Err(_) => panic!("ServicesSampler write lock poisoned"),
36+
}
37+
}
38+
2339
pub fn get(&self, service: &str) -> Option<RateSampler> {
24-
self.inner.read().unwrap().get(service).cloned()
40+
self.read_or_panic().get(service).cloned()
2541
}
2642

2743
pub fn update_rates<I: IntoIterator<Item = (String, f64)>>(&self, rates: I) {
2844
let new_rates: HashMap<_, _> = rates
2945
.into_iter()
3046
.map(|(s, r)| (s, RateSampler::new(r)))
3147
.collect();
32-
*self.inner.write().unwrap() = new_rates;
48+
*self.write_or_panic() = new_rates;
3349
}
3450

35-
// used for testing purposes
51+
// Test-only inspection helpers.
3652

37-
#[allow(dead_code)]
53+
#[cfg(test)]
3854
pub(crate) fn is_empty(&self) -> bool {
39-
self.inner.read().unwrap().is_empty()
55+
self.read_or_panic().is_empty()
4056
}
4157

42-
#[allow(dead_code)]
58+
#[cfg(test)]
4359
pub(crate) fn len(&self) -> usize {
44-
self.inner.read().unwrap().len()
60+
self.read_or_panic().len()
4561
}
4662

47-
#[allow(dead_code)]
63+
#[cfg(test)]
4864
pub(crate) fn contains_key(&self, service: &str) -> bool {
49-
self.inner.read().unwrap().contains_key(service)
65+
self.read_or_panic().contains_key(service)
5066
}
5167
}

libdd-sampling/src/constants.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
22
// SPDX-License-Identifier: Apache-2.0
33

4-
//! Shared constants for the datadog_opentelemetry::sampling crate
4+
//! Shared constants for the libdd-sampling crate
55
66
/// Sampling rate limits
77
pub mod rate {

libdd-sampling/src/datadog_sampler.rs

Lines changed: 86 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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]

libdd-sampling/src/dd_sampling.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ impl SamplingMechanism {
142142
}
143143

144144
/// Returns the string representation of the sampling mechanism.
145+
///
146+
/// The format is `"-N"` (e.g. `"-4"` for manual sampling). The leading `-` comes from the
147+
/// propagation tags RFC, which initially had a prefix component before the `-`; that prefix
148+
/// was dropped, but the `-` was retained as-is for backwards compatibility with existing
149+
/// existing tracers.
145150
pub fn to_cow(self) -> Cow<'static, str> {
146151
match self {
147152
mechanism::DEFAULT => Cow::Borrowed("-0"),

0 commit comments

Comments
 (0)