Skip to content

Commit 6949dbc

Browse files
feat: parse DD_TAGS and use them for the tracer payload function tags (#29)
* feat: add tags to config * feat: precompute function_tags * feat: inject function tags into the tracer payloads * chore: better tags type * feat: support space and comma separated tags
1 parent 9851203 commit 6949dbc

File tree

2 files changed

+168
-6
lines changed

2 files changed

+168
-6
lines changed

crates/datadog-trace-agent/src/config.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
use ddcommon::Endpoint;
55
use std::borrow::Cow;
6+
use std::collections::HashMap;
67
use std::env;
78
use std::str::FromStr;
9+
use std::sync::OnceLock;
810

911
use datadog_trace_obfuscation::obfuscation_config;
1012
use datadog_trace_utils::config_utils::{
@@ -15,6 +17,59 @@ use datadog_trace_utils::trace_utils;
1517

1618
const DEFAULT_DOGSTATSD_PORT: u16 = 8125;
1719

20+
#[derive(Debug)]
21+
pub struct Tags {
22+
tags: HashMap<String, String>,
23+
function_tags_string: OnceLock<String>,
24+
}
25+
26+
impl Tags {
27+
pub fn from_env_string(env_tags: &str) -> Self {
28+
let mut tags = HashMap::new();
29+
30+
// Space-separated key:value tags are the standard for tagging. For compatibility reasons
31+
// we also support comma-separated key:value tags as well.
32+
let normalized = env_tags.replace(',', " ");
33+
34+
for kv in normalized.split_whitespace() {
35+
let parts = kv.split(':').collect::<Vec<&str>>();
36+
if parts.len() == 2 {
37+
tags.insert(parts[0].to_string(), parts[1].to_string());
38+
}
39+
}
40+
Self {
41+
tags,
42+
function_tags_string: OnceLock::new(),
43+
}
44+
}
45+
46+
pub fn new() -> Self {
47+
Self {
48+
tags: HashMap::new(),
49+
function_tags_string: OnceLock::new(),
50+
}
51+
}
52+
53+
pub fn tags(&self) -> &HashMap<String, String> {
54+
&self.tags
55+
}
56+
57+
pub fn function_tags(&self) -> Option<&str> {
58+
if self.tags.is_empty() {
59+
return None;
60+
}
61+
Some(self.function_tags_string.get_or_init(|| {
62+
let mut kvs = self
63+
.tags
64+
.iter()
65+
.map(|(k, v)| format!("{k}:{v}"))
66+
.collect::<Vec<String>>();
67+
kvs.sort();
68+
kvs.join(",")
69+
}))
70+
}
71+
}
72+
1873
#[derive(Debug)]
1974
pub struct Config {
2075
pub dd_site: String,
@@ -24,6 +79,7 @@ pub struct Config {
2479
pub max_request_content_length: usize,
2580
pub obfuscation_config: obfuscation_config::ObfuscationConfig,
2681
pub os: String,
82+
pub tags: Tags,
2783
/// how often to flush stats, in seconds
2884
pub stats_flush_interval: u64,
2985
/// how often to flush traces, in seconds
@@ -69,6 +125,12 @@ impl Config {
69125
)
70126
})?;
71127

128+
let tags = if let Ok(env_tags) = env::var("DD_TAGS") {
129+
Tags::from_env_string(&env_tags)
130+
} else {
131+
Tags::new()
132+
};
133+
72134
#[allow(clippy::unwrap_used)]
73135
Ok(Config {
74136
app_name: Some(app_name),
@@ -94,6 +156,7 @@ impl Config {
94156
proxy_url: env::var("DD_PROXY_HTTPS")
95157
.or_else(|_| env::var("HTTPS_PROXY"))
96158
.ok(),
159+
tags,
97160
})
98161
}
99162
}
@@ -102,6 +165,7 @@ impl Config {
102165
mod tests {
103166
use duplicate::duplicate_item;
104167
use serial_test::serial;
168+
use std::collections::HashMap;
105169
use std::env;
106170

107171
use crate::config;
@@ -250,4 +314,81 @@ mod tests {
250314
env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME");
251315
env::remove_var("DD_DOGSTATSD_PORT");
252316
}
317+
318+
fn test_config_with_dd_tags(dd_tags: &str) -> config::Config {
319+
env::set_var("DD_API_KEY", "_not_a_real_key_");
320+
env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app");
321+
env::set_var("DD_TAGS", dd_tags);
322+
let config_res = config::Config::new();
323+
assert!(config_res.is_ok());
324+
let config = config_res.unwrap();
325+
env::remove_var("DD_API_KEY");
326+
env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME");
327+
env::remove_var("DD_TAGS");
328+
config
329+
}
330+
331+
#[test]
332+
#[serial]
333+
fn test_dd_tags_comma_separated() {
334+
let config = test_config_with_dd_tags("some:tag,another:thing,invalid:thing:here");
335+
let expected_tags = HashMap::from([
336+
("some".to_string(), "tag".to_string()),
337+
("another".to_string(), "thing".to_string()),
338+
]);
339+
assert_eq!(config.tags.tags(), &expected_tags);
340+
assert_eq!(config.tags.function_tags(), Some("another:thing,some:tag"));
341+
}
342+
343+
#[test]
344+
#[serial]
345+
fn test_dd_tags_space_separated() {
346+
let config = test_config_with_dd_tags("some:tag another:thing invalid:thing:here");
347+
let expected_tags = HashMap::from([
348+
("some".to_string(), "tag".to_string()),
349+
("another".to_string(), "thing".to_string()),
350+
]);
351+
assert_eq!(config.tags.tags(), &expected_tags);
352+
assert_eq!(config.tags.function_tags(), Some("another:thing,some:tag"));
353+
}
354+
355+
#[test]
356+
#[serial]
357+
fn test_dd_tags_mixed_separators() {
358+
let config = test_config_with_dd_tags("some:tag,another:thing extra:value");
359+
let expected_tags = HashMap::from([
360+
("some".to_string(), "tag".to_string()),
361+
("another".to_string(), "thing".to_string()),
362+
("extra".to_string(), "value".to_string()),
363+
]);
364+
assert_eq!(config.tags.tags(), &expected_tags);
365+
assert_eq!(
366+
config.tags.function_tags(),
367+
Some("another:thing,extra:value,some:tag")
368+
);
369+
}
370+
371+
#[test]
372+
#[serial]
373+
fn test_dd_tags_no_valid_tags() {
374+
// Test with only invalid tags
375+
let config = test_config_with_dd_tags("invalid:thing:here,also-bad");
376+
assert_eq!(config.tags.tags(), &HashMap::new());
377+
assert_eq!(config.tags.function_tags(), None);
378+
379+
// Test with empty string
380+
let config = test_config_with_dd_tags("");
381+
assert_eq!(config.tags.tags(), &HashMap::new());
382+
assert_eq!(config.tags.function_tags(), None);
383+
384+
// Test with just whitespace
385+
let config = test_config_with_dd_tags(" ");
386+
assert_eq!(config.tags.tags(), &HashMap::new());
387+
assert_eq!(config.tags.function_tags(), None);
388+
389+
// Test with just commas and spaces
390+
let config = test_config_with_dd_tags(" , , ");
391+
assert_eq!(config.tags.tags(), &HashMap::new());
392+
assert_eq!(config.tags.function_tags(), None);
393+
}
253394
}

crates/datadog-trace-agent/src/trace_processor.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ use datadog_trace_obfuscation::obfuscate::obfuscate_span;
1313
use datadog_trace_protobuf::pb;
1414
use datadog_trace_utils::trace_utils::{self};
1515
use datadog_trace_utils::trace_utils::{EnvironmentType, SendData};
16-
use datadog_trace_utils::tracer_payload::TraceChunkProcessor;
16+
use datadog_trace_utils::tracer_payload::{TraceChunkProcessor, TracerPayloadCollection};
1717

1818
use crate::{
1919
config::Config,
2020
http_utils::{self, log_and_create_http_response, log_and_create_traces_success_http_response},
2121
};
2222

23+
const TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY: &str = "_dd.tags.function";
24+
2325
#[async_trait]
2426
pub trait TraceProcessor {
2527
/// Deserializes traces from a hyper request body and sends them through the provided tokio mpsc
@@ -104,7 +106,7 @@ impl TraceProcessor for ServerlessTraceProcessor {
104106
);
105107
}
106108

107-
let payload = match trace_utils::collect_pb_trace_chunks(
109+
let mut payload = match trace_utils::collect_pb_trace_chunks(
108110
traces,
109111
&tracer_header_tags,
110112
&mut ChunkProcessor {
@@ -122,6 +124,18 @@ impl TraceProcessor for ServerlessTraceProcessor {
122124
}
123125
};
124126

127+
// Add function_tags to payload if we can
128+
if let Some(function_tags) = config.tags.function_tags() {
129+
if let TracerPayloadCollection::V07(ref mut tracer_payloads) = payload {
130+
for tracer_payload in tracer_payloads {
131+
tracer_payload.tags.insert(
132+
TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY.to_string(),
133+
function_tags.to_string(),
134+
);
135+
}
136+
}
137+
}
138+
125139
let send_data = SendData::new(body_size, payload, tracer_header_tags, &config.trace_intake);
126140

127141
// send trace payload to our trace flusher
@@ -150,8 +164,8 @@ mod tests {
150164
use tokio::sync::mpsc::{self, Receiver, Sender};
151165

152166
use crate::{
153-
config::Config,
154-
trace_processor::{self, TraceProcessor},
167+
config::{Config, Tags},
168+
trace_processor::{self, TraceProcessor, TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY},
155169
};
156170
use datadog_trace_protobuf::pb;
157171
use datadog_trace_utils::test_utils::{create_test_gcp_json_span, create_test_gcp_span};
@@ -188,6 +202,7 @@ mod tests {
188202
os: "linux".to_string(),
189203
obfuscation_config: ObfuscationConfig::new().unwrap(),
190204
proxy_url: None,
205+
tags: Tags::from_env_string("env:test,service:my-service"),
191206
}
192207
}
193208

@@ -251,7 +266,10 @@ mod tests {
251266
tags: HashMap::new(),
252267
dropped_trace: false,
253268
}],
254-
tags: HashMap::new(),
269+
tags: HashMap::from([(
270+
TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY.to_string(),
271+
"env:test,service:my-service".to_string(),
272+
)]),
255273
env: "test-env".to_string(),
256274
hostname: "".to_string(),
257275
app_version: "".to_string(),
@@ -324,7 +342,10 @@ mod tests {
324342
tags: HashMap::new(),
325343
dropped_trace: false,
326344
}],
327-
tags: HashMap::new(),
345+
tags: HashMap::from([(
346+
TRACER_PAYLOAD_FUNCTION_TAGS_TAG_KEY.to_string(),
347+
"env:test,service:my-service".to_string(),
348+
)]),
328349
env: "test-env".to_string(),
329350
hostname: "".to_string(),
330351
app_version: "".to_string(),

0 commit comments

Comments
 (0)