From 02fb6570ff7969c36b23d4ad702b1cd4d73b5daa Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Mon, 4 Aug 2025 14:48:05 -0400 Subject: [PATCH 01/34] create RC ConfigItem and wrap SamplingRules config --- dd-trace/src/configuration/configuration.rs | 211 +++++++++++++++++++- 1 file changed, 202 insertions(+), 9 deletions(-) diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index b77297aa..7c1e773a 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -44,7 +44,7 @@ fn default_provenance() -> String { pub const TRACER_VERSION: &str = "0.0.1"; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] struct ParsedSamplingRules { rules: Vec, } @@ -62,6 +62,101 @@ impl FromStr for ParsedSamplingRules { } } +/// Source of a configuration value +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigSource { + #[allow(dead_code)] // Used in tests, returned by source() + Default, + EnvVar, + #[allow(dead_code)] // Will be used when set_code is called from user code + Code, + #[allow(dead_code)] // Will be used for remote configuration + RemoteConfig, +} + +/// Configuration item that tracks the value of a setting and where it came from +// This allows us to manage configuration precedence +#[derive(Debug, Clone)] +pub struct ConfigItem { + name: String, + default_value: T, + env_value: Option, + code_value: Option, + rc_value: Option, +} + +impl ConfigItem { + /// Creates a new ConfigItem with a default value + pub fn new(name: impl Into, default: T) -> Self { + Self { + name: name.into(), + default_value: default, + env_value: None, + code_value: None, + rc_value: None, + } + } + + /// Sets a value from a specific source + pub fn set_value_source(&mut self, value: T, source: ConfigSource) { + match source { + ConfigSource::Code => self.code_value = Some(value), + ConfigSource::RemoteConfig => self.rc_value = Some(value), + ConfigSource::EnvVar => self.env_value = Some(value), + ConfigSource::Default => { + dd_warn!("Cannot set default value after initialization"); + } + } + } + + /// Sets the code value (convenience method) + pub fn set_code(&mut self, value: T) { + self.code_value = Some(value); + } + + /// Unsets the remote config value + #[allow(dead_code)] // Will be used when implementing remote configuration + pub fn unset_rc(&mut self) { + self.rc_value = None; + } + + /// Gets the current value based on priority: + /// remote_config > code > env_var > default + pub fn value(&self) -> &T { + self.rc_value + .as_ref() + .or(self.code_value.as_ref()) + .or(self.env_value.as_ref()) + .unwrap_or(&self.default_value) + } + + /// Gets the source of the current value + #[allow(dead_code)] // Used in tests and will be used for remote configuration + pub fn source(&self) -> ConfigSource { + if self.rc_value.is_some() { + ConfigSource::RemoteConfig + } else if self.code_value.is_some() { + ConfigSource::Code + } else if self.env_value.is_some() { + ConfigSource::EnvVar + } else { + ConfigSource::Default + } + } +} + +impl std::fmt::Display for ConfigItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "", + self.name, self.default_value, self.env_value, self.code_value, self.rc_value + ) + } +} + +type SamplingRulesConfigItem = ConfigItem; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TracePropagationStyle { Datadog, @@ -171,7 +266,7 @@ pub struct Config { // # Sampling /// A list of sampling rules. Each rule is matched against the root span of a trace /// If a rule matches, the trace is sampled with the associated sample rate. - trace_sampling_rules: Vec, + trace_sampling_rules: SamplingRulesConfigItem, /// Maximum number of spans to sample per second /// Only applied if trace_sampling_rules are matched @@ -227,6 +322,17 @@ impl Config { let parsed_sampling_rules_config = to_val(sources.get_parse::("DD_TRACE_SAMPLING_RULES")); + // Initialize the sampling rules ConfigItem + let mut sampling_rules_item = ConfigItem::new( + "DD_TRACE_SAMPLING_RULES", + ParsedSamplingRules::default(), // default is empty rules + ); + + // Set env value if it was parsed from environment + if let Some(rules) = parsed_sampling_rules_config { + sampling_rules_item.set_value_source(rules, ConfigSource::EnvVar); + } + Self { runtime_id: default.runtime_id, tracer_version: default.tracer_version, @@ -245,10 +351,8 @@ impl Config { .unwrap_or(default.trace_agent_url), dogstatsd_agent_url: default.dogstatsd_agent_url, - // Populate from parsed_sampling_rules_config or defaults - trace_sampling_rules: parsed_sampling_rules_config - .map(|psc| psc.rules) - .unwrap_or(default.trace_sampling_rules), + // Use the initialized ConfigItem + trace_sampling_rules: sampling_rules_item, trace_rate_limit: to_val(sources.get_parse("DD_TRACE_RATE_LIMIT")) .unwrap_or(default.trace_rate_limit), @@ -335,7 +439,7 @@ impl Config { } pub fn trace_sampling_rules(&self) -> &[SamplingRuleConfig] { - self.trace_sampling_rules.as_ref() + self.trace_sampling_rules.value().rules.as_ref() } pub fn trace_rate_limit(&self) -> i32 { @@ -394,7 +498,10 @@ fn default_config() -> Config { trace_agent_url: Cow::Borrowed("http://localhost:8126"), dogstatsd_agent_url: Cow::Borrowed("http://localhost:8125"), - trace_sampling_rules: Vec::new(), + trace_sampling_rules: ConfigItem::new( + "DD_TRACE_SAMPLING_RULES", + ParsedSamplingRules::default(), // Empty rules by default + ), trace_rate_limit: 100, enabled: true, log_level_filter: LevelFilter::default(), @@ -453,7 +560,9 @@ impl ConfigBuilder { } pub fn set_trace_sampling_rules(&mut self, rules: Vec) -> &mut Self { - self.config.trace_sampling_rules = rules; + // Create a new ParsedSamplingRules and set it as code value + let parsed_rules = ParsedSamplingRules { rules }; + self.config.trace_sampling_rules.set_code(parsed_rules); self } @@ -812,4 +921,88 @@ mod tests { assert!(!config.trace_stats_computation_enabled()); } + + #[test] + fn test_config_item_priority() { + // Test that ConfigItem respects priority: remote_config > code > env_var > default + let mut config_item = + ConfigItem::new("DD_TRACE_SAMPLING_RULES", ParsedSamplingRules::default()); + + // Default value + assert_eq!(config_item.source(), ConfigSource::Default); + assert_eq!(config_item.value().rules.len(), 0); + + // Env overrides default + config_item.set_value_source( + ParsedSamplingRules { + rules: vec![SamplingRuleConfig { + sample_rate: 0.3, + ..SamplingRuleConfig::default() + }], + }, + ConfigSource::EnvVar, + ); + assert_eq!(config_item.source(), ConfigSource::EnvVar); + assert_eq!(config_item.value().rules[0].sample_rate, 0.3); + + // Code overrides env + config_item.set_code(ParsedSamplingRules { + rules: vec![SamplingRuleConfig { + sample_rate: 0.5, + ..SamplingRuleConfig::default() + }], + }); + assert_eq!(config_item.source(), ConfigSource::Code); + assert_eq!(config_item.value().rules[0].sample_rate, 0.5); + + // Remote config overrides all + config_item.set_value_source( + ParsedSamplingRules { + rules: vec![SamplingRuleConfig { + sample_rate: 0.8, + ..SamplingRuleConfig::default() + }], + }, + ConfigSource::RemoteConfig, + ); + assert_eq!(config_item.source(), ConfigSource::RemoteConfig); + assert_eq!(config_item.value().rules[0].sample_rate, 0.8); + + // Unset RC falls back to code + config_item.unset_rc(); + assert_eq!(config_item.source(), ConfigSource::Code); + assert_eq!(config_item.value().rules[0].sample_rate, 0.5); + } + + #[test] + fn test_sampling_rules_with_config_item() { + // Test integration: env var is parsed, then overridden by code + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [( + "DD_TRACE_SAMPLING_RULES", + r#"[{"sample_rate":0.25,"service":"env-service"}]"#, + )], + ConfigSourceOrigin::EnvVar, + )); + + // First, env var should be used + let config = Config::builder_with_sources(&sources).build(); + assert_eq!(config.trace_sampling_rules().len(), 1); + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.25); + + // Builder override should take precedence + let config = Config::builder_with_sources(&sources) + .set_trace_sampling_rules(vec![SamplingRuleConfig { + sample_rate: 0.75, + service: Some("code-service".to_string()), + ..SamplingRuleConfig::default() + }]) + .build(); + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.75); + assert_eq!( + config.trace_sampling_rules()[0].service.as_ref().unwrap(), + "code-service" + ); + } } From c8d359fb4ded5b1f0bf5a83544fc9d7c9cb4c8c7 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Mon, 4 Aug 2025 18:02:25 -0400 Subject: [PATCH 02/34] sampling rules can be updated, made callback to do so --- dd-trace-sampling/src/datadog_sampler.rs | 172 ++++++++++++++++++----- dd-trace-sampling/src/lib.rs | 1 + dd-trace-sampling/src/rules_sampler.rs | 99 +++++++++++++ 3 files changed, 235 insertions(+), 37 deletions(-) create mode 100644 dd-trace-sampling/src/rules_sampler.rs diff --git a/dd-trace-sampling/src/datadog_sampler.rs b/dd-trace-sampling/src/datadog_sampler.rs index b6017caf..03afc3ce 100644 --- a/dd-trace-sampling/src/datadog_sampler.rs +++ b/dd-trace-sampling/src/datadog_sampler.rs @@ -24,6 +24,7 @@ use crate::glob_matcher::GlobMatcher; use crate::otel_mappings::PreSampledSpan; use crate::rate_limiter::RateLimiter; use crate::rate_sampler::RateSampler; +use crate::rules_sampler::{RulesSampler, SamplingRulesConfig}; use crate::utils; fn matcher_from_rule(rule: &str) -> Option { @@ -248,7 +249,7 @@ impl From<&str> for RuleProvenance { #[derive(Clone, Debug)] pub struct DatadogSampler { /// Sampling rules to apply, in order of precedence - rules: Vec, + rules: RulesSampler, /// Service-based samplers provided by the Agent service_samplers: ServicesSampler, @@ -271,7 +272,7 @@ impl DatadogSampler { let limiter = RateLimiter::new(rate_limit, None); DatadogSampler { - rules, + rules: RulesSampler::new(rules), service_samplers: ServicesSampler::default(), rate_limiter: limiter, resource, @@ -297,6 +298,42 @@ impl DatadogSampler { }) } + /// Creates a callback for updating sampling rules from remote configuration + /// + /// # Returns + /// A boxed function that takes a JSON string and updates the sampling rules + /// + /// # Example + /// The callback expects JSON in the following format: + /// ```json + /// { + /// "rules": [ + /// { + /// "sample_rate": 0.5, + /// "service": "web-*", + /// "name": "http.*", + /// "resource": "/api/*", + /// "tags": {"env": "prod"}, + /// "provenance": "customer" + /// } + /// ] + /// } + /// ``` + pub fn on_rules_update(&self) -> Box Fn(&'a str) + Send + Sync> { + let rules_sampler = self.rules.clone(); + Box::new(move |s: &str| { + let Ok(config) = serde_json::de::from_str::(s) else { + return; + }; + + // Convert the rule configs to SamplingRule instances + let new_rules = config.into_rules(); + + // Update the rules + rules_sampler.update_rules(new_rules); + }) + } + /// Computes a key for service-based sampling fn service_key(&self, span: &impl OtelSpan) -> String { // Get service directly from resource @@ -308,8 +345,8 @@ impl DatadogSampler { } /// Finds the highest precedence rule that matches the span - fn find_matching_rule(&self, span: &PreSampledSpan) -> Option<&SamplingRule> { - self.rules.iter().find(|rule| rule.matches(span)) + fn find_matching_rule(&self, span: &PreSampledSpan) -> Option { + self.rules.find_matching_rule(|rule| rule.matches(span)) } /// Returns the sampling mechanism used for the decision @@ -375,7 +412,7 @@ impl DatadogSampler { let matching_rule = self.find_matching_rule(&span); // Apply sampling logic - if let Some(rule) = matching_rule { + if let Some(rule) = &matching_rule { // Get the sample rate from the rule sample_rate = rule.sample_rate; @@ -407,7 +444,7 @@ impl DatadogSampler { } // Determine the sampling mechanism - let mechanism = self.get_sampling_mechanism(matching_rule, used_agent_sampler); + let mechanism = self.get_sampling_mechanism(matching_rule.as_ref(), used_agent_sampler); DdSamplingResult { is_keep, @@ -672,7 +709,6 @@ mod tests { let rule = SamplingRule::new(0.5, None, None, None, None, None); let sampler_with_rules = DatadogSampler::new(vec![rule], 200, create_empty_resource_arc()); assert_eq!(sampler_with_rules.rules.len(), 1); - assert_eq!(sampler_with_rules.rules[0].sample_rate, 0.5); } #[test] @@ -794,16 +830,9 @@ mod tests { matching_rule_for_attrs1.is_some(), "Expected rule1 to match for service1" ); - assert_eq!( - matching_rule_for_attrs1.unwrap().sample_rate, - 0.1, - "Expected rule1 sample rate" - ); - assert_eq!( - matching_rule_for_attrs1.unwrap().provenance, - "customer", - "Expected rule1 provenance" - ); + let rule = matching_rule_for_attrs1.unwrap(); + assert_eq!(rule.sample_rate, 0.1, "Expected rule1 sample rate"); + assert_eq!(rule.provenance, "customer", "Expected rule1 provenance"); } // Test with a specific service that should match the second rule (rule2) @@ -817,16 +846,9 @@ mod tests { matching_rule_for_attrs2.is_some(), "Expected rule2 to match for service2" ); - assert_eq!( - matching_rule_for_attrs2.unwrap().sample_rate, - 0.2, - "Expected rule2 sample rate" - ); - assert_eq!( - matching_rule_for_attrs2.unwrap().provenance, - "dynamic", - "Expected rule2 provenance" - ); + let rule = matching_rule_for_attrs2.unwrap(); + assert_eq!(rule.sample_rate, 0.2, "Expected rule2 sample rate"); + assert_eq!(rule.provenance, "dynamic", "Expected rule2 provenance"); } // Test with a service that matches the wildcard rule (rule3) @@ -840,16 +862,9 @@ mod tests { matching_rule_for_attrs3.is_some(), "Expected rule3 to match for service3" ); - assert_eq!( - matching_rule_for_attrs3.unwrap().sample_rate, - 0.3, - "Expected rule3 sample rate" - ); - assert_eq!( - matching_rule_for_attrs3.unwrap().provenance, - "default", - "Expected rule3 provenance" - ); + let rule = matching_rule_for_attrs3.unwrap(); + assert_eq!(rule.sample_rate, 0.3, "Expected rule3 sample rate"); + assert_eq!(rule.provenance, "default", "Expected rule3 provenance"); } // Test with a service that doesn't match any rule's service pattern @@ -1702,4 +1717,87 @@ mod tests { // But should still be sampled (default behavior) assert_eq!(result.to_otel_decision(), SamplingDecision::RecordAndSample); } + + #[test] + fn test_on_rules_update_callback() { + // Create a sampler with initial rules + let initial_rule = SamplingRule::new( + 0.1, + Some("initial-service".to_string()), + None, + None, + None, + Some("default".to_string()), + ); + + // Create a resource with a service name that will match our test rule + let test_resource = Arc::new(RwLock::new( + opentelemetry_sdk::Resource::builder_empty() + .with_attributes(vec![KeyValue::new( + semconv::resource::SERVICE_NAME, + "web-frontend", + )]) + .build(), + )); + + let sampler = DatadogSampler::new(vec![initial_rule], 100, test_resource); + + // Verify initial state + assert_eq!(sampler.rules.len(), 1); + + // Get the callback + let callback = sampler.on_rules_update(); + + // Create JSON for new rules + let json_config = r#"{ + "rules": [ + { + "sample_rate": 0.5, + "service": "web-*", + "name": "http.*", + "provenance": "customer" + }, + { + "sample_rate": 0.2, + "service": "api-*", + "resource": "/api/*", + "tags": {"env": "prod"}, + "provenance": "dynamic" + } + ] + }"#; + + // Apply the update + callback(json_config); + + // Verify the rules were updated + assert_eq!(sampler.rules.len(), 2); + + // Test that the new rules work by finding a matching rule + // Create attributes that will generate an operation name matching "http.*" + let attrs = vec![ + KeyValue::new(HTTP_REQUEST_METHOD, "GET"), // This will make operation name "http.client.request" + ]; + let resource_guard = sampler.resource.read().unwrap(); + let span = PreSampledSpan::new( + "test-span", + SpanKind::Client, + attrs.as_slice(), + &resource_guard, + ); + + let matching_rule = sampler.find_matching_rule(&span); + assert!(matching_rule.is_some(), "Expected to find a matching rule for service 'web-frontend' and name 'http.client.request'"); + let rule = matching_rule.unwrap(); + assert_eq!(rule.sample_rate, 0.5); + assert_eq!(rule.provenance, "customer"); + + // Test with invalid JSON - should not crash + callback("invalid json"); + assert_eq!(sampler.rules.len(), 2); // Should still have the same rules + + // Test with empty rules array + callback(r#"{"rules": []}"#); + assert_eq!(sampler.rules.len(), 0); // Should now have no rules + } } diff --git a/dd-trace-sampling/src/lib.rs b/dd-trace-sampling/src/lib.rs index 175a6075..e2fd1e3d 100644 --- a/dd-trace-sampling/src/lib.rs +++ b/dd-trace-sampling/src/lib.rs @@ -8,6 +8,7 @@ pub(crate) mod glob_matcher; pub(crate) mod otel_mappings; pub(crate) mod rate_limiter; pub(crate) mod rate_sampler; +pub(crate) mod rules_sampler; pub(crate) mod utils; // Re-export key public types diff --git a/dd-trace-sampling/src/rules_sampler.rs b/dd-trace-sampling/src/rules_sampler.rs new file mode 100644 index 00000000..d68d9bf0 --- /dev/null +++ b/dd-trace-sampling/src/rules_sampler.rs @@ -0,0 +1,99 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use crate::datadog_sampler::SamplingRule; + +#[derive(Debug, serde::Deserialize)] +pub(crate) struct SamplingRuleConfig { + pub sample_rate: f64, + #[serde(default)] + pub service: Option, + #[serde(default)] + pub name: Option, + #[serde(default)] + pub resource: Option, + #[serde(default)] + pub tags: Option>, + #[serde(default)] + pub provenance: Option, +} + +/// Configuration for sampling rules as received from remote configuration +#[derive(Debug, serde::Deserialize)] +pub(crate) struct SamplingRulesConfig { + pub rules: Vec, +} + +impl SamplingRulesConfig { + /// Converts the configuration into a vector of SamplingRule instances + pub fn into_rules(self) -> Vec { + self.rules + .into_iter() + .map(|config| { + SamplingRule::new( + config.sample_rate, + config.service, + config.name, + config.resource, + config.tags, + config.provenance, + ) + }) + .collect() + } +} + +/// Thread-safe container for sampling rules +#[derive(Debug, Default, Clone)] +pub(crate) struct RulesSampler { + inner: Arc>>, +} + +impl RulesSampler { + /// Creates a new RulesSampler with the given initial rules + pub fn new(rules: Vec) -> Self { + Self { + inner: Arc::new(RwLock::new(rules)), + } + } + + /// Gets a clone of the current rules + #[allow(dead_code)] + pub fn get_rules(&self) -> Vec { + self.inner.read().unwrap().clone() + } + + /// Updates the rules with a new set + pub fn update_rules(&self, new_rules: Vec) { + *self.inner.write().unwrap() = new_rules; + } + + /// Finds the first matching rule for a span + pub fn find_matching_rule(&self, matcher: F) -> Option + where + F: Fn(&SamplingRule) -> bool, + { + self.inner + .read() + .unwrap() + .iter() + .find(|rule| matcher(rule)) + .cloned() + } + + // used for testing purposes + #[allow(dead_code)] + pub(crate) fn is_empty(&self) -> bool { + self.inner.read().unwrap().is_empty() + } + + #[allow(dead_code)] + pub(crate) fn len(&self) -> usize { + self.inner.read().unwrap().len() + } +} From 142b52176b67d5755c6044b581138801c2e5b5bb Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 7 Aug 2025 14:19:25 -0400 Subject: [PATCH 03/34] handle extra services and rc enablement in configuration.rs --- dd-trace/src/configuration/configuration.rs | 241 +++++++++++++++++++- 1 file changed, 239 insertions(+), 2 deletions(-) diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index 7c1e773a..361f862f 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::sync::{Arc, Mutex}; use std::{borrow::Cow, fmt::Display, str::FromStr, sync::OnceLock}; use crate::dd_warn; @@ -68,7 +69,7 @@ pub enum ConfigSource { #[allow(dead_code)] // Used in tests, returned by source() Default, EnvVar, - #[allow(dead_code)] // Will be used when set_code is called from user code + #[allow(dead_code)] // Will be used when set_code is called from user code Code, #[allow(dead_code)] // Will be used for remote configuration RemoteConfig, @@ -157,6 +158,96 @@ impl std::fmt::Display for ConfigItem { type SamplingRulesConfigItem = ConfigItem; +/// Manages extra services discovered at runtime +/// This is used to track services beyond the main service for remote configuration +#[derive(Debug, Clone)] +struct ExtraServicesTracker { + /// Whether remote configuration is enabled + remote_config_enabled: bool, + /// Services that have been discovered + extra_services: Arc>>, + /// Services that have already been sent to the agent + extra_services_sent: Arc>>, + /// Queue of new services to process + extra_services_queue: Arc>>>, +} + +impl ExtraServicesTracker { + fn new(remote_config_enabled: bool) -> Self { + Self { + extra_services: Arc::new(Mutex::new(HashSet::new())), + extra_services_sent: Arc::new(Mutex::new(HashSet::new())), + extra_services_queue: Arc::new(Mutex::new(Some(VecDeque::new()))), + remote_config_enabled, + } + } + + fn add_extra_service(&self, service_name: &str, main_service: &str) { + if !self.remote_config_enabled { + return; + } + + if service_name == main_service { + return; + } + + let mut sent = match self.extra_services_sent.lock() { + Ok(s) => s, + Err(_) => return, + }; + + if sent.contains(service_name) { + return; + } + + let mut queue = match self.extra_services_queue.lock() { + Ok(q) => q, + Err(_) => return, + }; + + // Add to queue and mark as sent + if let Some(ref mut q) = *queue { + q.push_back(service_name.to_string()); + } + sent.insert(service_name.to_string()); + } + + /// Get all extra services, updating from the queue + fn get_extra_services(&self) -> Vec { + if !self.remote_config_enabled { + return Vec::new(); + } + + let mut queue = match self.extra_services_queue.lock() { + Ok(q) => q, + Err(_) => return Vec::new(), + }; + + let mut services = match self.extra_services.lock() { + Ok(s) => s, + Err(_) => return Vec::new(), + }; + + // Drain the queue into extra_services + if let Some(ref mut q) = *queue { + while let Some(service) = q.pop_front() { + services.insert(service); + + // Limit to 64 services + if services.len() > 64 { + // Remove one arbitrary service (HashSet doesn't guarantee order) + if let Some(to_remove) = services.iter().next().cloned() { + dd_warn!("ExtraServicesTracker:RemoteConfig: Exceeded 64 service limit, removing service: {}", to_remove); + services.remove(&to_remove); + } + } + } + } + + services.iter().cloned().collect() + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TracePropagationStyle { Datadog, @@ -290,6 +381,13 @@ pub struct Config { trace_propagation_style_extract: Option>, trace_propagation_style_inject: Option>, trace_propagation_extract_first: bool, + + /// Whether remote configuration is enabled + remote_config_enabled: bool, + + /// Tracks extra services discovered at runtime + /// Used for remote configuration to report all services + extra_services_tracker: ExtraServicesTracker, } impl Config { @@ -333,6 +431,10 @@ impl Config { sampling_rules_item.set_value_source(rules, ConfigSource::EnvVar); } + // Parse remote configuration enabled flag + let remote_config_enabled = + to_val(sources.get_parse::("DD_REMOTE_CONFIGURATION_ENABLED")).unwrap_or(true); // Default to enabled + Self { runtime_id: default.runtime_id, tracer_version: default.tracer_version, @@ -384,6 +486,8 @@ impl Config { .unwrap_or(default.trace_propagation_extract_first), #[cfg(feature = "test-utils")] wait_agent_info_ready: default.wait_agent_info_ready, + extra_services_tracker: ExtraServicesTracker::new(remote_config_enabled), + remote_config_enabled, } } @@ -485,6 +589,36 @@ impl Config { pub fn trace_propagation_extract_first(&self) -> bool { self.trace_propagation_extract_first } + + /// Updates sampling rules from remote configuration + /// This method is used by the remote config client to apply new rules + pub fn update_sampling_rules_from_remote(&mut self, rules: Vec) { + let parsed_rules = ParsedSamplingRules { rules }; + self.trace_sampling_rules + .set_value_source(parsed_rules, ConfigSource::RemoteConfig); + } + + /// Clears remote configuration sampling rules, falling back to code/env/default + pub fn clear_remote_sampling_rules(&mut self) { + self.trace_sampling_rules.unset_rc(); + } + + /// Add an extra service discovered at runtime + /// This is used for remote configuration + pub fn add_extra_service(&self, service_name: &str) { + self.extra_services_tracker + .add_extra_service(service_name, self.service()); + } + + /// Get all extra services discovered at runtime + pub fn get_extra_services(&self) -> Vec { + self.extra_services_tracker.get_extra_services() + } + + /// Check if remote configuration is enabled + pub fn remote_config_enabled(&self) -> bool { + self.remote_config_enabled + } } fn default_config() -> Config { @@ -515,6 +649,8 @@ fn default_config() -> Config { trace_propagation_style_extract: None, trace_propagation_style_inject: None, trace_propagation_extract_first: false, + extra_services_tracker: ExtraServicesTracker::new(true), + remote_config_enabled: true, } } @@ -615,6 +751,13 @@ impl ConfigBuilder { self } + pub fn set_remote_config_enabled(&mut self, enabled: bool) -> &mut Self { + self.config.remote_config_enabled = enabled; + // Also update the extra services tracker + self.config.extra_services_tracker = ExtraServicesTracker::new(enabled); + self + } + #[cfg(feature = "test-utils")] pub fn __internal_set_wait_agent_info_ready( &mut self, @@ -922,6 +1065,100 @@ mod tests { assert!(!config.trace_stats_computation_enabled()); } + #[test] + fn test_extra_services_tracking() { + let config = Config::builder() + .set_service("main-service".to_string()) + .build(); + + // Initially empty + assert_eq!(config.get_extra_services().len(), 0); + + // Add some extra services + config.add_extra_service("service-1"); + config.add_extra_service("service-2"); + config.add_extra_service("service-3"); + + // Should not add the main service + config.add_extra_service("main-service"); + + // Should not add duplicates + config.add_extra_service("service-1"); + + let services = config.get_extra_services(); + assert_eq!(services.len(), 3); + assert!(services.contains(&"service-1".to_string())); + assert!(services.contains(&"service-2".to_string())); + assert!(services.contains(&"service-3".to_string())); + assert!(!services.contains(&"main-service".to_string())); + } + + #[test] + fn test_extra_services_disabled_when_remote_config_disabled() { + let config = Config::builder() + .set_service("main-service".to_string()) + .set_remote_config_enabled(false) + .build(); + + // Add services when remote config is disabled + config.add_extra_service("service-1"); + config.add_extra_service("service-2"); + + // Should return empty since remote config is disabled + let services = config.get_extra_services(); + assert_eq!(services.len(), 0); + } + + #[test] + fn test_extra_services_limit() { + let config = Config::builder() + .set_service("main-service".to_string()) + .build(); + + // Add more than 64 services + for i in 0..70 { + config.add_extra_service(&format!("service-{}", i)); + } + + // Should be limited to 64 + let services = config.get_extra_services(); + assert_eq!(services.len(), 64); + } + + #[test] + fn test_remote_config_enabled_from_env() { + // Test with explicit true + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [("DD_REMOTE_CONFIGURATION_ENABLED", "true")], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources).build(); + assert!(config.remote_config_enabled()); + + // Test with explicit false + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [("DD_REMOTE_CONFIGURATION_ENABLED", "false")], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources).build(); + assert!(!config.remote_config_enabled()); + + // Test with invalid value (should default to true) + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [("DD_REMOTE_CONFIGURATION_ENABLED", "invalid")], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources).build(); + assert!(config.remote_config_enabled()); + + // Test without env var (should use default) + let config = Config::builder().build(); + assert!(config.remote_config_enabled()); // Default is true based on user's change + } + #[test] fn test_config_item_priority() { // Test that ConfigItem respects priority: remote_config > code > env_var > default From 7942f5e2e51aadeaaa9c4c2f86eef2e714c2bf67 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Fri, 8 Aug 2025 18:30:01 -0400 Subject: [PATCH 04/34] remote_config work stopping point before full rfc --- dd-trace/Cargo.toml | 7 + dd-trace/src/configuration/mod.rs | 5 +- dd-trace/src/configuration/remote_config.rs | 1134 +++++++++++++++++++ 3 files changed, 1145 insertions(+), 1 deletion(-) create mode 100644 dd-trace/src/configuration/remote_config.rs diff --git a/dd-trace/Cargo.toml b/dd-trace/Cargo.toml index c659b049..d80f4038 100644 --- a/dd-trace/Cargo.toml +++ b/dd-trace/Cargo.toml @@ -16,6 +16,13 @@ uuid = { version = "1.11.0", features = ["v4"] } anyhow = "1.0.97" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +reqwest = { version = "0.11", features = ["blocking", "json"] } +base64 = "0.21" +sha2 = "0.10" [features] test-utils = [] + +[[example]] +name = "remote_config" +path = "examples/remote_config.rs" diff --git a/dd-trace/src/configuration/mod.rs b/dd-trace/src/configuration/mod.rs index 39102a42..b916f1d0 100644 --- a/dd-trace/src/configuration/mod.rs +++ b/dd-trace/src/configuration/mod.rs @@ -3,6 +3,9 @@ #[allow(clippy::module_inception)] mod configuration; +pub mod remote_config; mod sources; -pub use configuration::{Config, ConfigBuilder, SamplingRuleConfig, TracePropagationStyle}; +pub use configuration::{ + Config, ConfigBuilder, ConfigItem, ConfigSource, SamplingRuleConfig, TracePropagationStyle, +}; diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs new file mode 100644 index 00000000..1f5ed759 --- /dev/null +++ b/dd-trace/src/configuration/remote_config.rs @@ -0,0 +1,1134 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::configuration::{Config, SamplingRuleConfig}; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; + +const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(5); +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); + +/// Capabilities that the client supports +#[derive(Debug, Clone)] +struct ClientCapabilities(u64); + +impl ClientCapabilities { + /// APM_TRACING_SAMPLE_RULES capability bit position + const APM_TRACING_SAMPLE_RULES: u64 = 1 << 29; + + fn new() -> Self { + Self(Self::APM_TRACING_SAMPLE_RULES) + } + + /// Encode capabilities as base64 string + fn encode(&self) -> String { + use base64::Engine; + let bytes = self.0.to_be_bytes(); + // Find first non-zero byte to minimize encoding size + let start = bytes + .iter() + .position(|&b| b != 0) + .unwrap_or(bytes.len() - 1); + base64::engine::general_purpose::STANDARD.encode(&bytes[start..]) + } +} + +/// Client state sent to the agent +#[derive(Debug, Clone, Serialize)] +struct ClientState { + /// Root version of the configuration + root_version: u64, + /// Versions of individual targets + targets_version: u64, + /// Configuration states + config_states: Vec, + /// Whether the client has an error + has_error: bool, + /// Error message if any + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + /// Backend client state (opaque string from server) + #[serde(skip_serializing_if = "Option::is_none")] + backend_client_state: Option, +} + +#[derive(Debug, Clone, Serialize)] +struct ConfigState { + /// ID of the configuration + id: String, + /// Version of the configuration + version: u64, + /// Product that owns this config + product: String, + /// Hash of the applied config + apply_state: u64, + /// Error if any while applying + apply_error: Option, +} + +/// Request sent to get configuration +#[derive(Debug, Serialize)] +struct ConfigRequest { + /// Client information + client: ClientInfo, + /// Cached target files + cached_target_files: Vec, +} + +#[derive(Debug, Serialize)] +struct ClientInfo { + /// State of the client + #[serde(skip_serializing_if = "Option::is_none")] + state: Option, + /// Client ID (runtime ID) + id: String, + /// Products this client is interested in + products: Vec, + /// Is this a tracer client + is_tracer: bool, + /// Tracer specific info + #[serde(skip_serializing_if = "Option::is_none")] + client_tracer: Option, + /// Client capabilities (base64 encoded) + capabilities: String, +} + +#[derive(Debug, Serialize)] +struct ClientTracer { + /// Runtime ID + runtime_id: String, + /// Language (rust) + language: String, + /// Tracer version + tracer_version: String, + /// Service name + service: String, + /// Additional services this tracer is monitoring + #[serde(default)] + extra_services: Vec, + /// Environment + #[serde(skip_serializing_if = "Option::is_none")] + env: Option, + /// App version + #[serde(skip_serializing_if = "Option::is_none")] + app_version: Option, + /// Global tags + tags: Vec, +} + +#[derive(Debug, Clone, Serialize)] +struct CachedTargetFile { + /// Path of the target file + path: String, + /// Length of the file + length: u64, + /// Hashes of the file + hashes: Vec, +} + +#[derive(Debug, Clone, Serialize)] +struct Hash { + /// Algorithm used (e.g., "sha256") + algorithm: String, + /// Hash value + hash: String, +} + +/// Response from the configuration endpoint +#[derive(Debug, Deserialize)] +struct ConfigResponse { + /// Root metadata (TUF roots) - base64 encoded + #[serde(default)] + roots: Option>, + /// Targets metadata - base64 encoded JSON + #[serde(default)] + targets: Option, + /// Target files containing actual config data + #[serde(default)] + target_files: Option>, + /// Client configs to apply + #[serde(default)] + client_configs: Option>, +} + +#[derive(Debug, Deserialize)] +struct TargetFile { + /// Path of the file + path: String, + /// Raw content (base64 encoded in responses) + raw: String, +} + +/// Configuration payload for APM tracing +/// Based on the apm-tracing.json schema from dd-go +/// See: https://github.com/DataDog/dd-go/blob/main/remote-config/apps/rc-product/schemas/apm-tracing.json +#[derive(Debug, Clone, Deserialize)] +struct ApmTracingConfig { + /// Sampling rules to apply + /// This field contains an array of sampling rules that can match based on service, name, resource, tags + #[serde(default, rename = "tracing_sampling_rules")] + tracing_sampling_rules: Option>, + // Add other APM tracing config fields as needed (e.g., tracing_header_tags, etc.) +} + +/// TUF targets metadata +/// AIDEV-NOTE: This is just an alias for SignedTargets to match the JSON structure +type TargetsMetadata = SignedTargets; + +/// Target description matching Python's TargetDesc +#[derive(Debug, Deserialize, Serialize)] +struct TargetDesc { + /// Length of the target file + length: u64, + /// Hashes of the target file (algorithm -> hash) + hashes: HashMap, + /// Custom metadata for this target + custom: Option, +} + +/// Targets structure matching Python's Targets +#[derive(Debug, Deserialize)] +struct Targets { + /// Type of the targets (usually "targets") + #[serde(rename = "_type")] + target_type: String, + /// Custom metadata + custom: Option, + /// Expiration time + expires: String, + /// Specification version + spec_version: String, + /// Target descriptions (path -> TargetDesc) + targets: HashMap, + /// Version of the targets + version: u64, +} + +#[derive(Debug, Deserialize)] +struct SignedTargets { + /// Signatures (we don't validate these currently) + signatures: Option>, + /// The signed targets data + signed: Targets, + /// Version of the signed targets + version: Option, +} + +/// Remote configuration client +/// +/// This client polls the Datadog Agent for configuration updates and applies them to the tracer. +/// Currently supports APM tracing sampling rules from the APM_TRACING product. +/// +/// The client expects to receive configuration files with paths like: +/// `datadog/2/APM_TRACING/{config_id}/config` +/// +/// These files contain JSON with a `tracing_sampling_rules` field that defines sampling rules. +/// +/// # Example +/// ```no_run +/// use std::sync::{Arc, Mutex}; +/// use dd_trace::{Config, ConfigBuilder}; +/// use dd_trace::configuration::remote_config::RemoteConfigClient; +/// +/// let config = Arc::new(ConfigBuilder::new().build()); +/// let mutable_config = Arc::new(Mutex::new(config.clone())); +/// +/// let mut client = RemoteConfigClient::new(config).unwrap(); +/// +/// // Set callback to update configuration when new rules arrive +/// let config_clone = mutable_config.clone(); +/// client.set_update_callback(move |rules| { +/// if let Ok(mut cfg) = config_clone.lock() { +/// cfg.update_sampling_rules_from_remote(rules); +/// } +/// }); +/// +/// // Start the client in a background thread +/// let handle = client.start(); +/// ``` +pub struct RemoteConfigClient { + /// Unique identifier for this client instance + /// AIDEV-NOTE: Different from runtime_id - each RemoteConfigClient gets its own UUID + client_id: String, + config: Arc, + agent_url: String, + client: reqwest::blocking::Client, + state: Arc>, + capabilities: ClientCapabilities, + poll_interval: Duration, + // Callback to update sampling rules + update_callback: Option) + Send + Sync>>, + // Cache of successfully applied configurations + cached_target_files: Arc>>, +} + +impl RemoteConfigClient { + /// Creates a new remote configuration client + pub fn new(config: Arc) -> Result { + let agent_url = format!("{}/v0.7/config", config.trace_agent_url()); + + // Create HTTP client with timeout + let client = reqwest::blocking::Client::builder() + .timeout(DEFAULT_TIMEOUT) + .build() + .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?; + + let state = Arc::new(Mutex::new(ClientState { + root_version: 1, // AIDEV-NOTE: Agent requires >= 1 (base TUF director root) + targets_version: 0, + config_states: Vec::new(), + has_error: false, + error: None, + backend_client_state: None, + })); + + Ok(Self { + client_id: uuid::Uuid::new_v4().to_string(), + config, + agent_url, + client, + state, + capabilities: ClientCapabilities::new(), + poll_interval: DEFAULT_POLL_INTERVAL, + update_callback: None, + cached_target_files: Arc::new(Mutex::new(Vec::new())), + }) + } + + /// Sets the callback to be called when sampling rules are updated + pub fn set_update_callback(&mut self, callback: F) + where + F: Fn(Vec) + Send + Sync + 'static, + { + self.update_callback = Some(Box::new(callback)); + } + + /// Starts the remote configuration client in a background thread + pub fn start(self) -> thread::JoinHandle<()> { + thread::spawn(move || { + self.run(); + }) + } + + /// Main polling loop + fn run(self) { + let mut last_poll = Instant::now(); + + loop { + // Wait for next poll interval + let elapsed = last_poll.elapsed(); + if elapsed < self.poll_interval { + thread::sleep(self.poll_interval - elapsed); + } + last_poll = Instant::now(); + + // Fetch and apply configuration + match self.fetch_and_apply_config() { + Ok(_) => { + // Clear any previous errors + if let Ok(mut state) = self.state.lock() { + state.has_error = false; + state.error = None; + } + } + Err(e) => { + crate::dd_warn!("RemoteConfigClient: Failed to fetch config: {}", e); + // Record error in state + if let Ok(mut state) = self.state.lock() { + state.has_error = true; + state.error = Some(format!("{e}")); + } + } + } + } + } + + /// Fetches configuration from the agent and applies it + fn fetch_and_apply_config(&self) -> Result<()> { + let request = self.build_request()?; + + // Send request to agent + let response = self + .client + .post(&self.agent_url) + .json(&request) + .send() + .map_err(|e| anyhow::anyhow!("Failed to send request: {}", e))?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!( + "Agent returned error status: {}", + response.status() + )); + } + + let config_response: ConfigResponse = response + .json() + .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?; + + // Process the configuration response + self.process_response(config_response)?; + + Ok(()) + } + + /// Builds the configuration request + fn build_request(&self) -> Result { + let state = self + .state + .lock() + .map_err(|_| anyhow::anyhow!("Failed to lock state"))?; + + let client_info = ClientInfo { + state: Some(state.clone()), + id: self.client_id.clone(), + products: vec!["APM_TRACING".to_string()], + is_tracer: true, + client_tracer: Some(ClientTracer { + runtime_id: self.config.runtime_id().to_string(), + language: "rust".to_string(), + tracer_version: self.config.tracer_version().to_string(), + service: self.config.service().to_string(), + extra_services: self.config.get_extra_services(), + env: self.config.env().map(|s| s.to_string()), + app_version: self.config.version().map(|s| s.to_string()), + tags: self.config.global_tags().map(|s| s.to_string()).collect(), + }), + capabilities: self.capabilities.encode(), + }; + + let cached_files = self + .cached_target_files + .lock() + .map_err(|_| anyhow::anyhow!("Failed to lock cached files"))? + .clone(); + + Ok(ConfigRequest { + client: client_info, + cached_target_files: cached_files, + }) + } + + /// Processes the configuration response + fn process_response(&self, response: ConfigResponse) -> Result<()> { + // Process targets metadata to update backend state and version + let mut path_to_custom: HashMap, Option)> = HashMap::new(); + let mut signed_targets: Option = None; + + if let Some(targets_str) = response.targets { + use base64::Engine; + let decoded = base64::engine::general_purpose::STANDARD + .decode(&targets_str) + .map_err(|e| anyhow::anyhow!("Failed to decode targets: {}", e))?; + + let targets_json = String::from_utf8(decoded) + .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in targets: {}", e))?; + + let targets: TargetsMetadata = serde_json::from_str(&targets_json) + .map_err(|e| anyhow::anyhow!("Failed to parse targets metadata: {}", e))?; + + // Store signed targets for validation + let targets_map = targets + .signed + .targets + .iter() + .map(|(k, v)| (k.clone(), serde_json::to_value(v).unwrap())) + .collect(); + signed_targets = Some(serde_json::Value::Object(targets_map)); + + // Build lookup for per-path id and version from targets.signed.targets[*].custom + for (path, desc) in &targets.signed.targets { + let custom = &desc.custom; + let id = custom + .as_ref() + .and_then(|c| c.get("id")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + // Datadog RC uses custom.v (int). Fallback to custom.version if needed + let version = custom + .as_ref() + .and_then(|c| c.get("v")) + .and_then(|v| v.as_u64()) + .or_else(|| { + custom + .as_ref() + .and_then(|c| c.get("version")) + .and_then(|v| v.as_u64()) + }); + path_to_custom.insert(path.clone(), (id, version)); + } + + // Update state with backend state and version + if let Ok(mut state) = self.state.lock() { + state.targets_version = targets.signed.version; + + if let Some(custom) = &targets.signed.custom { + if let Some(backend_state) = + custom.get("opaque_backend_state").and_then(|v| v.as_str()) + { + state.backend_client_state = Some(backend_state.to_string()); + } + } + } + } + + // Validate target files against signed targets and client configs + if let Some(target_files) = &response.target_files { + self.validate_signed_target_files( + target_files, + &signed_targets, + &response.client_configs, + )?; + } + + // Parse target files if present + if let Some(target_files) = response.target_files { + // Build a new cache - don't clear the old one yet! + let mut new_cache = Vec::new(); + let mut any_failure = false; + + for file in target_files { + // Check if this is an APM tracing config first - skip non-APM_TRACING configs + // Path format is like "datadog/2/APM_TRACING/{config_id}/config" + if !file.path.contains("APM_TRACING") { + // Skip non-APM_TRACING configs - we only support APM_TRACING currently + continue; + } + + // Target files contain base64 encoded JSON configs + use base64::Engine; + let decoded = base64::engine::general_purpose::STANDARD + .decode(&file.raw) + .map_err(|e| anyhow::anyhow!("Failed to decode config: {}", e))?; + + let config_str = String::from_utf8(decoded.clone()) + .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in config: {}", e))?; + + // Determine config id and version for state reporting (do this before applying) + let derived_id = extract_config_id_from_path(&file.path); + let (meta_id, meta_version) = path_to_custom + .get(&file.path) + .cloned() + .unwrap_or((None, None)); + let config_id = derived_id + .or(meta_id) + .unwrap_or_else(|| "apm-tracing-sampling".to_string()); + let config_version = meta_version.unwrap_or(1); + + // Apply the config and record success or failure state + match self.process_apm_tracing_config(&config_str) { + Ok(_) => { + // Calculate SHA256 hash of the raw content + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(&file.raw); + let hash_result = hasher.finalize(); + let hash_hex = format!("{hash_result:x}"); + + new_cache.push(CachedTargetFile { + path: file.path.clone(), + length: file.raw.len() as u64, + hashes: vec![Hash { + algorithm: "sha256".to_string(), + hash: hash_hex, + }], + }); + + // Update state to reflect successful application with accurate id/version + if let Ok(mut state) = self.state.lock() { + state.config_states.push(ConfigState { + id: config_id, + version: config_version, + product: "APM_TRACING".to_string(), + apply_state: 2, // 2 denotes success + apply_error: None, + }); + } + } + Err(e) => { + any_failure = true; + crate::dd_warn!( + "RemoteConfigClient: Failed to apply APM_TRACING config {}: {}", + config_id, + e + ); + if let Ok(mut state) = self.state.lock() { + // 3 denotes error + state.config_states.push(ConfigState { + id: config_id, + version: config_version, + product: "APM_TRACING".to_string(), + apply_state: 3, // 3 denotes error + apply_error: Some(format!("{e}")), + }); + } + // Do not add to cache on failure + continue; + } + } + } + + // Only update the cache if we successfully processed all configs + // This ensures we don't lose our previous cache state on errors + if let Ok(mut cache) = self.cached_target_files.lock() { + if !any_failure { + *cache = new_cache; + } + } + } + + Ok(()) + } + + /// Validates that target files exist in either signed targets or client configs + /// This validation ensures security by preventing unauthorized config files from being applied + fn validate_signed_target_files( + &self, + payload_target_files: &[TargetFile], + payload_targets_signed: &Option, + client_configs: &Option>, + ) -> Result<()> { + for target in payload_target_files { + let exists_in_signed_targets = payload_targets_signed + .as_ref() + .and_then(|targets| targets.get(&target.path)) + .is_some(); + + let exists_in_client_configs = client_configs + .as_ref() + .map(|configs| configs.contains(&target.path)) + .unwrap_or(false); + + if !exists_in_signed_targets && !exists_in_client_configs { + return Err(anyhow::anyhow!( + "target file {} not exists in client_config and signed targets", + target.path + )); + } + } + + Ok(()) + } + + /// Processes APM tracing configuration + fn process_apm_tracing_config(&self, config_json: &str) -> Result<()> { + let tracing_config: ApmTracingConfig = serde_json::from_str(config_json) + .map_err(|e| anyhow::anyhow!("Failed to parse APM tracing config: {}", e))?; + + // Extract sampling rules if present + if let Some(rules) = tracing_config.tracing_sampling_rules { + // Call the update callback with new rules + if let Some(ref callback) = self.update_callback { + callback(rules.clone()); + } + + crate::dd_info!( + "RemoteConfigClient: Applied {} sampling rules from remote config", + rules.len() + ); + } else { + crate::dd_info!( + "RemoteConfigClient: APM tracing config received but no sampling rules present" + ); + } + + Ok(()) + } +} + +// Helper to extract config id from known RC path pattern +fn extract_config_id_from_path(path: &str) -> Option { + // Expected: datadog/2/APM_TRACING/{config_id}/config + let parts: Vec<&str> = path.split('/').collect(); + for i in 0..parts.len() { + if parts[i] == "APM_TRACING" { + return parts.get(i + 1).map(|s| s.to_string()); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client_capabilities() { + let caps = ClientCapabilities::new(); + // Check that the encoded capabilities is a non-empty base64 string + let encoded = caps.encode(); + assert!(!encoded.is_empty()); + + // The encoded value should decode to contain our capability bit + use base64::Engine; + let decoded = base64::engine::general_purpose::STANDARD + .decode(&encoded) + .unwrap(); + + // Reconstruct the u64 from the variable-length big-endian bytes + let mut bytes = [0u8; 8]; + let offset = 8 - decoded.len(); + bytes[offset..].copy_from_slice(&decoded); + let value = u64::from_be_bytes(bytes); + + // Verify the capability bit is set + assert_eq!(value, ClientCapabilities::APM_TRACING_SAMPLE_RULES); + assert_eq!( + value & ClientCapabilities::APM_TRACING_SAMPLE_RULES, + ClientCapabilities::APM_TRACING_SAMPLE_RULES + ); + } + + #[test] + fn test_request_serialization() { + // Test that our request format matches the expected structure from Python + let state = ClientState { + root_version: 1, + targets_version: 122282776, + config_states: vec![], + has_error: false, + error: None, + backend_client_state: Some("test_backend_state".to_string()), + }; + + let client_info = ClientInfo { + state: Some(state), + id: "test-client-id".to_string(), + products: vec!["APM_TRACING".to_string()], + is_tracer: true, + client_tracer: Some(ClientTracer { + runtime_id: "test-runtime-id".to_string(), + language: "rust".to_string(), + tracer_version: "0.0.1".to_string(), + service: "test-service".to_string(), + extra_services: vec![], + env: Some("test-env".to_string()), + app_version: Some("1.0.0".to_string()), + tags: vec![], + }), + capabilities: ClientCapabilities::new().encode(), + }; + + let request = ConfigRequest { + client: client_info, + cached_target_files: Vec::new(), + }; + + // Serialize and verify the structure + let json = serde_json::to_value(&request).unwrap(); + + // Check top-level structure + assert!(json.get("client").is_some()); + // cached_target_files should be an empty array when empty + assert_eq!( + json.get("cached_target_files"), + Some(&serde_json::json!([])) + ); + + let client = &json["client"]; + + // Check client structure + assert_eq!(client["id"], "test-client-id"); + assert_eq!(client["products"], serde_json::json!(["APM_TRACING"])); + assert_eq!(client["is_tracer"], true); + + // Check client_tracer structure + let client_tracer = &client["client_tracer"]; + assert_eq!(client_tracer["runtime_id"], "test-runtime-id"); + assert_eq!(client_tracer["language"], "rust"); + assert_eq!(client_tracer["service"], "test-service"); + assert_eq!(client_tracer["extra_services"], serde_json::json!([])); + assert_eq!(client_tracer["env"], "test-env"); + assert_eq!(client_tracer["app_version"], "1.0.0"); + + // Check state structure + let state = &client["state"]; + assert_eq!(state["root_version"], 1); + assert_eq!(state["targets_version"], 122282776); + assert_eq!(state["has_error"], false); + assert_eq!(state["backend_client_state"], "test_backend_state"); + + // Check capabilities is a base64 encoded string + let capabilities = &client["capabilities"]; + assert!(capabilities.is_string()); + assert!(!capabilities.as_str().unwrap().is_empty()); + } + + #[test] + fn test_request_serialization_with_error() { + // Test that error field is included when has_error is true + let state = ClientState { + root_version: 1, + targets_version: 1, + config_states: vec![], + has_error: true, + error: Some("Test error message".to_string()), + backend_client_state: None, + }; + + let client_info = ClientInfo { + state: Some(state), + id: "test-client-id".to_string(), + products: vec!["APM_TRACING".to_string()], + is_tracer: true, + client_tracer: Some(ClientTracer { + runtime_id: "test-runtime-id".to_string(), + language: "rust".to_string(), + tracer_version: "0.0.1".to_string(), + service: "test-service".to_string(), + extra_services: vec!["service1".to_string(), "service2".to_string()], + env: None, + app_version: None, + tags: vec![], + }), + capabilities: ClientCapabilities::new().encode(), + }; + + let request = ConfigRequest { + client: client_info, + cached_target_files: Vec::new(), + }; + + let json = serde_json::to_value(&request).unwrap(); + let state = &json["client"]["state"]; + + // Verify error field is present when has_error is true + assert_eq!(state["has_error"], true); + assert_eq!(state["error"], "Test error message"); + + // Verify extra_services is populated + let client_tracer = &json["client"]["client_tracer"]; + assert_eq!( + client_tracer["extra_services"], + serde_json::json!(["service1", "service2"]) + ); + + // Verify None values are not included in JSON + assert!(client_tracer.get("env").is_none()); + assert!(client_tracer.get("app_version").is_none()); + assert!(state.get("backend_client_state").is_none()); + } + + #[test] + fn test_apm_tracing_config_parsing() { + let json = r#"{ + "tracing_sampling_rules": [ + { + "sample_rate": 0.5, + "service": "test-service", + "provenance": "dynamic" + } + ] + }"#; + + let config: ApmTracingConfig = serde_json::from_str(json).unwrap(); + assert!(config.tracing_sampling_rules.is_some()); + let rules = config.tracing_sampling_rules.unwrap(); + assert_eq!(rules.len(), 1); + assert_eq!(rules[0].sample_rate, 0.5); + assert_eq!(rules[0].service, Some("test-service".to_string())); + assert_eq!(rules[0].provenance, "dynamic"); + } + + #[test] + fn test_apm_tracing_config_full_schema() { + // Test parsing a more complete configuration + let json = r#"{ + "tracing_sampling_rules": [ + { + "sample_rate": 0.3, + "service": "web-api", + "name": "GET /users/*", + "resource": "UserController.list", + "tags": { + "environment": "production", + "region": "us-east-1" + }, + "provenance": "customer" + }, + { + "sample_rate": 1.0, + "service": "auth-service", + "provenance": "dynamic" + } + ] + }"#; + + let config: ApmTracingConfig = serde_json::from_str(json).unwrap(); + assert!(config.tracing_sampling_rules.is_some()); + let rules = config.tracing_sampling_rules.unwrap(); + assert_eq!(rules.len(), 2); + + // Check first rule + assert_eq!(rules[0].sample_rate, 0.3); + assert_eq!(rules[0].service, Some("web-api".to_string())); + assert_eq!(rules[0].name, Some("GET /users/*".to_string())); + assert_eq!(rules[0].resource, Some("UserController.list".to_string())); + assert_eq!(rules[0].tags.len(), 2); + assert_eq!( + rules[0].tags.get("environment"), + Some(&"production".to_string()) + ); + assert_eq!(rules[0].tags.get("region"), Some(&"us-east-1".to_string())); + assert_eq!(rules[0].provenance, "customer"); + + // Check second rule + assert_eq!(rules[1].sample_rate, 1.0); + assert_eq!(rules[1].service, Some("auth-service".to_string())); + assert_eq!(rules[1].provenance, "dynamic"); + } + + #[test] + fn test_apm_tracing_config_empty() { + let json = r#"{}"#; + + let config: ApmTracingConfig = serde_json::from_str(json).unwrap(); + assert!(config.tracing_sampling_rules.is_none()); + } + + #[test] + fn test_cached_target_files() { + // Test that cached_target_files is properly serialized + let cached_file = CachedTargetFile { + path: "datadog/2/APM_TRACING/config123/config".to_string(), + length: 256, + hashes: vec![Hash { + algorithm: "sha256".to_string(), + hash: "abc123def456".to_string(), + }], + }; + + let request = ConfigRequest { + client: ClientInfo { + state: None, + id: "test-id".to_string(), + products: vec!["APM_TRACING".to_string()], + is_tracer: true, + client_tracer: None, + capabilities: ClientCapabilities::new().encode(), + }, + cached_target_files: vec![cached_file.clone()], + }; + + let json = serde_json::to_value(&request).unwrap(); + let cached = &json["cached_target_files"][0]; + + assert_eq!(cached["path"], "datadog/2/APM_TRACING/config123/config"); + assert_eq!(cached["length"], 256); + assert_eq!(cached["hashes"][0]["algorithm"], "sha256"); + assert_eq!(cached["hashes"][0]["hash"], "abc123def456"); + } + + #[test] + fn test_validate_signed_target_files() { + // Create a mock RemoteConfigClient for testing + let config = Arc::new(Config::builder().build()); + let client = RemoteConfigClient::new(config).unwrap(); + + // Test case 1: Target file exists in signed targets + let target_files = vec![TargetFile { + path: "datadog/2/APM_TRACING/config123/config".to_string(), + raw: "base64_encoded_content".to_string(), + }]; + + let signed_targets = serde_json::json!({ + "datadog/2/APM_TRACING/config123/config": { + "custom": {"id": "config123", "v": 1} + } + }); + + let client_configs = None; + + // Should pass validation + assert!(client + .validate_signed_target_files(&target_files, &Some(signed_targets), &client_configs) + .is_ok()); + + // Test case 2: Target file exists in client configs + let target_files = vec![TargetFile { + path: "datadog/2/APM_TRACING/config456/config".to_string(), + raw: "base64_encoded_content".to_string(), + }]; + + let signed_targets = None; + let client_configs = Some(vec!["datadog/2/APM_TRACING/config456/config".to_string()]); + + // Should pass validation + assert!(client + .validate_signed_target_files(&target_files, &signed_targets, &client_configs) + .is_ok()); + + // Test case 3: Target file exists in both signed targets and client configs + let target_files = vec![TargetFile { + path: "datadog/2/APM_TRACING/config789/config".to_string(), + raw: "base64_encoded_content".to_string(), + }]; + + let signed_targets = serde_json::json!({ + "datadog/2/APM_TRACING/config789/config": { + "custom": {"id": "config789", "v": 1} + } + }); + let client_configs = Some(vec!["datadog/2/APM_TRACING/config789/config".to_string()]); + + // Should pass validation + assert!(client + .validate_signed_target_files(&target_files, &Some(signed_targets), &client_configs) + .is_ok()); + + // Test case 4: Target file exists in neither signed targets nor client configs + let target_files = vec![TargetFile { + path: "datadog/2/APM_TRACING/invalid_config/config".to_string(), + raw: "base64_encoded_content".to_string(), + }]; + + let signed_targets = serde_json::json!({ + "datadog/2/APM_TRACING/other_config/config": { + "custom": {"id": "other_config", "v": 1} + } + }); + let client_configs = Some(vec![ + "datadog/2/APM_TRACING/another_config/config".to_string() + ]); + + // Should fail validation + let result = client.validate_signed_target_files( + &target_files, + &Some(signed_targets), + &client_configs, + ); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("target file datadog/2/APM_TRACING/invalid_config/config not exists in client_config and signed targets")); + + // Test case 5: Empty target files should pass validation + let target_files = vec![]; + let signed_targets = None; + let client_configs = None; + + // Should pass validation + assert!(client + .validate_signed_target_files(&target_files, &signed_targets, &client_configs) + .is_ok()); + } + + #[test] + fn test_parse_example_response() { + // Create a ConfigResponse object that represents the example response + let config_response = ConfigResponse { + roots: None, + targets: Some("eyJzaWduZWQiOiB7Il90eXBlIjogInRhcmdldHMiLCAiY3VzdG9tIjogeyJvcGFxdWVfYmFja2VuZF9zdGF0ZSI6ICJleUpmb29JT2lBaVltRm9JbjA9In0sICJleHBpcmVzIjogIjIwMjQtMTItMzFUMjM6NTk6NTlaIiwgInNwZWNfdmVyc2lvbiI6ICIxLjAuMCIsICJ0YXJnZXRzIjoge30sICJ2ZXJzaW9uIjogM319Cg==".to_string()), // base64 encoded targets with proper structure + target_files: Some(vec![ + TargetFile { + path: "datadog/2/APM_TRACING/apm-tracing-sampling/config".to_string(), + raw: "eyJ0cmFjaW5nX3NhbXBsaW5nX3J1bGVzIjogW3sic2FtcGxlX3JhdGUiOiAwLjUsICJzZXJ2aWNlIjogInRlc3Qtc2VydmljZSJ9XX0=".to_string(), // base64 encoded APM config + }, + ]), + client_configs: Some(vec![ + "datadog/2/APM_TRACING/apm-tracing-sampling/config".to_string(), + ]), + }; + + // Create a RemoteConfigClient and set up a callback to capture processed rules + let config = Arc::new(Config::builder().build()); + let mut client = RemoteConfigClient::new(config).unwrap(); + + let processed_rules = Arc::new(Mutex::new(Vec::new())); + let rules_clone = processed_rules.clone(); + client.set_update_callback(move |rules| { + if let Ok(mut captured_rules) = rules_clone.lock() { + *captured_rules = rules; + } + }); + + // Process the response - this should update the client's state and process APM_TRACING configs + let result = client.process_response(config_response); + assert!(result.is_ok(), "process_response should succeed"); + + // Verify that the client's state was updated correctly + let state = client.state.lock().unwrap(); + assert_eq!(state.targets_version, 3); + assert_eq!( + state.backend_client_state, + Some("eyJfooIOiAiYmFoIn0=".to_string()) + ); + assert_eq!(state.has_error, false); + + // Verify that APM_TRACING config states were added + assert_eq!(state.config_states.len(), 1); + let config_state = &state.config_states[0]; + assert_eq!(config_state.product, "APM_TRACING"); + assert_eq!(config_state.apply_state, 2); // success + + // Verify that APM_TRACING cached files were added + let cached_files = client.cached_target_files.lock().unwrap(); + assert_eq!(cached_files.len(), 1); + assert_eq!( + cached_files[0].path, + "datadog/2/APM_TRACING/apm-tracing-sampling/config" + ); + assert_eq!(cached_files[0].length, 104); + assert_eq!(cached_files[0].hashes.len(), 1); + assert_eq!(cached_files[0].hashes[0].algorithm, "sha256"); + + // Verify that the callback was called with the processed rules + let captured_rules = processed_rules.lock().unwrap(); + assert_eq!(captured_rules.len(), 1); + assert_eq!(captured_rules[0].sample_rate, 0.5); + assert_eq!(captured_rules[0].service, Some("test-service".to_string())); + } + + #[test] + fn test_parse_multi_product_response() { + // This test verifies that our implementation correctly skips non-APM_TRACING + // configs and only processes APM_TRACING configs. The multi-product response contains + // ASM_FEATURES and LIVE_DEBUGGING configs which should be ignored. + + // Create a ConfigResponse object that represents a multi-product response + let config_response = ConfigResponse { + roots: None, + targets: Some("eyJzaWduZWQiOiB7Il90eXBlIjogInRhcmdldHMiLCAiY3VzdG9tIjogeyJvcGFxdWVfYmFja2VuZF9zdGF0ZSI6ICJleUpmb29JT2lBaVltRm9JbjA9In0sICJleHBpcmVzIjogIjIwMjQtMTItMzFUMjM6NTk6NTlaIiwgInNwZWNfdmVyc2lvbiI6ICIxLjAuMCIsICJ0YXJnZXRzIjoge30sICJ2ZXJzaW9uIjogMn19Cg==".to_string()), // base64 encoded targets with proper structure + target_files: Some(vec![ + TargetFile { + path: "datadog/2/ASM_FEATURES/ASM_FEATURES-base/config".to_string(), + raw: "eyJhc20tZmVhdHVyZXMiOiB7ImVuYWJsZWQiOiB0cnVlfX0=".to_string(), // base64 encoded config + }, + TargetFile { + path: "datadog/2/LIVE_DEBUGGING/LIVE_DEBUGGING-base/config".to_string(), + raw: "eyJsaXZlLWRlYnVnZ2luZyI6IHsiZW5hYmxlZCI6IGZhbHNlfX0=".to_string(), // base64 encoded config + }, + ]), + client_configs: Some(vec![ + "datadog/2/ASM_FEATURES/ASM_FEATURES-base/config".to_string(), + "datadog/2/LIVE_DEBUGGING/LIVE_DEBUGGING-base/config".to_string(), + ]), + }; + + // Create a RemoteConfigClient and process the response + let config = Arc::new(Config::builder().build()); + let client = RemoteConfigClient::new(config).unwrap(); + + // Process the response - this should update the client's state + let result = client.process_response(config_response); + assert!(result.is_ok(), "process_response should succeed"); + + // Verify that the client's state was updated correctly + let state = client.state.lock().unwrap(); + assert_eq!(state.targets_version, 2); + assert_eq!( + state.backend_client_state, + Some("eyJfooIOiAiYmFoIn0=".to_string()) + ); + assert_eq!(state.has_error, false); + + // Verify that no config states were added since we don't process non-APM_TRACING products + assert_eq!(state.config_states.len(), 0); + + // Verify that cached target files were not added since they're not APM_TRACING + let cached_files = client.cached_target_files.lock().unwrap(); + assert_eq!(cached_files.len(), 0); + } +} From 0c7b9cda58b6fc884048aead6376a604142cdd7f Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Sun, 10 Aug 2025 12:25:49 -0400 Subject: [PATCH 05/34] right before switch to coupling config and rc --- Cargo.lock | 354 ++++++++++++++++-- datadog-opentelemetry/Cargo.toml | 1 + .../examples/remote_config.rs | 67 ++++ datadog-opentelemetry/src/lib.rs | 46 ++- datadog-opentelemetry/src/sampler.rs | 5 + datadog-opentelemetry/tests/mod.rs | 36 ++ dd-trace/examples/remote_config.rs | 75 ++++ dd-trace/src/configuration/configuration.rs | 151 +++++++- dd-trace/src/configuration/remote_config.rs | 77 +++- 9 files changed, 777 insertions(+), 35 deletions(-) create mode 100644 datadog-opentelemetry/examples/remote_config.rs create mode 100644 dd-trace/examples/remote_config.rs diff --git a/Cargo.lock b/Cargo.lock index cfaeb5ce..1b50f0f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -183,7 +183,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -224,7 +224,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.0", "futures-lite", - "rustix", + "rustix 0.38.44", "tracing", ] @@ -240,7 +240,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -370,7 +370,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags", + "bitflags 2.9.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -402,12 +402,27 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.6.1" @@ -445,7 +460,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-native-certs 0.7.3", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_derive", @@ -650,6 +665,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -671,6 +695,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.11" @@ -766,6 +800,7 @@ dependencies = [ "opentelemetry-semantic-conventions 0.30.0", "opentelemetry_sdk", "rand 0.8.5", + "serde_json", "tinybytes", "tokio", ] @@ -836,8 +871,11 @@ name = "dd-trace" version = "0.0.1" dependencies = [ "anyhow", + "base64 0.21.7", + "reqwest 0.11.27", "serde", "serde_json", + "sha2", "uuid", ] @@ -944,6 +982,16 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1130,6 +1178,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1247,6 +1310,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1298,6 +1371,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.9.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.10" @@ -1521,6 +1613,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1543,7 +1636,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1589,6 +1682,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.11" @@ -1966,7 +2072,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.9.0", "libc", ] @@ -1982,6 +2088,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.7.5" @@ -2072,6 +2184,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2084,7 +2213,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.9.0", "cfg-if", "cfg_aliases", "libc", @@ -2130,12 +2259,50 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.30.0" @@ -2353,6 +2520,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "polling" version = "3.7.4" @@ -2363,7 +2536,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -2597,7 +2770,7 @@ version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] @@ -2655,6 +2828,46 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "reqwest" version = "0.12.15" @@ -2666,7 +2879,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.4.10", "hickory-resolver", "http 1.3.1", "http-body 1.0.1", @@ -2684,12 +2897,12 @@ dependencies = [ "quinn", "rustls", "rustls-native-certs 0.8.1", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tokio-rustls", "tower", @@ -2778,10 +2991,23 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -2807,7 +3033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", "security-framework 2.11.1", @@ -2825,6 +3051,15 @@ dependencies = [ "security-framework 3.2.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -2897,7 +3132,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.9.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -2910,7 +3145,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags", + "bitflags 2.9.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -3040,6 +3275,17 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3192,6 +3438,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3222,6 +3474,40 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.59.0", +] + [[package]] name = "term" version = "0.7.0" @@ -3251,7 +3537,7 @@ dependencies = [ "memchr", "parse-display", "pin-project-lite", - "reqwest", + "reqwest 0.12.15", "serde", "serde_json", "serde_with", @@ -3414,6 +3700,16 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" @@ -3457,7 +3753,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -3527,6 +3823,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3590,6 +3892,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3739,7 +4047,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.44", ] [[package]] @@ -4086,7 +4394,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] diff --git a/datadog-opentelemetry/Cargo.toml b/datadog-opentelemetry/Cargo.toml index d86364bb..1decbda8 100644 --- a/datadog-opentelemetry/Cargo.toml +++ b/datadog-opentelemetry/Cargo.toml @@ -21,6 +21,7 @@ opentelemetry_sdk = { workspace = true } opentelemetry = { workspace = true } opentelemetry-semantic-conventions = { workspace = true } rand = { workspace = true } +serde_json = { workspace = true } # Libdatadog dependencies data-pipeline = { workspace = true } diff --git a/datadog-opentelemetry/examples/remote_config.rs b/datadog-opentelemetry/examples/remote_config.rs new file mode 100644 index 00000000..d8cb7c24 --- /dev/null +++ b/datadog-opentelemetry/examples/remote_config.rs @@ -0,0 +1,67 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating automatic remote configuration integration +//! +//! This example shows how the datadog-opentelemetry crate automatically +//! initializes the RemoteConfigClient when remote configuration is enabled. +//! +//! Run this example with: +//! ```bash +//! cargo run --example remote_config +//! ``` +//! +//! The example will: +//! 1. Create a configuration with remote config enabled +//! 2. Initialize the OpenTelemetry tracer (which automatically starts the RemoteConfigClient) +//! 3. Create some test spans +//! 4. Keep running to demonstrate the remote config client working in the background + +use std::thread; +use std::time::Duration; + +use dd_trace::Config; +use opentelemetry::trace::{Tracer, TracerProvider}; +use opentelemetry_sdk::trace::TracerProviderBuilder; + +fn main() { + println!("Starting remote configuration example..."); + + // Create configuration with remote config enabled + let mut builder = Config::builder(); + builder.set_service("remote-config-example".to_string()); + builder.set_remote_config_enabled(true); // Enable remote configuration + builder.set_log_level_filter(dd_trace::log::LevelFilter::Debug); + + let config = builder.build(); + + println!("Initial sampling rules: {:?}", config.trace_sampling_rules()); + + // Initialize the OpenTelemetry tracer + // This automatically starts the RemoteConfigClient in the background + let tracer_provider = datadog_opentelemetry::init_datadog( + config, + TracerProviderBuilder::default(), + None, + ); + + let tracer = tracer_provider.tracer("remote-config-example"); + + println!("Tracer initialized. RemoteConfigClient is running in the background."); + println!("The client will automatically poll for configuration updates every 5 seconds."); + println!("Press Ctrl+C to exit"); + + // Create some test spans to demonstrate the tracer is working + for i in 1..=5 { + tracer.in_span("test-operation", |_cx| { + println!("Created span {}", i); + // Simulate some work + thread::sleep(Duration::from_millis(100)); + }); + + thread::sleep(Duration::from_secs(2)); + } + + println!("Example completed. The RemoteConfigClient continues running in the background."); + println!("In a real application, it would keep running and apply any remote configuration updates."); +} \ No newline at end of file diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 9282379e..6488cec8 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -8,7 +8,7 @@ mod span_processor; mod text_map_propagator; mod trace_id; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, Mutex}; use opentelemetry::{Key, KeyValue, Value}; use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; @@ -16,6 +16,7 @@ use opentelemetry_semantic_conventions::resource::SERVICE_NAME; use sampler::Sampler; use span_processor::{DatadogSpanProcessor, TraceRegistry}; use text_map_propagator::DatadogPropagator; +use serde_json; /// Initialize the Datadog OpenTelemetry exporter. /// @@ -69,8 +70,15 @@ fn make_tracer( tracer_provider_builder = tracer_provider_builder.with_resource(dd_resource); let propagator = DatadogPropagator::new(&config, registry.clone()); + // Get sampler callback before moving sampler into tracer provider + let sampler_callback = if config.remote_config_enabled() { + Some(sampler.on_rules_update()) + } else { + None + }; + let span_processor = DatadogSpanProcessor::new( - config, + config.clone(), registry.clone(), resource_slot.clone(), Some(agent_response_handler), @@ -81,6 +89,40 @@ fn make_tracer( .with_id_generator(trace_id::TraceidGenerator) .build(); + // Initialize remote configuration client if enabled + if config.remote_config_enabled() { + // AIDEV-NOTE: Create a mutable config that can be updated by remote config + let config_arc = Arc::new(config); + let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); + + // Add sampler callback to the config before creating the remote config client + if let Some(sampler_callback) = sampler_callback { + let sampler_callback = Arc::new(sampler_callback); + let sampler_callback_clone = sampler_callback.clone(); + mutable_config.lock().unwrap().add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), move |json_str| { + sampler_callback_clone(json_str); + }); + } + + // Create remote config client + if let Ok(mut client) = dd_trace::configuration::remote_config::RemoteConfigClient::new(config_arc) { + // Set up callback to handle configuration updates + let config_clone = mutable_config.clone(); + client.set_update_callback(move |rules| { + if let Ok(mut cfg) = config_clone.lock() { + cfg.update_sampling_rules_from_remote(rules); + dd_trace::dd_info!("RemoteConfigClient: Applied new sampling rules from remote config"); + } + }); + + // Start the client in background + let _handle = client.start(); + dd_trace::dd_info!("RemoteConfigClient: Started remote configuration client"); + } else { + dd_trace::dd_warn!("RemoteConfigClient: Failed to create remote config client"); + } + } + (tracer_provider, propagator) } diff --git a/datadog-opentelemetry/src/sampler.rs b/datadog-opentelemetry/src/sampler.rs index 2e26fe04..c5e56f4e 100644 --- a/datadog-opentelemetry/src/sampler.rs +++ b/datadog-opentelemetry/src/sampler.rs @@ -49,6 +49,11 @@ impl Sampler { pub fn on_agent_response(&self) -> Box Fn(&'a str) + Send + Sync> { self.sampler.on_agent_response() } + + /// Get the callback for updating sampling rules + pub fn on_rules_update(&self) -> Box Fn(&'a str) + Send + Sync> { + self.sampler.on_rules_update() + } } impl ShouldSample for Sampler { diff --git a/datadog-opentelemetry/tests/mod.rs b/datadog-opentelemetry/tests/mod.rs index 6a21a0d3..ac7694ea 100644 --- a/datadog-opentelemetry/tests/mod.rs +++ b/datadog-opentelemetry/tests/mod.rs @@ -234,6 +234,42 @@ mod datadog_test_agent { ); } + #[test] + fn test_remote_config_initialization() { + // Test that remote config client is initialized when remote config is enabled + let mut config_builder = dd_trace::Config::builder(); + config_builder.set_remote_config_enabled(true); + let config = config_builder.build(); + + // This should initialize the remote config client + let (tracer_provider, _propagator) = make_test_tracer( + config, + opentelemetry_sdk::trace::TracerProviderBuilder::default(), + ); + + // Verify the tracer provider was created successfully + let _tracer = tracer_provider.tracer("test"); + // If we get here, the tracer provider was created successfully + } + + #[test] + fn test_remote_config_disabled() { + // Test that remote config client is not initialized when remote config is disabled + let mut config_builder = dd_trace::Config::builder(); + config_builder.set_remote_config_enabled(false); + let config = config_builder.build(); + + // This should not initialize the remote config client + let (tracer_provider, _propagator) = make_test_tracer( + config, + opentelemetry_sdk::trace::TracerProviderBuilder::default(), + ); + + // Verify the tracer provider was created successfully + let _tracer = tracer_provider.tracer("test"); + // If we get here, the tracer provider was created successfully + } + #[track_caller] fn assert_subset, SS: IntoIterator>(set: S, subset: SS) where diff --git a/dd-trace/examples/remote_config.rs b/dd-trace/examples/remote_config.rs new file mode 100644 index 00000000..02a018be --- /dev/null +++ b/dd-trace/examples/remote_config.rs @@ -0,0 +1,75 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Example of using the remote configuration client to update sampling rules + +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use dd_trace::configuration::remote_config::RemoteConfigClient; +use dd_trace::Config; + +fn main() { + // Create initial configuration + let mut builder = Config::builder(); + builder.set_service("remote-config-example".to_string()); + builder.set_log_level_filter(dd_trace::log::LevelFilter::Debug); + + let config = Arc::new(builder.build()); + let config_mutex = Arc::new(Mutex::new(config.as_ref().clone())); + + println!("Starting remote configuration client..."); + println!( + "Initial sampling rules: {:?}", + config.trace_sampling_rules() + ); + + // Create remote config client + let mut client = + RemoteConfigClient::new(config.clone()).expect("Failed to create remote config client"); + + // Set up callback to handle configuration updates + let config_clone = config_mutex.clone(); + client.set_update_callback(move |rules| { + println!( + "Received {} new sampling rules from remote config", + rules.len() + ); + + if let Ok(mut cfg) = config_clone.lock() { + cfg.update_sampling_rules_from_remote(rules); + println!("Applied new sampling rules"); + + // Print current rules + for (i, rule) in cfg.trace_sampling_rules().iter().enumerate() { + println!( + " Rule {}: sample_rate={}, service={:?}, provenance={}", + i + 1, + rule.sample_rate, + rule.service, + rule.provenance + ); + } + } + }); + + // Start the client in background + let _handle = client.start(); + + println!("Remote config client started. Listening for configuration updates..."); + println!("Press Ctrl+C to exit"); + + // Keep main thread alive to observe updates + loop { + thread::sleep(Duration::from_secs(10)); + + // Periodically print current configuration + if let Ok(cfg) = config_mutex.lock() { + println!( + "\nCurrent sampling rules count: {}", + cfg.trace_sampling_rules().len() + ); + } + } +} diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index 361f862f..18e22c9c 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -318,7 +318,7 @@ impl ServiceName { } } -#[derive(Debug, Clone)] +#[derive(Clone)] #[non_exhaustive] /// Configuration for the Datadog Tracer /// @@ -388,6 +388,10 @@ pub struct Config { /// Tracks extra services discovered at runtime /// Used for remote configuration to report all services extra_services_tracker: ExtraServicesTracker, + + /// General callbacks to be called when configuration is updated from remote configuration + /// AIDEV-NOTE: This allows components like the DatadogSampler to be updated without circular imports + remote_config_callbacks: Arc>>>, } impl Config { @@ -488,6 +492,7 @@ impl Config { wait_agent_info_ready: default.wait_agent_info_ready, extra_services_tracker: ExtraServicesTracker::new(remote_config_enabled), remote_config_enabled, + remote_config_callbacks: Arc::new(Mutex::new(HashMap::new())), } } @@ -593,14 +598,77 @@ impl Config { /// Updates sampling rules from remote configuration /// This method is used by the remote config client to apply new rules pub fn update_sampling_rules_from_remote(&mut self, rules: Vec) { - let parsed_rules = ParsedSamplingRules { rules }; + let parsed_rules = ParsedSamplingRules { rules: rules.clone() }; self.trace_sampling_rules .set_value_source(parsed_rules, ConfigSource::RemoteConfig); + + // Notify the datadog_sampler_on_rules_update callback about the update + // This specifically calls the DatadogSampler's on_rules_update method + if let Ok(callbacks) = self.remote_config_callbacks.lock() { + if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { + // Convert rules to JSON string for the sampler callback + if let Ok(json_str) = serde_json::to_string(&rules) { + callback(&json_str); + } + } + } } /// Clears remote configuration sampling rules, falling back to code/env/default pub fn clear_remote_sampling_rules(&mut self) { self.trace_sampling_rules.unset_rc(); + + // Notify the datadog_sampler_on_rules_update callback about the clearing (pass empty rules) + // This specifically calls the DatadogSampler's on_rules_update method + if let Ok(callbacks) = self.remote_config_callbacks.lock() { + if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { + // Convert empty rules to JSON string for the sampler callback + if let Ok(json_str) = serde_json::to_string(&Vec::::new()) { + callback(&json_str); + } + } + } + } + + /// Add a callback to be called when sampling rules are updated from remote configuration + /// This allows components like the DatadogSampler to be updated without circular imports + /// + /// # Arguments + /// * `key` - A unique identifier for this callback (e.g., "datadog_sampler_on_rules_update") + /// * `callback` - The function to call when sampling rules are updated (receives JSON string) + /// + /// # Example + /// ``` + /// use dd_trace::Config; + /// use std::sync::Arc; + /// + /// let config = Config::builder().build(); + /// config.add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), |json_str| { + /// println!("Received new sampling rules: {}", json_str); + /// // Update your sampler here + /// }); + /// ``` + pub fn add_remote_config_callback(&self, key: String, callback: F) + where + F: Fn(&str) + Send + Sync + 'static, + { + if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { + callbacks.insert(key, Box::new(callback)); + } + } + + /// Remove a specific callback by key + pub fn remove_remote_config_callback(&self, key: &str) { + if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { + callbacks.remove(key); + } + } + + /// Remove all remote config callbacks + pub fn clear_remote_config_callbacks(&self) { + if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { + callbacks.clear(); + } } /// Add an extra service discovered at runtime @@ -621,6 +689,34 @@ impl Config { } } +impl std::fmt::Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Config") + .field("runtime_id", &self.runtime_id) + .field("tracer_version", &self.tracer_version) + .field("language_version", &self.language_version) + .field("service", &self.service) + .field("env", &self.env) + .field("version", &self.version) + .field("global_tags", &self.global_tags) + .field("trace_agent_url", &self.trace_agent_url) + .field("dogstatsd_agent_url", &self.dogstatsd_agent_url) + .field("trace_sampling_rules", &self.trace_sampling_rules) + .field("trace_rate_limit", &self.trace_rate_limit) + .field("enabled", &self.enabled) + .field("log_level_filter", &self.log_level_filter) + .field("trace_stats_computation_enabled", &self.trace_stats_computation_enabled) + .field("trace_propagation_style", &self.trace_propagation_style) + .field("trace_propagation_style_extract", &self.trace_propagation_style_extract) + .field("trace_propagation_style_inject", &self.trace_propagation_style_inject) + .field("trace_propagation_extract_first", &self.trace_propagation_extract_first) + .field("extra_services_tracker", &self.extra_services_tracker) + .field("remote_config_enabled", &self.remote_config_enabled) + .field("remote_config_callbacks", &"") + .finish() + } +} + fn default_config() -> Config { Config { runtime_id: Config::process_runtime_id(), @@ -651,6 +747,7 @@ fn default_config() -> Config { trace_propagation_extract_first: false, extra_services_tracker: ExtraServicesTracker::new(true), remote_config_enabled: true, + remote_config_callbacks: Arc::new(Mutex::new(HashMap::new())), } } @@ -1159,6 +1256,56 @@ mod tests { assert!(config.remote_config_enabled()); // Default is true based on user's change } + #[test] + fn test_sampling_rules_update_callbacks() { + let mut config = Config::builder().build(); + + // Track callback invocations + let callback_called = Arc::new(Mutex::new(false)); + let callback_rules = Arc::new(Mutex::new(Vec::::new())); + + let callback_called_clone = callback_called.clone(); + let callback_rules_clone = callback_rules.clone(); + + config.add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), move |json_str| { + *callback_called_clone.lock().unwrap() = true; + // Parse the JSON string back to rules for testing + if let Ok(rules) = serde_json::from_str::>(json_str) { + *callback_rules_clone.lock().unwrap() = rules; + } + }); + + // Initially callback should not be called + assert!(!*callback_called.lock().unwrap()); + assert!(callback_rules.lock().unwrap().is_empty()); + + // Update rules from remote config + let new_rules = vec![ + SamplingRuleConfig { + sample_rate: 0.5, + service: Some("test-service".to_string()), + provenance: "remote".to_string(), + ..SamplingRuleConfig::default() + } + ]; + + config.update_sampling_rules_from_remote(new_rules.clone()); + + // Callback should be called with the new rules + assert!(*callback_called.lock().unwrap()); + assert_eq!(*callback_rules.lock().unwrap(), new_rules); + + // Test clearing rules + *callback_called.lock().unwrap() = false; + callback_rules.lock().unwrap().clear(); + + config.clear_remote_sampling_rules(); + + // Callback should be called with empty rules + assert!(*callback_called.lock().unwrap()); + assert!(callback_rules.lock().unwrap().is_empty()); + } + #[test] fn test_config_item_priority() { // Test that ConfigItem respects priority: remote_config > code > env_var > default diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 1f5ed759..030880e9 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -239,7 +239,7 @@ struct SignedTargets { /// /// let mut client = RemoteConfigClient::new(config).unwrap(); /// -/// // Set callback to update configuration when new rules arrive +/// // Set multiple callbacks to update configuration when new rules arrive /// let config_clone = mutable_config.clone(); /// client.set_update_callback(move |rules| { /// if let Ok(mut cfg) = config_clone.lock() { @@ -247,6 +247,11 @@ struct SignedTargets { /// } /// }); /// +/// // Add another callback for logging +/// client.set_update_callback(move |rules| { +/// println!("Received {} new sampling rules", rules.len()); +/// }); +/// /// // Start the client in a background thread /// let handle = client.start(); /// ``` @@ -260,8 +265,8 @@ pub struct RemoteConfigClient { state: Arc>, capabilities: ClientCapabilities, poll_interval: Duration, - // Callback to update sampling rules - update_callback: Option) + Send + Sync>>, + // Callbacks to update sampling rules + update_callbacks: Vec) + Send + Sync>>, // Cache of successfully applied configurations cached_target_files: Arc>>, } @@ -294,17 +299,23 @@ impl RemoteConfigClient { state, capabilities: ClientCapabilities::new(), poll_interval: DEFAULT_POLL_INTERVAL, - update_callback: None, + update_callbacks: Vec::new(), cached_target_files: Arc::new(Mutex::new(Vec::new())), }) } - /// Sets the callback to be called when sampling rules are updated + /// Sets a callback to be called when sampling rules are updated + /// Multiple callbacks can be set - all will be called when new rules arrive pub fn set_update_callback(&mut self, callback: F) where F: Fn(Vec) + Send + Sync + 'static, { - self.update_callback = Some(Box::new(callback)); + self.update_callbacks.push(Box::new(callback)); + } + + /// Clears all update callbacks + pub fn clear_update_callbacks(&mut self) { + self.update_callbacks.clear(); } /// Starts the remote configuration client in a background thread @@ -621,8 +632,8 @@ impl RemoteConfigClient { // Extract sampling rules if present if let Some(rules) = tracing_config.tracing_sampling_rules { - // Call the update callback with new rules - if let Some(ref callback) = self.update_callback { + // Call all update callbacks with new rules + for callback in &self.update_callbacks { callback(rules.clone()); } @@ -1131,4 +1142,54 @@ mod tests { let cached_files = client.cached_target_files.lock().unwrap(); assert_eq!(cached_files.len(), 0); } + + #[test] + fn test_multiple_callbacks() { + // Test that multiple callbacks are called when sampling rules are updated + let config = Arc::new(Config::builder().build()); + let mut client = RemoteConfigClient::new(config).unwrap(); + + let callback1_called = Arc::new(Mutex::new(false)); + let callback2_called = Arc::new(Mutex::new(false)); + + let callback1_clone = callback1_called.clone(); + let callback2_clone = callback2_called.clone(); + + // Set multiple callbacks + client.set_update_callback(move |_rules| { + if let Ok(mut called) = callback1_clone.lock() { + *called = true; + } + }); + + client.set_update_callback(move |_rules| { + if let Ok(mut called) = callback2_clone.lock() { + *called = true; + } + }); + + // Process a config response with sampling rules + let config_response = ConfigResponse { + roots: None, + targets: Some("eyJzaWduZWQiOiB7Il90eXBlIjogInRhcmdldHMiLCAiY3VzdG9tIjogeyJvcGFxdWVfYmFja2VuZF9zdGF0ZSI6ICJleUpmb29JT2lBaVltRm9JbjA9In0sICJleHBpcmVzIjogIjIwMjQtMTItMzFUMjM6NTk6NTlaIiwgInNwZWNfdmVyc2lvbiI6ICIxLjAuMCIsICJ0YXJnZXRzIjoge30sICJ2ZXJzaW9uIjogM319Cg==".to_string()), + target_files: Some(vec![ + TargetFile { + path: "datadog/2/APM_TRACING/test-config/config".to_string(), + raw: "eyJ0cmFjaW5nX3NhbXBsaW5nX3J1bGVzIjogW3sic2FtcGxlX3JhdGUiOiAwLjUsICJzZXJ2aWNlIjogInRlc3Qtc2VydmljZSJ9XX0=".to_string(), + }, + ]), + client_configs: Some(vec![ + "datadog/2/APM_TRACING/test-config/config".to_string(), + ]), + }; + + let result = client.process_response(config_response); + assert!(result.is_ok(), "process_response should succeed"); + + // Verify that both callbacks were called + let callback1_result = callback1_called.lock().unwrap(); + let callback2_result = callback2_called.lock().unwrap(); + assert!(*callback1_result, "First callback should have been called"); + assert!(*callback2_result, "Second callback should have been called"); + } } From 5ebbf832f641ae26c4db6afcd3afb687418f24ea Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Sun, 10 Aug 2025 23:23:04 -0400 Subject: [PATCH 06/34] pass config into remoteconfig --- datadog-opentelemetry/src/lib.rs | 17 +-- dd-trace/src/configuration/remote_config.rs | 137 +++++++------------- 2 files changed, 51 insertions(+), 103 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 6488cec8..35f19c0c 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -104,17 +104,12 @@ fn make_tracer( }); } - // Create remote config client - if let Ok(mut client) = dd_trace::configuration::remote_config::RemoteConfigClient::new(config_arc) { - // Set up callback to handle configuration updates - let config_clone = mutable_config.clone(); - client.set_update_callback(move |rules| { - if let Ok(mut cfg) = config_clone.lock() { - cfg.update_sampling_rules_from_remote(rules); - dd_trace::dd_info!("RemoteConfigClient: Applied new sampling rules from remote config"); - } - }); - + // Create remote config client with mutable config + let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); + if let Ok(client) = dd_trace::configuration::remote_config::RemoteConfigClient::new(mutable_config) { + // The client now directly updates the config when new rules arrive + // No need for callbacks - the config is automatically updated + // Start the client in background let _handle = client.start(); dd_trace::dd_info!("RemoteConfigClient: Started remote configuration client"); diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 030880e9..7c1828ba 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -234,23 +234,11 @@ struct SignedTargets { /// use dd_trace::{Config, ConfigBuilder}; /// use dd_trace::configuration::remote_config::RemoteConfigClient; /// -/// let config = Arc::new(ConfigBuilder::new().build()); -/// let mutable_config = Arc::new(Mutex::new(config.clone())); +/// let config = Arc::new(Mutex::new(ConfigBuilder::new().build())); /// -/// let mut client = RemoteConfigClient::new(config).unwrap(); +/// let client = RemoteConfigClient::new(config).unwrap(); /// -/// // Set multiple callbacks to update configuration when new rules arrive -/// let config_clone = mutable_config.clone(); -/// client.set_update_callback(move |rules| { -/// if let Ok(mut cfg) = config_clone.lock() { -/// cfg.update_sampling_rules_from_remote(rules); -/// } -/// }); -/// -/// // Add another callback for logging -/// client.set_update_callback(move |rules| { -/// println!("Received {} new sampling rules", rules.len()); -/// }); +/// // The client directly updates the config when new rules arrive /// /// // Start the client in a background thread /// let handle = client.start(); @@ -259,22 +247,20 @@ pub struct RemoteConfigClient { /// Unique identifier for this client instance /// AIDEV-NOTE: Different from runtime_id - each RemoteConfigClient gets its own UUID client_id: String, - config: Arc, + config: Arc>, agent_url: String, client: reqwest::blocking::Client, state: Arc>, capabilities: ClientCapabilities, poll_interval: Duration, - // Callbacks to update sampling rules - update_callbacks: Vec) + Send + Sync>>, // Cache of successfully applied configurations cached_target_files: Arc>>, } impl RemoteConfigClient { /// Creates a new remote configuration client - pub fn new(config: Arc) -> Result { - let agent_url = format!("{}/v0.7/config", config.trace_agent_url()); + pub fn new(config: Arc>) -> Result { + let agent_url = format!("{}/v0.7/config", config.lock().unwrap().trace_agent_url()); // Create HTTP client with timeout let client = reqwest::blocking::Client::builder() @@ -299,24 +285,11 @@ impl RemoteConfigClient { state, capabilities: ClientCapabilities::new(), poll_interval: DEFAULT_POLL_INTERVAL, - update_callbacks: Vec::new(), cached_target_files: Arc::new(Mutex::new(Vec::new())), }) } - /// Sets a callback to be called when sampling rules are updated - /// Multiple callbacks can be set - all will be called when new rules arrive - pub fn set_update_callback(&mut self, callback: F) - where - F: Fn(Vec) + Send + Sync + 'static, - { - self.update_callbacks.push(Box::new(callback)); - } - /// Clears all update callbacks - pub fn clear_update_callbacks(&mut self) { - self.update_callbacks.clear(); - } /// Starts the remote configuration client in a background thread pub fn start(self) -> thread::JoinHandle<()> { @@ -394,20 +367,22 @@ impl RemoteConfigClient { .lock() .map_err(|_| anyhow::anyhow!("Failed to lock state"))?; + let config = self.config.lock().map_err(|_| anyhow::anyhow!("Failed to lock config"))?; + let client_info = ClientInfo { state: Some(state.clone()), id: self.client_id.clone(), products: vec!["APM_TRACING".to_string()], is_tracer: true, client_tracer: Some(ClientTracer { - runtime_id: self.config.runtime_id().to_string(), + runtime_id: config.runtime_id().to_string(), language: "rust".to_string(), - tracer_version: self.config.tracer_version().to_string(), - service: self.config.service().to_string(), - extra_services: self.config.get_extra_services(), - env: self.config.env().map(|s| s.to_string()), - app_version: self.config.version().map(|s| s.to_string()), - tags: self.config.global_tags().map(|s| s.to_string()).collect(), + tracer_version: config.tracer_version().to_string(), + service: config.service().to_string(), + extra_services: config.get_extra_services(), + env: config.env().map(|s| s.to_string()), + app_version: config.version().map(|s| s.to_string()), + tags: config.global_tags().map(|s| s.to_string()).collect(), }), capabilities: self.capabilities.encode(), }; @@ -632,15 +607,16 @@ impl RemoteConfigClient { // Extract sampling rules if present if let Some(rules) = tracing_config.tracing_sampling_rules { - // Call all update callbacks with new rules - for callback in &self.update_callbacks { - callback(rules.clone()); + // Directly update the config with new rules from remote configuration + if let Ok(mut config) = self.config.lock() { + config.update_sampling_rules_from_remote(rules.clone()); + crate::dd_info!( + "RemoteConfigClient: Applied {} sampling rules from remote config", + rules.len() + ); + } else { + crate::dd_warn!("RemoteConfigClient: Failed to lock config to update sampling rules"); } - - crate::dd_info!( - "RemoteConfigClient: Applied {} sampling rules from remote config", - rules.len() - ); } else { crate::dd_info!( "RemoteConfigClient: APM tracing config received but no sampling rules present" @@ -937,7 +913,7 @@ mod tests { #[test] fn test_validate_signed_target_files() { // Create a mock RemoteConfigClient for testing - let config = Arc::new(Config::builder().build()); + let config = Arc::new(Mutex::new(Config::builder().build())); let client = RemoteConfigClient::new(config).unwrap(); // Test case 1: Target file exists in signed targets @@ -1043,17 +1019,10 @@ mod tests { ]), }; - // Create a RemoteConfigClient and set up a callback to capture processed rules - let config = Arc::new(Config::builder().build()); - let mut client = RemoteConfigClient::new(config).unwrap(); + let config = Arc::new(Mutex::new(Config::builder().build())); + let client = RemoteConfigClient::new(config).unwrap(); - let processed_rules = Arc::new(Mutex::new(Vec::new())); - let rules_clone = processed_rules.clone(); - client.set_update_callback(move |rules| { - if let Ok(mut captured_rules) = rules_clone.lock() { - *captured_rules = rules; - } - }); + // For testing purposes, we'll verify the config was updated by checking the rules // Process the response - this should update the client's state and process APM_TRACING configs let result = client.process_response(config_response); @@ -1085,11 +1054,12 @@ mod tests { assert_eq!(cached_files[0].hashes.len(), 1); assert_eq!(cached_files[0].hashes[0].algorithm, "sha256"); - // Verify that the callback was called with the processed rules - let captured_rules = processed_rules.lock().unwrap(); - assert_eq!(captured_rules.len(), 1); - assert_eq!(captured_rules[0].sample_rate, 0.5); - assert_eq!(captured_rules[0].service, Some("test-service".to_string())); + // Verify that the config was updated with the processed rules + let config = client.config.lock().unwrap(); + let rules = config.trace_sampling_rules(); + assert_eq!(rules.len(), 1); + assert_eq!(rules[0].sample_rate, 0.5); + assert_eq!(rules[0].service, Some("test-service".to_string())); } #[test] @@ -1119,7 +1089,7 @@ mod tests { }; // Create a RemoteConfigClient and process the response - let config = Arc::new(Config::builder().build()); + let config = Arc::new(Mutex::new(Config::builder().build())); let client = RemoteConfigClient::new(config).unwrap(); // Process the response - this should update the client's state @@ -1144,29 +1114,11 @@ mod tests { } #[test] - fn test_multiple_callbacks() { - // Test that multiple callbacks are called when sampling rules are updated - let config = Arc::new(Config::builder().build()); - let mut client = RemoteConfigClient::new(config).unwrap(); - - let callback1_called = Arc::new(Mutex::new(false)); - let callback2_called = Arc::new(Mutex::new(false)); - - let callback1_clone = callback1_called.clone(); - let callback2_clone = callback2_called.clone(); - - // Set multiple callbacks - client.set_update_callback(move |_rules| { - if let Ok(mut called) = callback1_clone.lock() { - *called = true; - } - }); + fn test_config_update_from_remote() { + // Test that the config is updated when sampling rules are received + let config = Arc::new(Mutex::new(Config::builder().build())); + let client = RemoteConfigClient::new(config).unwrap(); - client.set_update_callback(move |_rules| { - if let Ok(mut called) = callback2_clone.lock() { - *called = true; - } - }); // Process a config response with sampling rules let config_response = ConfigResponse { @@ -1186,10 +1138,11 @@ mod tests { let result = client.process_response(config_response); assert!(result.is_ok(), "process_response should succeed"); - // Verify that both callbacks were called - let callback1_result = callback1_called.lock().unwrap(); - let callback2_result = callback2_called.lock().unwrap(); - assert!(*callback1_result, "First callback should have been called"); - assert!(*callback2_result, "Second callback should have been called"); + // Verify that the config was updated with the sampling rules + let config = client.config.lock().unwrap(); + let rules = config.trace_sampling_rules(); + assert_eq!(rules.len(), 1); + assert_eq!(rules[0].sample_rate, 0.5); + assert_eq!(rules[0].service, Some("test-service".to_string())); } } From 7503a0c0a157540613e1c3c41c943edc635ca4ed Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Mon, 11 Aug 2025 13:45:03 -0400 Subject: [PATCH 07/34] clarify passing of sampling rules json through updates --- dd-trace/examples/remote_config.rs | 43 +++-------- dd-trace/src/configuration/configuration.rs | 25 ++++--- dd-trace/src/configuration/remote_config.rs | 81 +++++++++++++-------- 3 files changed, 74 insertions(+), 75 deletions(-) diff --git a/dd-trace/examples/remote_config.rs b/dd-trace/examples/remote_config.rs index 02a018be..f6df4804 100644 --- a/dd-trace/examples/remote_config.rs +++ b/dd-trace/examples/remote_config.rs @@ -16,43 +16,22 @@ fn main() { builder.set_service("remote-config-example".to_string()); builder.set_log_level_filter(dd_trace::log::LevelFilter::Debug); - let config = Arc::new(builder.build()); - let config_mutex = Arc::new(Mutex::new(config.as_ref().clone())); + let config = Arc::new(Mutex::new(builder.build())); println!("Starting remote configuration client..."); - println!( - "Initial sampling rules: {:?}", - config.trace_sampling_rules() - ); - - // Create remote config client - let mut client = - RemoteConfigClient::new(config.clone()).expect("Failed to create remote config client"); - - // Set up callback to handle configuration updates - let config_clone = config_mutex.clone(); - client.set_update_callback(move |rules| { + if let Ok(cfg) = config.lock() { println!( - "Received {} new sampling rules from remote config", - rules.len() + "Initial sampling rules: {:?}", + cfg.trace_sampling_rules() ); + } - if let Ok(mut cfg) = config_clone.lock() { - cfg.update_sampling_rules_from_remote(rules); - println!("Applied new sampling rules"); + // Create remote config client + let client = + RemoteConfigClient::new(config.clone()).expect("Failed to create remote config client"); - // Print current rules - for (i, rule) in cfg.trace_sampling_rules().iter().enumerate() { - println!( - " Rule {}: sample_rate={}, service={:?}, provenance={}", - i + 1, - rule.sample_rate, - rule.service, - rule.provenance - ); - } - } - }); + // The client now directly updates the config when new rules arrive + // No callbacks needed - the config is automatically updated // Start the client in background let _handle = client.start(); @@ -65,7 +44,7 @@ fn main() { thread::sleep(Duration::from_secs(10)); // Periodically print current configuration - if let Ok(cfg) = config_mutex.lock() { + if let Ok(cfg) = config.lock() { println!( "\nCurrent sampling rules count: {}", cfg.trace_sampling_rules().len() diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index 18e22c9c..b036f5f5 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -597,8 +597,12 @@ impl Config { /// Updates sampling rules from remote configuration /// This method is used by the remote config client to apply new rules - pub fn update_sampling_rules_from_remote(&mut self, rules: Vec) { - let parsed_rules = ParsedSamplingRules { rules: rules.clone() }; + pub fn update_sampling_rules_from_remote(&mut self, rules_json: &str) -> Result<(), String> { + // Parse the JSON into SamplingRuleConfig objects + let rules: Vec = serde_json::from_str(rules_json) + .map_err(|e| format!("Failed to parse sampling rules JSON: {}", e))?; + + let parsed_rules = ParsedSamplingRules { rules }; self.trace_sampling_rules .set_value_source(parsed_rules, ConfigSource::RemoteConfig); @@ -606,12 +610,12 @@ impl Config { // This specifically calls the DatadogSampler's on_rules_update method if let Ok(callbacks) = self.remote_config_callbacks.lock() { if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - // Convert rules to JSON string for the sampler callback - if let Ok(json_str) = serde_json::to_string(&rules) { - callback(&json_str); - } + // Pass the rules array JSON directly to the callback (not wrapped in {"rules": ...}) + callback(rules_json); } } + + Ok(()) } /// Clears remote configuration sampling rules, falling back to code/env/default @@ -622,10 +626,8 @@ impl Config { // This specifically calls the DatadogSampler's on_rules_update method if let Ok(callbacks) = self.remote_config_callbacks.lock() { if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - // Convert empty rules to JSON string for the sampler callback - if let Ok(json_str) = serde_json::to_string(&Vec::::new()) { - callback(&json_str); - } + // Pass empty rules as JSON array + callback("[]"); } } } @@ -1289,7 +1291,8 @@ mod tests { } ]; - config.update_sampling_rules_from_remote(new_rules.clone()); + let rules_json = serde_json::to_string(&new_rules).unwrap(); + config.update_sampling_rules_from_remote(&rules_json).unwrap(); // Callback should be called with the new rules assert!(*callback_called.lock().unwrap()); diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 7c1828ba..1f68cc20 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -1,7 +1,7 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use crate::configuration::{Config, SamplingRuleConfig}; +use crate::configuration::Config; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -168,10 +168,8 @@ struct TargetFile { /// See: https://github.com/DataDog/dd-go/blob/main/remote-config/apps/rc-product/schemas/apm-tracing.json #[derive(Debug, Clone, Deserialize)] struct ApmTracingConfig { - /// Sampling rules to apply - /// This field contains an array of sampling rules that can match based on service, name, resource, tags #[serde(default, rename = "tracing_sampling_rules")] - tracing_sampling_rules: Option>, + tracing_sampling_rules: Option, // Add other APM tracing config fields as needed (e.g., tracing_header_tags, etc.) } @@ -602,24 +600,40 @@ impl RemoteConfigClient { /// Processes APM tracing configuration fn process_apm_tracing_config(&self, config_json: &str) -> Result<()> { + // Parse the config to extract sampling rules as raw JSON let tracing_config: ApmTracingConfig = serde_json::from_str(config_json) .map_err(|e| anyhow::anyhow!("Failed to parse APM tracing config: {}", e))?; // Extract sampling rules if present - if let Some(rules) = tracing_config.tracing_sampling_rules { - // Directly update the config with new rules from remote configuration - if let Ok(mut config) = self.config.lock() { - config.update_sampling_rules_from_remote(rules.clone()); + if let Some(rules_value) = tracing_config.tracing_sampling_rules { + if !rules_value.is_null() { + // Convert the raw JSON value to string for the config method + let rules_json = serde_json::to_string(&rules_value) + .map_err(|e| anyhow::anyhow!("Failed to serialize sampling rules: {}", e))?; + + // Directly update the config with the raw JSON + if let Ok(mut config) = self.config.lock() { + match config.update_sampling_rules_from_remote(&rules_json) { + Ok(()) => { + crate::dd_info!( + "RemoteConfigClient: Applied sampling rules from remote config" + ); + } + Err(e) => { + crate::dd_warn!("RemoteConfigClient: Failed to update sampling rules: {}", e); + } + } + } else { + crate::dd_warn!("RemoteConfigClient: Failed to lock config to update sampling rules"); + } + } else { crate::dd_info!( - "RemoteConfigClient: Applied {} sampling rules from remote config", - rules.len() + "RemoteConfigClient: APM tracing config received but tracing_sampling_rules is null" ); - } else { - crate::dd_warn!("RemoteConfigClient: Failed to lock config to update sampling rules"); } } else { crate::dd_info!( - "RemoteConfigClient: APM tracing config received but no sampling rules present" + "RemoteConfigClient: APM tracing config received but no tracing_sampling_rules present" ); } @@ -814,11 +828,14 @@ mod tests { let config: ApmTracingConfig = serde_json::from_str(json).unwrap(); assert!(config.tracing_sampling_rules.is_some()); - let rules = config.tracing_sampling_rules.unwrap(); + let rules_value = config.tracing_sampling_rules.unwrap(); + + // Parse the raw JSON value to verify the content + let rules: Vec = serde_json::from_value(rules_value).unwrap(); assert_eq!(rules.len(), 1); - assert_eq!(rules[0].sample_rate, 0.5); - assert_eq!(rules[0].service, Some("test-service".to_string())); - assert_eq!(rules[0].provenance, "dynamic"); + assert_eq!(rules[0]["sample_rate"], 0.5); + assert_eq!(rules[0]["service"], "test-service"); + assert_eq!(rules[0]["provenance"], "dynamic"); } #[test] @@ -847,26 +864,26 @@ mod tests { let config: ApmTracingConfig = serde_json::from_str(json).unwrap(); assert!(config.tracing_sampling_rules.is_some()); - let rules = config.tracing_sampling_rules.unwrap(); + let rules_value = config.tracing_sampling_rules.unwrap(); + + // Parse the raw JSON value to verify the content + let rules: Vec = serde_json::from_value(rules_value).unwrap(); assert_eq!(rules.len(), 2); // Check first rule - assert_eq!(rules[0].sample_rate, 0.3); - assert_eq!(rules[0].service, Some("web-api".to_string())); - assert_eq!(rules[0].name, Some("GET /users/*".to_string())); - assert_eq!(rules[0].resource, Some("UserController.list".to_string())); - assert_eq!(rules[0].tags.len(), 2); - assert_eq!( - rules[0].tags.get("environment"), - Some(&"production".to_string()) - ); - assert_eq!(rules[0].tags.get("region"), Some(&"us-east-1".to_string())); - assert_eq!(rules[0].provenance, "customer"); + assert_eq!(rules[0]["sample_rate"], 0.3); + assert_eq!(rules[0]["service"], "web-api"); + assert_eq!(rules[0]["name"], "GET /users/*"); + assert_eq!(rules[0]["resource"], "UserController.list"); + assert_eq!(rules[0]["tags"].as_object().unwrap().len(), 2); + assert_eq!(rules[0]["tags"]["environment"], "production"); + assert_eq!(rules[0]["tags"]["region"], "us-east-1"); + assert_eq!(rules[0]["provenance"], "customer"); // Check second rule - assert_eq!(rules[1].sample_rate, 1.0); - assert_eq!(rules[1].service, Some("auth-service".to_string())); - assert_eq!(rules[1].provenance, "dynamic"); + assert_eq!(rules[1]["sample_rate"], 1.0); + assert_eq!(rules[1]["service"], "auth-service"); + assert_eq!(rules[1]["provenance"], "dynamic"); } #[test] From 48b74bb425908d4799d09bb46f7f1a63f1dde324 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Mon, 11 Aug 2025 15:50:24 -0400 Subject: [PATCH 08/34] streamline data for callback and converting json to rules --- .../examples/remote_config.rs | 22 ++-- datadog-opentelemetry/src/lib.rs | 19 +-- datadog-opentelemetry/src/sampler.rs | 21 +--- dd-trace-sampling/src/datadog_sampler.rs | 109 +++++++++-------- dd-trace-sampling/src/rules_sampler.rs | 45 +------ dd-trace/examples/remote_config.rs | 5 +- dd-trace/src/configuration/configuration.rs | 111 ++++++++++-------- dd-trace/src/configuration/remote_config.rs | 41 ++++--- dd-trace/src/lib.rs | 2 +- 9 files changed, 181 insertions(+), 194 deletions(-) diff --git a/datadog-opentelemetry/examples/remote_config.rs b/datadog-opentelemetry/examples/remote_config.rs index d8cb7c24..e14c4047 100644 --- a/datadog-opentelemetry/examples/remote_config.rs +++ b/datadog-opentelemetry/examples/remote_config.rs @@ -35,15 +35,15 @@ fn main() { let config = builder.build(); - println!("Initial sampling rules: {:?}", config.trace_sampling_rules()); + println!( + "Initial sampling rules: {:?}", + config.trace_sampling_rules() + ); // Initialize the OpenTelemetry tracer // This automatically starts the RemoteConfigClient in the background - let tracer_provider = datadog_opentelemetry::init_datadog( - config, - TracerProviderBuilder::default(), - None, - ); + let tracer_provider = + datadog_opentelemetry::init_datadog(config, TracerProviderBuilder::default(), None); let tracer = tracer_provider.tracer("remote-config-example"); @@ -54,14 +54,16 @@ fn main() { // Create some test spans to demonstrate the tracer is working for i in 1..=5 { tracer.in_span("test-operation", |_cx| { - println!("Created span {}", i); + println!("Created span {i}"); // Simulate some work thread::sleep(Duration::from_millis(100)); }); - + thread::sleep(Duration::from_secs(2)); } println!("Example completed. The RemoteConfigClient continues running in the background."); - println!("In a real application, it would keep running and apply any remote configuration updates."); -} \ No newline at end of file + println!( + "In a real application, it would keep running and apply any remote configuration updates." + ); +} diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 35f19c0c..af68487e 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -8,7 +8,7 @@ mod span_processor; mod text_map_propagator; mod trace_id; -use std::sync::{Arc, RwLock, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use opentelemetry::{Key, KeyValue, Value}; use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; @@ -16,7 +16,6 @@ use opentelemetry_semantic_conventions::resource::SERVICE_NAME; use sampler::Sampler; use span_processor::{DatadogSpanProcessor, TraceRegistry}; use text_map_propagator::DatadogPropagator; -use serde_json; /// Initialize the Datadog OpenTelemetry exporter. /// @@ -99,17 +98,19 @@ fn make_tracer( if let Some(sampler_callback) = sampler_callback { let sampler_callback = Arc::new(sampler_callback); let sampler_callback_clone = sampler_callback.clone(); - mutable_config.lock().unwrap().add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), move |json_str| { - sampler_callback_clone(json_str); - }); + mutable_config.lock().unwrap().add_remote_config_callback( + "datadog_sampler_on_rules_update".to_string(), + move |rules| { + sampler_callback_clone(rules); + }, + ); } // Create remote config client with mutable config let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); - if let Ok(client) = dd_trace::configuration::remote_config::RemoteConfigClient::new(mutable_config) { - // The client now directly updates the config when new rules arrive - // No need for callbacks - the config is automatically updated - + if let Ok(client) = + dd_trace::configuration::remote_config::RemoteConfigClient::new(mutable_config) + { // Start the client in background let _handle = client.start(); dd_trace::dd_info!("RemoteConfigClient: Started remote configuration client"); diff --git a/datadog-opentelemetry/src/sampler.rs b/datadog-opentelemetry/src/sampler.rs index c5e56f4e..63cb65ca 100644 --- a/datadog-opentelemetry/src/sampler.rs +++ b/datadog-opentelemetry/src/sampler.rs @@ -12,6 +12,9 @@ use std::{ use crate::{span_processor::RegisterTracePropagationResult, TraceRegistry}; +/// Type alias for sampling rules update callback +type SamplingRulesCallback = Box Fn(&'a [dd_trace::SamplingRuleConfig]) + Send + Sync>; + #[derive(Debug, Clone)] pub struct Sampler { sampler: DatadogSampler, @@ -24,20 +27,8 @@ impl Sampler { resource: Arc>, trace_registry: Arc, ) -> Self { - let rules = cfg - .trace_sampling_rules() - .iter() - .map(|r| { - dd_trace_sampling::SamplingRule::new( - r.sample_rate, - r.service.clone(), - r.name.clone(), - r.resource.clone(), - Some(r.tags.clone()), - Some(r.provenance.clone()), - ) - }) - .collect::>(); + let rules = + dd_trace_sampling::SamplingRule::from_configs(cfg.trace_sampling_rules().to_vec()); let sampler = dd_trace_sampling::DatadogSampler::new(rules, cfg.trace_rate_limit(), resource); Self { @@ -51,7 +42,7 @@ impl Sampler { } /// Get the callback for updating sampling rules - pub fn on_rules_update(&self) -> Box Fn(&'a str) + Send + Sync> { + pub fn on_rules_update(&self) -> SamplingRulesCallback { self.sampler.on_rules_update() } } diff --git a/dd-trace-sampling/src/datadog_sampler.rs b/dd-trace-sampling/src/datadog_sampler.rs index 03afc3ce..b3cd2e4d 100644 --- a/dd-trace-sampling/src/datadog_sampler.rs +++ b/dd-trace-sampling/src/datadog_sampler.rs @@ -24,9 +24,12 @@ use crate::glob_matcher::GlobMatcher; use crate::otel_mappings::PreSampledSpan; use crate::rate_limiter::RateLimiter; use crate::rate_sampler::RateSampler; -use crate::rules_sampler::{RulesSampler, SamplingRulesConfig}; +use crate::rules_sampler::RulesSampler; use crate::utils; +/// Type alias for sampling rules update callback +type SamplingRulesCallback = Box Fn(&'a [dd_trace::SamplingRuleConfig]) + Send + Sync>; + fn matcher_from_rule(rule: &str) -> Option { (rule != NO_RULE).then(|| GlobMatcher::new(rule)) } @@ -51,6 +54,24 @@ pub struct SamplingRule { } impl SamplingRule { + /// Converts a vector of SamplingRuleConfig into SamplingRule objects + /// Centralizes the conversion logic to avoid duplication across different modules + pub fn from_configs(configs: Vec) -> Vec { + configs + .into_iter() + .map(|config| { + Self::new( + config.sample_rate, + config.service, + config.name, + config.resource, + Some(config.tags), + Some(config.provenance), + ) + }) + .collect() + } + /// Creates a new sampling rule pub fn new( sample_rate: f64, @@ -301,33 +322,27 @@ impl DatadogSampler { /// Creates a callback for updating sampling rules from remote configuration /// /// # Returns - /// A boxed function that takes a JSON string and updates the sampling rules + /// A boxed function that takes a slice of SamplingRuleConfig and updates the sampling rules /// /// # Example - /// The callback expects JSON in the following format: - /// ```json - /// { - /// "rules": [ - /// { - /// "sample_rate": 0.5, - /// "service": "web-*", - /// "name": "http.*", - /// "resource": "/api/*", - /// "tags": {"env": "prod"}, - /// "provenance": "customer" - /// } - /// ] - /// } + /// The callback receives sampling rules directly from the configuration: + /// ```rust + /// use dd_trace::SamplingRuleConfig; + /// + /// let rules = &[SamplingRuleConfig { + /// sample_rate: 0.5, + /// service: Some("web-*".to_string()), + /// name: Some("http.*".to_string()), + /// resource: Some("/api/*".to_string()), + /// tags: [("env".to_string(), "prod".to_string())].into(), + /// provenance: "customer".to_string(), + /// }]; /// ``` - pub fn on_rules_update(&self) -> Box Fn(&'a str) + Send + Sync> { + pub fn on_rules_update(&self) -> SamplingRulesCallback { let rules_sampler = self.rules.clone(); - Box::new(move |s: &str| { - let Ok(config) = serde_json::de::from_str::(s) else { - return; - }; - + Box::new(move |rule_configs: &[dd_trace::SamplingRuleConfig]| { // Convert the rule configs to SamplingRule instances - let new_rules = config.into_rules(); + let new_rules = SamplingRule::from_configs(rule_configs.to_vec()); // Update the rules rules_sampler.update_rules(new_rules); @@ -1748,27 +1763,28 @@ mod tests { // Get the callback let callback = sampler.on_rules_update(); - // Create JSON for new rules - let json_config = r#"{ - "rules": [ - { - "sample_rate": 0.5, - "service": "web-*", - "name": "http.*", - "provenance": "customer" - }, - { - "sample_rate": 0.2, - "service": "api-*", - "resource": "/api/*", - "tags": {"env": "prod"}, - "provenance": "dynamic" - } - ] - }"#; + // Create new rules directly as SamplingRuleConfig objects + let new_rules = vec![ + dd_trace::SamplingRuleConfig { + sample_rate: 0.5, + service: Some("web-*".to_string()), + name: Some("http.*".to_string()), + resource: None, + tags: std::collections::HashMap::new(), + provenance: "customer".to_string(), + }, + dd_trace::SamplingRuleConfig { + sample_rate: 0.2, + service: Some("api-*".to_string()), + name: None, + resource: Some("/api/*".to_string()), + tags: [("env".to_string(), "prod".to_string())].into(), + provenance: "dynamic".to_string(), + }, + ]; // Apply the update - callback(json_config); + callback(&new_rules); // Verify the rules were updated assert_eq!(sampler.rules.len(), 2); @@ -1776,7 +1792,8 @@ mod tests { // Test that the new rules work by finding a matching rule // Create attributes that will generate an operation name matching "http.*" let attrs = vec![ - KeyValue::new(HTTP_REQUEST_METHOD, "GET"), // This will make operation name "http.client.request" + KeyValue::new(HTTP_REQUEST_METHOD, "GET"), /* This will make operation name + * "http.client.request" */ ]; let resource_guard = sampler.resource.read().unwrap(); let span = PreSampledSpan::new( @@ -1792,12 +1809,8 @@ mod tests { assert_eq!(rule.sample_rate, 0.5); assert_eq!(rule.provenance, "customer"); - // Test with invalid JSON - should not crash - callback("invalid json"); - assert_eq!(sampler.rules.len(), 2); // Should still have the same rules - // Test with empty rules array - callback(r#"{"rules": []}"#); + callback(&[]); assert_eq!(sampler.rules.len(), 0); // Should now have no rules } } diff --git a/dd-trace-sampling/src/rules_sampler.rs b/dd-trace-sampling/src/rules_sampler.rs index d68d9bf0..cf11a0a2 100644 --- a/dd-trace-sampling/src/rules_sampler.rs +++ b/dd-trace-sampling/src/rules_sampler.rs @@ -1,53 +1,10 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, -}; +use std::sync::{Arc, RwLock}; use crate::datadog_sampler::SamplingRule; -#[derive(Debug, serde::Deserialize)] -pub(crate) struct SamplingRuleConfig { - pub sample_rate: f64, - #[serde(default)] - pub service: Option, - #[serde(default)] - pub name: Option, - #[serde(default)] - pub resource: Option, - #[serde(default)] - pub tags: Option>, - #[serde(default)] - pub provenance: Option, -} - -/// Configuration for sampling rules as received from remote configuration -#[derive(Debug, serde::Deserialize)] -pub(crate) struct SamplingRulesConfig { - pub rules: Vec, -} - -impl SamplingRulesConfig { - /// Converts the configuration into a vector of SamplingRule instances - pub fn into_rules(self) -> Vec { - self.rules - .into_iter() - .map(|config| { - SamplingRule::new( - config.sample_rate, - config.service, - config.name, - config.resource, - config.tags, - config.provenance, - ) - }) - .collect() - } -} - /// Thread-safe container for sampling rules #[derive(Debug, Default, Clone)] pub(crate) struct RulesSampler { diff --git a/dd-trace/examples/remote_config.rs b/dd-trace/examples/remote_config.rs index f6df4804..c2b4b52d 100644 --- a/dd-trace/examples/remote_config.rs +++ b/dd-trace/examples/remote_config.rs @@ -20,10 +20,7 @@ fn main() { println!("Starting remote configuration client..."); if let Ok(cfg) = config.lock() { - println!( - "Initial sampling rules: {:?}", - cfg.trace_sampling_rules() - ); + println!("Initial sampling rules: {:?}", cfg.trace_sampling_rules()); } // Create remote config client diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index b036f5f5..1cc532fd 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -11,6 +11,9 @@ use crate::log::LevelFilter; use super::sources::{CompositeConfigSourceResult, CompositeSource}; +/// Type alias for remote configuration callback functions +type RemoteConfigCallback = Box; + /// Configuration for a single sampling rule #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct SamplingRuleConfig { @@ -390,8 +393,8 @@ pub struct Config { extra_services_tracker: ExtraServicesTracker, /// General callbacks to be called when configuration is updated from remote configuration - /// AIDEV-NOTE: This allows components like the DatadogSampler to be updated without circular imports - remote_config_callbacks: Arc>>>, + /// Allows components like the DatadogSampler to be updated without circular imports + remote_config_callbacks: Arc>>, } impl Config { @@ -596,63 +599,62 @@ impl Config { } /// Updates sampling rules from remote configuration - /// This method is used by the remote config client to apply new rules pub fn update_sampling_rules_from_remote(&mut self, rules_json: &str) -> Result<(), String> { // Parse the JSON into SamplingRuleConfig objects let rules: Vec = serde_json::from_str(rules_json) .map_err(|e| format!("Failed to parse sampling rules JSON: {}", e))?; - + let parsed_rules = ParsedSamplingRules { rules }; self.trace_sampling_rules .set_value_source(parsed_rules, ConfigSource::RemoteConfig); - + // Notify the datadog_sampler_on_rules_update callback about the update // This specifically calls the DatadogSampler's on_rules_update method if let Ok(callbacks) = self.remote_config_callbacks.lock() { if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - // Pass the rules array JSON directly to the callback (not wrapped in {"rules": ...}) - callback(rules_json); + callback(self.trace_sampling_rules()); } } - + Ok(()) } /// Clears remote configuration sampling rules, falling back to code/env/default pub fn clear_remote_sampling_rules(&mut self) { self.trace_sampling_rules.unset_rc(); - + // Notify the datadog_sampler_on_rules_update callback about the clearing (pass empty rules) // This specifically calls the DatadogSampler's on_rules_update method if let Ok(callbacks) = self.remote_config_callbacks.lock() { if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - // Pass empty rules as JSON array - callback("[]"); + // Pass empty rules slice + callback(&[]); } } } /// Add a callback to be called when sampling rules are updated from remote configuration /// This allows components like the DatadogSampler to be updated without circular imports - /// + /// /// # Arguments /// * `key` - A unique identifier for this callback (e.g., "datadog_sampler_on_rules_update") - /// * `callback` - The function to call when sampling rules are updated (receives JSON string) - /// + /// * `callback` - The function to call when sampling rules are updated (receives + /// SamplingRuleConfig slice) + /// /// # Example /// ``` /// use dd_trace::Config; /// use std::sync::Arc; - /// + /// /// let config = Config::builder().build(); - /// config.add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), |json_str| { - /// println!("Received new sampling rules: {}", json_str); + /// config.add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), |rules| { + /// println!("Received {} new sampling rules", rules.len()); /// // Update your sampler here /// }); /// ``` pub fn add_remote_config_callback(&self, key: String, callback: F) where - F: Fn(&str) + Send + Sync + 'static, + F: Fn(&[SamplingRuleConfig]) + Send + Sync + 'static, { if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { callbacks.insert(key, Box::new(callback)); @@ -707,11 +709,23 @@ impl std::fmt::Debug for Config { .field("trace_rate_limit", &self.trace_rate_limit) .field("enabled", &self.enabled) .field("log_level_filter", &self.log_level_filter) - .field("trace_stats_computation_enabled", &self.trace_stats_computation_enabled) + .field( + "trace_stats_computation_enabled", + &self.trace_stats_computation_enabled, + ) .field("trace_propagation_style", &self.trace_propagation_style) - .field("trace_propagation_style_extract", &self.trace_propagation_style_extract) - .field("trace_propagation_style_inject", &self.trace_propagation_style_inject) - .field("trace_propagation_extract_first", &self.trace_propagation_extract_first) + .field( + "trace_propagation_style_extract", + &self.trace_propagation_style_extract, + ) + .field( + "trace_propagation_style_inject", + &self.trace_propagation_style_inject, + ) + .field( + "trace_propagation_extract_first", + &self.trace_propagation_extract_first, + ) .field("extra_services_tracker", &self.extra_services_tracker) .field("remote_config_enabled", &self.remote_config_enabled) .field("remote_config_callbacks", &"") @@ -1261,49 +1275,50 @@ mod tests { #[test] fn test_sampling_rules_update_callbacks() { let mut config = Config::builder().build(); - + // Track callback invocations let callback_called = Arc::new(Mutex::new(false)); let callback_rules = Arc::new(Mutex::new(Vec::::new())); - + let callback_called_clone = callback_called.clone(); let callback_rules_clone = callback_rules.clone(); - - config.add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), move |json_str| { - *callback_called_clone.lock().unwrap() = true; - // Parse the JSON string back to rules for testing - if let Ok(rules) = serde_json::from_str::>(json_str) { - *callback_rules_clone.lock().unwrap() = rules; - } - }); - + + config.add_remote_config_callback( + "datadog_sampler_on_rules_update".to_string(), + move |rules| { + *callback_called_clone.lock().unwrap() = true; + // Store the rules directly for testing + *callback_rules_clone.lock().unwrap() = rules.to_vec(); + }, + ); + // Initially callback should not be called assert!(!*callback_called.lock().unwrap()); assert!(callback_rules.lock().unwrap().is_empty()); - + // Update rules from remote config - let new_rules = vec![ - SamplingRuleConfig { - sample_rate: 0.5, - service: Some("test-service".to_string()), - provenance: "remote".to_string(), - ..SamplingRuleConfig::default() - } - ]; - + let new_rules = vec![SamplingRuleConfig { + sample_rate: 0.5, + service: Some("test-service".to_string()), + provenance: "remote".to_string(), + ..SamplingRuleConfig::default() + }]; + let rules_json = serde_json::to_string(&new_rules).unwrap(); - config.update_sampling_rules_from_remote(&rules_json).unwrap(); - + config + .update_sampling_rules_from_remote(&rules_json) + .unwrap(); + // Callback should be called with the new rules assert!(*callback_called.lock().unwrap()); assert_eq!(*callback_rules.lock().unwrap(), new_rules); - + // Test clearing rules *callback_called.lock().unwrap() = false; callback_rules.lock().unwrap().clear(); - + config.clear_remote_sampling_rules(); - + // Callback should be called with empty rules assert!(*callback_called.lock().unwrap()); assert!(callback_rules.lock().unwrap().is_empty()); diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 1f68cc20..fec4204c 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -143,6 +143,7 @@ struct Hash { struct ConfigResponse { /// Root metadata (TUF roots) - base64 encoded #[serde(default)] + #[allow(dead_code)] // Part of TUF specification but not used in current implementation roots: Option>, /// Targets metadata - base64 encoded JSON #[serde(default)] @@ -193,12 +194,15 @@ struct TargetDesc { struct Targets { /// Type of the targets (usually "targets") #[serde(rename = "_type")] + #[allow(dead_code)] // Part of TUF specification but not used in current implementation target_type: String, /// Custom metadata custom: Option, /// Expiration time + #[allow(dead_code)] // Part of TUF specification but not used in current implementation expires: String, /// Specification version + #[allow(dead_code)] // Part of TUF specification but not used in current implementation spec_version: String, /// Target descriptions (path -> TargetDesc) targets: HashMap, @@ -209,10 +213,12 @@ struct Targets { #[derive(Debug, Deserialize)] struct SignedTargets { /// Signatures (we don't validate these currently) + #[allow(dead_code)] // Part of TUF specification but not used in current implementation signatures: Option>, /// The signed targets data signed: Targets, /// Version of the signed targets + #[allow(dead_code)] // Part of TUF specification but not used in current implementation version: Option, } @@ -228,11 +234,11 @@ struct SignedTargets { /// /// # Example /// ```no_run -/// use std::sync::{Arc, Mutex}; -/// use dd_trace::{Config, ConfigBuilder}; /// use dd_trace::configuration::remote_config::RemoteConfigClient; +/// use dd_trace::Config; +/// use std::sync::{Arc, Mutex}; /// -/// let config = Arc::new(Mutex::new(ConfigBuilder::new().build())); +/// let config = Arc::new(Mutex::new(Config::builder().build())); /// /// let client = RemoteConfigClient::new(config).unwrap(); /// @@ -287,8 +293,6 @@ impl RemoteConfigClient { }) } - - /// Starts the remote configuration client in a background thread pub fn start(self) -> thread::JoinHandle<()> { thread::spawn(move || { @@ -365,7 +369,10 @@ impl RemoteConfigClient { .lock() .map_err(|_| anyhow::anyhow!("Failed to lock state"))?; - let config = self.config.lock().map_err(|_| anyhow::anyhow!("Failed to lock config"))?; + let config = self + .config + .lock() + .map_err(|_| anyhow::anyhow!("Failed to lock config"))?; let client_info = ClientInfo { state: Some(state.clone()), @@ -611,7 +618,6 @@ impl RemoteConfigClient { let rules_json = serde_json::to_string(&rules_value) .map_err(|e| anyhow::anyhow!("Failed to serialize sampling rules: {}", e))?; - // Directly update the config with the raw JSON if let Ok(mut config) = self.config.lock() { match config.update_sampling_rules_from_remote(&rules_json) { Ok(()) => { @@ -620,11 +626,16 @@ impl RemoteConfigClient { ); } Err(e) => { - crate::dd_warn!("RemoteConfigClient: Failed to update sampling rules: {}", e); + crate::dd_warn!( + "RemoteConfigClient: Failed to update sampling rules: {}", + e + ); } } } else { - crate::dd_warn!("RemoteConfigClient: Failed to lock config to update sampling rules"); + crate::dd_warn!( + "RemoteConfigClient: Failed to lock config to update sampling rules" + ); } } else { crate::dd_info!( @@ -829,7 +840,7 @@ mod tests { let config: ApmTracingConfig = serde_json::from_str(json).unwrap(); assert!(config.tracing_sampling_rules.is_some()); let rules_value = config.tracing_sampling_rules.unwrap(); - + // Parse the raw JSON value to verify the content let rules: Vec = serde_json::from_value(rules_value).unwrap(); assert_eq!(rules.len(), 1); @@ -865,7 +876,7 @@ mod tests { let config: ApmTracingConfig = serde_json::from_str(json).unwrap(); assert!(config.tracing_sampling_rules.is_some()); let rules_value = config.tracing_sampling_rules.unwrap(); - + // Parse the raw JSON value to verify the content let rules: Vec = serde_json::from_value(rules_value).unwrap(); assert_eq!(rules.len(), 2); @@ -1041,7 +1052,8 @@ mod tests { // For testing purposes, we'll verify the config was updated by checking the rules - // Process the response - this should update the client's state and process APM_TRACING configs + // Process the response - this should update the client's state and process APM_TRACING + // configs let result = client.process_response(config_response); assert!(result.is_ok(), "process_response should succeed"); @@ -1052,7 +1064,7 @@ mod tests { state.backend_client_state, Some("eyJfooIOiAiYmFoIn0=".to_string()) ); - assert_eq!(state.has_error, false); + assert!(!state.has_error); // Verify that APM_TRACING config states were added assert_eq!(state.config_states.len(), 1); @@ -1120,7 +1132,7 @@ mod tests { state.backend_client_state, Some("eyJfooIOiAiYmFoIn0=".to_string()) ); - assert_eq!(state.has_error, false); + assert!(!state.has_error); // Verify that no config states were added since we don't process non-APM_TRACING products assert_eq!(state.config_states.len(), 0); @@ -1136,7 +1148,6 @@ mod tests { let config = Arc::new(Mutex::new(Config::builder().build())); let client = RemoteConfigClient::new(config).unwrap(); - // Process a config response with sampling rules let config_response = ConfigResponse { roots: None, diff --git a/dd-trace/src/lib.rs b/dd-trace/src/lib.rs index 48bf0ee2..581f6731 100644 --- a/dd-trace/src/lib.rs +++ b/dd-trace/src/lib.rs @@ -4,7 +4,7 @@ pub mod configuration; pub mod constants; pub mod sampling; -pub use configuration::Config; +pub use configuration::{Config, ConfigBuilder, SamplingRuleConfig}; mod error; pub use error::{Error, Result}; From bef6fa2ad6c9476fb61368ba80a2d143c36cfcb8 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Mon, 11 Aug 2025 17:28:20 -0400 Subject: [PATCH 09/34] fallback only with empty incoming rc array --- dd-trace/src/configuration/configuration.rs | 79 +++++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index 1cc532fd..eef578c9 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -604,15 +604,20 @@ impl Config { let rules: Vec = serde_json::from_str(rules_json) .map_err(|e| format!("Failed to parse sampling rules JSON: {}", e))?; - let parsed_rules = ParsedSamplingRules { rules }; - self.trace_sampling_rules - .set_value_source(parsed_rules, ConfigSource::RemoteConfig); - - // Notify the datadog_sampler_on_rules_update callback about the update - // This specifically calls the DatadogSampler's on_rules_update method - if let Ok(callbacks) = self.remote_config_callbacks.lock() { - if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - callback(self.trace_sampling_rules()); + // If remote config sends empty rules, clear remote config to fall back to local rules + if rules.is_empty() { + self.clear_remote_sampling_rules(); + } else { + let parsed_rules = ParsedSamplingRules { rules }; + self.trace_sampling_rules + .set_value_source(parsed_rules, ConfigSource::RemoteConfig); + + // Notify the datadog_sampler_on_rules_update callback about the update + // This specifically calls the DatadogSampler's on_rules_update method + if let Ok(callbacks) = self.remote_config_callbacks.lock() { + if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { + callback(self.trace_sampling_rules()); + } } } @@ -623,12 +628,10 @@ impl Config { pub fn clear_remote_sampling_rules(&mut self) { self.trace_sampling_rules.unset_rc(); - // Notify the datadog_sampler_on_rules_update callback about the clearing (pass empty rules) - // This specifically calls the DatadogSampler's on_rules_update method if let Ok(callbacks) = self.remote_config_callbacks.lock() { if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - // Pass empty rules slice - callback(&[]); + // Now that rc_value is cleared, this will return the fallback rules + callback(self.trace_sampling_rules()); } } } @@ -1319,7 +1322,7 @@ mod tests { config.clear_remote_sampling_rules(); - // Callback should be called with empty rules + // Callback should be called with fallback rules (empty in this case since no env/code rules set) assert!(*callback_called.lock().unwrap()); assert!(callback_rules.lock().unwrap().is_empty()); } @@ -1407,4 +1410,52 @@ mod tests { "code-service" ); } + + #[test] + fn test_empty_remote_rules_fallback_behavior() { + let mut config = Config::builder().build(); + + // 1. Set up local rules via environment variable simulation + let local_rules = ParsedSamplingRules { + rules: vec![SamplingRuleConfig { + sample_rate: 0.3, + service: Some("local-service".to_string()), + provenance: "local".to_string(), + ..SamplingRuleConfig::default() + }], + }; + config.trace_sampling_rules.set_value_source(local_rules.clone(), ConfigSource::EnvVar); + + // Verify local rules are active + assert_eq!(config.trace_sampling_rules().len(), 1); + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.3); + assert_eq!(config.trace_sampling_rules.source(), ConfigSource::EnvVar); + + // 2. Remote config sends non-empty rules + let remote_rules_json = r#"[{"sample_rate": 0.8, "service": "remote-service", "provenance": "remote"}]"#; + config.update_sampling_rules_from_remote(remote_rules_json).unwrap(); + + // Verify remote rules override local rules + assert_eq!(config.trace_sampling_rules().len(), 1); + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.8); + assert_eq!(config.trace_sampling_rules.source(), ConfigSource::RemoteConfig); + + // 3. Remote config sends empty array [] + let empty_remote_rules_json = "[]"; + config.update_sampling_rules_from_remote(empty_remote_rules_json).unwrap(); + + // NEW BEHAVIOR: Empty remote rules now automatically fall back to local rules + assert_eq!(config.trace_sampling_rules().len(), 1); // Falls back to local rules + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.3); // Local rule values + assert_eq!(config.trace_sampling_rules.source(), ConfigSource::EnvVar); // Back to env source! + + // 4. Verify explicit clearing still works (for completeness) + // Since we're already on local rules, clear should keep us on local rules + config.clear_remote_sampling_rules(); + + // Should remain on local rules + assert_eq!(config.trace_sampling_rules().len(), 1); + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.3); + assert_eq!(config.trace_sampling_rules.source(), ConfigSource::EnvVar); + } } From 138452c28578080b382858ed5301883dfd135115 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Mon, 11 Aug 2025 17:44:54 -0400 Subject: [PATCH 10/34] add extra services in span processor --- datadog-opentelemetry/src/span_processor.rs | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/datadog-opentelemetry/src/span_processor.rs b/datadog-opentelemetry/src/span_processor.rs index 2bfc42ba..c147aff1 100644 --- a/datadog-opentelemetry/src/span_processor.rs +++ b/datadog-opentelemetry/src/span_processor.rs @@ -408,6 +408,37 @@ impl DatadogSpanProcessor { trace.finished_spans } + + /// Extracts the service name from a span and adds it to the config's extra services tracking. + /// This allows discovery of all services at runtime for proper remote configuration. + fn extract_and_add_service_from_span(&self, span: &SpanData) { + // First check span attributes for service.name + if let Some(service_name) = span.attributes.iter().find_map(|kv| { + if kv.key.as_str() == "service.name" { + Some(kv.value.to_string()) + } else { + None + } + }) { + // Only add if it's not the default service name + if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { + self.config.add_extra_service(&service_name); + } + return; + } + + // If not found in span attributes, check resource attributes + if let Ok(resource) = self.resource.read() { + let service_key = opentelemetry::Key::from_static_str("service.name"); + if let Some(service_attr) = resource.get(&service_key) { + let service_name = service_attr.to_string(); + // Only add if it's not the default service name + if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { + self.config.add_extra_service(&service_name); + } + } + } + } } impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { @@ -437,6 +468,11 @@ impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { fn on_end(&self, span: SpanData) { let trace_id = span.span_context.trace_id().to_bytes(); + + // AIDEV-NOTE: Extract service name from span and add to extra services for remote config + // This allows discovery of all services at runtime for proper remote configuration + self.extract_and_add_service_from_span(&span); + let Some(trace) = self.registry.finish_span(trace_id, span) else { return; }; @@ -626,4 +662,6 @@ mod tests { Some(Value::String("otel-service".into())) ); } + + } From 13b8f2a609259a42bfa99b10cdf6ee15af238d68 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 01:36:59 -0400 Subject: [PATCH 11/34] remove unused methods, add more tests for rc, remove example --- .../examples/remote_config.rs | 69 ------ datadog-opentelemetry/src/lib.rs | 2 +- datadog-opentelemetry/src/span_processor.rs | 2 +- datadog-opentelemetry/tests/mod.rs | 14 +- dd-trace/src/configuration/configuration.rs | 30 +-- dd-trace/src/configuration/remote_config.rs | 198 +++++++++++++++++- 6 files changed, 212 insertions(+), 103 deletions(-) delete mode 100644 datadog-opentelemetry/examples/remote_config.rs diff --git a/datadog-opentelemetry/examples/remote_config.rs b/datadog-opentelemetry/examples/remote_config.rs deleted file mode 100644 index e14c4047..00000000 --- a/datadog-opentelemetry/examples/remote_config.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -//! Example demonstrating automatic remote configuration integration -//! -//! This example shows how the datadog-opentelemetry crate automatically -//! initializes the RemoteConfigClient when remote configuration is enabled. -//! -//! Run this example with: -//! ```bash -//! cargo run --example remote_config -//! ``` -//! -//! The example will: -//! 1. Create a configuration with remote config enabled -//! 2. Initialize the OpenTelemetry tracer (which automatically starts the RemoteConfigClient) -//! 3. Create some test spans -//! 4. Keep running to demonstrate the remote config client working in the background - -use std::thread; -use std::time::Duration; - -use dd_trace::Config; -use opentelemetry::trace::{Tracer, TracerProvider}; -use opentelemetry_sdk::trace::TracerProviderBuilder; - -fn main() { - println!("Starting remote configuration example..."); - - // Create configuration with remote config enabled - let mut builder = Config::builder(); - builder.set_service("remote-config-example".to_string()); - builder.set_remote_config_enabled(true); // Enable remote configuration - builder.set_log_level_filter(dd_trace::log::LevelFilter::Debug); - - let config = builder.build(); - - println!( - "Initial sampling rules: {:?}", - config.trace_sampling_rules() - ); - - // Initialize the OpenTelemetry tracer - // This automatically starts the RemoteConfigClient in the background - let tracer_provider = - datadog_opentelemetry::init_datadog(config, TracerProviderBuilder::default(), None); - - let tracer = tracer_provider.tracer("remote-config-example"); - - println!("Tracer initialized. RemoteConfigClient is running in the background."); - println!("The client will automatically poll for configuration updates every 5 seconds."); - println!("Press Ctrl+C to exit"); - - // Create some test spans to demonstrate the tracer is working - for i in 1..=5 { - tracer.in_span("test-operation", |_cx| { - println!("Created span {i}"); - // Simulate some work - thread::sleep(Duration::from_millis(100)); - }); - - thread::sleep(Duration::from_secs(2)); - } - - println!("Example completed. The RemoteConfigClient continues running in the background."); - println!( - "In a real application, it would keep running and apply any remote configuration updates." - ); -} diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index af68487e..70d5866f 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -90,7 +90,7 @@ fn make_tracer( // Initialize remote configuration client if enabled if config.remote_config_enabled() { - // AIDEV-NOTE: Create a mutable config that can be updated by remote config + // Create a mutable config that can be updated by remote config let config_arc = Arc::new(config); let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); diff --git a/datadog-opentelemetry/src/span_processor.rs b/datadog-opentelemetry/src/span_processor.rs index c147aff1..075947af 100644 --- a/datadog-opentelemetry/src/span_processor.rs +++ b/datadog-opentelemetry/src/span_processor.rs @@ -469,7 +469,7 @@ impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { fn on_end(&self, span: SpanData) { let trace_id = span.span_context.trace_id().to_bytes(); - // AIDEV-NOTE: Extract service name from span and add to extra services for remote config + // Extract service name from span and add to extra services for remote config // This allows discovery of all services at runtime for proper remote configuration self.extract_and_add_service_from_span(&span); diff --git a/datadog-opentelemetry/tests/mod.rs b/datadog-opentelemetry/tests/mod.rs index ac7694ea..6425f876 100644 --- a/datadog-opentelemetry/tests/mod.rs +++ b/datadog-opentelemetry/tests/mod.rs @@ -237,9 +237,8 @@ mod datadog_test_agent { #[test] fn test_remote_config_initialization() { // Test that remote config client is initialized when remote config is enabled - let mut config_builder = dd_trace::Config::builder(); - config_builder.set_remote_config_enabled(true); - let config = config_builder.build(); + std::env::set_var("DD_REMOTE_CONFIGURATION_ENABLED", "true"); + let config = dd_trace::Config::builder().build(); // This should initialize the remote config client let (tracer_provider, _propagator) = make_test_tracer( @@ -250,14 +249,15 @@ mod datadog_test_agent { // Verify the tracer provider was created successfully let _tracer = tracer_provider.tracer("test"); // If we get here, the tracer provider was created successfully + + std::env::remove_var("DD_REMOTE_CONFIGURATION_ENABLED"); } #[test] fn test_remote_config_disabled() { // Test that remote config client is not initialized when remote config is disabled - let mut config_builder = dd_trace::Config::builder(); - config_builder.set_remote_config_enabled(false); - let config = config_builder.build(); + std::env::set_var("DD_REMOTE_CONFIGURATION_ENABLED", "false"); + let config = dd_trace::Config::builder().build(); // This should not initialize the remote config client let (tracer_provider, _propagator) = make_test_tracer( @@ -268,6 +268,8 @@ mod datadog_test_agent { // Verify the tracer provider was created successfully let _tracer = tracer_provider.tracer("test"); // If we get here, the tracer provider was created successfully + + std::env::remove_var("DD_REMOTE_CONFIGURATION_ENABLED"); } #[track_caller] diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index eef578c9..c94338a8 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -664,20 +664,6 @@ impl Config { } } - /// Remove a specific callback by key - pub fn remove_remote_config_callback(&self, key: &str) { - if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { - callbacks.remove(key); - } - } - - /// Remove all remote config callbacks - pub fn clear_remote_config_callbacks(&self) { - if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { - callbacks.clear(); - } - } - /// Add an extra service discovered at runtime /// This is used for remote configuration pub fn add_extra_service(&self, service_name: &str) { @@ -867,13 +853,6 @@ impl ConfigBuilder { self } - pub fn set_remote_config_enabled(&mut self, enabled: bool) -> &mut Self { - self.config.remote_config_enabled = enabled; - // Also update the extra services tracker - self.config.extra_services_tracker = ExtraServicesTracker::new(enabled); - self - } - #[cfg(feature = "test-utils")] pub fn __internal_set_wait_agent_info_ready( &mut self, @@ -1211,9 +1190,14 @@ mod tests { #[test] fn test_extra_services_disabled_when_remote_config_disabled() { - let config = Config::builder() + // Use environment variable to disable remote config + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [("DD_REMOTE_CONFIGURATION_ENABLED", "false")], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources) .set_service("main-service".to_string()) - .set_remote_config_enabled(false) .build(); // Add services when remote config is disabled diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index fec4204c..23c2263c 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -175,7 +175,7 @@ struct ApmTracingConfig { } /// TUF targets metadata -/// AIDEV-NOTE: This is just an alias for SignedTargets to match the JSON structure +/// This is just an alias for SignedTargets to match the JSON structure type TargetsMetadata = SignedTargets; /// Target description matching Python's TargetDesc @@ -249,7 +249,7 @@ struct SignedTargets { /// ``` pub struct RemoteConfigClient { /// Unique identifier for this client instance - /// AIDEV-NOTE: Different from runtime_id - each RemoteConfigClient gets its own UUID + /// Different from runtime_id - each RemoteConfigClient gets its own UUID client_id: String, config: Arc>, agent_url: String, @@ -273,7 +273,7 @@ impl RemoteConfigClient { .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?; let state = Arc::new(Mutex::new(ClientState { - root_version: 1, // AIDEV-NOTE: Agent requires >= 1 (base TUF director root) + root_version: 1, // Agent requires >= 1 (base TUF director root) targets_version: 0, config_states: Vec::new(), has_error: false, @@ -1173,4 +1173,196 @@ mod tests { assert_eq!(rules[0].sample_rate, 0.5); assert_eq!(rules[0].service, Some("test-service".to_string())); } + + #[test] + fn test_tuf_targets_parsing() { + // Test parsing of a realistic TUF targets file structure + // Based on the example provided in the user query + let tuf_targets_json = r#"{ + "signatures": [ + { + "keyid": "5c4ece41241a1bb513f6e3e5df74ab7d5183dfffbd71bfd43127920d880569fd", + "sig": "4dd483db8b4aff81a9afd2ed4eaeb23fe3aca9a148a7a8942e24e8c5ef911e2692f94492b882727b257dacfbf6bcea09d6e26ea28ac145fcb4254ea046be3b03" + } + ], + "signed": { + "_type": "targets", + "custom": { + "opaque_backend_state": "eyJ2ZXJzaW9uIjoxLCJzdGF0ZSI6eyJmaWxlX2hhc2hlcyI6WyJGZXJOT1FyMStmTThKWk9TY0crZllucnhXMWpKN0w0ZlB5aGtxUWVCT3dJPSIsInd1aW9BVm1Qcy9oNEpXMDh1dnI1bi9meERLQ3lKdG1sQmRjaDNOcFdLZDg9IiwiOGFDYVJFc3hIV3R3SFNFWm5SV0pJYmtENXVBNUtETENoZG8vZ0RNdnJJMD0iXX19" + }, + "expires": "2022-09-22T09:01:04Z", + "spec_version": "1.0.0", + "targets": { + "datadog/2/APM_SAMPLING/dynamic_rates/config": { + "custom": { + "v": 27423 + }, + "hashes": { + "sha256": "c2e8a801598fb3f878256d3cbafaf99ff7f10ca0b226d9a505d721dcda5629df" + }, + "length": 58409 + }, + "employee/ASM_DD/1.recommended.json/config": { + "custom": { + "v": 1 + }, + "hashes": { + "sha256": "15eacd390af5f9f33c259392706f9f627af15b58c9ecbe1f3f2864a907813b02" + }, + "length": 235228 + }, + "employee/CWS_DD/4.default.policy/config": { + "custom": { + "v": 1 + }, + "hashes": { + "sha256": "f1a09a444b311d6b701d21199d158921b903e6e0392832c285da3f80332fac8d" + }, + "length": 34777 + } + }, + "version": 23755701 + } +}"#; + + // Parse the TUF targets structure + let targets: SignedTargets = serde_json::from_str(tuf_targets_json) + .expect("Should successfully parse TUF targets JSON"); + + // Verify signatures array is parsed correctly + assert!(targets.signatures.is_some()); + let signatures = targets.signatures.unwrap(); + assert_eq!(signatures.len(), 1); + + // Verify the signed targets structure + assert_eq!(targets.signed.target_type, "targets"); + assert_eq!(targets.signed.expires, "2022-09-22T09:01:04Z"); + assert_eq!(targets.signed.spec_version, "1.0.0"); + assert_eq!(targets.signed.version, 23755701); + + // Verify custom metadata with opaque_backend_state + assert!(targets.signed.custom.is_some()); + let custom = targets.signed.custom.unwrap(); + let backend_state = custom.get("opaque_backend_state") + .and_then(|v| v.as_str()) + .expect("Should have opaque_backend_state"); + assert_eq!(backend_state, "eyJ2ZXJzaW9uIjoxLCJzdGF0ZSI6eyJmaWxlX2hhc2hlcyI6WyJGZXJOT1FyMStmTThKWk9TY0crZllucnhXMWpKN0w0ZlB5aGtxUWVCT3dJPSIsInd1aW9BVm1Qcy9oNEpXMDh1dnI1bi9meERLQ3lKdG1sQmRjaDNOcFdLZDg9IiwiOGFDYVJFc3hIV3R3SFNFWm5SV0pJYmtENXVBNUtETENoZG8vZ0RNdnJJMD0iXX19"); + + // Verify targets parsing + assert_eq!(targets.signed.targets.len(), 3); + + // Test APM_SAMPLING target + let apm_sampling = targets.signed.targets.get("datadog/2/APM_SAMPLING/dynamic_rates/config") + .expect("Should have APM_SAMPLING target"); + assert_eq!(apm_sampling.length, 58409); + assert_eq!( + apm_sampling.hashes.get("sha256").unwrap(), + "c2e8a801598fb3f878256d3cbafaf99ff7f10ca0b226d9a505d721dcda5629df" + ); + let apm_custom = apm_sampling.custom.as_ref().unwrap(); + assert_eq!(apm_custom.get("v").unwrap().as_u64().unwrap(), 27423); + + // Test ASM_DD target + let asm_dd = targets.signed.targets.get("employee/ASM_DD/1.recommended.json/config") + .expect("Should have ASM_DD target"); + assert_eq!(asm_dd.length, 235228); + assert_eq!( + asm_dd.hashes.get("sha256").unwrap(), + "15eacd390af5f9f33c259392706f9f627af15b58c9ecbe1f3f2864a907813b02" + ); + let asm_custom = asm_dd.custom.as_ref().unwrap(); + assert_eq!(asm_custom.get("v").unwrap().as_u64().unwrap(), 1); + + // Test CWS_DD target + let cws_dd = targets.signed.targets.get("employee/CWS_DD/4.default.policy/config") + .expect("Should have CWS_DD target"); + assert_eq!(cws_dd.length, 34777); + assert_eq!( + cws_dd.hashes.get("sha256").unwrap(), + "f1a09a444b311d6b701d21199d158921b903e6e0392832c285da3f80332fac8d" + ); + let cws_custom = cws_dd.custom.as_ref().unwrap(); + assert_eq!(cws_custom.get("v").unwrap().as_u64().unwrap(), 1); + } + + #[test] + fn test_tuf_targets_integration_with_remote_config() { + // Test that we can process a TUF targets response through the remote config system + let config = Arc::new(Mutex::new(Config::builder().build())); + let client = RemoteConfigClient::new(config).unwrap(); + + // Create a realistic TUF targets JSON and base64 encode it + let tuf_targets_json = r#"{ + "signatures": [ + { + "keyid": "5c4ece41241a1bb513f6e3e5df74ab7d5183dfffbd71bfd43127920d880569fd", + "sig": "4dd483db8b4aff81a9afd2ed4eaeb23fe3aca9a148a7a8942e24e8c5ef911e2692f94492b882727b257dacfbf6bcea09d6e26ea28ac145fcb4254ea046be3b03" + } + ], + "signed": { + "_type": "targets", + "custom": { + "opaque_backend_state": "eyJ2ZXJzaW9uIjoxLCJzdGF0ZSI6eyJmaWxlX2hhc2hlcyI6WyJGZXJOT1FyMStmTThKWk9TY0crZllucnhXMWpKN0w0ZlB5aGtxUWVCT3dJPSIsInd1aW9BVm1Qcy9oNEpXMDh1dnI1bi9meERLQ3lKdG1sQmRjaDNOcFdLZDg9IiwiOGFDYVJFc3hIV3R3SFNFWm5SV0pJYmtENXVBNUtETENoZG8vZ0RNdnJJMD0iXX19" + }, + "expires": "2022-09-22T09:01:04Z", + "spec_version": "1.0.0", + "targets": { + "datadog/2/APM_TRACING/test-sampling/config": { + "custom": { + "v": 100 + }, + "hashes": { + "sha256": "c2e8a801598fb3f878256d3cbafaf99ff7f10ca0b226d9a505d721dcda5629df" + }, + "length": 58409 + } + }, + "version": 23755701 + } +}"#; + + use base64::Engine; + let encoded_targets = base64::engine::general_purpose::STANDARD.encode(tuf_targets_json.as_bytes()); + + // Create a config response with the TUF targets and a corresponding target file + let config_response = ConfigResponse { + roots: None, + targets: Some(encoded_targets), + target_files: Some(vec![ + TargetFile { + path: "datadog/2/APM_TRACING/test-sampling/config".to_string(), + raw: "eyJ0cmFjaW5nX3NhbXBsaW5nX3J1bGVzIjogW3sic2FtcGxlX3JhdGUiOiAwLjc1LCAic2VydmljZSI6ICJ0ZXN0LWFwcC1zZXJ2aWNlIn1dfQ==".to_string(), // base64 encoded sampling rules + }, + ]), + client_configs: Some(vec![ + "datadog/2/APM_TRACING/test-sampling/config".to_string(), + ]), + }; + + // Process the response + let result = client.process_response(config_response); + assert!(result.is_ok(), "process_response should succeed: {:?}", result); + + // Verify state was updated with targets metadata + let state = client.state.lock().unwrap(); + assert_eq!(state.targets_version, 23755701); + assert_eq!( + state.backend_client_state, + Some("eyJ2ZXJzaW9uIjoxLCJzdGF0ZSI6eyJmaWxlX2hhc2hlcyI6WyJGZXJOT1FyMStmTThKWk9TY0crZllucnhXMWpKN0w0ZlB5aGtxUWVCT3dJPSIsInd1aW9BVm1Qcy9oNEpXMDh1dnI1bi9meERLQ3lKdG1sQmRjaDNOcFdLZDg9IiwiOGFDYVJFc3hIV3R3SFNFWm5SV0pJYmtENXVBNUtETENoZG8vZ0RNdnJJMD0iXX19".to_string()) + ); + + // Verify config states were updated with version from targets custom.v + assert_eq!(state.config_states.len(), 1); + let config_state = &state.config_states[0]; + assert!(config_state.id == "test-sampling" || config_state.id == "apm-tracing-sampling"); + assert_eq!(config_state.version, 100); // From custom.v in targets + assert_eq!(config_state.product, "APM_TRACING"); + + // Verify that the sampling rules were applied to the config + let config = client.config.lock().unwrap(); + let rules = config.trace_sampling_rules(); + assert_eq!(rules.len(), 1); + assert_eq!(rules[0].sample_rate, 0.75); + assert_eq!(rules[0].service, Some("test-app-service".to_string())); + } } From 126febf53cdf7c94ac3e23fd03f40655c32db3f8 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 14:07:52 -0400 Subject: [PATCH 12/34] only log debug,lint --- datadog-opentelemetry/src/lib.rs | 2 +- datadog-opentelemetry/src/span_processor.rs | 6 ++--- datadog-opentelemetry/tests/mod.rs | 4 +-- dd-trace/src/configuration/configuration.rs | 26 ++++++++++++------ dd-trace/src/configuration/remote_config.rs | 30 +++++++++++++++------ 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 70d5866f..0b89ae81 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -90,7 +90,7 @@ fn make_tracer( // Initialize remote configuration client if enabled if config.remote_config_enabled() { - // Create a mutable config that can be updated by remote config + // Create a mutable config that can be updated by remote config let config_arc = Arc::new(config); let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); diff --git a/datadog-opentelemetry/src/span_processor.rs b/datadog-opentelemetry/src/span_processor.rs index 075947af..367f99d5 100644 --- a/datadog-opentelemetry/src/span_processor.rs +++ b/datadog-opentelemetry/src/span_processor.rs @@ -468,11 +468,11 @@ impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { fn on_end(&self, span: SpanData) { let trace_id = span.span_context.trace_id().to_bytes(); - + // Extract service name from span and add to extra services for remote config // This allows discovery of all services at runtime for proper remote configuration self.extract_and_add_service_from_span(&span); - + let Some(trace) = self.registry.finish_span(trace_id, span) else { return; }; @@ -662,6 +662,4 @@ mod tests { Some(Value::String("otel-service".into())) ); } - - } diff --git a/datadog-opentelemetry/tests/mod.rs b/datadog-opentelemetry/tests/mod.rs index 6425f876..bf0b00e5 100644 --- a/datadog-opentelemetry/tests/mod.rs +++ b/datadog-opentelemetry/tests/mod.rs @@ -249,7 +249,7 @@ mod datadog_test_agent { // Verify the tracer provider was created successfully let _tracer = tracer_provider.tracer("test"); // If we get here, the tracer provider was created successfully - + std::env::remove_var("DD_REMOTE_CONFIGURATION_ENABLED"); } @@ -268,7 +268,7 @@ mod datadog_test_agent { // Verify the tracer provider was created successfully let _tracer = tracer_provider.tracer("test"); // If we get here, the tracer provider was created successfully - + std::env::remove_var("DD_REMOTE_CONFIGURATION_ENABLED"); } diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index c94338a8..a089f58b 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -602,7 +602,7 @@ impl Config { pub fn update_sampling_rules_from_remote(&mut self, rules_json: &str) -> Result<(), String> { // Parse the JSON into SamplingRuleConfig objects let rules: Vec = serde_json::from_str(rules_json) - .map_err(|e| format!("Failed to parse sampling rules JSON: {}", e))?; + .map_err(|e| format!("Failed to parse sampling rules JSON: {e}"))?; // If remote config sends empty rules, clear remote config to fall back to local rules if rules.is_empty() { @@ -1217,7 +1217,7 @@ mod tests { // Add more than 64 services for i in 0..70 { - config.add_extra_service(&format!("service-{}", i)); + config.add_extra_service(&format!("service-{i}")); } // Should be limited to 64 @@ -1408,7 +1408,9 @@ mod tests { ..SamplingRuleConfig::default() }], }; - config.trace_sampling_rules.set_value_source(local_rules.clone(), ConfigSource::EnvVar); + config + .trace_sampling_rules + .set_value_source(local_rules.clone(), ConfigSource::EnvVar); // Verify local rules are active assert_eq!(config.trace_sampling_rules().len(), 1); @@ -1416,17 +1418,25 @@ mod tests { assert_eq!(config.trace_sampling_rules.source(), ConfigSource::EnvVar); // 2. Remote config sends non-empty rules - let remote_rules_json = r#"[{"sample_rate": 0.8, "service": "remote-service", "provenance": "remote"}]"#; - config.update_sampling_rules_from_remote(remote_rules_json).unwrap(); + let remote_rules_json = + r#"[{"sample_rate": 0.8, "service": "remote-service", "provenance": "remote"}]"#; + config + .update_sampling_rules_from_remote(remote_rules_json) + .unwrap(); // Verify remote rules override local rules assert_eq!(config.trace_sampling_rules().len(), 1); assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.8); - assert_eq!(config.trace_sampling_rules.source(), ConfigSource::RemoteConfig); + assert_eq!( + config.trace_sampling_rules.source(), + ConfigSource::RemoteConfig + ); // 3. Remote config sends empty array [] let empty_remote_rules_json = "[]"; - config.update_sampling_rules_from_remote(empty_remote_rules_json).unwrap(); + config + .update_sampling_rules_from_remote(empty_remote_rules_json) + .unwrap(); // NEW BEHAVIOR: Empty remote rules now automatically fall back to local rules assert_eq!(config.trace_sampling_rules().len(), 1); // Falls back to local rules @@ -1436,7 +1446,7 @@ mod tests { // 4. Verify explicit clearing still works (for completeness) // Since we're already on local rules, clear should keep us on local rules config.clear_remote_sampling_rules(); - + // Should remain on local rules assert_eq!(config.trace_sampling_rules().len(), 1); assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.3); diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 23c2263c..a78e186d 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -1233,7 +1233,7 @@ mod tests { assert!(targets.signatures.is_some()); let signatures = targets.signatures.unwrap(); assert_eq!(signatures.len(), 1); - + // Verify the signed targets structure assert_eq!(targets.signed.target_type, "targets"); assert_eq!(targets.signed.expires, "2022-09-22T09:01:04Z"); @@ -1243,7 +1243,8 @@ mod tests { // Verify custom metadata with opaque_backend_state assert!(targets.signed.custom.is_some()); let custom = targets.signed.custom.unwrap(); - let backend_state = custom.get("opaque_backend_state") + let backend_state = custom + .get("opaque_backend_state") .and_then(|v| v.as_str()) .expect("Should have opaque_backend_state"); assert_eq!(backend_state, "eyJ2ZXJzaW9uIjoxLCJzdGF0ZSI6eyJmaWxlX2hhc2hlcyI6WyJGZXJOT1FyMStmTThKWk9TY0crZllucnhXMWpKN0w0ZlB5aGtxUWVCT3dJPSIsInd1aW9BVm1Qcy9oNEpXMDh1dnI1bi9meERLQ3lKdG1sQmRjaDNOcFdLZDg9IiwiOGFDYVJFc3hIV3R3SFNFWm5SV0pJYmtENXVBNUtETENoZG8vZ0RNdnJJMD0iXX19"); @@ -1252,7 +1253,10 @@ mod tests { assert_eq!(targets.signed.targets.len(), 3); // Test APM_SAMPLING target - let apm_sampling = targets.signed.targets.get("datadog/2/APM_SAMPLING/dynamic_rates/config") + let apm_sampling = targets + .signed + .targets + .get("datadog/2/APM_SAMPLING/dynamic_rates/config") .expect("Should have APM_SAMPLING target"); assert_eq!(apm_sampling.length, 58409); assert_eq!( @@ -1263,7 +1267,10 @@ mod tests { assert_eq!(apm_custom.get("v").unwrap().as_u64().unwrap(), 27423); // Test ASM_DD target - let asm_dd = targets.signed.targets.get("employee/ASM_DD/1.recommended.json/config") + let asm_dd = targets + .signed + .targets + .get("employee/ASM_DD/1.recommended.json/config") .expect("Should have ASM_DD target"); assert_eq!(asm_dd.length, 235228); assert_eq!( @@ -1274,7 +1281,10 @@ mod tests { assert_eq!(asm_custom.get("v").unwrap().as_u64().unwrap(), 1); // Test CWS_DD target - let cws_dd = targets.signed.targets.get("employee/CWS_DD/4.default.policy/config") + let cws_dd = targets + .signed + .targets + .get("employee/CWS_DD/4.default.policy/config") .expect("Should have CWS_DD target"); assert_eq!(cws_dd.length, 34777); assert_eq!( @@ -1322,7 +1332,8 @@ mod tests { }"#; use base64::Engine; - let encoded_targets = base64::engine::general_purpose::STANDARD.encode(tuf_targets_json.as_bytes()); + let encoded_targets = + base64::engine::general_purpose::STANDARD.encode(tuf_targets_json.as_bytes()); // Create a config response with the TUF targets and a corresponding target file let config_response = ConfigResponse { @@ -1335,13 +1346,16 @@ mod tests { }, ]), client_configs: Some(vec![ - "datadog/2/APM_TRACING/test-sampling/config".to_string(), + "datadog/2/APM_TRACING/test-sampling/config".to_string() ]), }; // Process the response let result = client.process_response(config_response); - assert!(result.is_ok(), "process_response should succeed: {:?}", result); + assert!( + result.is_ok(), + "process_response should succeed: {result:?}" + ); // Verify state was updated with targets metadata let state = client.state.lock().unwrap(); From e99ed333883fdd8cfde5d348ab30d64dde1c54b9 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 15:18:56 -0400 Subject: [PATCH 13/34] create general type for callbacks for extensibility --- datadog-opentelemetry/src/lib.rs | 245 +++----------------- dd-trace/src/configuration/configuration.rs | 54 +++-- dd-trace/src/configuration/mod.rs | 3 +- dd-trace/src/lib.rs | 2 +- 4 files changed, 73 insertions(+), 231 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index ed12bd90..434a4909 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -1,68 +1,6 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! # Datadog Opentelemetry -//! -//! A datadog layer of compatibility for the opentelemetry SDK -//! -//! ## Usage -//! -//! This is the minimal example to initialize the SDK. -//! -//! This will read datadog and opentelemetry configuration from environment variables and other -//! available sources. -//! And initialize and set up the tracer provider and the text map propagator globally. -//! -//! ```rust -//! # fn main() { -//! datadog_opentelemetry::tracing().init(); -//! # } -//! ``` -//! -//! It is also possible to customize the datadog configuration passed to the tracer provider. -//! -//! ```rust -//! // Custom datadog configuration -//! datadog_opentelemetry::tracing() -//! .with_config( -//! dd_trace::Config::builder() -//! .set_service("my_service".to_string()) -//! .set_env("my_env".to_string()) -//! .set_version("1.0.0".to_string()) -//! .build(), -//! ) -//! .init(); -//! ``` -//! -//! Or to pass options to the OpenTelemetry SDK TracerProviderBuilder -//! ```rust -//! # #[derive(Debug)] -//! # struct MySpanProcessor; -//! # -//! # impl opentelemetry_sdk::trace::SpanProcessor for MySpanProcessor { -//! # fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) { -//! # } -//! # fn on_end(&self, span: opentelemetry_sdk::trace::SpanData) {} -//! # fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { -//! # Ok(()) -//! # } -//! # fn shutdown_with_timeout( -//! # &self, -//! # timeout: std::time::Duration, -//! # ) -> opentelemetry_sdk::error::OTelSdkResult { -//! # Ok(()) -//! # } -//! # fn set_resource(&mut self, _resource: &opentelemetry_sdk::Resource) {} -//! # } -//! # -//! // Custom otel tracer sdk options -//! datadog_opentelemetry::tracing() -//! .with_max_attributes_per_span(64) -//! // Custom span processor -//! .with_span_processor(MySpanProcessor) -//! .init(); -//! ``` - mod ddtrace_transform; mod sampler; mod span_exporter; @@ -72,6 +10,7 @@ mod trace_id; use std::sync::{Arc, Mutex, RwLock}; +use dd_trace::configuration::RemoteConfigUpdate; use opentelemetry::{Key, KeyValue, Value}; use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; use opentelemetry_semantic_conventions::resource::SERVICE_NAME; @@ -79,163 +18,43 @@ use sampler::Sampler; use span_processor::{DatadogSpanProcessor, TraceRegistry}; use text_map_propagator::DatadogPropagator; -pub struct DatadogTracingBuilder { - config: Option, - resource: Option, - tracer_provider: opentelemetry_sdk::trace::TracerProviderBuilder, -} - -impl DatadogTracingBuilder { - pub fn with_config(mut self, config: dd_trace::Config) -> Self { - self.config = Some(config); - self - } - - pub fn with_resource(mut self, resource: opentelemetry_sdk::Resource) -> Self { - self.resource = Some(resource); - self - } - - pub fn init(self) -> SdkTracerProvider { - let config = self - .config - .unwrap_or_else(|| dd_trace::Config::builder().build()); - let (tracer_provider, propagator) = - make_tracer(config, self.tracer_provider, self.resource); - - opentelemetry::global::set_text_map_propagator(propagator); - opentelemetry::global::set_tracer_provider(tracer_provider.clone()); - tracer_provider - } -} - -impl DatadogTracingBuilder { - // Methods forwarded to the otel tracer provider builder - - pub fn with_span_processor( - mut self, - processor: T, - ) -> Self { - self.tracer_provider = self.tracer_provider.with_span_processor(processor); - self - } - - /// Specify the number of events to be recorded per span. - pub fn with_max_events_per_span(mut self, max_events: u32) -> Self { - self.tracer_provider = self.tracer_provider.with_max_events_per_span(max_events); - self - } - - /// Specify the number of attributes to be recorded per span. - pub fn with_max_attributes_per_span(mut self, max_attributes: u32) -> Self { - self.tracer_provider = self - .tracer_provider - .with_max_attributes_per_span(max_attributes); - self - } - - /// Specify the number of events to be recorded per span. - pub fn with_max_links_per_span(mut self, max_links: u32) -> Self { - self.tracer_provider = self.tracer_provider.with_max_links_per_span(max_links); - self - } - - /// Specify the number of attributes one event can have. - pub fn with_max_attributes_per_event(mut self, max_attributes: u32) -> Self { - self.tracer_provider = self - .tracer_provider - .with_max_attributes_per_event(max_attributes); - self - } - - /// Specify the number of attributes one link can have. - pub fn with_max_attributes_per_link(mut self, max_attributes: u32) -> Self { - self.tracer_provider = self - .tracer_provider - .with_max_attributes_per_link(max_attributes); - self - } - - /// Specify all limit via the span_limits - pub fn with_span_limits(mut self, span_limits: opentelemetry_sdk::trace::SpanLimits) -> Self { - self.tracer_provider = self.tracer_provider.with_span_limits(span_limits); - self - } -} +// Type alias to simplify complex callback type +type SamplerCallback = Arc>; -/// Initialize a new Datadog Tracing builder +/// Initialize the Datadog OpenTelemetry exporter. /// -/// # Usage +/// This function sets up the global OpenTelemetry SDK provider for compatibility with datadog. /// +/// # Usage /// ```rust -/// // Default configuration -/// datadog_opentelemetry::tracing().init(); -/// ``` +/// use dd_trace::Config; +/// use opentelemetry_sdk::trace::TracerProviderBuilder; /// -/// It is also possible to customize the datadog configuration passed to the tracer provider. +/// // This picks up env var configuration and other datadog configuration sources +/// let datadog_config = Config::builder().build(); /// -/// ```rust -/// // Custom datadog configuration -/// datadog_opentelemetry::tracing() -/// .with_config( -/// dd_trace::Config::builder() -/// .set_service("my_service".to_string()) -/// .set_env("my_env".to_string()) -/// .set_version("1.0.0".to_string()) -/// .build(), -/// ) -/// .init(); +/// datadog_opentelemetry::init_datadog( +/// datadog_config, +/// TracerProviderBuilder::default(), // Pass any opentelemetry specific configuration here +/// // .with_max_attributes_per_span(max_attributes) +/// None, +/// ); /// ``` -/// -/// Or to pass options to the OpenTelemetry SDK TracerProviderBuilder -/// ```rust -/// # #[derive(Debug)] -/// # struct MySpanProcessor; -/// # -/// # impl opentelemetry_sdk::trace::SpanProcessor for MySpanProcessor { -/// # fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) { -/// # } -/// # fn on_end(&self, span: opentelemetry_sdk::trace::SpanData) {} -/// # fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { -/// # Ok(()) -/// # } -/// # fn shutdown_with_timeout( -/// # &self, -/// # timeout: std::time::Duration, -/// # ) -> opentelemetry_sdk::error::OTelSdkResult { -/// # Ok(()) -/// # } -/// # fn set_resource(&mut self, _resource: &opentelemetry_sdk::Resource) {} -/// # } -/// # -/// // Custom otel tracer sdk options -/// datadog_opentelemetry::tracing() -/// .with_max_attributes_per_span(64) -/// // Custom span processor -/// .with_span_processor(MySpanProcessor) -/// .init(); -/// ``` -pub fn tracing() -> DatadogTracingBuilder { - DatadogTracingBuilder { - config: None, - tracer_provider: opentelemetry_sdk::trace::SdkTracerProvider::builder(), - resource: None, - } -} - -#[deprecated(note = "Use `datadog_opentelemetry::tracing()` instead")] -// TODO: update system tests to use the new API and remove this function pub fn init_datadog( config: dd_trace::Config, + // TODO(paullgdc): Should we take a builder or create it ourselves? + // because some customer might want to set max__per_span using + // the builder APIs + // Or maybe we need a builder API called DatadogDistribution that takes + // all parameters and has an install method? tracer_provider_builder: opentelemetry_sdk::trace::TracerProviderBuilder, resource: Option, ) -> SdkTracerProvider { - DatadogTracingBuilder { - config: Some(config), - tracer_provider: tracer_provider_builder, - resource, - } - .init() + let (tracer_provider, propagator) = make_tracer(config, tracer_provider_builder, resource); + + opentelemetry::global::set_text_map_propagator(propagator); + opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + tracer_provider } /// Create an instance of the tracer provider @@ -282,11 +101,13 @@ fn make_tracer( // Add sampler callback to the config before creating the remote config client if let Some(sampler_callback) = sampler_callback { let sampler_callback = Arc::new(sampler_callback); - let sampler_callback_clone = sampler_callback.clone(); + let sampler_callback_clone: SamplerCallback = sampler_callback.clone(); mutable_config.lock().unwrap().add_remote_config_callback( "datadog_sampler_on_rules_update".to_string(), - move |rules| { - sampler_callback_clone(rules); + move |update| match update { + RemoteConfigUpdate::SamplingRules(rules) => { + sampler_callback_clone(rules); + } }, ); } @@ -298,9 +119,9 @@ fn make_tracer( { // Start the client in background let _handle = client.start(); - dd_trace::dd_info!("RemoteConfigClient: Started remote configuration client"); + dd_trace::dd_debug!("RemoteConfigClient: Started remote configuration client"); } else { - dd_trace::dd_warn!("RemoteConfigClient: Failed to create remote config client"); + dd_trace::dd_debug!("RemoteConfigClient: Failed to create remote config client"); } } diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index bef6c31d..d0eb86a7 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -13,8 +13,19 @@ use crate::log::LevelFilter; use super::sources::{CompositeConfigSourceResult, CompositeSource}; -/// Type alias for remote configuration callback functions -type RemoteConfigCallback = Box; +/// Different types of remote configuration updates that can trigger callbacks +#[derive(Debug, Clone)] +pub enum RemoteConfigUpdate { + /// Sampling rules were updated from remote configuration + SamplingRules(Vec), + // Future remote config update types should be added here as new variants. + // E.g. + // - FeatureFlags(HashMap) +} + +/// Type alias for remote configuration callback functions +/// Callbacks receive a RemoteConfigUpdate enum to handle different config types +type RemoteConfigCallback = Box; /// Configuration for a single sampling rule #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] @@ -654,7 +665,9 @@ impl Config { // This specifically calls the DatadogSampler's on_rules_update method if let Ok(callbacks) = self.remote_config_callbacks.lock() { if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - callback(self.trace_sampling_rules()); + let update = + RemoteConfigUpdate::SamplingRules(self.trace_sampling_rules().to_vec()); + callback(&update); } } } @@ -669,33 +682,39 @@ impl Config { if let Ok(callbacks) = self.remote_config_callbacks.lock() { if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { // Now that rc_value is cleared, this will return the fallback rules - callback(self.trace_sampling_rules()); + let update = + RemoteConfigUpdate::SamplingRules(self.trace_sampling_rules().to_vec()); + callback(&update); } } } - /// Add a callback to be called when sampling rules are updated from remote configuration - /// This allows components like the DatadogSampler to be updated without circular imports + /// Add a callback to be called when remote configuration is updated + /// This allows components to be updated without circular imports /// /// # Arguments /// * `key` - A unique identifier for this callback (e.g., "datadog_sampler_on_rules_update") - /// * `callback` - The function to call when sampling rules are updated (receives - /// SamplingRuleConfig slice) + /// * `callback` - The function to call when remote config is updated (receives + /// RemoteConfigUpdate enum) /// /// # Example /// ``` - /// use dd_trace::Config; - /// use std::sync::Arc; + /// use dd_trace::{configuration::RemoteConfigUpdate, Config}; /// /// let config = Config::builder().build(); - /// config.add_remote_config_callback("datadog_sampler_on_rules_update".to_string(), |rules| { - /// println!("Received {} new sampling rules", rules.len()); - /// // Update your sampler here + /// config.add_remote_config_callback("my_component_callback".to_string(), |update| { + /// match update { + /// RemoteConfigUpdate::SamplingRules(rules) => { + /// println!("Received {} new sampling rules", rules.len()); + /// // Update your sampler here + /// } // Future remote config types can be handled here by adding new match arms + /// // as new variants are added to the RemoteConfigUpdate enum + /// } /// }); /// ``` pub fn add_remote_config_callback(&self, key: String, callback: F) where - F: Fn(&[SamplingRuleConfig]) + Send + Sync + 'static, + F: Fn(&RemoteConfigUpdate) + Send + Sync + 'static, { if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { callbacks.insert(key, Box::new(callback)); @@ -1330,10 +1349,11 @@ mod tests { config.add_remote_config_callback( "datadog_sampler_on_rules_update".to_string(), - move |rules| { + move |update| { *callback_called_clone.lock().unwrap() = true; - // Store the rules directly for testing - *callback_rules_clone.lock().unwrap() = rules.to_vec(); + // Store the rules - for now we only have SamplingRules variant + let RemoteConfigUpdate::SamplingRules(rules) = update; + *callback_rules_clone.lock().unwrap() = rules.clone(); }, ); diff --git a/dd-trace/src/configuration/mod.rs b/dd-trace/src/configuration/mod.rs index b916f1d0..c09c64d3 100644 --- a/dd-trace/src/configuration/mod.rs +++ b/dd-trace/src/configuration/mod.rs @@ -7,5 +7,6 @@ pub mod remote_config; mod sources; pub use configuration::{ - Config, ConfigBuilder, ConfigItem, ConfigSource, SamplingRuleConfig, TracePropagationStyle, + Config, ConfigBuilder, ConfigItem, ConfigSource, RemoteConfigUpdate, SamplingRuleConfig, + TracePropagationStyle, }; diff --git a/dd-trace/src/lib.rs b/dd-trace/src/lib.rs index 0ea5036e..621a6c9b 100644 --- a/dd-trace/src/lib.rs +++ b/dd-trace/src/lib.rs @@ -4,7 +4,7 @@ pub mod configuration; pub mod constants; pub mod sampling; -pub use configuration::{Config, ConfigBuilder, SamplingRuleConfig}; +pub use configuration::{Config, ConfigBuilder, RemoteConfigUpdate, SamplingRuleConfig}; mod error; pub use error::{Error, Result}; From 09324f37eac4e21a9ce8a4dce741ea535f73b479 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 15:56:11 -0400 Subject: [PATCH 14/34] update random examples so tests pass --- .../examples/propagator/src/server.rs | 10 +++++----- .../examples/simple_tracing/src/main.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/datadog-opentelemetry/examples/propagator/src/server.rs b/datadog-opentelemetry/examples/propagator/src/server.rs index 5a8b5687..104b10dc 100644 --- a/datadog-opentelemetry/examples/propagator/src/server.rs +++ b/datadog-opentelemetry/examples/propagator/src/server.rs @@ -18,7 +18,7 @@ use opentelemetry_http::{Bytes, HeaderExtractor, HeaderInjector}; use opentelemetry_sdk::{ error::OTelSdkResult, logs::{LogProcessor, SdkLogRecord, SdkLoggerProvider}, - trace::{SdkTracerProvider, SpanProcessor}, + trace::{SdkTracerProvider, SpanProcessor, TracerProviderBuilder}, }; use opentelemetry_semantic_conventions::trace; use opentelemetry_stdout::{LogExporter, SpanExporter}; @@ -219,13 +219,13 @@ fn init_tracer() -> SdkTracerProvider { .set_env("staging".to_string()) .build(); - datadog_opentelemetry::tracing() - .with_config(config) + let tracer_provider_builder = TracerProviderBuilder::default() .with_span_processor(EnrichWithBaggageSpanProcessor) .with_span_processor(opentelemetry_sdk::trace::SimpleSpanProcessor::new( SpanExporter::default(), - )) - .init() + )); + + datadog_opentelemetry::init_datadog(config, tracer_provider_builder, None) } fn init_logs() -> SdkLoggerProvider { diff --git a/datadog-opentelemetry/examples/simple_tracing/src/main.rs b/datadog-opentelemetry/examples/simple_tracing/src/main.rs index c933d425..0bece7f2 100644 --- a/datadog-opentelemetry/examples/simple_tracing/src/main.rs +++ b/datadog-opentelemetry/examples/simple_tracing/src/main.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use opentelemetry::trace::Tracer; +use opentelemetry_sdk::trace::TracerProviderBuilder; fn foo() { opentelemetry::global::tracer("foo").in_span("foo", |_cx| { @@ -15,13 +16,12 @@ fn bar() { } fn main() { - let tracer_provider = datadog_opentelemetry::tracing() - .with_config( - dd_trace::Config::builder() - .set_service("simple_tracing".to_string()) - .build(), - ) - .init(); + let config = dd_trace::Config::builder() + .set_service("simple_tracing".to_string()) + .build(); + + let tracer_provider = + datadog_opentelemetry::init_datadog(config, TracerProviderBuilder::default(), None); foo(); From bd3370c02e768d0b0bb6462d51687da5b26e2d44 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 16:23:57 -0400 Subject: [PATCH 15/34] fix licensing --- LICENSE-3rdparty.csv | 59 +++++++++++++++++++++++++++++++++++--------- license-tool.toml | 6 ++++- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index bbfc9cd5..935992b7 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -33,6 +33,7 @@ basic-cookies,https://github.com/drjokepu/basic-cookies,MIT,Tamas Czinege bit-vec,https://github.com/contain-rs/bit-vec,MIT OR Apache-2.0,Alexis Beingessner bitflags,https://github.com/bitflags/bitflags,MIT OR Apache-2.0,The Rust Project Developers +block-buffer,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers blocking,https://github.com/smol-rs/blocking,Apache-2.0 OR MIT,Stjepan Glavina bollard,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors bollard-stubs,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors @@ -45,7 +46,7 @@ cargo-platform,https://github.com/rust-lang/cargo,MIT OR Apache-2.0,The cargo-pl cargo_metadata,https://github.com/oli-obk/cargo_metadata,MIT,Oliver Schneider cc,https://github.com/rust-lang/cc-rs,MIT OR Apache-2.0,Alex Crichton cexpr,https://github.com/jethrogb/rust-cexpr,Apache-2.0 OR MIT,Jethro Beekman -cfg-if,https://github.com/alexcrichton/cfg-if,MIT OR Apache-2.0,Alex Crichton +cfg-if,https://github.com/rust-lang/cfg-if,MIT OR Apache-2.0,Alex Crichton chrono,https://github.com/chronotope/chrono,MIT OR Apache-2.0,The chrono Authors clang-sys,https://github.com/KyleMayes/clang-sys,Apache-2.0,Kyle Mayes concurrent-queue,https://github.com/smol-rs/concurrent-queue,Apache-2.0 OR MIT,"Stjepan Glavina , Taiki Endo , John Nunley " @@ -53,9 +54,13 @@ const_format,https://github.com/rodrimati1992/const_format_crates,Zlib,rodrimati const_format_proc_macros,https://github.com/rodrimati1992/const_format_crates,Zlib,rodrimati1992 core-foundation,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers core-foundation-sys,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers +cpufeatures,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers +critical-section,https://github.com/rust-embedded/critical-section,MIT OR Apache-2.0,The critical-section Authors crossbeam-channel,https://github.com/crossbeam-rs/crossbeam,MIT OR Apache-2.0,The crossbeam-channel Authors +crossbeam-epoch,https://github.com/crossbeam-rs/crossbeam,MIT OR Apache-2.0,The crossbeam-epoch Authors crossbeam-utils,https://github.com/crossbeam-rs/crossbeam,MIT OR Apache-2.0,The crossbeam-utils Authors crunchy,https://github.com/eira-fransham/crunchy,MIT,Eira Fransham +crypto-common,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers darling,https://github.com/TedDriggs/darling,MIT,Ted Driggs darling_core,https://github.com/TedDriggs/darling,MIT,Ted Driggs darling_macro,https://github.com/TedDriggs/darling,MIT,Ted Driggs @@ -69,6 +74,7 @@ ddcommon,https://github.com/Datadog/libdatadog,Apache-2.0,The ddcommon Authors ddtelemetry,https://github.com/Datadog/libdatadog,Apache-2.0,The ddtelemetry Authors deranged,https://github.com/jhpratt/deranged,MIT OR Apache-2.0,Jacob Pratt diff,https://github.com/utkarshkukreti/diff.rs,MIT OR Apache-2.0,Utkarsh Kukreti +digest,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers dirs,https://github.com/soc/dirs-rs,MIT OR Apache-2.0,Simon Ochsenreither dirs-next,https://github.com/xdg-rs/dirs,MIT OR Apache-2.0,The @xdg-rs members dirs-sys,https://github.com/dirs-dev/dirs-sys-rs,MIT OR Apache-2.0,Simon Ochsenreither @@ -76,6 +82,7 @@ dirs-sys-next,https://github.com/xdg-rs/dirs/tree/master/dirs-sys,MIT OR Apache- displaydoc,https://github.com/yaahc/displaydoc,MIT OR Apache-2.0,Jane Lusby docker_credential,https://github.com/keirlawson/docker_credential,MIT OR Apache-2.0,Keir Lawson dogstatsd-client,https://github.com/Datadog/libdatadog,Apache-2.0,The dogstatsd-client Authors +dyn-clone,https://github.com/dtolnay/dyn-clone,MIT OR Apache-2.0,David Tolnay either,https://github.com/rayon-rs/either,MIT OR Apache-2.0,bluss ena,https://github.com/rust-lang/ena,MIT OR Apache-2.0,Niko Matsakis encoding_rs,https://github.com/hsivonen/encoding_rs,(Apache-2.0 OR MIT) AND BSD-3-Clause,Henri Sivonen @@ -89,6 +96,8 @@ fastrand,https://github.com/smol-rs/fastrand,Apache-2.0 OR MIT,Stjepan Glavina < fixedbitset,https://github.com/petgraph/fixedbitset,MIT OR Apache-2.0,bluss fnv,https://github.com/servo/rust-fnv,Apache-2.0 OR MIT,Alex Crichton foldhash,https://github.com/orlp/foldhash,Zlib,Orson Peters +foreign-types,https://github.com/sfackler/foreign-types,MIT OR Apache-2.0,Steven Fackler +foreign-types-shared,https://github.com/sfackler/foreign-types,MIT OR Apache-2.0,Steven Fackler form_urlencoded,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers futures,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures Authors futures-channel,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-channel Authors @@ -100,6 +109,8 @@ futures-macro,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futu futures-sink,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-sink Authors futures-task,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-task Authors futures-util,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-util Authors +generator,https://github.com/Xudong-Huang/generator-rs,MIT OR Apache-2.0,Xudong Huang +generic-array,https://github.com/fizyk20/generic-array,MIT,"BartΕ‚omiej KamiΕ„ski , Aaron Trent " getrandom,https://github.com/rust-random/getrandom,MIT OR Apache-2.0,The Rand Project Developers gimli,https://github.com/gimli-rs/gimli,MIT OR Apache-2.0,The gimli Authors glob,https://github.com/rust-lang/glob,MIT OR Apache-2.0,The Rust Project Developers @@ -121,27 +132,27 @@ httpmock,https://github.com/alexliesenfeld/httpmock,MIT,Alexander Liesenfeld hyper-named-pipe,https://github.com/fussybeaver/hyper-named-pipe,Apache-2.0,The hyper-named-pipe Authors hyper-rustls,https://github.com/rustls/hyper-rustls,Apache-2.0 OR ISC OR MIT,The hyper-rustls Authors +hyper-tls,https://github.com/hyperium/hyper-tls,MIT OR Apache-2.0,Sean McArthur hyper-util,https://github.com/hyperium/hyper-util,MIT,Sean McArthur hyperlocal,https://github.com/softprops/hyperlocal,MIT,softprops iana-time-zone,https://github.com/strawlab/iana-time-zone,MIT OR Apache-2.0,"Andrew Straw , RenΓ© Kijewski , Ryan Lopopolo " iana-time-zone-haiku,https://github.com/strawlab/iana-time-zone,MIT OR Apache-2.0,RenΓ© Kijewski icu_collections,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers -icu_locid,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers -icu_locid_transform,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers -icu_locid_transform_data,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers +icu_locale_core,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_normalizer,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_normalizer_data,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_properties,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_properties_data,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_provider,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers -icu_provider_macros,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers ident_case,https://github.com/TedDriggs/ident_case,MIT OR Apache-2.0,Ted Driggs idna,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers idna_adapter,https://github.com/hsivonen/idna_adapter,Apache-2.0 OR MIT,The rust-url developers indexmap,https://github.com/bluss/indexmap,Apache-2.0 OR MIT,The indexmap Authors indexmap,https://github.com/indexmap-rs/indexmap,Apache-2.0 OR MIT,The indexmap Authors +io-uring,https://github.com/tokio-rs/io-uring,MIT OR Apache-2.0,quininer ipconfig,https://github.com/liranringel/ipconfig,MIT OR Apache-2.0,Liran Ringel ipnet,https://github.com/krisprice/ipnet,MIT OR Apache-2.0,Kris Price +iri-string,https://github.com/lo48576/iri-string,MIT OR Apache-2.0,YOSHIOKA Takuma itertools,https://github.com/rust-itertools/itertools,MIT OR Apache-2.0,bluss itoa,https://github.com/dtolnay/itoa,MIT OR Apache-2.0,David Tolnay jobserver,https://github.com/rust-lang/jobserver-rs,MIT OR Apache-2.0,Alex Crichton @@ -154,27 +165,33 @@ levenshtein,https://github.com/wooorm/levenshtein-rs,MIT,Titus Wormer libredox,https://gitlab.redox-os.org/redox-os/libredox,MIT,4lDO2 <4lDO2@protonmail.com> -linked-hash-map,https://github.com/contain-rs/linked-hash-map,MIT OR Apache-2.0,"Stepan Koltsov , Andrew Paseltiner " linux-raw-sys,https://github.com/sunfishcode/linux-raw-sys,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Dan Gohman litemap,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers lock_api,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras log,https://github.com/rust-lang/log,MIT OR Apache-2.0,The Rust Project Developers +loom,https://github.com/tokio-rs/loom,MIT,Carl Lerche lru,https://github.com/jeromefroe/lru-rs,MIT,Jerome Froelich -lru-cache,https://github.com/contain-rs/lru-cache,MIT OR Apache-2.0,Stepan Koltsov +lru-slab,https://github.com/Ralith/lru-slab,MIT OR Apache-2.0 OR Zlib,Benjamin Saunders matchers,https://github.com/hawkw/matchers,MIT,Eliza Weisman memchr,https://github.com/BurntSushi/memchr,Unlicense OR MIT,"Andrew Gallant , bluss" mime,https://github.com/hyperium/mime,MIT OR Apache-2.0,Sean McArthur minimal-lexical,https://github.com/Alexhuszagh/minimal-lexical,MIT OR Apache-2.0,Alex Huszagh miniz_oxide,https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide,MIT OR Zlib OR Apache-2.0,"Frommi , oyvindln , Rich Geldreich richgel99@gmail.com" mio,https://github.com/tokio-rs/mio,MIT,"Carl Lerche , Thomas de Zeeuw , Tokio Contributors " +moka,https://github.com/moka-rs/moka,MIT OR Apache-2.0,The moka Authors +native-tls,https://github.com/sfackler/rust-native-tls,MIT OR Apache-2.0,Steven Fackler new_debug_unreachable,https://github.com/mbrubeck/rust-debug-unreachable,MIT,"Matt Brubeck , Jonathan Reem " nix,https://github.com/nix-rust/nix,MIT,The nix-rust Project Developers nom,https://github.com/Geal/nom,MIT,contact@geoffroycouprie.com +nu-ansi-term,https://github.com/nushell/nu-ansi-term,MIT,"ogham@bsago.me, Ryan Scheel (Havvy) , Josh Triplett , The Nushell Project Developers" num-conv,https://github.com/jhpratt/num-conv,MIT OR Apache-2.0,Jacob Pratt num-traits,https://github.com/rust-num/num-traits,MIT OR Apache-2.0,The Rust Project Developers object,https://github.com/gimli-rs/object,Apache-2.0 OR MIT,The object Authors once_cell,https://github.com/matklad/once_cell,MIT OR Apache-2.0,Aleksey Kladov +openssl,https://github.com/sfackler/rust-openssl,Apache-2.0,Steven Fackler +openssl-macros,https://github.com/sfackler/rust-openssl,MIT OR Apache-2.0,The openssl-macros Authors openssl-probe,https://github.com/alexcrichton/openssl-probe,MIT OR Apache-2.0,Alex Crichton +openssl-sys,https://github.com/sfackler/rust-openssl,MIT,"Alex Crichton , Steven Fackler " opentelemetry,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry,Apache-2.0,The opentelemetry Authors opentelemetry-appender-tracing,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-appender-tracing,Apache-2.0,The opentelemetry-appender-tracing Authors opentelemetry-http,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http,Apache-2.0,The opentelemetry-http Authors @@ -182,6 +199,7 @@ opentelemetry-semantic-conventions,https://github.com/open-telemetry/opentelemet opentelemetry-stdout,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-stdout,Apache-2.0,The opentelemetry-stdout Authors opentelemetry_sdk,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk,Apache-2.0,The opentelemetry_sdk Authors option-ext,https://github.com/soc/option-ext,MPL-2.0,Simon Ochsenreither +overload,https://github.com/danaugrs/overload,MIT,Daniel Salvadori parking,https://github.com/smol-rs/parking,Apache-2.0 OR MIT,"Stjepan Glavina , The Rust Project Developers" parking_lot,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras parking_lot_core,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras @@ -198,6 +216,8 @@ pin-project-lite,https://github.com/taiki-e/pin-project-lite,Apache-2.0 OR MIT,T pin-utils,https://github.com/rust-lang-nursery/pin-utils,MIT OR Apache-2.0,Josef Brandl piper,https://github.com/smol-rs/piper,MIT OR Apache-2.0,"Stjepan Glavina , John Nunley " polling,https://github.com/smol-rs/polling,Apache-2.0 OR MIT,"Stjepan Glavina , John Nunley " +portable-atomic,https://github.com/taiki-e/portable-atomic,Apache-2.0 OR MIT,The portable-atomic Authors +potential_utf,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers powerfmt,https://github.com/jhpratt/powerfmt,MIT OR Apache-2.0,Jacob Pratt ppv-lite86,https://github.com/cryptocorrosion/cryptocorrosion,MIT OR Apache-2.0,The CryptoCorrosion Contributors precomputed-hash,https://github.com/emilio/precomputed-hash,MIT,Emilio Cobos Álvarez @@ -215,6 +235,8 @@ rand_chacha,https://github.com/rust-random/rand,MIT OR Apache-2.0,"The Rand Proj rand_core,https://github.com/rust-random/rand,MIT OR Apache-2.0,"The Rand Project Developers, The Rust Project Developers" redox_syscall,https://gitlab.redox-os.org/redox-os/syscall,MIT,Jeremy Soller redox_users,https://gitlab.redox-os.org/redox-os/users,MIT,"Jose Narvaez , Wesley Hershberger " +ref-cast,https://github.com/dtolnay/ref-cast,MIT OR Apache-2.0,David Tolnay +ref-cast-impl,https://github.com/dtolnay/ref-cast,MIT OR Apache-2.0,David Tolnay regex,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " regex-automata,https://github.com/BurntSushi/regex-automata,Unlicense OR MIT,Andrew Gallant regex-automata,https://github.com/rust-lang/regex/tree/master/regex-automata,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " @@ -241,6 +263,8 @@ rustversion,https://github.com/dtolnay/rustversion,MIT OR Apache-2.0,David Tolna ryu,https://github.com/dtolnay/ryu,Apache-2.0 OR BSL-1.0,David Tolnay same-file,https://github.com/BurntSushi/same-file,Unlicense OR MIT,Andrew Gallant schannel,https://github.com/steffengy/schannel-rs,MIT,"Steven Fackler , Steffen Butzer " +schemars,https://github.com/GREsau/schemars,MIT,Graham Esau +scoped-tls,https://github.com/alexcrichton/scoped-tls,MIT OR Apache-2.0,Alex Crichton scopeguard,https://github.com/bluss/scopeguard,MIT OR Apache-2.0,bluss security-framework,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " security-framework-sys,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " @@ -254,6 +278,7 @@ serde_repr,https://github.com/dtolnay/serde-repr,MIT OR Apache-2.0,David Tolnay serde_urlencoded,https://github.com/nox/serde_urlencoded,MIT OR Apache-2.0,Anthony Ramine serde_with,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,"Jonas Bushart, Marcin KaΕΊmierczak" serde_with_macros,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,Jonas Bushart +sha2,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developers sharded-slab,https://github.com/hawkw/sharded-slab,MIT,Eliza Weisman shlex,https://github.com/comex/rust-shlex,MIT OR Apache-2.0,"comex , Fenhl , Adrian Taylor , Alex Touchet , Daniel Parks , Garrett Berg " signal-hook-registry,https://github.com/vorner/signal-hook,Apache-2.0 OR MIT,"Michal 'vorner' Vaner , Masaki Hara " @@ -273,6 +298,10 @@ syn,https://github.com/dtolnay/syn,MIT OR Apache-2.0,David Tolnay synstructure,https://github.com/mystor/synstructure,MIT,Nika Layzell sys-info,https://github.com/FillZpp/sys-info-rs,MIT,Siyu Wang +system-configuration,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN +system-configuration-sys,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN +tagptr,https://github.com/oliver-giersch/tagptr,MIT OR Apache-2.0,Oliver Giersch +tempfile,https://github.com/Stebalien/tempfile,MIT OR Apache-2.0,"Steven Allen , The Rust Project Developers, Ashley Mannix , Jason White " term,https://github.com/Stebalien/term,MIT OR Apache-2.0,"The Rust Project Developers, Steven Allen" testcontainers,https://github.com/testcontainers/testcontainers-rs,MIT OR Apache-2.0,"Thomas Eizinger, Artem Medvedev , Mervyn McCreight" thiserror,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay @@ -288,25 +317,28 @@ tinyvec,https://github.com/Lokathor/tinyvec,Zlib OR Apache-2.0 OR MIT,Lokathor < tinyvec_macros,https://github.com/Soveu/tinyvec_macros,MIT OR Apache-2.0 OR Zlib,Soveu tokio,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-macros,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors +tokio-native-tls,https://github.com/tokio-rs/tls,MIT,Tokio Contributors tokio-rustls,https://github.com/rustls/tokio-rustls,MIT OR Apache-2.0,The tokio-rustls Authors tokio-stream,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-util,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tower,https://github.com/tower-rs/tower,MIT,Tower Maintainers +tower-http,https://github.com/tower-rs/tower-http,MIT,Tower Maintainers tower-layer,https://github.com/tower-rs/tower,MIT,Tower Maintainers tower-service,https://github.com/tower-rs/tower,MIT,Tower Maintainers tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , Tokio Contributors " -tracing-attributes,https://github.com/tokio-rs/tracing,MIT,"Tokio Contributors , Eliza Weisman , David Barsky " tracing-core,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors +tracing-log,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors tracing-subscriber,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , David Barsky , Tokio Contributors " try-lock,https://github.com/seanmonstar/try-lock,MIT,Sean McArthur +typenum,https://github.com/paholg/typenum,MIT OR Apache-2.0,"Paho Lurie-Gregg , Andre Bogus " unicode-ident,https://github.com/dtolnay/unicode-ident,(MIT OR Apache-2.0) AND Unicode-3.0,David Tolnay unicode-xid,https://github.com/unicode-rs/unicode-xid,MIT OR Apache-2.0,"erick.tryzelaar , kwantam , Manish Goregaokar " untrusted,https://github.com/briansmith/untrusted,ISC,Brian Smith url,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers urlencoding,https://github.com/kornelski/rust_urlencoding,MIT,"Kornel , Bertram Truong " -utf16_iter,https://github.com/hsivonen/utf16_iter,Apache-2.0 OR MIT,Henri Sivonen utf8_iter,https://github.com/hsivonen/utf8_iter,Apache-2.0 OR MIT,Henri Sivonen uuid,https://github.com/uuid-rs/uuid,Apache-2.0 OR MIT,"Ashley Mannix, Dylan DPC, Hunar Roop Kahlon" +valuable,https://github.com/tokio-rs/valuable,MIT,The valuable Authors value-bag,https://github.com/sval-rs/value-bag,Apache-2.0 OR MIT,Ashley Mannix walkdir,https://github.com/BurntSushi/walkdir,Unlicense OR MIT,Andrew Gallant want,https://github.com/seanmonstar/want,MIT,Sean McArthur @@ -327,15 +359,20 @@ winapi,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian winapi-util,https://github.com/BurntSushi/winapi-util,Unlicense OR MIT,Andrew Gallant winapi-x86_64-pc-windows-gnu,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian +windows,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft +windows-collections,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-collections Authors windows-core,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft +windows-future,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-future Authors windows-implement,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-interface,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-link,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft +windows-numerics,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-numerics Authors windows-registry,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-result,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-strings,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-sys,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-targets,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft +windows-threading,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows_aarch64_gnullvm,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows_aarch64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows_i686_gnu,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft @@ -346,17 +383,15 @@ windows_x86_64_gnullvm,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0 windows_x86_64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft winreg,https://github.com/gentoo90/winreg-rs,MIT,Igor Shaula wit-bindgen-rt,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,The wit-bindgen-rt Authors -write16,https://github.com/hsivonen/write16,Apache-2.0 OR MIT,The write16 Authors writeable,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers yansi,https://github.com/SergioBenitez/yansi,MIT OR Apache-2.0,Sergio Benitez yoke,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar yoke-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar -zerocopy,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,Joshua Liebow-Feeser zerocopy,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " -zerocopy-derive,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,Joshua Liebow-Feeser zerocopy-derive,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " zerofrom,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zerofrom-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zeroize,https://github.com/RustCrypto/utils/tree/master/zeroize,Apache-2.0 OR MIT,The RustCrypto Project Developers +zerotrie,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerovec,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerovec-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar diff --git a/license-tool.toml b/license-tool.toml index 5f7d8f2b..9d1d3f92 100644 --- a/license-tool.toml +++ b/license-tool.toml @@ -2,4 +2,8 @@ # bollard-stubs is generated from the bollard repository # Missing repository information fixed with package override -"bollard-stubs" = { origin = "https://github.com/fussybeaver/bollard" } \ No newline at end of file +"bollard-stubs" = { origin = "https://github.com/fussybeaver/bollard" } + +# openssl-macros is part of the rust-openssl repository +# Missing repository information fixed with package override +"openssl-macros" = { origin = "https://github.com/sfackler/rust-openssl" } \ No newline at end of file From ea68246082724d85b3ca9bc18d3f89f2e7a29265 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 16:59:29 -0400 Subject: [PATCH 16/34] Update LICENSE-3rdparty.csv and fix openssl-macros license tool configuration --- LICENSE-3rdparty.csv | 48 -------------------------------------------- 1 file changed, 48 deletions(-) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 935992b7..3d873bf5 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -36,7 +36,6 @@ bitflags,https://github.com/bitflags/bitflags,MIT OR Apache-2.0,The Rust Project block-buffer,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers blocking,https://github.com/smol-rs/blocking,Apache-2.0 OR MIT,Stjepan Glavina bollard,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors -bollard-stubs,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors bumpalo,https://github.com/fitzgen/bumpalo,MIT OR Apache-2.0,Nick Fitzgerald byteorder,https://github.com/BurntSushi/byteorder,Unlicense OR MIT,Andrew Gallant bytes,https://github.com/tokio-rs/bytes,MIT,"Carl Lerche , Sean McArthur " @@ -53,7 +52,6 @@ concurrent-queue,https://github.com/smol-rs/concurrent-queue,Apache-2.0 OR MIT," const_format,https://github.com/rodrimati1992/const_format_crates,Zlib,rodrimati1992 const_format_proc_macros,https://github.com/rodrimati1992/const_format_crates,Zlib,rodrimati1992 core-foundation,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers -core-foundation-sys,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers cpufeatures,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers critical-section,https://github.com/rust-embedded/critical-section,MIT OR Apache-2.0,The critical-section Authors crossbeam-channel,https://github.com/crossbeam-rs/crossbeam,MIT OR Apache-2.0,The crossbeam-channel Authors @@ -62,8 +60,6 @@ crossbeam-utils,https://github.com/crossbeam-rs/crossbeam,MIT OR Apache-2.0,The crunchy,https://github.com/eira-fransham/crunchy,MIT,Eira Fransham crypto-common,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers darling,https://github.com/TedDriggs/darling,MIT,Ted Driggs -darling_core,https://github.com/TedDriggs/darling,MIT,Ted Driggs -darling_macro,https://github.com/TedDriggs/darling,MIT,Ted Driggs data-encoding,https://github.com/ia0/data-encoding,MIT,Julien Cretin data-pipeline,https://github.com/Datadog/libdatadog,Apache-2.0,The data-pipeline Authors datadog-ddsketch,https://github.com/Datadog/libdatadog,Apache-2.0,The datadog-ddsketch Authors @@ -97,8 +93,6 @@ fixedbitset,https://github.com/petgraph/fixedbitset,MIT OR Apache-2.0,bluss fnv,https://github.com/servo/rust-fnv,Apache-2.0 OR MIT,Alex Crichton foldhash,https://github.com/orlp/foldhash,Zlib,Orson Peters foreign-types,https://github.com/sfackler/foreign-types,MIT OR Apache-2.0,Steven Fackler -foreign-types-shared,https://github.com/sfackler/foreign-types,MIT OR Apache-2.0,Steven Fackler -form_urlencoded,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers futures,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures Authors futures-channel,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-channel Authors futures-core,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-core Authors @@ -125,7 +119,6 @@ hickory-resolver,https://github.com/hickory-dns/hickory-dns,MIT OR Apache-2.0,Th home,https://github.com/rust-lang/cargo,MIT OR Apache-2.0,Brian Anderson http,https://github.com/hyperium/http,MIT OR Apache-2.0,"Alex Crichton , Carl Lerche , Sean McArthur " http-body,https://github.com/hyperium/http-body,MIT,"Carl Lerche , Lucio Franco , Sean McArthur " -http-body-util,https://github.com/hyperium/http-body,MIT,"Carl Lerche , Lucio Franco , Sean McArthur " httparse,https://github.com/seanmonstar/httparse,MIT OR Apache-2.0,Sean McArthur httpdate,https://github.com/pyfisch/httpdate,MIT OR Apache-2.0,Pyfisch httpmock,https://github.com/alexliesenfeld/httpmock,MIT,Alexander Liesenfeld @@ -145,7 +138,6 @@ icu_properties,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Projec icu_properties_data,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_provider,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers ident_case,https://github.com/TedDriggs/ident_case,MIT OR Apache-2.0,Ted Driggs -idna,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers idna_adapter,https://github.com/hsivonen/idna_adapter,Apache-2.0 OR MIT,The rust-url developers indexmap,https://github.com/bluss/indexmap,Apache-2.0 OR MIT,The indexmap Authors indexmap,https://github.com/indexmap-rs/indexmap,Apache-2.0 OR MIT,The indexmap Authors @@ -167,7 +159,6 @@ libloading,https://github.com/nagisa/rust_libloading,ISC,Simonas Kazlauskas linux-raw-sys,https://github.com/sunfishcode/linux-raw-sys,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Dan Gohman litemap,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers -lock_api,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras log,https://github.com/rust-lang/log,MIT OR Apache-2.0,The Rust Project Developers loom,https://github.com/tokio-rs/loom,MIT,Carl Lerche lru,https://github.com/jeromefroe/lru-rs,MIT,Jerome Froelich @@ -202,11 +193,8 @@ option-ext,https://github.com/soc/option-ext,MPL-2.0,Simon Ochsenreither parking,https://github.com/smol-rs/parking,Apache-2.0 OR MIT,"Stjepan Glavina , The Rust Project Developers" parking_lot,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras -parking_lot_core,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras parse-display,https://github.com/frozenlib/parse-display,MIT OR Apache-2.0,frozenlib -parse-display-derive,https://github.com/frozenlib/parse-display,MIT OR Apache-2.0,frozenlib paste,https://github.com/dtolnay/paste,MIT OR Apache-2.0,David Tolnay -percent-encoding,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers petgraph,https://github.com/petgraph/petgraph,MIT OR Apache-2.0,"bluss, mitchmindtree" phf_shared,https://github.com/rust-phf/rust-phf,MIT,Steven Fackler pico-args,https://github.com/RazrFalcon/pico-args,MIT,Yevhenii Reizner @@ -224,7 +212,6 @@ precomputed-hash,https://github.com/emilio/precomputed-hash,MIT,Emilio Cobos Ál prettyplease,https://github.com/dtolnay/prettyplease,MIT OR Apache-2.0,David Tolnay proc-macro2,https://github.com/dtolnay/proc-macro2,MIT OR Apache-2.0,"David Tolnay , Alex Crichton " prost,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " -prost-derive,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " quinn,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn Authors quinn-proto,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn-proto Authors quinn-udp,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn-udp Authors @@ -232,11 +219,9 @@ quote,https://github.com/dtolnay/quote,MIT OR Apache-2.0,David Tolnay redox_users,https://gitlab.redox-os.org/redox-os/users,MIT,"Jose Narvaez , Wesley Hershberger " ref-cast,https://github.com/dtolnay/ref-cast,MIT OR Apache-2.0,David Tolnay -ref-cast-impl,https://github.com/dtolnay/ref-cast,MIT OR Apache-2.0,David Tolnay regex,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " regex-automata,https://github.com/BurntSushi/regex-automata,Unlicense OR MIT,Andrew Gallant regex-automata,https://github.com/rust-lang/regex/tree/master/regex-automata,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " @@ -267,11 +252,9 @@ schemars,https://github.com/GREsau/schemars,MIT,Graham Esau scopeguard,https://github.com/bluss/scopeguard,MIT OR Apache-2.0,bluss security-framework,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " -security-framework-sys,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " semver,https://github.com/dtolnay/semver,MIT OR Apache-2.0,David Tolnay serde,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_bytes,https://github.com/serde-rs/bytes,MIT OR Apache-2.0,David Tolnay -serde_derive,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_json,https://github.com/serde-rs/json,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_regex,https://github.com/tailhook/serde-regex,MIT OR Apache-2.0,paul@colomiets.name serde_repr,https://github.com/dtolnay/serde-repr,MIT OR Apache-2.0,David Tolnay @@ -292,39 +275,29 @@ static_assertions,https://github.com/nvzqz/static-assertions-rs,MIT OR Apache-2. string_cache,https://github.com/servo/string-cache,MIT OR Apache-2.0,The Servo Project Developers strsim,https://github.com/rapidfuzz/strsim-rs,MIT,"Danny Guo , maxbachmann " structmeta,https://github.com/frozenlib/structmeta,MIT OR Apache-2.0,frozenlib -structmeta-derive,https://github.com/frozenlib/structmeta,MIT OR Apache-2.0,frozenlib subtle,https://github.com/dalek-cryptography/subtle,BSD-3-Clause,"Isis Lovecruft , Henry de Valence " syn,https://github.com/dtolnay/syn,MIT OR Apache-2.0,David Tolnay sync_wrapper,https://github.com/Actyx/sync_wrapper,Apache-2.0,Actyx AG synstructure,https://github.com/mystor/synstructure,MIT,Nika Layzell sys-info,https://github.com/FillZpp/sys-info-rs,MIT,Siyu Wang system-configuration,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN -system-configuration-sys,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN tagptr,https://github.com/oliver-giersch/tagptr,MIT OR Apache-2.0,Oliver Giersch tempfile,https://github.com/Stebalien/tempfile,MIT OR Apache-2.0,"Steven Allen , The Rust Project Developers, Ashley Mannix , Jason White " term,https://github.com/Stebalien/term,MIT OR Apache-2.0,"The Rust Project Developers, Steven Allen" testcontainers,https://github.com/testcontainers/testcontainers-rs,MIT OR Apache-2.0,"Thomas Eizinger, Artem Medvedev , Mervyn McCreight" thiserror,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay -thiserror-impl,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay thread_local,https://github.com/Amanieu/thread_local-rs,MIT OR Apache-2.0,Amanieu d'Antras time,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" -time-core,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" -time-macros,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" tiny-keccak,https://github.com/debris/tiny-keccak,CC0-1.0,debris tinybytes,https://github.com/Datadog/libdatadog,Apache-2.0,The tinybytes Authors tinystr,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers tinyvec,https://github.com/Lokathor/tinyvec,Zlib OR Apache-2.0 OR MIT,Lokathor tinyvec_macros,https://github.com/Soveu/tinyvec_macros,MIT OR Apache-2.0 OR Zlib,Soveu tokio,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors -tokio-macros,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-native-tls,https://github.com/tokio-rs/tls,MIT,Tokio Contributors tokio-rustls,https://github.com/rustls/tokio-rustls,MIT OR Apache-2.0,The tokio-rustls Authors -tokio-stream,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors -tokio-util,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tower,https://github.com/tower-rs/tower,MIT,Tower Maintainers tower-http,https://github.com/tower-rs/tower-http,MIT,Tower Maintainers -tower-layer,https://github.com/tower-rs/tower,MIT,Tower Maintainers -tower-service,https://github.com/tower-rs/tower,MIT,Tower Maintainers tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , Tokio Contributors " tracing-core,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors tracing-log,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors @@ -356,31 +329,11 @@ webpki-roots,https://github.com/rustls/webpki-roots,CDLA-Permissive-2.0,The webp which,https://github.com/harryfei/which-rs,MIT,Harry Fei widestring,https://github.com/starkat99/widestring-rs,MIT OR Apache-2.0,The widestring Authors winapi,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian -winapi-i686-pc-windows-gnu,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian winapi-util,https://github.com/BurntSushi/winapi-util,Unlicense OR MIT,Andrew Gallant -winapi-x86_64-pc-windows-gnu,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian windows,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-collections,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-collections Authors -windows-core,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-future,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-future Authors -windows-implement,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows-interface,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows-link,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-numerics,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-numerics Authors -windows-registry,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows-result,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows-strings,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows-sys,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows-targets,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows-threading,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_aarch64_gnullvm,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_aarch64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_i686_gnu,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_i686_gnullvm,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_i686_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_x86_64_gnu,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_x86_64_gnullvm,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft -windows_x86_64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft winreg,https://github.com/gentoo90/winreg-rs,MIT,Igor Shaula wit-bindgen-rt,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,The wit-bindgen-rt Authors writeable,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers @@ -388,7 +341,6 @@ yansi,https://github.com/SergioBenitez/yansi,MIT OR Apache-2.0,Sergio Benitez yoke-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zerocopy,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " -zerocopy-derive,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " zerofrom,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zerofrom-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zeroize,https://github.com/RustCrypto/utils/tree/master/zeroize,Apache-2.0 OR MIT,The RustCrypto Project Developers From bb4f06ad3c8c3d50713a1a02e0aca778d47e79ee Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 17:25:41 -0400 Subject: [PATCH 17/34] make parsing of rc more extensible for future products --- dd-trace/examples/remote_config.rs | 51 ----- dd-trace/src/configuration/remote_config.rs | 222 +++++++++++++++++--- 2 files changed, 195 insertions(+), 78 deletions(-) delete mode 100644 dd-trace/examples/remote_config.rs diff --git a/dd-trace/examples/remote_config.rs b/dd-trace/examples/remote_config.rs deleted file mode 100644 index c2b4b52d..00000000 --- a/dd-trace/examples/remote_config.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -//! Example of using the remote configuration client to update sampling rules - -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; - -use dd_trace::configuration::remote_config::RemoteConfigClient; -use dd_trace::Config; - -fn main() { - // Create initial configuration - let mut builder = Config::builder(); - builder.set_service("remote-config-example".to_string()); - builder.set_log_level_filter(dd_trace::log::LevelFilter::Debug); - - let config = Arc::new(Mutex::new(builder.build())); - - println!("Starting remote configuration client..."); - if let Ok(cfg) = config.lock() { - println!("Initial sampling rules: {:?}", cfg.trace_sampling_rules()); - } - - // Create remote config client - let client = - RemoteConfigClient::new(config.clone()).expect("Failed to create remote config client"); - - // The client now directly updates the config when new rules arrive - // No callbacks needed - the config is automatically updated - - // Start the client in background - let _handle = client.start(); - - println!("Remote config client started. Listening for configuration updates..."); - println!("Press Ctrl+C to exit"); - - // Keep main thread alive to observe updates - loop { - thread::sleep(Duration::from_secs(10)); - - // Periodically print current configuration - if let Ok(cfg) = config.lock() { - println!( - "\nCurrent sampling rules count: {}", - cfg.trace_sampling_rules().len() - ); - } - } -} diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index a78e186d..75347a56 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -259,6 +259,8 @@ pub struct RemoteConfigClient { poll_interval: Duration, // Cache of successfully applied configurations cached_target_files: Arc>>, + // Registry of product handlers for processing different config types + product_registry: ProductRegistry, } impl RemoteConfigClient { @@ -290,6 +292,7 @@ impl RemoteConfigClient { capabilities: ClientCapabilities::new(), poll_interval: DEFAULT_POLL_INTERVAL, cached_target_files: Arc::new(Mutex::new(Vec::new())), + product_registry: ProductRegistry::new(), }) } @@ -322,7 +325,7 @@ impl RemoteConfigClient { } } Err(e) => { - crate::dd_warn!("RemoteConfigClient: Failed to fetch config: {}", e); + crate::dd_debug!("RemoteConfigClient: Failed to fetch config: {}", e); // Record error in state if let Ok(mut state) = self.state.lock() { state.has_error = true; @@ -483,12 +486,23 @@ impl RemoteConfigClient { let mut any_failure = false; for file in target_files { - // Check if this is an APM tracing config first - skip non-APM_TRACING configs - // Path format is like "datadog/2/APM_TRACING/{config_id}/config" - if !file.path.contains("APM_TRACING") { - // Skip non-APM_TRACING configs - we only support APM_TRACING currently - continue; - } + // Extract product from path to determine which handler to use + // Path format is like "datadog/2/{PRODUCT}/{config_id}/config" + let product = match extract_product_from_path(&file.path) { + Some(p) => p, + None => { + crate::dd_debug!("RemoteConfigClient: Failed to extract product from path: {}", file.path); + continue; + } + }; + + // Check if we have a handler for this product + let handler = match self.product_registry.get_handler(&product) { + Some(h) => h, + None => { + continue; + } + }; // Target files contain base64 encoded JSON configs use base64::Engine; @@ -507,11 +521,11 @@ impl RemoteConfigClient { .unwrap_or((None, None)); let config_id = derived_id .or(meta_id) - .unwrap_or_else(|| "apm-tracing-sampling".to_string()); + .unwrap_or_else(|| format!("{}-config", product.to_lowercase())); let config_version = meta_version.unwrap_or(1); // Apply the config and record success or failure state - match self.process_apm_tracing_config(&config_str) { + match handler.process_config(&config_str, &self.config) { Ok(_) => { // Calculate SHA256 hash of the raw content use sha2::{Digest, Sha256}; @@ -534,7 +548,7 @@ impl RemoteConfigClient { state.config_states.push(ConfigState { id: config_id, version: config_version, - product: "APM_TRACING".to_string(), + product: product.clone(), apply_state: 2, // 2 denotes success apply_error: None, }); @@ -542,8 +556,9 @@ impl RemoteConfigClient { } Err(e) => { any_failure = true; - crate::dd_warn!( - "RemoteConfigClient: Failed to apply APM_TRACING config {}: {}", + crate::dd_debug!( + "RemoteConfigClient: Failed to apply {} config {}: {}", + product, config_id, e ); @@ -552,7 +567,7 @@ impl RemoteConfigClient { state.config_states.push(ConfigState { id: config_id, version: config_version, - product: "APM_TRACING".to_string(), + product: product, apply_state: 3, // 3 denotes error apply_error: Some(format!("{e}")), }); @@ -605,8 +620,33 @@ impl RemoteConfigClient { Ok(()) } - /// Processes APM tracing configuration - fn process_apm_tracing_config(&self, config_json: &str) -> Result<()> { + +} + +/// Product handler trait for processing different remote config products +/// Each product (APM_TRACING, ASM_FEATURES, etc.) implements this trait to handle their specific configuration format +trait ProductHandler { + /// Process the configuration for this product + fn process_config( + &self, + config_json: &str, + config: &Arc>, + ) -> Result<()>; + + /// Get the product name this handler supports + fn product_name(&self) -> &'static str; +} + +struct ApmTracingHandler; + + + +impl ProductHandler for ApmTracingHandler { + fn process_config( + &self, + config_json: &str, + config: &Arc>, + ) -> Result<()> { // Parse the config to extract sampling rules as raw JSON let tracing_config: ApmTracingConfig = serde_json::from_str(config_json) .map_err(|e| anyhow::anyhow!("Failed to parse APM tracing config: {}", e))?; @@ -618,48 +658,90 @@ impl RemoteConfigClient { let rules_json = serde_json::to_string(&rules_value) .map_err(|e| anyhow::anyhow!("Failed to serialize sampling rules: {}", e))?; - if let Ok(mut config) = self.config.lock() { - match config.update_sampling_rules_from_remote(&rules_json) { + if let Ok(mut cfg) = config.lock() { + match cfg.update_sampling_rules_from_remote(&rules_json) { Ok(()) => { - crate::dd_info!( + crate::dd_debug!( "RemoteConfigClient: Applied sampling rules from remote config" ); } Err(e) => { - crate::dd_warn!( + crate::dd_debug!( "RemoteConfigClient: Failed to update sampling rules: {}", e ); } } } else { - crate::dd_warn!( + crate::dd_debug!( "RemoteConfigClient: Failed to lock config to update sampling rules" ); } } else { - crate::dd_info!( + crate::dd_debug!( "RemoteConfigClient: APM tracing config received but tracing_sampling_rules is null" ); } } else { - crate::dd_info!( + crate::dd_debug!( "RemoteConfigClient: APM tracing config received but no tracing_sampling_rules present" ); } Ok(()) } + + fn product_name(&self) -> &'static str { + "APM_TRACING" + } +} + +/// Product registry that maps product names to their handlers +/// This makes it easy to add new products without modifying the main processing logic +struct ProductRegistry { + handlers: HashMap>, +} + +impl ProductRegistry { + fn new() -> Self { + let mut registry = Self { + handlers: HashMap::new(), + }; + + // Register all supported products + registry.register(Box::new(ApmTracingHandler)); + + registry + } + + fn register(&mut self, handler: Box) { + self.handlers.insert(handler.product_name().to_string(), handler); + } + + fn get_handler(&self, product: &str) -> Option<&Box> { + self.handlers.get(product) + } + +} + +/// Extract product name from remote config path +/// Path format is: datadog/2/{PRODUCT}/{config_id}/config +fn extract_product_from_path(path: &str) -> Option { + let parts: Vec<&str> = path.split('/').collect(); + // Look for pattern: datadog/2/PRODUCT/... + if parts.len() >= 3 && parts[0] == "datadog" && parts[1] == "2" { + return Some(parts[2].to_string()); + } + None } // Helper to extract config id from known RC path pattern fn extract_config_id_from_path(path: &str) -> Option { - // Expected: datadog/2/APM_TRACING/{config_id}/config + // Expected: datadog/2/{PRODUCT}/{config_id}/config let parts: Vec<&str> = path.split('/').collect(); - for i in 0..parts.len() { - if parts[i] == "APM_TRACING" { - return parts.get(i + 1).map(|s| s.to_string()); - } + // Look for pattern: datadog/2/PRODUCT/config_id/config + if parts.len() >= 5 && parts[0] == "datadog" && parts[1] == "2" && parts[4] == "config" { + return Some(parts[3].to_string()); } None } @@ -1295,6 +1377,92 @@ mod tests { assert_eq!(cws_custom.get("v").unwrap().as_u64().unwrap(), 1); } + #[test] + fn test_extract_product_from_path() { + // Test APM_TRACING path + assert_eq!( + extract_product_from_path("datadog/2/APM_TRACING/config123/config"), + Some("APM_TRACING".to_string()) + ); + + // Test ASM_FEATURES path + assert_eq!( + extract_product_from_path("datadog/2/ASM_FEATURES/ASM_FEATURES-base/config"), + Some("ASM_FEATURES".to_string()) + ); + + // Test LIVE_DEBUGGING path + assert_eq!( + extract_product_from_path("datadog/2/LIVE_DEBUGGING/LIVE_DEBUGGING-base/config"), + Some("LIVE_DEBUGGING".to_string()) + ); + + // Test APM_SAMPLING path + assert_eq!( + extract_product_from_path("datadog/2/APM_SAMPLING/dynamic_rates/config"), + Some("APM_SAMPLING".to_string()) + ); + + // Test invalid paths + assert_eq!(extract_product_from_path("invalid/path"), None); + assert_eq!(extract_product_from_path("datadog/1/APM_TRACING/config"), None); + assert_eq!(extract_product_from_path("datadog/APM_TRACING/config"), None); + assert_eq!(extract_product_from_path(""), None); + } + + #[test] + fn test_extract_config_id_from_path() { + // Test APM_TRACING path + assert_eq!( + extract_config_id_from_path("datadog/2/APM_TRACING/config123/config"), + Some("config123".to_string()) + ); + + // Test ASM_FEATURES path + assert_eq!( + extract_config_id_from_path("datadog/2/ASM_FEATURES/ASM_FEATURES-base/config"), + Some("ASM_FEATURES-base".to_string()) + ); + + // Test invalid paths + assert_eq!(extract_config_id_from_path("invalid/path"), None); + assert_eq!(extract_config_id_from_path("datadog/2/APM_TRACING/config"), None); // Missing /config at end + assert_eq!(extract_config_id_from_path("datadog/2/APM_TRACING"), None); // Too short + assert_eq!(extract_config_id_from_path(""), None); + } + + #[test] + fn test_product_registry() { + let registry = ProductRegistry::new(); + + // Should have APM_TRACING handler registered + assert!(registry.get_handler("APM_TRACING").is_some()); + + // Should not have unknown products + assert!(registry.get_handler("UNKNOWN_PRODUCT").is_none()); + } + + #[test] + fn test_apm_tracing_handler() { + let handler = ApmTracingHandler; + assert_eq!(handler.product_name(), "APM_TRACING"); + + // Test processing config - this should not panic for valid JSON + let config = Arc::new(Mutex::new(Config::builder().build())); + let config_json = r#"{"tracing_sampling_rules": [{"sample_rate": 0.5, "service": "test"}]}"#; + + // This should succeed + let result = handler.process_config(config_json, &config); + assert!(result.is_ok()); + + // Test invalid JSON + let invalid_json = "invalid json"; + let result = handler.process_config(invalid_json, &config); + assert!(result.is_err()); + } + + + #[test] fn test_tuf_targets_integration_with_remote_config() { // Test that we can process a TUF targets response through the remote config system From f4b33882c661a875aed99919a6d3cce0ac1cc3bd Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 17:44:51 -0400 Subject: [PATCH 18/34] remove uneeded api change --- .../examples/propagator/src/server.rs | 10 +- .../examples/simple_tracing/src/main.rs | 14 +- datadog-opentelemetry/src/lib.rs | 233 ++++++++++++++++-- dd-trace/Cargo.toml | 4 - dd-trace/src/configuration/remote_config.rs | 6 +- 5 files changed, 224 insertions(+), 43 deletions(-) diff --git a/datadog-opentelemetry/examples/propagator/src/server.rs b/datadog-opentelemetry/examples/propagator/src/server.rs index 104b10dc..5a8b5687 100644 --- a/datadog-opentelemetry/examples/propagator/src/server.rs +++ b/datadog-opentelemetry/examples/propagator/src/server.rs @@ -18,7 +18,7 @@ use opentelemetry_http::{Bytes, HeaderExtractor, HeaderInjector}; use opentelemetry_sdk::{ error::OTelSdkResult, logs::{LogProcessor, SdkLogRecord, SdkLoggerProvider}, - trace::{SdkTracerProvider, SpanProcessor, TracerProviderBuilder}, + trace::{SdkTracerProvider, SpanProcessor}, }; use opentelemetry_semantic_conventions::trace; use opentelemetry_stdout::{LogExporter, SpanExporter}; @@ -219,13 +219,13 @@ fn init_tracer() -> SdkTracerProvider { .set_env("staging".to_string()) .build(); - let tracer_provider_builder = TracerProviderBuilder::default() + datadog_opentelemetry::tracing() + .with_config(config) .with_span_processor(EnrichWithBaggageSpanProcessor) .with_span_processor(opentelemetry_sdk::trace::SimpleSpanProcessor::new( SpanExporter::default(), - )); - - datadog_opentelemetry::init_datadog(config, tracer_provider_builder, None) + )) + .init() } fn init_logs() -> SdkLoggerProvider { diff --git a/datadog-opentelemetry/examples/simple_tracing/src/main.rs b/datadog-opentelemetry/examples/simple_tracing/src/main.rs index 0bece7f2..c933d425 100644 --- a/datadog-opentelemetry/examples/simple_tracing/src/main.rs +++ b/datadog-opentelemetry/examples/simple_tracing/src/main.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use opentelemetry::trace::Tracer; -use opentelemetry_sdk::trace::TracerProviderBuilder; fn foo() { opentelemetry::global::tracer("foo").in_span("foo", |_cx| { @@ -16,12 +15,13 @@ fn bar() { } fn main() { - let config = dd_trace::Config::builder() - .set_service("simple_tracing".to_string()) - .build(); - - let tracer_provider = - datadog_opentelemetry::init_datadog(config, TracerProviderBuilder::default(), None); + let tracer_provider = datadog_opentelemetry::tracing() + .with_config( + dd_trace::Config::builder() + .set_service("simple_tracing".to_string()) + .build(), + ) + .init(); foo(); diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 434a4909..b00e318b 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -1,6 +1,68 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +//! # Datadog Opentelemetry +//! +//! A datadog layer of compatibility for the opentelemetry SDK +//! +//! ## Usage +//! +//! This is the minimal example to initialize the SDK. +//! +//! This will read datadog and opentelemetry configuration from environment variables and other +//! available sources. +//! And initialize and set up the tracer provider and the text map propagator globally. +//! +//! ```rust +//! # fn main() { +//! datadog_opentelemetry::tracing().init(); +//! # } +//! ``` +//! +//! It is also possible to customize the datadog configuration passed to the tracer provider. +//! +//! ```rust +//! // Custom datadog configuration +//! datadog_opentelemetry::tracing() +//! .with_config( +//! dd_trace::Config::builder() +//! .set_service("my_service".to_string()) +//! .set_env("my_env".to_string()) +//! .set_version("1.0.0".to_string()) +//! .build(), +//! ) +//! .init(); +//! ``` +//! +//! Or to pass options to the OpenTelemetry SDK TracerProviderBuilder +//! ```rust +//! # #[derive(Debug)] +//! # struct MySpanProcessor; +//! # +//! # impl opentelemetry_sdk::trace::SpanProcessor for MySpanProcessor { +//! # fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) { +//! # } +//! # fn on_end(&self, span: opentelemetry_sdk::trace::SpanData) {} +//! # fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { +//! # Ok(()) +//! # } +//! # fn shutdown_with_timeout( +//! # &self, +//! # timeout: std::time::Duration, +//! # ) -> opentelemetry_sdk::error::OTelSdkResult { +//! # Ok(()) +//! # } +//! # fn set_resource(&mut self, _resource: &opentelemetry_sdk::Resource) {} +//! # } +//! # +//! // Custom otel tracer sdk options +//! datadog_opentelemetry::tracing() +//! .with_max_attributes_per_span(64) +//! // Custom span processor +//! .with_span_processor(MySpanProcessor) +//! .init(); +//! ``` + mod ddtrace_transform; mod sampler; mod span_exporter; @@ -18,43 +80,166 @@ use sampler::Sampler; use span_processor::{DatadogSpanProcessor, TraceRegistry}; use text_map_propagator::DatadogPropagator; -// Type alias to simplify complex callback type +// Type alias to simplify complex callback type for remote config type SamplerCallback = Arc>; -/// Initialize the Datadog OpenTelemetry exporter. -/// -/// This function sets up the global OpenTelemetry SDK provider for compatibility with datadog. +pub struct DatadogTracingBuilder { + config: Option, + resource: Option, + tracer_provider: opentelemetry_sdk::trace::TracerProviderBuilder, +} + +impl DatadogTracingBuilder { + pub fn with_config(mut self, config: dd_trace::Config) -> Self { + self.config = Some(config); + self + } + + pub fn with_resource(mut self, resource: opentelemetry_sdk::Resource) -> Self { + self.resource = Some(resource); + self + } + + pub fn init(self) -> SdkTracerProvider { + let config = self + .config + .unwrap_or_else(|| dd_trace::Config::builder().build()); + let (tracer_provider, propagator) = + make_tracer(config, self.tracer_provider, self.resource); + + opentelemetry::global::set_text_map_propagator(propagator); + opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + tracer_provider + } +} + +impl DatadogTracingBuilder { + // Methods forwarded to the otel tracer provider builder + + pub fn with_span_processor( + mut self, + processor: T, + ) -> Self { + self.tracer_provider = self.tracer_provider.with_span_processor(processor); + self + } + + /// Specify the number of events to be recorded per span. + pub fn with_max_events_per_span(mut self, max_events: u32) -> Self { + self.tracer_provider = self.tracer_provider.with_max_events_per_span(max_events); + self + } + + /// Specify the number of attributes to be recorded per span. + pub fn with_max_attributes_per_span(mut self, max_attributes: u32) -> Self { + self.tracer_provider = self + .tracer_provider + .with_max_attributes_per_span(max_attributes); + self + } + + /// Specify the number of events to be recorded per span. + pub fn with_max_links_per_span(mut self, max_links: u32) -> Self { + self.tracer_provider = self.tracer_provider.with_max_links_per_span(max_links); + self + } + + /// Specify the number of attributes one event can have. + pub fn with_max_attributes_per_event(mut self, max_attributes: u32) -> Self { + self.tracer_provider = self + .tracer_provider + .with_max_attributes_per_event(max_attributes); + self + } + + /// Specify the number of attributes one link can have. + pub fn with_max_attributes_per_link(mut self, max_attributes: u32) -> Self { + self.tracer_provider = self + .tracer_provider + .with_max_attributes_per_link(max_attributes); + self + } + + /// Specify all limit via the span_limits + pub fn with_span_limits(mut self, span_limits: opentelemetry_sdk::trace::SpanLimits) -> Self { + self.tracer_provider = self.tracer_provider.with_span_limits(span_limits); + self + } +} + +/// Initialize a new Datadog Tracing builder /// /// # Usage +/// /// ```rust -/// use dd_trace::Config; -/// use opentelemetry_sdk::trace::TracerProviderBuilder; +/// // Default configuration +/// datadog_opentelemetry::tracing().init(); +/// ``` /// -/// // This picks up env var configuration and other datadog configuration sources -/// let datadog_config = Config::builder().build(); +/// It is also possible to customize the datadog configuration passed to the tracer provider. /// -/// datadog_opentelemetry::init_datadog( -/// datadog_config, -/// TracerProviderBuilder::default(), // Pass any opentelemetry specific configuration here -/// // .with_max_attributes_per_span(max_attributes) -/// None, -/// ); +/// ```rust +/// // Custom datadog configuration +/// datadog_opentelemetry::tracing() +/// .with_config( +/// dd_trace::Config::builder() +/// .set_service("my_service".to_string()) +/// .set_env("my_env".to_string()) +/// .set_version("1.0.0".to_string()) +/// .build(), +/// ) +/// .init(); /// ``` +/// +/// Or to pass options to the OpenTelemetry SDK TracerProviderBuilder +/// ```rust +/// # #[derive(Debug)] +/// # struct MySpanProcessor; +/// # +/// # impl opentelemetry_sdk::trace::SpanProcessor for MySpanProcessor { +/// # fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) { +/// # } +/// # fn on_end(&self, span: opentelemetry_sdk::trace::SpanData) {} +/// # fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { +/// # Ok(()) +/// # } +/// # fn shutdown_with_timeout( +/// # &self, +/// # timeout: std::time::Duration, +/// # ) -> opentelemetry_sdk::error::OTelSdkResult { +/// # Ok(()) +/// # } +/// # fn set_resource(&mut self, _resource: &opentelemetry_sdk::Resource) {} +/// # } +/// # +/// // Custom otel tracer sdk options +/// datadog_opentelemetry::tracing() +/// .with_max_attributes_per_span(64) +/// // Custom span processor +/// .with_span_processor(MySpanProcessor) +/// .init(); +/// ``` +pub fn tracing() -> DatadogTracingBuilder { + DatadogTracingBuilder { + config: None, + tracer_provider: opentelemetry_sdk::trace::SdkTracerProvider::builder(), + resource: None, + } +} + +#[deprecated(note = "Use `datadog_opentelemetry::tracing()` instead")] +// TODO: update system tests to use the new API and remove this function pub fn init_datadog( config: dd_trace::Config, - // TODO(paullgdc): Should we take a builder or create it ourselves? - // because some customer might want to set max__per_span using - // the builder APIs - // Or maybe we need a builder API called DatadogDistribution that takes - // all parameters and has an install method? tracer_provider_builder: opentelemetry_sdk::trace::TracerProviderBuilder, resource: Option, ) -> SdkTracerProvider { - let (tracer_provider, propagator) = make_tracer(config, tracer_provider_builder, resource); - - opentelemetry::global::set_text_map_propagator(propagator); - opentelemetry::global::set_tracer_provider(tracer_provider.clone()); - tracer_provider + DatadogTracingBuilder { + config: Some(config), + tracer_provider: tracer_provider_builder, + resource, + } + .init() } /// Create an instance of the tracer provider diff --git a/dd-trace/Cargo.toml b/dd-trace/Cargo.toml index 70b88643..18e5e303 100644 --- a/dd-trace/Cargo.toml +++ b/dd-trace/Cargo.toml @@ -26,7 +26,3 @@ ddtelemetry = { workspace = true } [features] test-utils = [] - -[[example]] -name = "remote_config" -path = "examples/remote_config.rs" diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 75347a56..a24882b3 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -567,7 +567,7 @@ impl RemoteConfigClient { state.config_states.push(ConfigState { id: config_id, version: config_version, - product: product, + product, apply_state: 3, // 3 denotes error apply_error: Some(format!("{e}")), }); @@ -718,8 +718,8 @@ impl ProductRegistry { self.handlers.insert(handler.product_name().to_string(), handler); } - fn get_handler(&self, product: &str) -> Option<&Box> { - self.handlers.get(product) + fn get_handler(&self, product: &str) -> Option<&(dyn ProductHandler + Send + Sync)> { + self.handlers.get(product).map(|handler| handler.as_ref()) } } From 3bbd416c83662737af816f397fb086be37c6a65b Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 12 Aug 2025 19:28:34 -0400 Subject: [PATCH 19/34] touch ups --- datadog-opentelemetry/src/lib.rs | 5 +- datadog-opentelemetry/src/sampler.rs | 5 +- datadog-opentelemetry/src/span_processor.rs | 33 +++--- datadog-opentelemetry/tests/mod.rs | 38 ------- dd-trace-sampling/src/datadog_sampler.rs | 28 ++--- dd-trace-sampling/src/lib.rs | 2 +- dd-trace-sampling/src/rules_sampler.rs | 7 -- dd-trace/src/configuration/configuration.rs | 11 +- dd-trace/src/configuration/remote_config.rs | 114 ++++++++------------ 9 files changed, 77 insertions(+), 166 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index b00e318b..9bcf589b 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -81,7 +81,8 @@ use span_processor::{DatadogSpanProcessor, TraceRegistry}; use text_map_propagator::DatadogPropagator; // Type alias to simplify complex callback type for remote config -type SamplerCallback = Arc>; +// Arc wrapper around SamplingRulesCallback for shared ownership in remote config +type SamplerCallback = Arc; pub struct DatadogTracingBuilder { config: Option, @@ -277,9 +278,7 @@ fn make_tracer( .with_id_generator(trace_id::TraceidGenerator) .build(); - // Initialize remote configuration client if enabled if config.remote_config_enabled() { - // Create a mutable config that can be updated by remote config let config_arc = Arc::new(config); let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); diff --git a/datadog-opentelemetry/src/sampler.rs b/datadog-opentelemetry/src/sampler.rs index 63cb65ca..f30418a5 100644 --- a/datadog-opentelemetry/src/sampler.rs +++ b/datadog-opentelemetry/src/sampler.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use dd_trace::{constants::SAMPLING_DECISION_MAKER_TAG_KEY, Config}; -use dd_trace_sampling::DatadogSampler; +use dd_trace_sampling::{DatadogSampler, SamplingRulesCallback}; use opentelemetry::trace::TraceContextExt; use opentelemetry_sdk::{trace::ShouldSample, Resource}; use std::{ @@ -12,9 +12,6 @@ use std::{ use crate::{span_processor::RegisterTracePropagationResult, TraceRegistry}; -/// Type alias for sampling rules update callback -type SamplingRulesCallback = Box Fn(&'a [dd_trace::SamplingRuleConfig]) + Send + Sync>; - #[derive(Debug, Clone)] pub struct Sampler { sampler: DatadogSampler, diff --git a/datadog-opentelemetry/src/span_processor.rs b/datadog-opentelemetry/src/span_processor.rs index 09772d5e..c93ea5b9 100644 --- a/datadog-opentelemetry/src/span_processor.rs +++ b/datadog-opentelemetry/src/span_processor.rs @@ -417,30 +417,31 @@ impl DatadogSpanProcessor { /// This allows discovery of all services at runtime for proper remote configuration. fn extract_and_add_service_from_span(&self, span: &SpanData) { // First check span attributes for service.name - if let Some(service_name) = span.attributes.iter().find_map(|kv| { + let service_name = if let Some(service_name) = span.attributes.iter().find_map(|kv| { if kv.key.as_str() == "service.name" { Some(kv.value.to_string()) } else { None } }) { - // Only add if it's not the default service name - if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { - self.config.add_extra_service(&service_name); - } - return; - } - - // If not found in span attributes, check resource attributes - if let Ok(resource) = self.resource.read() { - let service_key = opentelemetry::Key::from_static_str("service.name"); - if let Some(service_attr) = resource.get(&service_key) { - let service_name = service_attr.to_string(); - // Only add if it's not the default service name - if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { - self.config.add_extra_service(&service_name); + service_name + } else { + // If not found in span attributes, check resource attributes + if let Ok(resource) = self.resource.read() { + let service_key = opentelemetry::Key::from_static_str("service.name"); + if let Some(service_attr) = resource.get(&service_key) { + service_attr.to_string() + } else { + return; // No service name found anywhere } + } else { + return; // Could not read resource } + }; + + // Only add if it's not empty or the default service name + if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { + self.config.add_extra_service(&service_name); } } } diff --git a/datadog-opentelemetry/tests/mod.rs b/datadog-opentelemetry/tests/mod.rs index bf0b00e5..6a21a0d3 100644 --- a/datadog-opentelemetry/tests/mod.rs +++ b/datadog-opentelemetry/tests/mod.rs @@ -234,44 +234,6 @@ mod datadog_test_agent { ); } - #[test] - fn test_remote_config_initialization() { - // Test that remote config client is initialized when remote config is enabled - std::env::set_var("DD_REMOTE_CONFIGURATION_ENABLED", "true"); - let config = dd_trace::Config::builder().build(); - - // This should initialize the remote config client - let (tracer_provider, _propagator) = make_test_tracer( - config, - opentelemetry_sdk::trace::TracerProviderBuilder::default(), - ); - - // Verify the tracer provider was created successfully - let _tracer = tracer_provider.tracer("test"); - // If we get here, the tracer provider was created successfully - - std::env::remove_var("DD_REMOTE_CONFIGURATION_ENABLED"); - } - - #[test] - fn test_remote_config_disabled() { - // Test that remote config client is not initialized when remote config is disabled - std::env::set_var("DD_REMOTE_CONFIGURATION_ENABLED", "false"); - let config = dd_trace::Config::builder().build(); - - // This should not initialize the remote config client - let (tracer_provider, _propagator) = make_test_tracer( - config, - opentelemetry_sdk::trace::TracerProviderBuilder::default(), - ); - - // Verify the tracer provider was created successfully - let _tracer = tracer_provider.tracer("test"); - // If we get here, the tracer provider was created successfully - - std::env::remove_var("DD_REMOTE_CONFIGURATION_ENABLED"); - } - #[track_caller] fn assert_subset, SS: IntoIterator>(set: S, subset: SS) where diff --git a/dd-trace-sampling/src/datadog_sampler.rs b/dd-trace-sampling/src/datadog_sampler.rs index 6fcb59cc..d31a6f99 100644 --- a/dd-trace-sampling/src/datadog_sampler.rs +++ b/dd-trace-sampling/src/datadog_sampler.rs @@ -7,6 +7,11 @@ use dd_trace::constants::{ }; use dd_trace::sampling::{mechanism, SamplingDecision as DdSamplingDecision, SamplingMechanism}; +/// Type alias for sampling rules update callback +/// Consolidated callback type used across crates for remote config sampling updates +pub type SamplingRulesCallback = + Box Fn(&'a [dd_trace::SamplingRuleConfig]) + Send + Sync>; + use datadog_opentelemetry_mappings::{ get_dd_key_for_otlp_attribute, get_otel_env, get_otel_operation_name_v2, get_otel_resource_v2, get_otel_service, get_otel_status_code, OtelSpan, @@ -27,9 +32,6 @@ use crate::rate_sampler::RateSampler; use crate::rules_sampler::RulesSampler; use crate::utils; -/// Type alias for sampling rules update callback -type SamplingRulesCallback = Box Fn(&'a [dd_trace::SamplingRuleConfig]) + Send + Sync>; - fn matcher_from_rule(rule: &str) -> Option { (rule != NO_RULE).then(|| GlobMatcher::new(rule)) } @@ -55,7 +57,7 @@ pub struct SamplingRule { impl SamplingRule { /// Converts a vector of SamplingRuleConfig into SamplingRule objects - /// Centralizes the conversion logic to avoid duplication across different modules + /// Centralizes the conversion logic pub fn from_configs(configs: Vec) -> Vec { configs .into_iter() @@ -321,31 +323,13 @@ impl DatadogSampler { } /// Creates a callback for updating sampling rules from remote configuration - /// /// # Returns /// A boxed function that takes a slice of SamplingRuleConfig and updates the sampling rules - /// - /// # Example - /// The callback receives sampling rules directly from the configuration: - /// ```rust - /// use dd_trace::SamplingRuleConfig; - /// - /// let rules = &[SamplingRuleConfig { - /// sample_rate: 0.5, - /// service: Some("web-*".to_string()), - /// name: Some("http.*".to_string()), - /// resource: Some("/api/*".to_string()), - /// tags: [("env".to_string(), "prod".to_string())].into(), - /// provenance: "customer".to_string(), - /// }]; - /// ``` pub fn on_rules_update(&self) -> SamplingRulesCallback { let rules_sampler = self.rules.clone(); Box::new(move |rule_configs: &[dd_trace::SamplingRuleConfig]| { - // Convert the rule configs to SamplingRule instances let new_rules = SamplingRule::from_configs(rule_configs.to_vec()); - // Update the rules rules_sampler.update_rules(new_rules); }) } diff --git a/dd-trace-sampling/src/lib.rs b/dd-trace-sampling/src/lib.rs index e2fd1e3d..69269e79 100644 --- a/dd-trace-sampling/src/lib.rs +++ b/dd-trace-sampling/src/lib.rs @@ -12,4 +12,4 @@ pub(crate) mod rules_sampler; pub(crate) mod utils; // Re-export key public types -pub use datadog_sampler::{DatadogSampler, SamplingRule}; +pub use datadog_sampler::{DatadogSampler, SamplingRule, SamplingRulesCallback}; diff --git a/dd-trace-sampling/src/rules_sampler.rs b/dd-trace-sampling/src/rules_sampler.rs index cf11a0a2..19a4fe3c 100644 --- a/dd-trace-sampling/src/rules_sampler.rs +++ b/dd-trace-sampling/src/rules_sampler.rs @@ -12,19 +12,12 @@ pub(crate) struct RulesSampler { } impl RulesSampler { - /// Creates a new RulesSampler with the given initial rules pub fn new(rules: Vec) -> Self { Self { inner: Arc::new(RwLock::new(rules)), } } - /// Gets a clone of the current rules - #[allow(dead_code)] - pub fn get_rules(&self) -> Vec { - self.inner.read().unwrap().clone() - } - /// Updates the rules with a new set pub fn update_rules(&self, new_rules: Vec) { *self.inner.write().unwrap() = new_rules; diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index d0eb86a7..ad8f4d5f 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -449,7 +449,6 @@ impl Config { let parsed_sampling_rules_config = to_val(sources.get_parse::("DD_TRACE_SAMPLING_RULES")); - // Initialize the sampling rules ConfigItem let mut sampling_rules_item = ConfigItem::new( "DD_TRACE_SAMPLING_RULES", ParsedSamplingRules::default(), // default is empty rules @@ -462,7 +461,7 @@ impl Config { // Parse remote configuration enabled flag let remote_config_enabled = - to_val(sources.get_parse::("DD_REMOTE_CONFIGURATION_ENABLED")).unwrap_or(true); // Default to enabled + to_val(sources.get_parse::("DD_REMOTE_CONFIGURATION_ENABLED")).unwrap_or(true); Self { runtime_id: default.runtime_id, @@ -647,7 +646,6 @@ impl Config { self.trace_propagation_extract_first } - /// Updates sampling rules from remote configuration pub fn update_sampling_rules_from_remote(&mut self, rules_json: &str) -> Result<(), String> { // Parse the JSON into SamplingRuleConfig objects let rules: Vec = serde_json::from_str(rules_json) @@ -675,7 +673,6 @@ impl Config { Ok(()) } - /// Clears remote configuration sampling rules, falling back to code/env/default pub fn clear_remote_sampling_rules(&mut self) { self.trace_sampling_rules.unset_rc(); @@ -707,7 +704,7 @@ impl Config { /// RemoteConfigUpdate::SamplingRules(rules) => { /// println!("Received {} new sampling rules", rules.len()); /// // Update your sampler here - /// } // Future remote config types can be handled here by adding new match arms + /// } // Future remote config types can be added here /// // as new variants are added to the RemoteConfigUpdate enum /// } /// }); @@ -1459,7 +1456,7 @@ mod tests { assert_eq!(config.trace_sampling_rules().len(), 1); assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.25); - // Builder override should take precedence + // Code override should take precedence let config = Config::builder_with_sources(&sources) .set_trace_sampling_rules(vec![SamplingRuleConfig { sample_rate: 0.75, @@ -1517,7 +1514,7 @@ mod tests { .update_sampling_rules_from_remote(empty_remote_rules_json) .unwrap(); - // NEW BEHAVIOR: Empty remote rules now automatically fall back to local rules + // Empty remote rules automatically fall back to local rules assert_eq!(config.trace_sampling_rules().len(), 1); // Falls back to local rules assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.3); // Local rule values assert_eq!(config.trace_sampling_rules.source(), ConfigSource::EnvVar); // Back to env source! diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index a24882b3..c8f679ca 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -9,8 +9,8 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; -const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(5); -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); +const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(5); // 5 seconds is the highest interval allowed by the spec +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2); /// Capabilities that the client supports #[derive(Debug, Clone)] @@ -166,7 +166,7 @@ struct TargetFile { /// Configuration payload for APM tracing /// Based on the apm-tracing.json schema from dd-go -/// See: https://github.com/DataDog/dd-go/blob/main/remote-config/apps/rc-product/schemas/apm-tracing.json +/// See: https://github.com/DataDog/dd-go/blob/prod/remote-config/apps/rc-schema-validation/schemas/apm-tracing.json #[derive(Debug, Clone, Deserialize)] struct ApmTracingConfig { #[serde(default, rename = "tracing_sampling_rules")] @@ -178,7 +178,6 @@ struct ApmTracingConfig { /// This is just an alias for SignedTargets to match the JSON structure type TargetsMetadata = SignedTargets; -/// Target description matching Python's TargetDesc #[derive(Debug, Deserialize, Serialize)] struct TargetDesc { /// Length of the target file @@ -189,7 +188,6 @@ struct TargetDesc { custom: Option, } -/// Targets structure matching Python's Targets #[derive(Debug, Deserialize)] struct Targets { /// Type of the targets (usually "targets") @@ -230,23 +228,8 @@ struct SignedTargets { /// The client expects to receive configuration files with paths like: /// `datadog/2/APM_TRACING/{config_id}/config` /// -/// These files contain JSON with a `tracing_sampling_rules` field that defines sampling rules. -/// -/// # Example -/// ```no_run -/// use dd_trace::configuration::remote_config::RemoteConfigClient; -/// use dd_trace::Config; -/// use std::sync::{Arc, Mutex}; -/// -/// let config = Arc::new(Mutex::new(Config::builder().build())); -/// -/// let client = RemoteConfigClient::new(config).unwrap(); -/// -/// // The client directly updates the config when new rules arrive -/// -/// // Start the client in a background thread -/// let handle = client.start(); -/// ``` +/// These files contain JSON with a various fields, one of which is `tracing_sampling_rules` field +/// that defines sampling rules. pub struct RemoteConfigClient { /// Unique identifier for this client instance /// Different from runtime_id - each RemoteConfigClient gets its own UUID @@ -481,7 +464,7 @@ impl RemoteConfigClient { // Parse target files if present if let Some(target_files) = response.target_files { - // Build a new cache - don't clear the old one yet! + // Build a new cache let mut new_cache = Vec::new(); let mut any_failure = false; @@ -491,7 +474,10 @@ impl RemoteConfigClient { let product = match extract_product_from_path(&file.path) { Some(p) => p, None => { - crate::dd_debug!("RemoteConfigClient: Failed to extract product from path: {}", file.path); + crate::dd_debug!( + "RemoteConfigClient: Failed to extract product from path: {}", + file.path + ); continue; } }; @@ -525,6 +511,8 @@ impl RemoteConfigClient { let config_version = meta_version.unwrap_or(1); // Apply the config and record success or failure state + // Right now we only support APM_TRACING handler, but in the future we will support + // other products match handler.process_config(&config_str, &self.config) { Ok(_) => { // Calculate SHA256 hash of the raw content @@ -591,7 +579,6 @@ impl RemoteConfigClient { } /// Validates that target files exist in either signed targets or client configs - /// This validation ensures security by preventing unauthorized config files from being applied fn validate_signed_target_files( &self, payload_target_files: &[TargetFile], @@ -619,34 +606,23 @@ impl RemoteConfigClient { Ok(()) } - - } /// Product handler trait for processing different remote config products -/// Each product (APM_TRACING, ASM_FEATURES, etc.) implements this trait to handle their specific configuration format +/// Each product (APM_TRACING, AGENT_CONFIG, etc.) implements this trait to handle their specific +/// configuration format trait ProductHandler { /// Process the configuration for this product - fn process_config( - &self, - config_json: &str, - config: &Arc>, - ) -> Result<()>; - + fn process_config(&self, config_json: &str, config: &Arc>) -> Result<()>; + /// Get the product name this handler supports fn product_name(&self) -> &'static str; } struct ApmTracingHandler; - - impl ProductHandler for ApmTracingHandler { - fn process_config( - &self, - config_json: &str, - config: &Arc>, - ) -> Result<()> { + fn process_config(&self, config_json: &str, config: &Arc>) -> Result<()> { // Parse the config to extract sampling rules as raw JSON let tracing_config: ApmTracingConfig = serde_json::from_str(config_json) .map_err(|e| anyhow::anyhow!("Failed to parse APM tracing config: {}", e))?; @@ -690,7 +666,7 @@ impl ProductHandler for ApmTracingHandler { Ok(()) } - + fn product_name(&self) -> &'static str { "APM_TRACING" } @@ -707,21 +683,21 @@ impl ProductRegistry { let mut registry = Self { handlers: HashMap::new(), }; - + // Register all supported products registry.register(Box::new(ApmTracingHandler)); registry } - + fn register(&mut self, handler: Box) { - self.handlers.insert(handler.product_name().to_string(), handler); + self.handlers + .insert(handler.product_name().to_string(), handler); } - + fn get_handler(&self, product: &str) -> Option<&(dyn ProductHandler + Send + Sync)> { self.handlers.get(product).map(|handler| handler.as_ref()) } - } /// Extract product name from remote config path @@ -779,7 +755,7 @@ mod tests { #[test] fn test_request_serialization() { - // Test that our request format matches the expected structure from Python + // Test that our request format matches the expected structure let state = ClientState { root_version: 1, targets_version: 122282776, @@ -1385,28 +1361,28 @@ mod tests { Some("APM_TRACING".to_string()) ); - // Test ASM_FEATURES path - assert_eq!( - extract_product_from_path("datadog/2/ASM_FEATURES/ASM_FEATURES-base/config"), - Some("ASM_FEATURES".to_string()) - ); - // Test LIVE_DEBUGGING path assert_eq!( extract_product_from_path("datadog/2/LIVE_DEBUGGING/LIVE_DEBUGGING-base/config"), Some("LIVE_DEBUGGING".to_string()) ); - // Test APM_SAMPLING path + // Test AGENT_CONFIG path assert_eq!( - extract_product_from_path("datadog/2/APM_SAMPLING/dynamic_rates/config"), - Some("APM_SAMPLING".to_string()) + extract_product_from_path("datadog/2/AGENT_CONFIG/dynamic_rates/config"), + Some("AGENT_CONFIG".to_string()) ); // Test invalid paths assert_eq!(extract_product_from_path("invalid/path"), None); - assert_eq!(extract_product_from_path("datadog/1/APM_TRACING/config"), None); - assert_eq!(extract_product_from_path("datadog/APM_TRACING/config"), None); + assert_eq!( + extract_product_from_path("datadog/1/APM_TRACING/config"), + None + ); + assert_eq!( + extract_product_from_path("datadog/APM_TRACING/config"), + None + ); assert_eq!(extract_product_from_path(""), None); } @@ -1426,7 +1402,10 @@ mod tests { // Test invalid paths assert_eq!(extract_config_id_from_path("invalid/path"), None); - assert_eq!(extract_config_id_from_path("datadog/2/APM_TRACING/config"), None); // Missing /config at end + assert_eq!( + extract_config_id_from_path("datadog/2/APM_TRACING/config"), + None + ); // Missing /config at end assert_eq!(extract_config_id_from_path("datadog/2/APM_TRACING"), None); // Too short assert_eq!(extract_config_id_from_path(""), None); } @@ -1434,10 +1413,10 @@ mod tests { #[test] fn test_product_registry() { let registry = ProductRegistry::new(); - + // Should have APM_TRACING handler registered assert!(registry.get_handler("APM_TRACING").is_some()); - + // Should not have unknown products assert!(registry.get_handler("UNKNOWN_PRODUCT").is_none()); } @@ -1446,23 +1425,22 @@ mod tests { fn test_apm_tracing_handler() { let handler = ApmTracingHandler; assert_eq!(handler.product_name(), "APM_TRACING"); - + // Test processing config - this should not panic for valid JSON let config = Arc::new(Mutex::new(Config::builder().build())); - let config_json = r#"{"tracing_sampling_rules": [{"sample_rate": 0.5, "service": "test"}]}"#; - + let config_json = + r#"{"tracing_sampling_rules": [{"sample_rate": 0.5, "service": "test"}]}"#; + // This should succeed let result = handler.process_config(config_json, &config); assert!(result.is_ok()); - + // Test invalid JSON let invalid_json = "invalid json"; let result = handler.process_config(invalid_json, &config); assert!(result.is_err()); } - - #[test] fn test_tuf_targets_integration_with_remote_config() { // Test that we can process a TUF targets response through the remote config system From ef49d8338fb1368c939b291a4a70cf0cc5aa589f Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Wed, 13 Aug 2025 13:52:28 -0400 Subject: [PATCH 20/34] only pass shared config when needed --- datadog-opentelemetry/src/lib.rs | 22 +++++---- datadog-opentelemetry/src/span_processor.rs | 53 ++++++++++++++++----- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 9bcf589b..2a1dbabd 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -251,6 +251,8 @@ fn make_tracer( ) -> (SdkTracerProvider, DatadogPropagator) { let registry = Arc::new(TraceRegistry::new()); let resource_slot = Arc::new(RwLock::new(Resource::builder_empty().build())); + // Sampler only needs config for initialization (reads initial sampling rules) + // Runtime updates come via config callback, so no need for shared config let sampler = Sampler::new(&config, resource_slot.clone(), registry.clone()); let agent_response_handler = sampler.on_agent_response(); @@ -266,8 +268,14 @@ fn make_tracer( None }; + // Create shared config only for SpanProcessor (needs service discovery) and remote config + // Other components get cloned config since they don't use values that can be updated via remote + // config (e.g., propagation style, tracer version, language settings are static after + // initialization) + let shared_config = Arc::new(Mutex::new(config.clone())); + let span_processor = DatadogSpanProcessor::new( - config.clone(), + shared_config.clone(), registry.clone(), resource_slot.clone(), Some(agent_response_handler), @@ -279,14 +287,11 @@ fn make_tracer( .build(); if config.remote_config_enabled() { - let config_arc = Arc::new(config); - let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); - - // Add sampler callback to the config before creating the remote config client + // Add sampler callback to the shared config before creating the remote config client if let Some(sampler_callback) = sampler_callback { let sampler_callback = Arc::new(sampler_callback); let sampler_callback_clone: SamplerCallback = sampler_callback.clone(); - mutable_config.lock().unwrap().add_remote_config_callback( + shared_config.lock().unwrap().add_remote_config_callback( "datadog_sampler_on_rules_update".to_string(), move |update| match update { RemoteConfigUpdate::SamplingRules(rules) => { @@ -296,10 +301,9 @@ fn make_tracer( ); } - // Create remote config client with mutable config - let mutable_config = Arc::new(Mutex::new(config_arc.as_ref().clone())); + // Create remote config client with shared config if let Ok(client) = - dd_trace::configuration::remote_config::RemoteConfigClient::new(mutable_config) + dd_trace::configuration::remote_config::RemoteConfigClient::new(shared_config) { // Start the client in background let _handle = client.start(); diff --git a/datadog-opentelemetry/src/span_processor.rs b/datadog-opentelemetry/src/span_processor.rs index c93ea5b9..ce25c755 100644 --- a/datadog-opentelemetry/src/span_processor.rs +++ b/datadog-opentelemetry/src/span_processor.rs @@ -4,7 +4,7 @@ use std::{ collections::{hash_map, HashMap}, str::FromStr, - sync::{Arc, RwLock}, + sync::{Arc, Mutex, RwLock}, }; use dd_trace::{ @@ -296,7 +296,7 @@ pub(crate) struct DatadogSpanProcessor { registry: Arc, span_exporter: DatadogExporter, resource: Arc>, - config: dd_trace::Config, + config: Arc>, } impl std::fmt::Debug for DatadogSpanProcessor { @@ -308,14 +308,16 @@ impl std::fmt::Debug for DatadogSpanProcessor { impl DatadogSpanProcessor { #[allow(clippy::type_complexity)] pub(crate) fn new( - config: dd_trace::Config, + config: Arc>, registry: Arc, resource: Arc>, agent_response_handler: Option Fn(&'a str) + Send + Sync>>, ) -> Self { + // Extract config clone before moving the Arc + let config_clone = config.lock().unwrap().clone(); Self { registry, - span_exporter: DatadogExporter::new(config.clone(), agent_response_handler), + span_exporter: DatadogExporter::new(config_clone, agent_response_handler), resource, config, } @@ -441,7 +443,7 @@ impl DatadogSpanProcessor { // Only add if it's not empty or the default service name if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { - self.config.add_extra_service(&service_name); + self.config.lock().unwrap().add_extra_service(&service_name); } } } @@ -507,7 +509,7 @@ impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { } fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { - let dd_resource = create_dd_resource(resource.clone(), &self.config); + let dd_resource = create_dd_resource(resource.clone(), &self.config.lock().unwrap()); if let Err(e) = self.span_exporter.set_resource(dd_resource.clone()) { dd_trace::dd_error!( "DatadogSpanProcessor.set_resource message='Failed to set resource' error='{e}'", @@ -520,13 +522,13 @@ impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { let service_name = dd_resource .get(&Key::from_static_str(SERVICE_NAME)) .map(|service_name| service_name.as_str().to_string()); - init_telemetry(&self.config, service_name); + init_telemetry(&self.config.lock().unwrap(), service_name); } } #[cfg(test)] mod tests { - use std::sync::{Arc, RwLock}; + use std::sync::{Arc, Mutex, RwLock}; use dd_trace::Config; use opentelemetry::{Key, KeyValue, Value}; @@ -542,7 +544,12 @@ mod tests { let registry = Arc::new(TraceRegistry::new()); let resource = Arc::new(RwLock::new(Resource::builder_empty().build())); - let mut processor = DatadogSpanProcessor::new(config, registry, resource.clone(), None); + let mut processor = DatadogSpanProcessor::new( + Arc::new(Mutex::new(config)), + registry, + resource.clone(), + None, + ); let otel_resource = Resource::builder() // .with_service_name("otel-service") @@ -571,7 +578,12 @@ mod tests { let registry = Arc::new(TraceRegistry::new()); let resource = Arc::new(RwLock::new(Resource::builder_empty().build())); - let mut processor = DatadogSpanProcessor::new(config, registry, resource.clone(), None); + let mut processor = DatadogSpanProcessor::new( + Arc::new(Mutex::new(config)), + registry, + resource.clone(), + None, + ); let attributes = [KeyValue::new("key_schema", "value_schema")]; @@ -609,7 +621,12 @@ mod tests { let registry = Arc::new(TraceRegistry::new()); let resource = Arc::new(RwLock::new(Resource::builder_empty().build())); - let mut processor = DatadogSpanProcessor::new(config, registry, resource.clone(), None); + let mut processor = DatadogSpanProcessor::new( + Arc::new(Mutex::new(config)), + registry, + resource.clone(), + None, + ); let otel_resource = Resource::builder_empty() .with_attribute(KeyValue::new("key1", "value1")) @@ -637,7 +654,12 @@ mod tests { let registry = Arc::new(TraceRegistry::new()); let resource = Arc::new(RwLock::new(Resource::builder_empty().build())); - let mut processor = DatadogSpanProcessor::new(config, registry, resource.clone(), None); + let mut processor = DatadogSpanProcessor::new( + Arc::new(Mutex::new(config)), + registry, + resource.clone(), + None, + ); let otel_resource = Resource::builder() .with_service_name("otel-service") @@ -659,7 +681,12 @@ mod tests { let registry = Arc::new(TraceRegistry::new()); let resource = Arc::new(RwLock::new(Resource::builder_empty().build())); - let mut processor = DatadogSpanProcessor::new(config, registry, resource.clone(), None); + let mut processor = DatadogSpanProcessor::new( + Arc::new(Mutex::new(config)), + registry, + resource.clone(), + None, + ); let otel_resource = Resource::builder() .with_service_name("otel-service") From 4abec473ef623c4de524bf01fe005947ab17e2be Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Wed, 13 Aug 2025 14:21:32 -0400 Subject: [PATCH 21/34] lazy init rc http client creation to avoid panic --- dd-trace/src/configuration/remote_config.rs | 46 ++++++++++++--------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index c8f679ca..fab7b571 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -220,15 +220,15 @@ struct SignedTargets { version: Option, } -/// Remote configuration client +/// Remote configuration client that polls the Datadog Agent for configuration updates. /// -/// This client polls the Datadog Agent for configuration updates and applies them to the tracer. -/// Currently supports APM tracing sampling rules from the APM_TRACING product. +/// This client is responsible for: +/// - Fetching remote configuration from the Datadog Agent +/// - Processing APM_TRACING product updates (specifically sampling rules) +/// - Maintaining client state and capabilities +/// - Providing a callback mechanism for configuration updates /// -/// The client expects to receive configuration files with paths like: -/// `datadog/2/APM_TRACING/{config_id}/config` -/// -/// These files contain JSON with a various fields, one of which is `tracing_sampling_rules` field +/// The client currently handles a single product type (APM_TRACING) /// that defines sampling rules. pub struct RemoteConfigClient { /// Unique identifier for this client instance @@ -236,7 +236,9 @@ pub struct RemoteConfigClient { client_id: String, config: Arc>, agent_url: String, - client: reqwest::blocking::Client, + // HTTP client creation deferred to background thread to avoid Tokio panic + // Store timeout instead of creating client immediately + client_timeout: Duration, state: Arc>, capabilities: ClientCapabilities, poll_interval: Duration, @@ -251,12 +253,6 @@ impl RemoteConfigClient { pub fn new(config: Arc>) -> Result { let agent_url = format!("{}/v0.7/config", config.lock().unwrap().trace_agent_url()); - // Create HTTP client with timeout - let client = reqwest::blocking::Client::builder() - .timeout(DEFAULT_TIMEOUT) - .build() - .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?; - let state = Arc::new(Mutex::new(ClientState { root_version: 1, // Agent requires >= 1 (base TUF director root) targets_version: 0, @@ -270,7 +266,7 @@ impl RemoteConfigClient { client_id: uuid::Uuid::new_v4().to_string(), config, agent_url, - client, + client_timeout: DEFAULT_TIMEOUT, state, capabilities: ClientCapabilities::new(), poll_interval: DEFAULT_POLL_INTERVAL, @@ -288,6 +284,19 @@ impl RemoteConfigClient { /// Main polling loop fn run(self) { + // Create HTTP client in the background thread to avoid blocking client creation in async + // context + let client = match reqwest::blocking::Client::builder() + .timeout(self.client_timeout) + .build() + { + Ok(client) => client, + Err(e) => { + crate::dd_debug!("RemoteConfigClient: Failed to create HTTP client: {}", e); + return; + } + }; + let mut last_poll = Instant::now(); loop { @@ -299,7 +308,7 @@ impl RemoteConfigClient { last_poll = Instant::now(); // Fetch and apply configuration - match self.fetch_and_apply_config() { + match self.fetch_and_apply_config(&client) { Ok(_) => { // Clear any previous errors if let Ok(mut state) = self.state.lock() { @@ -320,12 +329,11 @@ impl RemoteConfigClient { } /// Fetches configuration from the agent and applies it - fn fetch_and_apply_config(&self) -> Result<()> { + fn fetch_and_apply_config(&self, client: &reqwest::blocking::Client) -> Result<()> { let request = self.build_request()?; // Send request to agent - let response = self - .client + let response = client .post(&self.agent_url) .json(&request) .send() From 9d9ba8007510015600100d886467c07692439a7d Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 13:25:51 -0400 Subject: [PATCH 22/34] increase timeout by 1 second to avoid occasional connection failures --- datadog-opentelemetry/src/lib.rs | 48 +++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 2a1dbabd..66014402 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -75,7 +75,7 @@ use std::sync::{Arc, Mutex, RwLock}; use dd_trace::configuration::RemoteConfigUpdate; use opentelemetry::{Key, KeyValue, Value}; use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; -use opentelemetry_semantic_conventions::resource::SERVICE_NAME; +use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, DEPLOYMENT_ENVIRONMENT_NAME}; use sampler::Sampler; use span_processor::{DatadogSpanProcessor, TraceRegistry}; use text_map_propagator::DatadogPropagator; @@ -341,28 +341,42 @@ fn merge_resource>( fn create_dd_resource(resource: Resource, cfg: &dd_trace::Config) -> Resource { let otel_service_name: Option = resource.get(&Key::from_static_str(SERVICE_NAME)); + + // Collect attributes to add + let mut attributes = Vec::new(); + + // Handle service name if otel_service_name.is_none() || otel_service_name.unwrap().as_str() == "unknown_service" { // If the OpenTelemetry service name is not set or is "unknown_service", // we override it with the Datadog service name. - merge_resource( - Some(resource), - [( - Key::from_static_str(SERVICE_NAME), - Value::from(cfg.service().to_string()), - )], - ) + attributes.push(( + Key::from_static_str(SERVICE_NAME), + Value::from(cfg.service().to_string()), + )); } else if !cfg.service_is_default() { // If the service is configured, we override the OpenTelemetry service name - merge_resource( - Some(resource), - [( - Key::from_static_str(SERVICE_NAME), - Value::from(cfg.service().to_string()), - )], - ) - } else { - // If the service is not configured, we keep the OpenTelemetry service name + attributes.push(( + Key::from_static_str(SERVICE_NAME), + Value::from(cfg.service().to_string()), + )); + } + + // Handle environment - add it if configured and not already present + if let Some(env) = cfg.env() { + let otel_env: Option = resource.get(&Key::from_static_str(DEPLOYMENT_ENVIRONMENT_NAME)); + if otel_env.is_none() { + attributes.push(( + Key::from_static_str(DEPLOYMENT_ENVIRONMENT_NAME), + Value::from(env.to_string()), + )); + } + } + + if attributes.is_empty() { + // If no attributes to add, return the original resource resource + } else { + merge_resource(Some(resource), attributes) } } From c5327222b5e45937a0cba878f437c143b4017be8 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 13:28:11 -0400 Subject: [PATCH 23/34] testing equipment --- Cargo.lock | 10 ++ Cargo.toml | 1 + datadog-opentelemetry/examples/README.md | 51 ++++++-- .../examples/remote_config_test/README.md | 97 ++++++++++++++ .../examples/remote_config_test/src/main.rs | 111 ++++++++++++++++ dd-trace/src/configuration/remote_config.rs | 2 +- debug_remote_config.rs | 119 ++++++++++++++++++ docker-compose.yml | 33 +++++ remote_config_test_3sec_output.log | 27 ++++ remote_config_test_output.log | 27 ++++ run_remote_config_test.sh | 34 +++++ test_request.json | 25 ++++ 12 files changed, 527 insertions(+), 10 deletions(-) create mode 100644 datadog-opentelemetry/examples/remote_config_test/README.md create mode 100644 datadog-opentelemetry/examples/remote_config_test/src/main.rs create mode 100644 debug_remote_config.rs create mode 100644 docker-compose.yml create mode 100644 remote_config_test_3sec_output.log create mode 100644 remote_config_test_output.log create mode 100755 run_remote_config_test.sh create mode 100644 test_request.json diff --git a/Cargo.lock b/Cargo.lock index 1cbbe49a..5a0c587b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2936,6 +2936,16 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "remote_config_test" +version = "0.1.0" +dependencies = [ + "datadog-opentelemetry", + "dd-trace", + "opentelemetry", + "tokio", +] + [[package]] name = "reqwest" version = "0.11.27" diff --git a/Cargo.toml b/Cargo.toml index ec4662bf..ee8b0712 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "datadog-opentelemetry", "datadog-opentelemetry/examples/propagator", "datadog-opentelemetry/examples/simple_tracing", + "datadog-opentelemetry/examples/remote_config_test", "datadog-opentelemetry-mappings", "dd-trace", "dd-trace-propagation", diff --git a/datadog-opentelemetry/examples/README.md b/datadog-opentelemetry/examples/README.md index 0a08e111..a5fd5aeb 100644 --- a/datadog-opentelemetry/examples/README.md +++ b/datadog-opentelemetry/examples/README.md @@ -1,22 +1,55 @@ # Examples -## simple_tracing +This directory contains example applications demonstrating various features of the `datadog-opentelemetry` crate. -Demonstrates basic usage of Datadog OpenTelemetry tracing. Creates nested spans and demonstrates tracer initialization and shutdown. +## Available Examples +### simple_tracing +A basic example showing how to initialize the Datadog tracer and create spans. + +**Run:** ```bash cargo run -p simple_tracing ``` -## propagator - -HTTP server that demonstrates trace context propagation between services. Shows how to extract trace context from incoming requests and inject it into outgoing requests. +### propagator +An example demonstrating trace propagation between services. +**Run:** ```bash cargo run -p propagator ``` -The server runs on `http://localhost:3000` with endpoints: -- `/health` - Health check endpoint -- `/echo` - Echo request body -- `/jump` - Makes outbound request to port 3001 +### remote_config_test +A test application for manually testing the remote configuration feature for sampling rules. This application continuously emits spans under a specific service name (`dd-trace-rs-rc-test-service`) to test remote configuration updates from the Datadog backend. + +**Features:** +- Initializes tracer with remote configuration enabled +- Emits ~2 traces per second with realistic span structures +- Creates different operation types: `user_login`, `data_fetch`, `file_upload`, `analytics_event` +- Each trace has a parent span with 2 child spans (`database_query` and `external_api_call`) + +**Run:** +```bash +# From project root +cargo run -p remote_config_test + +# Or use the convenience script +./run_remote_config_test.sh + +# With debug logging to see remote config activity +DD_LOG_LEVEL=DEBUG cargo run -p remote_config_test +``` + +**Prerequisites:** +- Datadog Agent running on `localhost:8126` +- Agent must have remote configuration enabled +- Agent should be configured to receive remote config from Datadog backend + +**Testing Remote Configuration:** +1. Start the application +2. Verify spans are being sent to your APM dashboard +3. Create a sampling rule in your Datadog backend for service `dd-trace-rs-rc-test-service` +4. Monitor the application and APM dashboard to see sampling rate changes + +See the [remote_config_test README](remote_config_test/README.md) for detailed instructions. diff --git a/datadog-opentelemetry/examples/remote_config_test/README.md b/datadog-opentelemetry/examples/remote_config_test/README.md new file mode 100644 index 00000000..f8adc01f --- /dev/null +++ b/datadog-opentelemetry/examples/remote_config_test/README.md @@ -0,0 +1,97 @@ +# Remote Configuration Test Application + +This is a test application for manually testing the remote configuration feature for sampling rules in `dd-trace-rs`. It continuously emits spans under a specific service name so you can test remote configuration updates from the Datadog backend. + +## What it does + +- Initializes the Datadog tracer with remote configuration enabled +- Continuously emits spans under the service name `dd-trace-rs-rc-test-service` +- Creates realistic traces with multiple spans (parent + 2 child spans) +- Simulates different operation types: `user_login`, `data_fetch`, `file_upload`, `analytics_event` +- Emits approximately 2 traces per second +- Logs progress every 10 traces + +## Prerequisites + +1. **Datadog Agent**: You need a Datadog Agent running on `localhost:8126` that is configured to receive remote configuration updates from the Datadog backend. + +2. **Agent Configuration**: Your agent should have remote configuration enabled. Check your agent's configuration for: + ```yaml + remote_configuration: + enabled: true + ``` + +## How to run + +### Option 1: From the project root +```bash +# Build and run the test application +cargo run --bin remote_config_test -p remote_config_test +``` + +### Option 2: From the example directory +```bash +cd datadog-opentelemetry/examples/remote_config_test +cargo run +``` + +## Environment Variables + +You can customize the behavior using these environment variables: + +- `DD_TRACE_AGENT_URL`: Agent URL (default: `http://localhost:8126`) +- `DD_LOG_LEVEL`: Log level for Datadog tracing (default: `INFO`) +- `DD_REMOTE_CONFIGURATION_ENABLED`: Enable/disable remote config (default: `true`) + +Example with custom agent URL: +```bash +DD_TRACE_AGENT_URL=http://localhost:8126 cargo run +``` + +## Testing Remote Configuration + +1. **Start the application**: Run the test application using one of the methods above. + +2. **Verify spans are being sent**: Check your Datadog APM dashboard to confirm spans from `dd-trace-rs-rc-test-service` are being received. + +3. **Create a sampling rule**: In your Datadog backend, create a sampling rule for the service: + - Service: `dd-trace-rs-rc-test-service` + - Sample rate: Choose a rate (e.g., 0.1 for 10% sampling) + +4. **Monitor for changes**: Watch the application logs and your APM dashboard to see if the sampling rate changes when the remote configuration is applied. + +## Expected Behavior + +- **Initial**: The application should emit spans at whatever the default sampling rate is +- **After remote config update**: The sampling rate should change according to your remote configuration rule +- **Remote config polling**: The client polls for configuration updates every 5 seconds (as per the spec) + +## Debugging + +### Increase logging verbosity +```bash +DD_LOG_LEVEL=DEBUG cargo run +``` + +### Check agent connectivity +Make sure your agent is accessible: +```bash +curl http://localhost:8126/v0.7/config +``` + +### Check spans are reaching the agent +```bash +curl http://localhost:8126/info +``` + +## What to look for + +When remote configuration is working correctly, you should see: + +1. **In application logs**: Periodic remote config client activity +2. **In agent logs**: Remote configuration requests and responses +3. **In APM dashboard**: Changes in sampling rate for the `dd-trace-rs-rc-test-service` + +## Stopping the application + +Press `Ctrl+C` to gracefully stop the application. It will flush any remaining spans before shutting down. \ No newline at end of file diff --git a/datadog-opentelemetry/examples/remote_config_test/src/main.rs b/datadog-opentelemetry/examples/remote_config_test/src/main.rs new file mode 100644 index 00000000..bfb73ebf --- /dev/null +++ b/datadog-opentelemetry/examples/remote_config_test/src/main.rs @@ -0,0 +1,111 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use opentelemetry::trace::{Tracer, TraceContextExt}; +use opentelemetry::Context; +use std::time::Duration; +use tokio::time::sleep; + +const SERVICE_NAME: &str = "dd-trace-rs-rc-test-service"; + +async fn process_request(request_id: u64, request_type: &str) { + // Create the main span for this request + let tracer = opentelemetry::global::tracer("request-processor"); + let main_span = tracer.start(format!("process_{}", request_type)); + let cx = Context::current_with_span(main_span); + + // Database query - create span, do work, end span + { + let db_span = tracer.start_with_context("database_query", &cx); + let _db_cx = cx.with_span(db_span); + sleep(Duration::from_millis(20 + (request_id % 30))).await; + } // span ends here + + // External API call - create span, do work, end span + { + let api_span = tracer.start_with_context("external_api_call", &cx); + let _api_cx = cx.with_span(api_span); + sleep(Duration::from_millis(30 + (request_id % 40))).await; + } // span ends here +} // main span ends here + +async fn background_worker() { + let mut counter = 0u64; + loop { + counter += 1; + + // Simulate different types of requests + let request_type = match counter % 4 { + 0 => "user_login", + 1 => "data_fetch", + 2 => "file_upload", + _ => "analytics_event", + }; + + process_request(counter, request_type).await; + + // Sleep between requests - emit roughly 2 spans per second + sleep(Duration::from_millis(500)).await; + + // Log every 10 requests to show we're still running + if counter % 10 == 0 { + println!("Emitted {} traces so far", counter); + } + } +} + +#[tokio::main] +async fn main() { + println!("Starting remote config test application"); + println!("Service name: {}", SERVICE_NAME); + println!("Agent URL: {}", std::env::var("DD_TRACE_AGENT_URL").unwrap_or_else(|_| "http://localhost:8126".to_string())); + + // Initialize the Datadog tracer with remote config enabled + let config = dd_trace::Config::builder() + .set_service(SERVICE_NAME.to_string()) + .set_env("dd-trace-rs-test-env".to_string()) + .set_version("1.0.0".to_string()) + // Remote config is enabled by default, but let's be explicit + .build(); + + // Enable debug logging to see remote config activity + if std::env::var("DD_LOG_LEVEL").unwrap_or_default().to_lowercase() == "debug" { + // Note: set_max_level is not public, but the config will handle log level internally + eprintln!("Debug logging enabled"); + } + + // Verify configuration values + println!("Config - Service: {}", config.service()); + println!("Config - Environment: {:?}", config.env()); + println!("Config - Version: {:?}", config.version()); + + let tracer_provider = datadog_opentelemetry::tracing() + .with_config(config) + .init(); + + println!("Tracer initialized with remote config enabled"); + println!("Starting to emit spans continuously..."); + println!("You can now create sampling rules in the Datadog backend for service: {}", SERVICE_NAME); + println!("Press Ctrl+C to stop"); + + // Run the background worker that emits spans + let worker_handle = tokio::spawn(background_worker()); + + // Wait for Ctrl+C + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("Received Ctrl+C, shutting down..."); + } + _ = worker_handle => { + println!("Worker finished unexpectedly"); + } + } + + // Shutdown the tracer to flush remaining spans + println!("Shutting down tracer..."); + if let Err(e) = tracer_provider.shutdown_with_timeout(Duration::from_secs(5)) { + eprintln!("Error shutting down tracer: {}", e); + } + + println!("Application stopped"); +} \ No newline at end of file diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index fab7b571..5c430ddd 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -10,7 +10,7 @@ use std::thread; use std::time::{Duration, Instant}; const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(5); // 5 seconds is the highest interval allowed by the spec -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2); +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); // lowest timeout with no failures /// Capabilities that the client supports #[derive(Debug, Clone)] diff --git a/debug_remote_config.rs b/debug_remote_config.rs new file mode 100644 index 00000000..0b57273b --- /dev/null +++ b/debug_remote_config.rs @@ -0,0 +1,119 @@ +// Quick debug script to see what JSON our remote config client generates +use dd_trace::Config; +use std::sync::{Arc, Mutex}; + +// Copy the relevant structs and logic from remote_config.rs to test serialization +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +struct ClientState { + root_version: u64, + targets_version: u64, + config_states: Vec, + has_error: bool, + error: Option, + backend_client_state: Option, +} + +#[derive(Debug, Clone, Serialize)] +struct ConfigState { + id: String, + version: u64, + product: String, + apply_state: u64, + apply_error: Option, +} + +#[derive(Debug, Serialize)] +struct ConfigRequest { + client: ClientInfo, + cached_target_files: Vec, +} + +#[derive(Debug, Serialize)] +struct ClientInfo { + #[serde(skip_serializing_if = "Option::is_none")] + state: Option, + id: String, + products: Vec, + is_tracer: bool, + #[serde(skip_serializing_if = "Option::is_none")] + client_tracer: Option, + capabilities: String, +} + +#[derive(Debug, Serialize)] +struct ClientTracer { + runtime_id: String, + language: String, + tracer_version: String, + service: String, + #[serde(default)] + extra_services: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + env: Option, + #[serde(skip_serializing_if = "Option::is_none")] + app_version: Option, + tags: Vec, +} + +#[derive(Debug, Clone, Serialize)] +struct CachedTargetFile { + path: String, + length: u64, + hashes: Vec, +} + +#[derive(Debug, Clone, Serialize)] +struct Hash { + algorithm: String, + hash: String, +} + +fn main() { + let config = Arc::new(Mutex::new( + Config::builder() + .set_service("dd-trace-rs-rc-test-service".to_string()) + .set_env("dd-trace-rs-test-env".to_string()) + .set_version("1.0.0".to_string()) + .build() + )); + + let cfg = config.lock().unwrap(); + + let state = ClientState { + root_version: 1, + targets_version: 0, + config_states: Vec::new(), + has_error: false, + error: None, + backend_client_state: None, + }; + + let client_info = ClientInfo { + state: Some(state), + id: "test-client-id".to_string(), + products: vec!["APM_TRACING".to_string()], + is_tracer: true, + client_tracer: Some(ClientTracer { + runtime_id: cfg.runtime_id().to_string(), + language: "rust".to_string(), + tracer_version: cfg.tracer_version().to_string(), + service: cfg.service().to_string(), + extra_services: cfg.get_extra_services(), + env: cfg.env().map(|s| s.to_string()), + app_version: cfg.version().map(|s| s.to_string()), + tags: cfg.global_tags().map(|s| s.to_string()).collect(), + }), + capabilities: "test-capabilities".to_string(), // Simplified for testing + }; + + let request = ConfigRequest { + client: client_info, + cached_target_files: Vec::new(), + }; + + let json = serde_json::to_string_pretty(&request).unwrap(); + println!("Request JSON:"); + println!("{}", json); +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..7f1b2281 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + datadog-agent: + env_file: + - ~/sandbox.docker.env + image: datadog/agent:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /proc/:/host/proc/:ro + - /sys/fs/cgroup/:/host/sys/fs/cgroup:ro + ports: + - "8126:8126/tcp" # APM traces + - "8125:8125/udp" # DogStatsD metrics + environment: + + - DD_APM_ENABLED=true + - DD_APM_NON_LOCAL_TRAFFIC=true + - DD_LOG_LEVEL=TRACE + - DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true + - DD_AC_EXCLUDE=name:datadog-agent + - DD_LOGS_ENABLED=true + # Remote Configuration settings + - DD_REMOTE_CONFIGURATION_ENABLED=true + - DD_APM_RECEIVER_TIMEOUT=30s + # Additional APM settings for better debugging + - DD_APM_ANALYZED_SPANS=*:1.0 + # Enable additional telemetry for debugging + - DD_APM_TELEMETRY_ENABLED=true + - DD_TRACE_DEBUG=true + - DD_SITE=data​d0g.c​om + restart: unless-stopped + container_name: dd-agent \ No newline at end of file diff --git a/remote_config_test_3sec_output.log b/remote_config_test_3sec_output.log new file mode 100644 index 00000000..82ba828b --- /dev/null +++ b/remote_config_test_3sec_output.log @@ -0,0 +1,27 @@ +Starting remote config test application +Service name: dd-trace-rs-rc-test-service +Agent URL: http://localhost:8126 +Debug logging enabled +Config - Service: dd-trace-rs-rc-test-service +Config - Environment: Some("dd-trace-rs-test-env") +Config - Version: Some("1.0.0") +DEBUG datadog-opentelemetry/src/lib.rs:310 - RemoteConfigClient: Started remote configuration client +Tracer initialized with remote config enabled +Starting to emit spans continuously... +You can now create sampling rules in the Datadog backend for service: dd-trace-rs-rc-test-service +Press Ctrl+C to stop +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 10 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 20 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 30 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 40 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 50 traces so far diff --git a/remote_config_test_output.log b/remote_config_test_output.log new file mode 100644 index 00000000..71e74b2b --- /dev/null +++ b/remote_config_test_output.log @@ -0,0 +1,27 @@ +Starting remote config test application +Service name: dd-trace-rs-rc-test-service +Agent URL: http://localhost:8126 +Debug logging enabled +Config - Service: dd-trace-rs-rc-test-service +Config - Environment: Some("dd-trace-rs-test-env") +Config - Version: Some("1.0.0") +DEBUG datadog-opentelemetry/src/lib.rs:310 - RemoteConfigClient: Started remote configuration client +Tracer initialized with remote config enabled +Starting to emit spans continuously... +You can now create sampling rules in the Datadog backend for service: dd-trace-rs-rc-test-service +Press Ctrl+C to stop +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 10 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 20 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:320 - RemoteConfigClient: Failed to fetch config: Failed to send request: error sending request for url (http://localhost:8126/v0.7/config): connection closed before message completed +Emitted 30 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 40 traces so far +DEBUG dd-trace/src/configuration/remote_config.rs:333 - RemoteConfigClient: Sending request to http://localhost:8126/v0.7/config with 1 products +DEBUG dd-trace/src/configuration/remote_config.rs:342 - RemoteConfigClient: Received response with status: 200 OK +Emitted 50 traces so far diff --git a/run_remote_config_test.sh b/run_remote_config_test.sh new file mode 100755 index 00000000..bd263d11 --- /dev/null +++ b/run_remote_config_test.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Remote Configuration Test Script +# This script runs the remote configuration test application + +echo "πŸ”§ Remote Configuration Test for dd-trace-rs" +echo "=============================================" +echo + +# Check if agent is reachable +echo "🌐 Checking agent connectivity..." +if curl -s "http://localhost:8126/info" > /dev/null 2>&1; then + echo "βœ… Agent at localhost:8126 is reachable" +else + echo "❌ Agent at localhost:8126 is not reachable" + echo " Make sure your Datadog Agent is running with remote config enabled" + echo +fi + +# Show current environment +echo "πŸ“‹ Environment variables:" +echo " DD_TRACE_AGENT_URL: ${DD_TRACE_AGENT_URL:-http://localhost:8126}" +echo " DD_LOG_LEVEL: ${DD_LOG_LEVEL:-INFO}" +echo " DD_REMOTE_CONFIGURATION_ENABLED: ${DD_REMOTE_CONFIGURATION_ENABLED:-true}" +echo + +echo "πŸš€ Starting remote config test application..." +echo " Service name: dd-trace-rs-rc-test-service" +echo " The app will emit ~2 traces per second" +echo " Press Ctrl+C to stop" +echo + +# Run the application +exec cargo run -p remote_config_test \ No newline at end of file diff --git a/test_request.json b/test_request.json new file mode 100644 index 00000000..379ac936 --- /dev/null +++ b/test_request.json @@ -0,0 +1,25 @@ +{ + "client": { + "state": { + "root_version": 1, + "targets_version": 0, + "config_states": [], + "has_error": false + }, + "id": "test-client-id", + "products": ["APM_TRACING"], + "is_tracer": true, + "client_tracer": { + "runtime_id": "test-runtime-id", + "language": "rust", + "tracer_version": "0.0.1", + "service": "dd-trace-rs-rc-test-service", + "extra_services": [], + "env": "dd-trace-rs-test-env", + "app_version": "1.0.0", + "tags": [] + }, + "capabilities": "AAAAAAAQAAA=" + }, + "cached_target_files": [] +} From 65f05101eaa132bf77de69ddcffa2801eaa43db1 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 13:31:06 -0400 Subject: [PATCH 24/34] Revert "increase timeout by 1 second to avoid occasional connection failures" This reverts commit 9d9ba8007510015600100d886467c07692439a7d. --- datadog-opentelemetry/src/lib.rs | 48 +++++++++++--------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 66014402..2a1dbabd 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -75,7 +75,7 @@ use std::sync::{Arc, Mutex, RwLock}; use dd_trace::configuration::RemoteConfigUpdate; use opentelemetry::{Key, KeyValue, Value}; use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; -use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, DEPLOYMENT_ENVIRONMENT_NAME}; +use opentelemetry_semantic_conventions::resource::SERVICE_NAME; use sampler::Sampler; use span_processor::{DatadogSpanProcessor, TraceRegistry}; use text_map_propagator::DatadogPropagator; @@ -341,42 +341,28 @@ fn merge_resource>( fn create_dd_resource(resource: Resource, cfg: &dd_trace::Config) -> Resource { let otel_service_name: Option = resource.get(&Key::from_static_str(SERVICE_NAME)); - - // Collect attributes to add - let mut attributes = Vec::new(); - - // Handle service name if otel_service_name.is_none() || otel_service_name.unwrap().as_str() == "unknown_service" { // If the OpenTelemetry service name is not set or is "unknown_service", // we override it with the Datadog service name. - attributes.push(( - Key::from_static_str(SERVICE_NAME), - Value::from(cfg.service().to_string()), - )); + merge_resource( + Some(resource), + [( + Key::from_static_str(SERVICE_NAME), + Value::from(cfg.service().to_string()), + )], + ) } else if !cfg.service_is_default() { // If the service is configured, we override the OpenTelemetry service name - attributes.push(( - Key::from_static_str(SERVICE_NAME), - Value::from(cfg.service().to_string()), - )); - } - - // Handle environment - add it if configured and not already present - if let Some(env) = cfg.env() { - let otel_env: Option = resource.get(&Key::from_static_str(DEPLOYMENT_ENVIRONMENT_NAME)); - if otel_env.is_none() { - attributes.push(( - Key::from_static_str(DEPLOYMENT_ENVIRONMENT_NAME), - Value::from(env.to_string()), - )); - } - } - - if attributes.is_empty() { - // If no attributes to add, return the original resource - resource + merge_resource( + Some(resource), + [( + Key::from_static_str(SERVICE_NAME), + Value::from(cfg.service().to_string()), + )], + ) } else { - merge_resource(Some(resource), attributes) + // If the service is not configured, we keep the OpenTelemetry service name + resource } } From 7dd1fcd2977d1b2f239a3223a4b83501dd65e68a Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 13:31:56 -0400 Subject: [PATCH 25/34] increase timeout by 1 second to avoid occasional connection failures --- dd-trace/src/configuration/remote_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index fab7b571..5c430ddd 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -10,7 +10,7 @@ use std::thread; use std::time::{Duration, Instant}; const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(5); // 5 seconds is the highest interval allowed by the spec -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2); +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); // lowest timeout with no failures /// Capabilities that the client supports #[derive(Debug, Clone)] From 745baa813093f5a72a3ed85d5a0a793cd735f192 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 14:20:16 -0400 Subject: [PATCH 26/34] hold callbacks in struct instead of hashmap --- datadog-opentelemetry/src/lib.rs | 10 +- dd-trace/src/configuration/configuration.rs | 121 +++++++++++++------- 2 files changed, 84 insertions(+), 47 deletions(-) diff --git a/datadog-opentelemetry/src/lib.rs b/datadog-opentelemetry/src/lib.rs index 2a1dbabd..3a513a01 100644 --- a/datadog-opentelemetry/src/lib.rs +++ b/datadog-opentelemetry/src/lib.rs @@ -291,14 +291,14 @@ fn make_tracer( if let Some(sampler_callback) = sampler_callback { let sampler_callback = Arc::new(sampler_callback); let sampler_callback_clone: SamplerCallback = sampler_callback.clone(); - shared_config.lock().unwrap().add_remote_config_callback( - "datadog_sampler_on_rules_update".to_string(), - move |update| match update { + shared_config + .lock() + .unwrap() + .set_sampling_rules_callback(move |update| match update { RemoteConfigUpdate::SamplingRules(rules) => { sampler_callback_clone(rules); } - }, - ); + }); } // Create remote config client with shared config diff --git a/dd-trace/src/configuration/configuration.rs b/dd-trace/src/configuration/configuration.rs index ad8f4d5f..bfa769b0 100644 --- a/dd-trace/src/configuration/configuration.rs +++ b/dd-trace/src/configuration/configuration.rs @@ -24,9 +24,60 @@ pub enum RemoteConfigUpdate { } /// Type alias for remote configuration callback functions -/// Callbacks receive a RemoteConfigUpdate enum to handle different config types +/// This reduces type complexity and improves readability type RemoteConfigCallback = Box; +/// Struct-based callback system for remote configuration updates +pub struct RemoteConfigCallbacks { + pub sampling_rules_update: Option, + // Future callback types can be added here as new fields + // e.g. pub feature_flags_update: Option, +} + +impl std::fmt::Debug for RemoteConfigCallbacks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteConfigCallbacks") + .field( + "sampling_rules_update", + &self.sampling_rules_update.as_ref().map(|_| ""), + ) + .finish() + } +} + +impl RemoteConfigCallbacks { + pub fn new() -> Self { + Self { + sampling_rules_update: None, + } + } + + pub fn set_sampling_rules_callback(&mut self, callback: F) + where + F: Fn(&RemoteConfigUpdate) + Send + Sync + 'static, + { + self.sampling_rules_update = Some(Box::new(callback)); + } + + /// Calls all relevant callbacks for the given update type + /// Provides a unified interface for future callback types + pub fn notify_update(&self, update: &RemoteConfigUpdate) { + match update { + RemoteConfigUpdate::SamplingRules(_) => { + if let Some(ref callback) = self.sampling_rules_update { + callback(update); + } + } // Future update types can be handled here + } + } +} + +impl Default for RemoteConfigCallbacks { + fn default() -> Self { + Self::new() + } +} + /// Configuration for a single sampling rule #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct SamplingRuleConfig { @@ -416,7 +467,7 @@ pub struct Config { /// General callbacks to be called when configuration is updated from remote configuration /// Allows components like the DatadogSampler to be updated without circular imports - remote_config_callbacks: Arc>>, + remote_config_callbacks: Arc>, } impl Config { @@ -527,7 +578,7 @@ impl Config { wait_agent_info_ready: default.wait_agent_info_ready, extra_services_tracker: ExtraServicesTracker::new(remote_config_enabled), remote_config_enabled, - remote_config_callbacks: Arc::new(Mutex::new(HashMap::new())), + remote_config_callbacks: Arc::new(Mutex::new(RemoteConfigCallbacks::new())), } } @@ -659,15 +710,10 @@ impl Config { self.trace_sampling_rules .set_value_source(parsed_rules, ConfigSource::RemoteConfig); - // Notify the datadog_sampler_on_rules_update callback about the update - // This specifically calls the DatadogSampler's on_rules_update method - if let Ok(callbacks) = self.remote_config_callbacks.lock() { - if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - let update = - RemoteConfigUpdate::SamplingRules(self.trace_sampling_rules().to_vec()); - callback(&update); - } - } + // Notify callbacks about the sampling rules update + self.remote_config_callbacks.lock().unwrap().notify_update( + &RemoteConfigUpdate::SamplingRules(self.trace_sampling_rules().to_vec()), + ); } Ok(()) @@ -676,22 +722,16 @@ impl Config { pub fn clear_remote_sampling_rules(&mut self) { self.trace_sampling_rules.unset_rc(); - if let Ok(callbacks) = self.remote_config_callbacks.lock() { - if let Some(callback) = callbacks.get("datadog_sampler_on_rules_update") { - // Now that rc_value is cleared, this will return the fallback rules - let update = - RemoteConfigUpdate::SamplingRules(self.trace_sampling_rules().to_vec()); - callback(&update); - } - } + self.remote_config_callbacks.lock().unwrap().notify_update( + &RemoteConfigUpdate::SamplingRules(self.trace_sampling_rules().to_vec()), + ); } - /// Add a callback to be called when remote configuration is updated - /// This allows components to be updated without circular imports + /// Add a callback to be called when sampling rules are updated via remote configuration + /// This allows components like DatadogSampler to be updated without circular imports /// /// # Arguments - /// * `key` - A unique identifier for this callback (e.g., "datadog_sampler_on_rules_update") - /// * `callback` - The function to call when remote config is updated (receives + /// * `callback` - The function to call when sampling rules are updated (receives /// RemoteConfigUpdate enum) /// /// # Example @@ -699,23 +739,23 @@ impl Config { /// use dd_trace::{configuration::RemoteConfigUpdate, Config}; /// /// let config = Config::builder().build(); - /// config.add_remote_config_callback("my_component_callback".to_string(), |update| { + /// config.set_sampling_rules_callback(|update| { /// match update { /// RemoteConfigUpdate::SamplingRules(rules) => { /// println!("Received {} new sampling rules", rules.len()); /// // Update your sampler here - /// } // Future remote config types can be added here - /// // as new variants are added to the RemoteConfigUpdate enum + /// } /// } /// }); /// ``` - pub fn add_remote_config_callback(&self, key: String, callback: F) + pub fn set_sampling_rules_callback(&self, callback: F) where F: Fn(&RemoteConfigUpdate) + Send + Sync + 'static, { - if let Ok(mut callbacks) = self.remote_config_callbacks.lock() { - callbacks.insert(key, Box::new(callback)); - } + self.remote_config_callbacks + .lock() + .unwrap() + .set_sampling_rules_callback(callback); } /// Add an extra service discovered at runtime @@ -771,7 +811,7 @@ impl std::fmt::Debug for Config { ) .field("extra_services_tracker", &self.extra_services_tracker) .field("remote_config_enabled", &self.remote_config_enabled) - .field("remote_config_callbacks", &"") + .field("remote_config_callbacks", &self.remote_config_callbacks) .finish() } } @@ -811,7 +851,7 @@ fn default_config() -> Config { trace_propagation_extract_first: false, extra_services_tracker: ExtraServicesTracker::new(true), remote_config_enabled: true, - remote_config_callbacks: Arc::new(Mutex::new(HashMap::new())), + remote_config_callbacks: Arc::new(Mutex::new(RemoteConfigCallbacks::new())), } } @@ -1344,15 +1384,12 @@ mod tests { let callback_called_clone = callback_called.clone(); let callback_rules_clone = callback_rules.clone(); - config.add_remote_config_callback( - "datadog_sampler_on_rules_update".to_string(), - move |update| { - *callback_called_clone.lock().unwrap() = true; - // Store the rules - for now we only have SamplingRules variant - let RemoteConfigUpdate::SamplingRules(rules) = update; - *callback_rules_clone.lock().unwrap() = rules.clone(); - }, - ); + config.set_sampling_rules_callback(move |update| { + *callback_called_clone.lock().unwrap() = true; + // Store the rules - for now we only have SamplingRules variant + let RemoteConfigUpdate::SamplingRules(rules) = update; + *callback_rules_clone.lock().unwrap() = rules.clone(); + }); // Initially callback should not be called assert!(!*callback_called.lock().unwrap()); From be6b057c6b8af0d85579a729dbf2635e2cf96347 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 15:31:56 -0400 Subject: [PATCH 27/34] switch rc client from reqwest to hyper --- Cargo.lock | 267 ++------------------ dd-trace/Cargo.toml | 5 +- dd-trace/src/configuration/remote_config.rs | 70 +++-- 3 files changed, 81 insertions(+), 261 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cbbe49a..d8551117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,7 +368,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cexpr", "clang-sys", "itertools 0.12.1", @@ -400,12 +400,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.9.1" @@ -458,7 +452,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-native-certs 0.7.3", - "rustls-pemfile 2.2.0", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_derive", @@ -886,11 +880,14 @@ dependencies = [ "anyhow", "base64 0.21.7", "ddtelemetry", - "reqwest 0.11.27", + "http-body-util", + "hyper 1.6.0", + "hyper-util", "rustc_version_runtime", "serde", "serde_json", "sha2", + "tokio", "uuid", ] @@ -1199,21 +1196,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1406,25 +1388,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.10.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.12" @@ -1649,7 +1612,6 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1672,7 +1634,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.12", + "h2", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1717,19 +1679,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-util" version = "0.1.16" @@ -1749,7 +1698,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.0", - "system-configuration 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", @@ -1936,7 +1885,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cfg-if", "libc", ] @@ -2102,7 +2051,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags", "libc", ] @@ -2237,23 +2186,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2266,7 +2198,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -2326,50 +2258,12 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "opentelemetry" version = "0.30.0" @@ -2593,12 +2487,6 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "polling" version = "3.10.0" @@ -2858,7 +2746,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] @@ -2936,46 +2824,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "reqwest" version = "0.12.23" @@ -2986,7 +2834,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", - "h2 0.4.12", + "h2", "hickory-resolver", "http 1.3.1", "http-body 1.0.1", @@ -3007,7 +2855,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-rustls", "tower", @@ -3115,7 +2963,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3128,7 +2976,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", @@ -3157,7 +3005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 2.2.0", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework 2.11.1", @@ -3175,15 +3023,6 @@ dependencies = [ "security-framework 3.3.0", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -3287,7 +3126,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3300,7 +3139,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.1", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3602,12 +3441,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3638,36 +3471,15 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -3686,19 +3498,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "tempfile" -version = "3.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", -] - [[package]] name = "term" version = "0.7.0" @@ -3728,7 +3527,7 @@ dependencies = [ "memchr", "parse-display", "pin-project-lite", - "reqwest 0.12.23", + "reqwest", "serde", "serde_json", "serde_with", @@ -3892,16 +3691,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.2" @@ -3945,7 +3734,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -3957,7 +3746,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags", "bytes", "futures-util", "http 1.3.1", @@ -4107,12 +3896,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -4662,7 +4445,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] diff --git a/dd-trace/Cargo.toml b/dd-trace/Cargo.toml index 18e5e303..428fc579 100644 --- a/dd-trace/Cargo.toml +++ b/dd-trace/Cargo.toml @@ -16,7 +16,10 @@ anyhow = "1.0.97" rustc_version_runtime = "0.3.0" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -reqwest = { version = "0.11", features = ["blocking", "json"] } +hyper = { version = "1.0", features = ["client", "http1", "http2"] } +hyper-util = { version = "0.1", features = ["client", "client-legacy", "http1", "http2", "tokio"] } +http-body-util = "0.1" +tokio = { version = "1.0", features = ["rt", "rt-multi-thread"] } base64 = "0.21" sha2 = "0.10" uuid = { version = "1.11.0", features = ["v4"] } diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 5c430ddd..1608c1ec 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -9,6 +9,12 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; +// HTTP client imports +use http_body_util::{BodyExt, Full}; +use hyper::{body::Bytes, Method, Request, Uri}; +use hyper_util::client::legacy::{connect::HttpConnector, Client}; +use hyper_util::rt::TokioExecutor; + const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(5); // 5 seconds is the highest interval allowed by the spec const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); // lowest timeout with no failures @@ -236,8 +242,7 @@ pub struct RemoteConfigClient { client_id: String, config: Arc>, agent_url: String, - // HTTP client creation deferred to background thread to avoid Tokio panic - // Store timeout instead of creating client immediately + // HTTP client timeout configuration client_timeout: Duration, state: Arc>, capabilities: ClientCapabilities, @@ -284,15 +289,11 @@ impl RemoteConfigClient { /// Main polling loop fn run(self) { - // Create HTTP client in the background thread to avoid blocking client creation in async - // context - let client = match reqwest::blocking::Client::builder() - .timeout(self.client_timeout) - .build() - { - Ok(client) => client, + // Create Tokio runtime in the background thread + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, Err(e) => { - crate::dd_debug!("RemoteConfigClient: Failed to create HTTP client: {}", e); + crate::dd_debug!("RemoteConfigClient: Failed to create Tokio runtime: {}", e); return; } }; @@ -308,7 +309,7 @@ impl RemoteConfigClient { last_poll = Instant::now(); // Fetch and apply configuration - match self.fetch_and_apply_config(&client) { + match rt.block_on(self.fetch_and_apply_config()) { Ok(_) => { // Clear any previous errors if let Ok(mut state) = self.state.lock() { @@ -329,14 +330,39 @@ impl RemoteConfigClient { } /// Fetches configuration from the agent and applies it - fn fetch_and_apply_config(&self, client: &reqwest::blocking::Client) -> Result<()> { - let request = self.build_request()?; + async fn fetch_and_apply_config(&self) -> Result<()> { + // Create HTTP connector with timeout configuration + let mut connector = HttpConnector::new(); + connector.set_connect_timeout(Some(self.client_timeout)); + + // Create HTTP client for this request + let client = Client::builder(TokioExecutor::new()).build(connector); + + let request_payload = self.build_request()?; + + // Serialize the request to JSON + let json_body = serde_json::to_string(&request_payload) + .map_err(|e| anyhow::anyhow!("Failed to serialize request: {}", e))?; + + // Parse the agent URL + let uri: Uri = self + .agent_url + .parse() + .map_err(|e| anyhow::anyhow!("Invalid agent URL: {}", e))?; + + // Build HTTP request + let req = Request::builder() + .method(Method::POST) + .uri(uri) + .header("content-type", "application/json") + .header("user-agent", "dd-trace-rs") + .body(Full::new(Bytes::from(json_body))) + .map_err(|e| anyhow::anyhow!("Failed to build request: {}", e))?; // Send request to agent let response = client - .post(&self.agent_url) - .json(&request) - .send() + .request(req) + .await .map_err(|e| anyhow::anyhow!("Failed to send request: {}", e))?; if !response.status().is_success() { @@ -346,8 +372,16 @@ impl RemoteConfigClient { )); } - let config_response: ConfigResponse = response - .json() + // Collect the response body + let body_bytes = response + .into_body() + .collect() + .await + .map_err(|e| anyhow::anyhow!("Failed to read response body: {}", e))? + .to_bytes(); + + // Parse JSON response + let config_response: ConfigResponse = serde_json::from_slice(&body_bytes) .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?; // Process the configuration response From d39950d73578ae91cb9455fce69544c3a95c4af7 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 15:33:28 -0400 Subject: [PATCH 28/34] add cargo toml for testing app --- .../examples/remote_config_test/Cargo.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 datadog-opentelemetry/examples/remote_config_test/Cargo.toml diff --git a/datadog-opentelemetry/examples/remote_config_test/Cargo.toml b/datadog-opentelemetry/examples/remote_config_test/Cargo.toml new file mode 100644 index 00000000..3ed8bee4 --- /dev/null +++ b/datadog-opentelemetry/examples/remote_config_test/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "remote_config_test" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "remote_config_test" +path = "src/main.rs" + +[dependencies] +datadog-opentelemetry = { path = "../.." } +dd-trace = { path = "../../../dd-trace" } +opentelemetry = { workspace = true } +tokio = { workspace = true, features = ["full", "signal"] } \ No newline at end of file From ab6a179551d5d67b25b69ef1cf8cb3f32b2f00c1 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 15:55:27 -0400 Subject: [PATCH 29/34] fix agent site --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7f1b2281..e7aa0105 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,6 @@ services: # Enable additional telemetry for debugging - DD_APM_TELEMETRY_ENABLED=true - DD_TRACE_DEBUG=true - - DD_SITE=data​d0g.c​om + - DD_SITE=datad0g.com restart: unless-stopped container_name: dd-agent \ No newline at end of file From 96cee572063b382f614f504d99d1da9aea55580a Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Thu, 14 Aug 2025 17:25:34 -0400 Subject: [PATCH 30/34] move updating service tracker to span exporter --- datadog-opentelemetry/src/span_exporter.rs | 110 ++++++++++++++++++-- datadog-opentelemetry/src/span_processor.rs | 36 ------- 2 files changed, 99 insertions(+), 47 deletions(-) diff --git a/datadog-opentelemetry/src/span_exporter.rs b/datadog-opentelemetry/src/span_exporter.rs index 3b1ebc31..f7105462 100644 --- a/datadog-opentelemetry/src/span_exporter.rs +++ b/datadog-opentelemetry/src/span_exporter.rs @@ -65,6 +65,8 @@ struct Batch { last_flush: std::time::Instant, span_count: usize, max_buffered_spans: usize, + /// Configuration for service discovery via remote config + config: Arc, } // Pre-allocate the batch buffer to avoid reallocations on small sizes. @@ -72,12 +74,13 @@ struct Batch { const PRE_ALLOCATE_CHUNKS: usize = 400; impl Batch { - fn new(max_buffered_spans: usize) -> Self { + fn new(max_buffered_spans: usize, config: Arc) -> Self { Self { chunks: Vec::with_capacity(PRE_ALLOCATE_CHUNKS), last_flush: std::time::Instant::now(), span_count: 0, max_buffered_spans, + config, } } @@ -101,12 +104,39 @@ impl Batch { if chunk.is_empty() { return Ok(()); } + + // Extract service names from spans for remote configuration discovery + for span in &chunk { + self.extract_and_add_service_from_span(span); + } + let chunk_len: usize = chunk.len(); self.chunks.push(TraceChunk { chunk }); self.span_count += chunk_len; Ok(()) } + /// Extracts the service name from a span and adds it to the config's extra services tracking. + /// This allows discovery of all services at runtime for proper remote configuration. + fn extract_and_add_service_from_span(&self, span: &SpanData) { + let service_name = if let Some(service_name) = span.attributes.iter().find_map(|kv| { + if kv.key.as_str() == "service.name" { + Some(kv.value.to_string()) + } else { + None + } + }) { + service_name + } else { + return; + }; + + // Only add if it's not empty or the default service name + if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { + self.config.add_extra_service(&service_name); + } + } + /// Export the trace chunk and reset the batch fn export(&mut self) -> Vec { let chunks = std::mem::replace(&mut self.chunks, Vec::with_capacity(PRE_ALLOCATE_CHUNKS)); @@ -134,7 +164,11 @@ impl DatadogExporter { config: dd_trace::Config, agent_response_handler: Option Fn(&'a str) + Send + Sync>>, ) -> Self { - let (tx, rx) = channel(SPAN_FLUSH_THRESHOLD, MAX_BUFFERED_SPANS); + let (tx, rx) = channel( + SPAN_FLUSH_THRESHOLD, + MAX_BUFFERED_SPANS, + Arc::new(config.clone()), + ); let trace_exporter = { let mut builder = TraceExporterBuilder::default(); builder @@ -275,13 +309,17 @@ impl fmt::Debug for DatadogExporter { } } -fn channel(flush_trigger_number_of_spans: usize, max_number_of_spans: usize) -> (Sender, Receiver) { +fn channel( + flush_trigger_number_of_spans: usize, + max_number_of_spans: usize, + config: Arc, +) -> (Sender, Receiver) { let waiter = Arc::new(Waiter { state: Mutex::new(SharedState { flush_needed: false, shutdown_needed: false, has_shutdown: false, - batch: Batch::new(max_number_of_spans), + batch: Batch::new(max_number_of_spans, config), set_resource: None, }), notifier: Condvar::new(), @@ -589,7 +627,7 @@ struct TraceExporterHandle { #[cfg(test)] mod tests { use core::time; - use std::{borrow::Cow, time::Duration}; + use std::{borrow::Cow, sync::Arc, time::Duration}; use opentelemetry::SpanId; use opentelemetry_sdk::trace::{SpanData, SpanEvents, SpanLinks}; @@ -617,7 +655,7 @@ mod tests { #[test] fn test_receiver_sender_flush() { - let (tx, rx) = channel(2, 4); + let (tx, rx) = channel(2, 4, Arc::new(dd_trace::Config::builder().build())); std::thread::scope(|s| { s.spawn(|| tx.add_trace_chunk(vec![empty_span_data()])); s.spawn(|| tx.add_trace_chunk(vec![empty_span_data(), empty_span_data()])); @@ -633,7 +671,7 @@ mod tests { #[test] fn test_receiver_sender_batch_drop() { - let (tx, rx) = channel(2, 4); + let (tx, rx) = channel(2, 4, Arc::new(dd_trace::Config::builder().build())); for i in 1..=3 { tx.add_trace_chunk(vec![empty_span_data(); i]).unwrap(); } @@ -655,7 +693,7 @@ mod tests { #[test] fn test_receiver_sender_timeout() { - let (tx, rx) = channel(2, 4); + let (tx, rx) = channel(2, 4, Arc::new(dd_trace::Config::builder().build())); std::thread::scope(|s| { s.spawn(|| tx.add_trace_chunk(vec![empty_span_data()])); s.spawn(|| { @@ -674,7 +712,7 @@ mod tests { #[test] fn test_trigger_shutdown() { - let (tx, rx) = channel(2, 4); + let (tx, rx) = channel(2, 4, Arc::new(dd_trace::Config::builder().build())); std::thread::scope(|s| { s.spawn(|| tx.add_trace_chunk(vec![empty_span_data()]).unwrap()); s.spawn(|| { @@ -698,7 +736,7 @@ mod tests { #[test] fn test_wait_for_shutdown() { - let (tx, rx) = channel(2, 4); + let (tx, rx) = channel(2, 4, Arc::new(dd_trace::Config::builder().build())); std::thread::scope(|s| { s.spawn(|| { @@ -720,8 +758,58 @@ mod tests { #[test] fn test_already_shutdown() { - let (tx, rx) = channel(2, 4); + let (tx, rx) = channel(2, 4, Arc::new(dd_trace::Config::builder().build())); drop(rx); assert_eq!(tx.trigger_shutdown(), Err(SenderError::AlreadyShutdown)); } + + #[test] + fn test_service_extraction_from_spans() { + use opentelemetry::{Key, KeyValue, Value}; + + let config = Arc::new( + dd_trace::Config::builder() + .set_service("main-service".to_string()) + .build(), + ); + let (tx, _rx) = channel(2, 10, config.clone()); + + // Create a span with a service.name attribute + let mut span_with_service = empty_span_data(); + span_with_service.attributes = vec![KeyValue::new( + Key::from_static_str("service.name"), + Value::from("discovered-service"), + )]; + + // Create a span without service.name attribute + let span_without_service = empty_span_data(); + + // Create a span with the default service name (should be ignored) + let mut span_with_default_service = empty_span_data(); + span_with_default_service.attributes = vec![KeyValue::new( + Key::from_static_str("service.name"), + Value::from("otlpresourcenoservicename"), + )]; + + // Add spans to the batch + tx.add_trace_chunk(vec![span_with_service]).unwrap(); + tx.add_trace_chunk(vec![span_without_service]).unwrap(); + tx.add_trace_chunk(vec![span_with_default_service]).unwrap(); + + // Add another span with the same service (should not duplicate) + let mut span_duplicate_service = empty_span_data(); + span_duplicate_service.attributes = vec![KeyValue::new( + Key::from_static_str("service.name"), + Value::from("discovered-service"), + )]; + tx.add_trace_chunk(vec![span_duplicate_service]).unwrap(); + + // Verify that only the discovered service was added (not main-service, not default, no + // duplicates) + let extra_services = config.get_extra_services(); + assert_eq!(extra_services.len(), 1); + assert!(extra_services.contains(&"discovered-service".to_string())); + assert!(!extra_services.contains(&"main-service".to_string())); + assert!(!extra_services.contains(&"otlpresourcenoservicename".to_string())); + } } diff --git a/datadog-opentelemetry/src/span_processor.rs b/datadog-opentelemetry/src/span_processor.rs index ce25c755..07f2a8b3 100644 --- a/datadog-opentelemetry/src/span_processor.rs +++ b/datadog-opentelemetry/src/span_processor.rs @@ -414,38 +414,6 @@ impl DatadogSpanProcessor { trace.finished_spans } - - /// Extracts the service name from a span and adds it to the config's extra services tracking. - /// This allows discovery of all services at runtime for proper remote configuration. - fn extract_and_add_service_from_span(&self, span: &SpanData) { - // First check span attributes for service.name - let service_name = if let Some(service_name) = span.attributes.iter().find_map(|kv| { - if kv.key.as_str() == "service.name" { - Some(kv.value.to_string()) - } else { - None - } - }) { - service_name - } else { - // If not found in span attributes, check resource attributes - if let Ok(resource) = self.resource.read() { - let service_key = opentelemetry::Key::from_static_str("service.name"); - if let Some(service_attr) = resource.get(&service_key) { - service_attr.to_string() - } else { - return; // No service name found anywhere - } - } else { - return; // Could not read resource - } - }; - - // Only add if it's not empty or the default service name - if !service_name.is_empty() && service_name != "otlpresourcenoservicename" { - self.config.lock().unwrap().add_extra_service(&service_name); - } - } } impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { @@ -476,10 +444,6 @@ impl opentelemetry_sdk::trace::SpanProcessor for DatadogSpanProcessor { fn on_end(&self, span: SpanData) { let trace_id = span.span_context.trace_id().to_bytes(); - // Extract service name from span and add to extra services for remote config - // This allows discovery of all services at runtime for proper remote configuration - self.extract_and_add_service_from_span(&span); - let Some(trace) = self.registry.finish_span(trace_id, span) else { return; }; From b2ff4c788f9f575eed630e96a63f48fef0573e14 Mon Sep 17 00:00:00 2001 From: Zachary Groves <32471391+ZStriker19@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:44:22 -0400 Subject: [PATCH 31/34] Update dd-trace/src/configuration/remote_config.rs Co-authored-by: paullegranddc <82819397+paullegranddc@users.noreply.github.com> --- dd-trace/src/configuration/remote_config.rs | 25 +++++---------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index 1608c1ec..d102148e 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -742,26 +742,13 @@ impl ProductRegistry { } } -/// Extract product name from remote config path +/// Extract product name and id from remote config path /// Path format is: datadog/2/{PRODUCT}/{config_id}/config -fn extract_product_from_path(path: &str) -> Option { - let parts: Vec<&str> = path.split('/').collect(); - // Look for pattern: datadog/2/PRODUCT/... - if parts.len() >= 3 && parts[0] == "datadog" && parts[1] == "2" { - return Some(parts[2].to_string()); - } - None -} - -// Helper to extract config id from known RC path pattern -fn extract_config_id_from_path(path: &str) -> Option { - // Expected: datadog/2/{PRODUCT}/{config_id}/config - let parts: Vec<&str> = path.split('/').collect(); - // Look for pattern: datadog/2/PRODUCT/config_id/config - if parts.len() >= 5 && parts[0] == "datadog" && parts[1] == "2" && parts[4] == "config" { - return Some(parts[3].to_string()); - } - None +fn extract_product_and_id_from_path(path: &str) -> Option<(String, String)> { + let mut components = path.strip_prefix("datadog/2/")?.strip_suffix("/config")?.split("/"); + let (product, config_id) = (components.next()?.to_string(), components.next()?.to_string()); + if !components.remainder().is_empty() { return None } + Some((product, config_id)) } #[cfg(test)] From e34f89e833ec44414abcf4cf2b572281058f0e04 Mon Sep 17 00:00:00 2001 From: Zachary Groves <32471391+ZStriker19@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:44:40 -0400 Subject: [PATCH 32/34] Update dd-trace/Cargo.toml Co-authored-by: paullegranddc <82819397+paullegranddc@users.noreply.github.com> --- dd-trace/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-trace/Cargo.toml b/dd-trace/Cargo.toml index 428fc579..51dbb6a1 100644 --- a/dd-trace/Cargo.toml +++ b/dd-trace/Cargo.toml @@ -16,8 +16,8 @@ anyhow = "1.0.97" rustc_version_runtime = "0.3.0" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -hyper = { version = "1.0", features = ["client", "http1", "http2"] } -hyper-util = { version = "0.1", features = ["client", "client-legacy", "http1", "http2", "tokio"] } +hyper = { version = "1.6", features = ["client", "http1"] } +hyper-util = { version = "0.1.16", features = ["client", "client-legacy", "http1", "tokio"] } http-body-util = "0.1" tokio = { version = "1.0", features = ["rt", "rt-multi-thread"] } base64 = "0.21" From 45af6dea0959cf0d8444673c15da77c3b462ce4b Mon Sep 17 00:00:00 2001 From: Zach Groves Date: Tue, 19 Aug 2025 15:18:36 -0400 Subject: [PATCH 33/34] fix method combining --- dd-trace/src/configuration/remote_config.rs | 82 +++++++++++++-------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/dd-trace/src/configuration/remote_config.rs b/dd-trace/src/configuration/remote_config.rs index d102148e..d3585a80 100644 --- a/dd-trace/src/configuration/remote_config.rs +++ b/dd-trace/src/configuration/remote_config.rs @@ -511,18 +511,19 @@ impl RemoteConfigClient { let mut any_failure = false; for file in target_files { - // Extract product from path to determine which handler to use + // Extract product and config_id from path to determine which handler to use // Path format is like "datadog/2/{PRODUCT}/{config_id}/config" - let product = match extract_product_from_path(&file.path) { - Some(p) => p, - None => { - crate::dd_debug!( - "RemoteConfigClient: Failed to extract product from path: {}", - file.path - ); - continue; - } - }; + let (product, derived_config_id) = + match extract_product_and_id_from_path(&file.path) { + Some((p, id)) => (p, Some(id)), + None => { + crate::dd_debug!( + "RemoteConfigClient: Failed to extract product from path: {}", + file.path + ); + continue; + } + }; // Check if we have a handler for this product let handler = match self.product_registry.get_handler(&product) { @@ -542,7 +543,7 @@ impl RemoteConfigClient { .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in config: {}", e))?; // Determine config id and version for state reporting (do this before applying) - let derived_id = extract_config_id_from_path(&file.path); + let derived_id = derived_config_id; let (meta_id, meta_version) = path_to_custom .get(&file.path) .cloned() @@ -745,10 +746,19 @@ impl ProductRegistry { /// Extract product name and id from remote config path /// Path format is: datadog/2/{PRODUCT}/{config_id}/config fn extract_product_and_id_from_path(path: &str) -> Option<(String, String)> { - let mut components = path.strip_prefix("datadog/2/")?.strip_suffix("/config")?.split("/"); - let (product, config_id) = (components.next()?.to_string(), components.next()?.to_string()); - if !components.remainder().is_empty() { return None } - Some((product, config_id)) + let mut components = path + .strip_prefix("datadog/2/")? + .strip_suffix("/config")? + .split("/"); + let (product, config_id) = ( + components.next()?.to_string(), + components.next()?.to_string(), + ); + // Check if there are any remaining components after product and config_id + if components.next().is_some() { + return None; + } + Some((product, config_id)) } #[cfg(test)] @@ -1386,57 +1396,71 @@ mod tests { fn test_extract_product_from_path() { // Test APM_TRACING path assert_eq!( - extract_product_from_path("datadog/2/APM_TRACING/config123/config"), + extract_product_and_id_from_path("datadog/2/APM_TRACING/config123/config") + .map(|(p, _)| p), Some("APM_TRACING".to_string()) ); // Test LIVE_DEBUGGING path assert_eq!( - extract_product_from_path("datadog/2/LIVE_DEBUGGING/LIVE_DEBUGGING-base/config"), + extract_product_and_id_from_path("datadog/2/LIVE_DEBUGGING/LIVE_DEBUGGING-base/config") + .map(|(p, _)| p), Some("LIVE_DEBUGGING".to_string()) ); // Test AGENT_CONFIG path assert_eq!( - extract_product_from_path("datadog/2/AGENT_CONFIG/dynamic_rates/config"), + extract_product_and_id_from_path("datadog/2/AGENT_CONFIG/dynamic_rates/config") + .map(|(p, _)| p), Some("AGENT_CONFIG".to_string()) ); // Test invalid paths - assert_eq!(extract_product_from_path("invalid/path"), None); assert_eq!( - extract_product_from_path("datadog/1/APM_TRACING/config"), + extract_product_and_id_from_path("invalid/path").map(|(p, _)| p), None ); assert_eq!( - extract_product_from_path("datadog/APM_TRACING/config"), + extract_product_and_id_from_path("datadog/1/APM_TRACING/config").map(|(p, _)| p), None ); - assert_eq!(extract_product_from_path(""), None); + assert_eq!( + extract_product_and_id_from_path("datadog/APM_TRACING/config").map(|(p, _)| p), + None + ); + assert_eq!(extract_product_and_id_from_path("").map(|(p, _)| p), None); } #[test] fn test_extract_config_id_from_path() { // Test APM_TRACING path assert_eq!( - extract_config_id_from_path("datadog/2/APM_TRACING/config123/config"), + extract_product_and_id_from_path("datadog/2/APM_TRACING/config123/config") + .map(|(_, id)| id), Some("config123".to_string()) ); // Test ASM_FEATURES path assert_eq!( - extract_config_id_from_path("datadog/2/ASM_FEATURES/ASM_FEATURES-base/config"), + extract_product_and_id_from_path("datadog/2/ASM_FEATURES/ASM_FEATURES-base/config") + .map(|(_, id)| id), Some("ASM_FEATURES-base".to_string()) ); // Test invalid paths - assert_eq!(extract_config_id_from_path("invalid/path"), None); assert_eq!( - extract_config_id_from_path("datadog/2/APM_TRACING/config"), + extract_product_and_id_from_path("invalid/path").map(|(_, id)| id), + None + ); + assert_eq!( + extract_product_and_id_from_path("datadog/2/APM_TRACING/config").map(|(_, id)| id), None ); // Missing /config at end - assert_eq!(extract_config_id_from_path("datadog/2/APM_TRACING"), None); // Too short - assert_eq!(extract_config_id_from_path(""), None); + assert_eq!( + extract_product_and_id_from_path("datadog/2/APM_TRACING").map(|(_, id)| id), + None + ); // Too short + assert_eq!(extract_product_and_id_from_path("").map(|(_, id)| id), None); } #[test] From d79450705ca94877fa4466e9f263aac6451e1783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Thu, 28 Aug 2025 15:49:56 +0200 Subject: [PATCH 34/34] fix(test): add defaults to the test run script --- run_remote_config_test.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/run_remote_config_test.sh b/run_remote_config_test.sh index bd263d11..a7379dec 100755 --- a/run_remote_config_test.sh +++ b/run_remote_config_test.sh @@ -17,11 +17,16 @@ else echo fi +# Set up defaults if not set +export DD_TRACE_AGENT_URL="${DD_TRACE_AGENT_URL:-http://localhost:8126}" +export DD_LOG_LEVEL="${DD_LOG_LEVEL:-INFO}" +export DD_REMOTE_CONFIGURATION_ENABLED="${DD_REMOTE_CONFIGURATION_ENABLED:-true}" + # Show current environment echo "πŸ“‹ Environment variables:" -echo " DD_TRACE_AGENT_URL: ${DD_TRACE_AGENT_URL:-http://localhost:8126}" -echo " DD_LOG_LEVEL: ${DD_LOG_LEVEL:-INFO}" -echo " DD_REMOTE_CONFIGURATION_ENABLED: ${DD_REMOTE_CONFIGURATION_ENABLED:-true}" +echo " DD_TRACE_AGENT_URL: ${DD_TRACE_AGENT_URL}" +echo " DD_LOG_LEVEL: ${DD_LOG_LEVEL}" +echo " DD_REMOTE_CONFIGURATION_ENABLED: ${DD_REMOTE_CONFIGURATION_ENABLED}" echo echo "πŸš€ Starting remote config test application..." @@ -31,4 +36,4 @@ echo " Press Ctrl+C to stop" echo # Run the application -exec cargo run -p remote_config_test \ No newline at end of file +exec cargo run -p remote_config_test