From 146dfe292f22d7199a3784d1e2892d0130a17505 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasechnik Date: Fri, 11 Jul 2025 16:02:29 -0400 Subject: [PATCH 1/5] feat: add tags to config --- crates/datadog-trace-agent/src/config.rs | 39 +++++++++++++++++++ .../src/trace_processor.rs | 1 + 2 files changed, 40 insertions(+) diff --git a/crates/datadog-trace-agent/src/config.rs b/crates/datadog-trace-agent/src/config.rs index 0ea40508..fac51b63 100644 --- a/crates/datadog-trace-agent/src/config.rs +++ b/crates/datadog-trace-agent/src/config.rs @@ -3,6 +3,7 @@ use ddcommon::Endpoint; use std::borrow::Cow; +use std::collections::HashMap; use std::env; use std::str::FromStr; @@ -24,6 +25,7 @@ pub struct Config { pub max_request_content_length: usize, pub obfuscation_config: obfuscation_config::ObfuscationConfig, pub os: String, + pub tags: HashMap, /// how often to flush stats, in seconds pub stats_flush_interval: u64, /// how often to flush traces, in seconds @@ -94,14 +96,31 @@ impl Config { proxy_url: env::var("DD_PROXY_HTTPS") .or_else(|_| env::var("HTTPS_PROXY")) .ok(), + tags: parse_env_tags(), }) } } +fn parse_env_tags() -> HashMap { + let mut tags = HashMap::new(); + + if let Ok(env_tags) = env::var("DD_TAGS") { + for kv in env_tags.split(',') { + let parts = kv.split(':').collect::>(); + if parts.len() == 2 { + tags.insert(parts[0].to_string(), parts[1].to_string()); + } + } + } + + tags +} + #[cfg(test)] mod tests { use duplicate::duplicate_item; use serial_test::serial; + use std::collections::HashMap; use std::env; use crate::config; @@ -250,4 +269,24 @@ mod tests { env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); env::remove_var("DD_DOGSTATSD_PORT"); } + + #[test] + #[serial] + fn test_dd_tags() { + env::set_var("DD_API_KEY", "_not_a_real_key_"); + env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); + env::set_var("DD_TAGS", "some:tag,another:thing"); + let config_res = config::Config::new(); + println!("{:?}", config_res); + assert!(config_res.is_ok()); + let config = config_res.unwrap(); + let expected_tags = HashMap::from([ + ("some".to_string(), "tag".to_string()), + ("another".to_string(), "thing".to_string()), + ]); + assert_eq!(config.tags, expected_tags); + env::remove_var("DD_API_KEY"); + env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); + env::remove_var("DD_TAGS"); + } } diff --git a/crates/datadog-trace-agent/src/trace_processor.rs b/crates/datadog-trace-agent/src/trace_processor.rs index afe3f4c9..076a9dfd 100644 --- a/crates/datadog-trace-agent/src/trace_processor.rs +++ b/crates/datadog-trace-agent/src/trace_processor.rs @@ -188,6 +188,7 @@ mod tests { os: "linux".to_string(), obfuscation_config: ObfuscationConfig::new().unwrap(), proxy_url: None, + tags: HashMap::new(), } } From ddafdd341671b823ab2744475b6a72bb81c7f804 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasechnik Date: Fri, 11 Jul 2025 16:48:34 -0400 Subject: [PATCH 2/5] feat: precompute function_tags --- crates/datadog-trace-agent/src/config.rs | 27 +++++++++++++++++-- .../src/trace_processor.rs | 1 + 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/datadog-trace-agent/src/config.rs b/crates/datadog-trace-agent/src/config.rs index fac51b63..27fe0f3b 100644 --- a/crates/datadog-trace-agent/src/config.rs +++ b/crates/datadog-trace-agent/src/config.rs @@ -26,6 +26,12 @@ pub struct Config { pub obfuscation_config: obfuscation_config::ObfuscationConfig, pub os: String, pub tags: HashMap, + // the comma-delimited, key:value form of tags so that we only compute it once + // TODO: it is unfortuante that are defining this value separately from the tags value above, + // since this is derived from the other and they should be kept synchronized. A better approach + // would probably be a tags struct which we instantiate from the hash which provides a + // read-only function_tags form as well. + pub function_tags: Option, /// how often to flush stats, in seconds pub stats_flush_interval: u64, /// how often to flush traces, in seconds @@ -71,6 +77,18 @@ impl Config { ) })?; + let tags = parse_env_tags(); + let function_tags = if tags.is_empty() { + None + } else { + let mut kvs = tags + .iter() + .map(|(k, v)| format!("{k}:{v}")) + .collect::>(); + kvs.sort(); + Some(kvs.join(",")) + }; + #[allow(clippy::unwrap_used)] Ok(Config { app_name: Some(app_name), @@ -96,7 +114,8 @@ impl Config { proxy_url: env::var("DD_PROXY_HTTPS") .or_else(|_| env::var("HTTPS_PROXY")) .ok(), - tags: parse_env_tags(), + tags, + function_tags, }) } } @@ -275,7 +294,7 @@ mod tests { fn test_dd_tags() { env::set_var("DD_API_KEY", "_not_a_real_key_"); env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); - env::set_var("DD_TAGS", "some:tag,another:thing"); + env::set_var("DD_TAGS", "some:tag,another:thing,invalid:thing:here"); let config_res = config::Config::new(); println!("{:?}", config_res); assert!(config_res.is_ok()); @@ -285,6 +304,10 @@ mod tests { ("another".to_string(), "thing".to_string()), ]); assert_eq!(config.tags, expected_tags); + assert_eq!( + config.function_tags, + Some("another:thing,some:tag".to_string()) + ); env::remove_var("DD_API_KEY"); env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); env::remove_var("DD_TAGS"); diff --git a/crates/datadog-trace-agent/src/trace_processor.rs b/crates/datadog-trace-agent/src/trace_processor.rs index 076a9dfd..4e2b6af8 100644 --- a/crates/datadog-trace-agent/src/trace_processor.rs +++ b/crates/datadog-trace-agent/src/trace_processor.rs @@ -189,6 +189,7 @@ mod tests { obfuscation_config: ObfuscationConfig::new().unwrap(), proxy_url: None, tags: HashMap::new(), + function_tags: None, } } From c15de94a40eede0f7eb67e074d8f5ade1199a775 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasechnik Date: Fri, 11 Jul 2025 17:12:23 -0400 Subject: [PATCH 3/5] feat: inject function tags into the tracer payloads --- .../src/trace_processor.rs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/datadog-trace-agent/src/trace_processor.rs b/crates/datadog-trace-agent/src/trace_processor.rs index 4e2b6af8..b7ac9584 100644 --- a/crates/datadog-trace-agent/src/trace_processor.rs +++ b/crates/datadog-trace-agent/src/trace_processor.rs @@ -13,13 +13,15 @@ use datadog_trace_obfuscation::obfuscate::obfuscate_span; use datadog_trace_protobuf::pb; use datadog_trace_utils::trace_utils::{self}; use datadog_trace_utils::trace_utils::{EnvironmentType, SendData}; -use datadog_trace_utils::tracer_payload::TraceChunkProcessor; +use datadog_trace_utils::tracer_payload::{TraceChunkProcessor, TracerPayloadCollection}; use crate::{ config::Config, http_utils::{self, log_and_create_http_response, log_and_create_traces_success_http_response}, }; +const TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY: &str = "_dd.tags.function"; + #[async_trait] pub trait TraceProcessor { /// Deserializes traces from a hyper request body and sends them through the provided tokio mpsc @@ -104,7 +106,7 @@ impl TraceProcessor for ServerlessTraceProcessor { ); } - let payload = match trace_utils::collect_pb_trace_chunks( + let mut payload = match trace_utils::collect_pb_trace_chunks( traces, &tracer_header_tags, &mut ChunkProcessor { @@ -122,6 +124,18 @@ impl TraceProcessor for ServerlessTraceProcessor { } }; + // Add function_tags to payload if we can + if let Some(function_tags) = &config.function_tags { + if let TracerPayloadCollection::V07(ref mut tracer_payloads) = payload { + for tracer_payload in tracer_payloads { + tracer_payload.tags.insert( + TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY.to_string(), + function_tags.clone(), + ); + } + } + } + let send_data = SendData::new(body_size, payload, tracer_header_tags, &config.trace_intake); // send trace payload to our trace flusher @@ -151,7 +165,7 @@ mod tests { use crate::{ config::Config, - trace_processor::{self, TraceProcessor}, + trace_processor::{self, TraceProcessor, TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY}, }; use datadog_trace_protobuf::pb; use datadog_trace_utils::test_utils::{create_test_gcp_json_span, create_test_gcp_span}; @@ -188,8 +202,11 @@ mod tests { os: "linux".to_string(), obfuscation_config: ObfuscationConfig::new().unwrap(), proxy_url: None, - tags: HashMap::new(), - function_tags: None, + tags: HashMap::from([ + ("env".to_string(), "test".to_string()), + ("service".to_string(), "my-service".to_string()), + ]), + function_tags: Some("env:test,service:my-service".to_string()), } } @@ -253,7 +270,10 @@ mod tests { tags: HashMap::new(), dropped_trace: false, }], - tags: HashMap::new(), + tags: HashMap::from([( + TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY.to_string(), + "env:test,service:my-service".to_string(), + )]), env: "test-env".to_string(), hostname: "".to_string(), app_version: "".to_string(), @@ -326,7 +346,10 @@ mod tests { tags: HashMap::new(), dropped_trace: false, }], - tags: HashMap::new(), + tags: HashMap::from([( + TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY.to_string(), + "env:test,service:my-service".to_string(), + )]), env: "test-env".to_string(), hostname: "".to_string(), app_version: "".to_string(), From 84a6464da24e483f4e8051db822de6bbc5b6b804 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasechnik Date: Fri, 11 Jul 2025 17:23:30 -0400 Subject: [PATCH 4/5] chore: better tags type --- crates/datadog-trace-agent/src/config.rs | 92 +++++++++++-------- .../src/trace_processor.rs | 12 +-- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/crates/datadog-trace-agent/src/config.rs b/crates/datadog-trace-agent/src/config.rs index 27fe0f3b..652f14c3 100644 --- a/crates/datadog-trace-agent/src/config.rs +++ b/crates/datadog-trace-agent/src/config.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::env; use std::str::FromStr; +use std::sync::OnceLock; use datadog_trace_obfuscation::obfuscation_config; use datadog_trace_utils::config_utils::{ @@ -16,6 +17,54 @@ use datadog_trace_utils::trace_utils; const DEFAULT_DOGSTATSD_PORT: u16 = 8125; +#[derive(Debug)] +pub struct Tags { + tags: HashMap, + function_tags_string: OnceLock, +} + +impl Tags { + pub fn from_env_string(env_tags: &str) -> Self { + let mut tags = HashMap::new(); + for kv in env_tags.split(',') { + let parts = kv.split(':').collect::>(); + if parts.len() == 2 { + tags.insert(parts[0].to_string(), parts[1].to_string()); + } + } + Self { + tags, + function_tags_string: OnceLock::new(), + } + } + + pub fn new() -> Self { + Self { + tags: HashMap::new(), + function_tags_string: OnceLock::new(), + } + } + + pub fn tags(&self) -> &HashMap { + &self.tags + } + + pub fn function_tags(&self) -> Option<&str> { + if self.tags.is_empty() { + return None; + } + Some(self.function_tags_string.get_or_init(|| { + let mut kvs = self + .tags + .iter() + .map(|(k, v)| format!("{k}:{v}")) + .collect::>(); + kvs.sort(); + kvs.join(",") + })) + } +} + #[derive(Debug)] pub struct Config { pub dd_site: String, @@ -25,13 +74,7 @@ pub struct Config { pub max_request_content_length: usize, pub obfuscation_config: obfuscation_config::ObfuscationConfig, pub os: String, - pub tags: HashMap, - // the comma-delimited, key:value form of tags so that we only compute it once - // TODO: it is unfortuante that are defining this value separately from the tags value above, - // since this is derived from the other and they should be kept synchronized. A better approach - // would probably be a tags struct which we instantiate from the hash which provides a - // read-only function_tags form as well. - pub function_tags: Option, + pub tags: Tags, /// how often to flush stats, in seconds pub stats_flush_interval: u64, /// how often to flush traces, in seconds @@ -77,16 +120,10 @@ impl Config { ) })?; - let tags = parse_env_tags(); - let function_tags = if tags.is_empty() { - None + let tags = if let Ok(env_tags) = env::var("DD_TAGS") { + Tags::from_env_string(&env_tags) } else { - let mut kvs = tags - .iter() - .map(|(k, v)| format!("{k}:{v}")) - .collect::>(); - kvs.sort(); - Some(kvs.join(",")) + Tags::new() }; #[allow(clippy::unwrap_used)] @@ -115,26 +152,10 @@ impl Config { .or_else(|_| env::var("HTTPS_PROXY")) .ok(), tags, - function_tags, }) } } -fn parse_env_tags() -> HashMap { - let mut tags = HashMap::new(); - - if let Ok(env_tags) = env::var("DD_TAGS") { - for kv in env_tags.split(',') { - let parts = kv.split(':').collect::>(); - if parts.len() == 2 { - tags.insert(parts[0].to_string(), parts[1].to_string()); - } - } - } - - tags -} - #[cfg(test)] mod tests { use duplicate::duplicate_item; @@ -303,11 +324,8 @@ mod tests { ("some".to_string(), "tag".to_string()), ("another".to_string(), "thing".to_string()), ]); - assert_eq!(config.tags, expected_tags); - assert_eq!( - config.function_tags, - Some("another:thing,some:tag".to_string()) - ); + assert_eq!(config.tags.tags(), &expected_tags); + assert_eq!(config.tags.function_tags(), Some("another:thing,some:tag")); env::remove_var("DD_API_KEY"); env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); env::remove_var("DD_TAGS"); diff --git a/crates/datadog-trace-agent/src/trace_processor.rs b/crates/datadog-trace-agent/src/trace_processor.rs index b7ac9584..dbdf65d1 100644 --- a/crates/datadog-trace-agent/src/trace_processor.rs +++ b/crates/datadog-trace-agent/src/trace_processor.rs @@ -125,12 +125,12 @@ impl TraceProcessor for ServerlessTraceProcessor { }; // Add function_tags to payload if we can - if let Some(function_tags) = &config.function_tags { + if let Some(function_tags) = config.tags.function_tags() { if let TracerPayloadCollection::V07(ref mut tracer_payloads) = payload { for tracer_payload in tracer_payloads { tracer_payload.tags.insert( TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY.to_string(), - function_tags.clone(), + function_tags.to_string(), ); } } @@ -164,7 +164,7 @@ mod tests { use tokio::sync::mpsc::{self, Receiver, Sender}; use crate::{ - config::Config, + config::{Config, Tags}, trace_processor::{self, TraceProcessor, TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY}, }; use datadog_trace_protobuf::pb; @@ -202,11 +202,7 @@ mod tests { os: "linux".to_string(), obfuscation_config: ObfuscationConfig::new().unwrap(), proxy_url: None, - tags: HashMap::from([ - ("env".to_string(), "test".to_string()), - ("service".to_string(), "my-service".to_string()), - ]), - function_tags: Some("env:test,service:my-service".to_string()), + tags: Tags::from_env_string("env:test,service:my-service"), } } From e9d49dbcf26f752437706a00a8ced309f9dbee83 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasechnik Date: Wed, 16 Jul 2025 09:55:28 -1000 Subject: [PATCH 5/5] feat: support space and comma separated tags --- crates/datadog-trace-agent/src/config.rs | 79 +++++++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/crates/datadog-trace-agent/src/config.rs b/crates/datadog-trace-agent/src/config.rs index 652f14c3..31459257 100644 --- a/crates/datadog-trace-agent/src/config.rs +++ b/crates/datadog-trace-agent/src/config.rs @@ -26,7 +26,12 @@ pub struct Tags { impl Tags { pub fn from_env_string(env_tags: &str) -> Self { let mut tags = HashMap::new(); - for kv in env_tags.split(',') { + + // Space-separated key:value tags are the standard for tagging. For compatibility reasons + // we also support comma-separated key:value tags as well. + let normalized = env_tags.replace(',', " "); + + for kv in normalized.split_whitespace() { let parts = kv.split(':').collect::>(); if parts.len() == 2 { tags.insert(parts[0].to_string(), parts[1].to_string()); @@ -310,24 +315,80 @@ mod tests { env::remove_var("DD_DOGSTATSD_PORT"); } - #[test] - #[serial] - fn test_dd_tags() { + fn test_config_with_dd_tags(dd_tags: &str) -> config::Config { env::set_var("DD_API_KEY", "_not_a_real_key_"); env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); - env::set_var("DD_TAGS", "some:tag,another:thing,invalid:thing:here"); + env::set_var("DD_TAGS", dd_tags); let config_res = config::Config::new(); - println!("{:?}", config_res); assert!(config_res.is_ok()); let config = config_res.unwrap(); + env::remove_var("DD_API_KEY"); + env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); + env::remove_var("DD_TAGS"); + config + } + + #[test] + #[serial] + fn test_dd_tags_comma_separated() { + let config = test_config_with_dd_tags("some:tag,another:thing,invalid:thing:here"); let expected_tags = HashMap::from([ ("some".to_string(), "tag".to_string()), ("another".to_string(), "thing".to_string()), ]); assert_eq!(config.tags.tags(), &expected_tags); assert_eq!(config.tags.function_tags(), Some("another:thing,some:tag")); - env::remove_var("DD_API_KEY"); - env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); - env::remove_var("DD_TAGS"); + } + + #[test] + #[serial] + fn test_dd_tags_space_separated() { + let config = test_config_with_dd_tags("some:tag another:thing invalid:thing:here"); + let expected_tags = HashMap::from([ + ("some".to_string(), "tag".to_string()), + ("another".to_string(), "thing".to_string()), + ]); + assert_eq!(config.tags.tags(), &expected_tags); + assert_eq!(config.tags.function_tags(), Some("another:thing,some:tag")); + } + + #[test] + #[serial] + fn test_dd_tags_mixed_separators() { + let config = test_config_with_dd_tags("some:tag,another:thing extra:value"); + let expected_tags = HashMap::from([ + ("some".to_string(), "tag".to_string()), + ("another".to_string(), "thing".to_string()), + ("extra".to_string(), "value".to_string()), + ]); + assert_eq!(config.tags.tags(), &expected_tags); + assert_eq!( + config.tags.function_tags(), + Some("another:thing,extra:value,some:tag") + ); + } + + #[test] + #[serial] + fn test_dd_tags_no_valid_tags() { + // Test with only invalid tags + let config = test_config_with_dd_tags("invalid:thing:here,also-bad"); + assert_eq!(config.tags.tags(), &HashMap::new()); + assert_eq!(config.tags.function_tags(), None); + + // Test with empty string + let config = test_config_with_dd_tags(""); + assert_eq!(config.tags.tags(), &HashMap::new()); + assert_eq!(config.tags.function_tags(), None); + + // Test with just whitespace + let config = test_config_with_dd_tags(" "); + assert_eq!(config.tags.tags(), &HashMap::new()); + assert_eq!(config.tags.function_tags(), None); + + // Test with just commas and spaces + let config = test_config_with_dd_tags(" , , "); + assert_eq!(config.tags.tags(), &HashMap::new()); + assert_eq!(config.tags.function_tags(), None); } }