diff --git a/datadog-opentelemetry/src/core/constants.rs b/datadog-opentelemetry/src/core/constants.rs index f8c14284..909f94ef 100644 --- a/datadog-opentelemetry/src/core/constants.rs +++ b/datadog-opentelemetry/src/core/constants.rs @@ -24,3 +24,6 @@ pub const SAMPLING_AGENT_RATE_TAG_KEY: &str = "_dd.agent_psr"; /// Rate limiter effective rate metric key. pub const RL_EFFECTIVE_RATE: &str = "_dd.limit_psr"; + +/// Knuth Sampling Rate propagated tag key. +pub const SAMPLING_KNUTH_RATE_TAG_KEY: &str = "_dd.p.ksr"; diff --git a/datadog-opentelemetry/src/sampling/datadog_sampler.rs b/datadog-opentelemetry/src/sampling/datadog_sampler.rs index 07e1bc7b..b51a5536 100644 --- a/datadog-opentelemetry/src/sampling/datadog_sampler.rs +++ b/datadog-opentelemetry/src/sampling/datadog_sampler.rs @@ -4,7 +4,7 @@ use crate::core::configuration::SamplingRuleConfig; use crate::core::constants::{ RL_EFFECTIVE_RATE, SAMPLING_AGENT_RATE_TAG_KEY, SAMPLING_DECISION_MAKER_TAG_KEY, - SAMPLING_PRIORITY_TAG_KEY, SAMPLING_RULE_RATE_TAG_KEY, + SAMPLING_KNUTH_RATE_TAG_KEY, SAMPLING_PRIORITY_TAG_KEY, SAMPLING_RULE_RATE_TAG_KEY, }; use crate::core::sampling::{mechanism, SamplingMechanism, SamplingPriority}; @@ -458,6 +458,49 @@ impl DatadogSampler { } } +/// Formats a sampling rate with up to 6 significant digits, stripping trailing zeros. +/// +/// This matches the Go behavior of `strconv.FormatFloat(rate, 'g', 6, 64)`. +/// +/// # Examples +/// - `1.0` → `Some("1")` +/// - `0.5` → `Some("0.5")` +/// - `0.7654321` → `Some("0.765432")` +/// - `0.100000` → `Some("0.1")` +/// - `-0.1` → `None` +/// - `1.1` → `None` +fn format_sampling_rate(rate: f64) -> Option { + if rate.is_nan() || !(0.0..=1.0).contains(&rate) { + return None; + } + + if rate == 0.0 { + return Some("0".to_string()); + } + + let digits = 6_i32; + let magnitude = rate.abs().log10().floor() as i32; + let scale = 10f64.powi(digits - 1 - magnitude); + let rounded = (rate * scale).round() / scale; + + // Determine decimal places needed for 6 significant digits + let decimal_places = if magnitude >= digits - 1 { + 0 + } else { + (digits - 1 - magnitude) as usize + }; + + let s = format!("{:.prec$}", rounded, prec = decimal_places); + // Strip trailing zeros after decimal point + Some(if s.contains('.') { + let s = s.trim_end_matches('0'); + let s = s.trim_end_matches('.'); + s.to_string() + } else { + s + }) +} + pub(crate) struct DdSamplingResult { pub is_keep: bool, pub trace_root_info: Option, @@ -503,11 +546,17 @@ impl DdSamplingResult { match mechanism { mechanism::AGENT_RATE_BY_SERVICE => { result.push(KeyValue::new(SAMPLING_AGENT_RATE_TAG_KEY, root_info.rate)); + if let Some(rate_str) = format_sampling_rate(root_info.rate) { + result.push(KeyValue::new(SAMPLING_KNUTH_RATE_TAG_KEY, rate_str)); + } } mechanism::REMOTE_USER_TRACE_SAMPLING_RULE | mechanism::REMOTE_DYNAMIC_TRACE_SAMPLING_RULE | mechanism::LOCAL_USER_TRACE_SAMPLING_RULE => { result.push(KeyValue::new(SAMPLING_RULE_RATE_TAG_KEY, root_info.rate)); + if let Some(rate_str) = format_sampling_rate(root_info.rate) { + result.push(KeyValue::new(SAMPLING_KNUTH_RATE_TAG_KEY, rate_str)); + } } _ => {} } @@ -932,13 +981,14 @@ mod tests { let attrs = sampling_result.to_dd_sampling_tags(); - // Verify the number of attributes - assert_eq!(attrs.len(), 3); + // Verify the number of attributes (decision_maker + priority + rule_rate + ksr) + assert_eq!(attrs.len(), 4); // Check individual attributes let mut found_decision_maker = false; let mut found_priority = false; let mut found_rule_rate = false; + let mut found_ksr = false; for attr in &attrs { match attr.key.as_str() { @@ -969,6 +1019,14 @@ mod tests { assert_eq!(value_float, sample_rate); found_rule_rate = true; } + SAMPLING_KNUTH_RATE_TAG_KEY => { + let value_str = match &attr.value { + opentelemetry::Value::String(s) => s.to_string(), + _ => panic!("Expected string value for ksr tag"), + }; + assert_eq!(value_str, "0.5"); + found_ksr = true; + } _ => {} } } @@ -976,6 +1034,7 @@ mod tests { assert!(found_decision_maker, "Missing decision maker tag"); assert!(found_priority, "Missing priority tag"); assert!(found_rule_rate, "Missing rule rate tag"); + assert!(found_ksr, "Missing knuth sampling rate tag"); // Test with rate limiting let rate_limit = 100; @@ -993,7 +1052,7 @@ mod tests { let attrs_with_limit = sampling_result.to_dd_sampling_tags(); // With rate limiting, there should be one more attribute - assert_eq!(attrs_with_limit.len(), 4); + assert_eq!(attrs_with_limit.len(), 5); // Check for rate limit attribute let mut found_limit = false; @@ -1028,24 +1087,40 @@ mod tests { let agent_attrs = sampling_result.to_dd_sampling_tags(); - // Verify the number of attributes (should be 3) - assert_eq!(agent_attrs.len(), 3); + // Verify the number of attributes (should be 4: decision_maker + priority + + // agent_rate + ksr) + assert_eq!(agent_attrs.len(), 4); - // Check for agent rate tag specifically + // Check for agent rate tag and ksr tag let mut found_agent_rate = false; + let mut found_ksr = false; for attr in &agent_attrs { - if attr.key.as_str() == SAMPLING_AGENT_RATE_TAG_KEY { - let value_float = match attr.value { - opentelemetry::Value::F64(f) => f, - _ => panic!("Expected float value for agent rate tag"), - }; - assert_eq!(value_float, agent_rate); - found_agent_rate = true; - break; + match attr.key.as_str() { + SAMPLING_AGENT_RATE_TAG_KEY => { + let value_float = match attr.value { + opentelemetry::Value::F64(f) => f, + _ => panic!("Expected float value for agent rate tag"), + }; + assert_eq!(value_float, agent_rate); + found_agent_rate = true; + } + SAMPLING_KNUTH_RATE_TAG_KEY => { + let value_str = match &attr.value { + opentelemetry::Value::String(s) => s.to_string(), + _ => panic!("Expected string value for ksr tag"), + }; + assert_eq!(value_str, "0.75"); + found_ksr = true; + } + _ => {} } } assert!(found_agent_rate, "Missing agent rate tag"); + assert!( + found_ksr, + "Missing knuth sampling rate tag for agent mechanism" + ); // Also check that the SAMPLING_RULE_RATE_TAG_KEY is NOT present for agent mechanism for attr in &agent_attrs { @@ -1057,6 +1132,43 @@ mod tests { } } + #[test] + fn test_format_sampling_rate() { + // Exact values + assert_eq!(format_sampling_rate(1.0), Some("1".to_string())); + assert_eq!(format_sampling_rate(0.5), Some("0.5".to_string())); + assert_eq!(format_sampling_rate(0.1), Some("0.1".to_string())); + assert_eq!(format_sampling_rate(0.0), Some("0".to_string())); + + // Trailing zeros should be stripped + assert_eq!(format_sampling_rate(0.100000), Some("0.1".to_string())); + assert_eq!(format_sampling_rate(0.500000), Some("0.5".to_string())); + + // Truncation to 6 significant digits + assert_eq!( + format_sampling_rate(0.7654321), + Some("0.765432".to_string()) + ); + assert_eq!( + format_sampling_rate(0.123456789), + Some("0.123457".to_string()) + ); + + // Small values + assert_eq!(format_sampling_rate(0.001), Some("0.001".to_string())); + + // Boundary values + assert_eq!(format_sampling_rate(0.75), Some("0.75".to_string())); + assert_eq!(format_sampling_rate(0.999999), Some("0.999999".to_string())); + + // Invalid rates + assert_eq!(format_sampling_rate(-0.1), None); + assert_eq!(format_sampling_rate(1.1), None); + assert_eq!(format_sampling_rate(f64::NAN), None); + assert_eq!(format_sampling_rate(f64::INFINITY), None); + assert_eq!(format_sampling_rate(f64::NEG_INFINITY), None); + } + #[test] fn test_should_sample_parent_context() { let sampler = DatadogSampler::new(vec![], 100, create_empty_resource_arc()); diff --git a/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_remote_config_sampling_rates.json b/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_remote_config_sampling_rates.json index ec27ff93..50cef39c 100644 --- a/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_remote_config_sampling_rates.json +++ b/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_remote_config_sampling_rates.json @@ -9,6 +9,7 @@ "type": "custom", "meta": { "_dd.p.dm": "-11", + "_dd.p.ksr": "1", "otel.scope.name": "test", "otel.status_code": "Unset", "otel.trace_id": "68b1de6200000000578668d18e7e6e97", diff --git a/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_sampling_extraction.json b/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_sampling_extraction.json index c72c3407..4e958163 100644 --- a/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_sampling_extraction.json +++ b/datadog-opentelemetry/tests/snapshots/opentelemetry_api/test_sampling_extraction.json @@ -9,6 +9,7 @@ "type": "web", "meta": { "_dd.p.dm": "-3", + "_dd.p.ksr": "1", "otel.scope.name": "test", "otel.status_code": "Unset", "otel.trace_id": "684068c7000000007eaf83ddeb833e92",