From e2f014a80d17cdd1741e82485713bcd2d24d0059 Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Tue, 5 May 2026 17:16:53 -0400 Subject: [PATCH 1/7] use span tags from all root spans in tracer payload --- libdd-trace-utils/src/send_data/mod.rs | 8 ++-- libdd-trace-utils/src/trace_utils.rs | 65 +++++++++++++------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/libdd-trace-utils/src/send_data/mod.rs b/libdd-trace-utils/src/send_data/mod.rs index c884566d17..229c5c2949 100644 --- a/libdd-trace-utils/src/send_data/mod.rs +++ b/libdd-trace-utils/src/send_data/mod.rs @@ -483,10 +483,10 @@ mod tests { fn setup_payload(header_tags: &TracerHeaderTags) -> TracerPayload { let root_tags = RootSpanTags { - env: "TEST", - app_version: "1.0", - hostname: "test_bench", - runtime_id: "id", + env: "TEST".to_string(), + app_version: "1.0".to_string(), + hostname: "test_bench".to_string(), + runtime_id: "id".to_string(), }; let chunk = construct_trace_chunk(vec![Span { diff --git a/libdd-trace-utils/src/trace_utils.rs b/libdd-trace-utils/src/trace_utils.rs index cd4d3bfb3f..e91023c44d 100644 --- a/libdd-trace-utils/src/trace_utils.rs +++ b/libdd-trace-utils/src/trace_utils.rs @@ -264,11 +264,11 @@ where /// Tags gathered from a trace's root span #[derive(Default)] -pub struct RootSpanTags<'a> { - pub env: &'a str, - pub app_version: &'a str, - pub hostname: &'a str, - pub runtime_id: &'a str, +pub struct RootSpanTags { + pub env: String, + pub app_version: String, + pub hostname: String, + pub runtime_id: String, } pub(crate) fn construct_trace_chunk(trace: Vec) -> pb::TraceChunk { @@ -287,13 +287,13 @@ pub(crate) fn construct_tracer_payload( root_span_tags: RootSpanTags, ) -> pb::TracerPayload { pb::TracerPayload { - app_version: root_span_tags.app_version.to_string(), + app_version: root_span_tags.app_version, language_name: tracer_tags.lang.to_string(), container_id: tracer_tags.container_id.to_string(), - env: root_span_tags.env.to_string(), - runtime_id: root_span_tags.runtime_id.to_string(), + env: root_span_tags.env, + runtime_id: root_span_tags.runtime_id, chunks, - hostname: root_span_tags.hostname.to_string(), + hostname: root_span_tags.hostname, language_version: tracer_tags.lang_version.to_string(), tags: HashMap::new(), tracer_version: tracer_tags.tracer_version.to_string(), @@ -569,19 +569,6 @@ pub fn enrich_span_with_azure_function_metadata(span: &mut pb::Span) { } } -/// Used to populate root_span_tags fields if they exist in the root span's meta tags -macro_rules! parse_root_span_tags { - ( - $root_span_meta_map:ident, - { $($tag:literal => $($root_span_tags_struct_field:ident).+ ,)+ } - ) => { - $( - if let Some(root_span_tag_value) = $root_span_meta_map.get($tag) { - $($root_span_tags_struct_field).+ = root_span_tag_value; - } - )+ - } -} pub fn collect_trace_chunks( traces: Vec>>, @@ -617,7 +604,6 @@ pub fn collect_pb_trace_chunks( let mut trace_chunks: Vec = Vec::new(); // We'll skip setting the global metadata and rely on the agent to unpack these - let mut gathered_root_span_tags = !is_agentless; let mut root_span_tags = RootSpanTags::default(); for trace in traces.iter_mut() { @@ -656,18 +642,31 @@ pub fn collect_pb_trace_chunks( trace_chunks.push(chunk); - if !gathered_root_span_tags { - gathered_root_span_tags = true; + if is_agentless { + // Check each root span tag field independently so that a later trace can fill in + // fields that were missing from an earlier trace (e.g. command_execution span has no + // version, but a subsequent azure_functions.invoke span does). let meta_map = &trace[root_span_index].meta; - parse_root_span_tags!( - meta_map, - { - "env" => root_span_tags.env, - "version" => root_span_tags.app_version, - "_dd.hostname" => root_span_tags.hostname, - "runtime-id" => root_span_tags.runtime_id, + if root_span_tags.env.is_empty() { + if let Some(v) = meta_map.get("env") { + root_span_tags.env = v.clone(); } - ); + } + if root_span_tags.app_version.is_empty() { + if let Some(v) = meta_map.get("version") { + root_span_tags.app_version = v.clone(); + } + } + if root_span_tags.hostname.is_empty() { + if let Some(v) = meta_map.get("_dd.hostname") { + root_span_tags.hostname = v.clone(); + } + } + if root_span_tags.runtime_id.is_empty() { + if let Some(v) = meta_map.get("runtime-id") { + root_span_tags.runtime_id = v.clone(); + } + } } } From fd5c1b9464757c8390da3104cc59a59134ad0f38 Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Wed, 6 May 2026 15:33:15 -0400 Subject: [PATCH 2/7] search non root spans for fields --- libdd-trace-utils/src/send_data/mod.rs | 6 +- libdd-trace-utils/src/trace_utils.rs | 144 ++++++++++++++++++++----- 2 files changed, 121 insertions(+), 29 deletions(-) diff --git a/libdd-trace-utils/src/send_data/mod.rs b/libdd-trace-utils/src/send_data/mod.rs index 229c5c2949..c33736693e 100644 --- a/libdd-trace-utils/src/send_data/mod.rs +++ b/libdd-trace-utils/src/send_data/mod.rs @@ -456,7 +456,7 @@ mod tests { use super::*; use crate::send_with_retry::{RetryBackoffType, RetryStrategy}; use crate::test_utils::create_test_no_alloc_span; - use crate::trace_utils::{construct_trace_chunk, construct_tracer_payload, RootSpanTags}; + use crate::trace_utils::{construct_trace_chunk, construct_tracer_payload, TracerPayloadTags}; use crate::tracer_header_tags::TracerHeaderTags; use httpmock::prelude::*; use httpmock::MockServer; @@ -482,7 +482,7 @@ mod tests { }; fn setup_payload(header_tags: &TracerHeaderTags) -> TracerPayload { - let root_tags = RootSpanTags { + let tracer_payload_tags = TracerPayloadTags { env: "TEST".to_string(), app_version: "1.0".to_string(), hostname: "test_bench".to_string(), @@ -507,7 +507,7 @@ mod tests { span_events: vec![], }]); - construct_tracer_payload(vec![chunk], header_tags, root_tags) + construct_tracer_payload(vec![chunk], header_tags, tracer_payload_tags) } fn compute_payload_len(collection: &TracerPayloadCollection) -> usize { diff --git a/libdd-trace-utils/src/trace_utils.rs b/libdd-trace-utils/src/trace_utils.rs index e91023c44d..e589ac2a7d 100644 --- a/libdd-trace-utils/src/trace_utils.rs +++ b/libdd-trace-utils/src/trace_utils.rs @@ -262,15 +262,32 @@ where Ok((body_size, traces)) } -/// Tags gathered from a trace's root span +/// Tags extracted from a tracer payload's traces, used to populate top level tracer payload fields. #[derive(Default)] -pub struct RootSpanTags { +pub struct TracerPayloadTags { pub env: String, pub app_version: String, pub hostname: String, pub runtime_id: String, } +/// Returns the first value of `field` found in `trace`, searching the root span first then all +/// other spans. +fn search_trace_for_field(root: &pb::Span, trace: &[pb::Span], field: &str) -> Option { + if let Some(v) = root.meta.get(field) { + return Some(v.clone()); + } + for span in trace { + if span.span_id == root.span_id { + continue; + } + if let Some(v) = span.meta.get(field) { + return Some(v.clone()); + } + } + None +} + pub(crate) fn construct_trace_chunk(trace: Vec) -> pb::TraceChunk { pb::TraceChunk { priority: normalizer::SamplerPriority::None as i32, @@ -284,16 +301,16 @@ pub(crate) fn construct_trace_chunk(trace: Vec) -> pb::TraceChunk { pub(crate) fn construct_tracer_payload( chunks: Vec, tracer_tags: &TracerHeaderTags, - root_span_tags: RootSpanTags, + tracer_payload_tags: TracerPayloadTags, ) -> pb::TracerPayload { pb::TracerPayload { - app_version: root_span_tags.app_version, + app_version: tracer_payload_tags.app_version, language_name: tracer_tags.lang.to_string(), container_id: tracer_tags.container_id.to_string(), - env: root_span_tags.env, - runtime_id: root_span_tags.runtime_id, + env: tracer_payload_tags.env, + runtime_id: tracer_payload_tags.runtime_id, chunks, - hostname: root_span_tags.hostname, + hostname: tracer_payload_tags.hostname, language_version: tracer_tags.lang_version.to_string(), tags: HashMap::new(), tracer_version: tracer_tags.tracer_version.to_string(), @@ -569,7 +586,6 @@ pub fn enrich_span_with_azure_function_metadata(span: &mut pb::Span) { } } - pub fn collect_trace_chunks( traces: Vec>>, use_v05_format: bool, @@ -604,7 +620,7 @@ pub fn collect_pb_trace_chunks( let mut trace_chunks: Vec = Vec::new(); // We'll skip setting the global metadata and rely on the agent to unpack these - let mut root_span_tags = RootSpanTags::default(); + let mut tracer_payload_tags = TracerPayloadTags::default(); for trace in traces.iter_mut() { if is_agentless { @@ -643,35 +659,34 @@ pub fn collect_pb_trace_chunks( trace_chunks.push(chunk); if is_agentless { - // Check each root span tag field independently so that a later trace can fill in - // fields that were missing from an earlier trace (e.g. command_execution span has no - // version, but a subsequent azure_functions.invoke span does). - let meta_map = &trace[root_span_index].meta; - if root_span_tags.env.is_empty() { - if let Some(v) = meta_map.get("env") { - root_span_tags.env = v.clone(); + // Check each field independently so that a later trace can fill in fields missing + // from an earlier trace. + let root = &trace[root_span_index]; + if tracer_payload_tags.env.is_empty() { + if let Some(v) = search_trace_for_field(root, trace, "env") { + tracer_payload_tags.env = v; } } - if root_span_tags.app_version.is_empty() { - if let Some(v) = meta_map.get("version") { - root_span_tags.app_version = v.clone(); + if tracer_payload_tags.app_version.is_empty() { + if let Some(v) = search_trace_for_field(root, trace, "version") { + tracer_payload_tags.app_version = v; } } - if root_span_tags.hostname.is_empty() { - if let Some(v) = meta_map.get("_dd.hostname") { - root_span_tags.hostname = v.clone(); + if tracer_payload_tags.hostname.is_empty() { + if let Some(v) = search_trace_for_field(root, trace, "_dd.hostname") { + tracer_payload_tags.hostname = v; } } - if root_span_tags.runtime_id.is_empty() { - if let Some(v) = meta_map.get("runtime-id") { - root_span_tags.runtime_id = v.clone(); + if tracer_payload_tags.runtime_id.is_empty() { + if let Some(v) = search_trace_for_field(root, trace, "runtime-id") { + tracer_payload_tags.runtime_id = v; } } } } Ok(TracerPayloadCollection::V07(vec![ - construct_tracer_payload(trace_chunks, tracer_header_tags, root_span_tags), + construct_tracer_payload(trace_chunks, tracer_header_tags, tracer_payload_tags), ])) } @@ -1277,4 +1292,81 @@ mod tests { assert!(!span.meta.contains_key("aas.site.kind")); assert!(!span.meta.contains_key("aas.site.type")); } + + #[test] + fn test_collect_pb_trace_chunks_searches_multiple_root_spans_for_fields() { + // First trace root span has no version. Second trace root span has a version. + // The second root span should populate the version field. + let first_root_span = create_test_span(1, 1, 0, 1, true); + + let mut second_root_span = create_test_span(2, 3, 0, 1, true); + second_root_span + .meta + .insert("version".to_string(), "1.2.3".to_string()); + + let result = collect_pb_trace_chunks( + vec![vec![first_root_span], vec![second_root_span]], + &TracerHeaderTags::default(), + &mut tracer_payload::DefaultTraceChunkProcessor, + true, + ) + .unwrap(); + + let TracerPayloadCollection::V07(payloads) = result else { + panic!("expected TracerPayloadCollection::V07"); + }; + assert_eq!(payloads[0].app_version, "1.2.3"); + } + + #[test] + fn test_collect_pb_trace_chunks_searches_non_root_spans_for_fields() { + // Root span has no version. Child span has a version. + // The child span should populate the version field. + let root_span = create_test_span(1, 1, 0, 1, true); + let mut child_span = create_test_span(1, 2, 1, 1, false); + child_span + .meta + .insert("version".to_string(), "1.2.3".to_string()); + + let result = collect_pb_trace_chunks( + vec![vec![root_span, child_span]], + &TracerHeaderTags::default(), + &mut tracer_payload::DefaultTraceChunkProcessor, + true, + ) + .unwrap(); + + let TracerPayloadCollection::V07(payloads) = result else { + panic!("expected TracerPayloadCollection::V07"); + }; + assert_eq!(payloads[0].app_version, "1.2.3"); + } + + #[test] + fn test_collect_pb_trace_chunks_root_span_takes_priority_over_child() { + // Root span has a version. Child has a different version. + // The root span should populate the version field. + let mut root_span = create_test_span(1, 1, 0, 1, true); + root_span + .meta + .insert("version".to_string(), "root-version".to_string()); + + let mut child_span = create_test_span(1, 2, 1, 1, false); + child_span + .meta + .insert("version".to_string(), "child-version".to_string()); + + let result = collect_pb_trace_chunks( + vec![vec![root_span, child_span]], + &TracerHeaderTags::default(), + &mut tracer_payload::DefaultTraceChunkProcessor, + true, + ) + .unwrap(); + + let TracerPayloadCollection::V07(payloads) = result else { + panic!("expected TracerPayloadCollection::V07"); + }; + assert_eq!(payloads[0].app_version, "root-version"); + } } From 3c10b32a71b0fec572eb6281f31e4f8942335fbe Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Wed, 6 May 2026 17:04:45 -0400 Subject: [PATCH 3/7] only return non-empty values in search_trace_for_field --- libdd-trace-utils/src/trace_utils.rs | 38 +++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/libdd-trace-utils/src/trace_utils.rs b/libdd-trace-utils/src/trace_utils.rs index e589ac2a7d..b8785d4f9d 100644 --- a/libdd-trace-utils/src/trace_utils.rs +++ b/libdd-trace-utils/src/trace_utils.rs @@ -271,18 +271,22 @@ pub struct TracerPayloadTags { pub runtime_id: String, } -/// Returns the first value of `field` found in `trace`, searching the root span first then all -/// other spans. +/// Returns the first non-empty value of `field` found in `trace`, searching the root span first +/// then all other spans. fn search_trace_for_field(root: &pb::Span, trace: &[pb::Span], field: &str) -> Option { if let Some(v) = root.meta.get(field) { - return Some(v.clone()); + if !v.is_empty() { + return Some(v.clone()); + } } for span in trace { if span.span_id == root.span_id { continue; } if let Some(v) = span.meta.get(field) { - return Some(v.clone()); + if !v.is_empty() { + return Some(v.clone()); + } } } None @@ -1369,4 +1373,30 @@ mod tests { }; assert_eq!(payloads[0].app_version, "root-version"); } + + #[test] + fn test_collect_pb_trace_chunks_skips_empty_root_span_value() { + // Root span has version: "". Child span has a non-empty version. + // The child span should populate the version field. + let mut root_span = create_test_span(1, 1, 0, 1, true); + root_span.meta.insert("version".to_string(), "".to_string()); + + let mut child_span = create_test_span(1, 2, 1, 1, false); + child_span + .meta + .insert("version".to_string(), "1.2.3".to_string()); + + let result = collect_pb_trace_chunks( + vec![vec![root_span, child_span]], + &TracerHeaderTags::default(), + &mut tracer_payload::DefaultTraceChunkProcessor, + true, + ) + .unwrap(); + + let TracerPayloadCollection::V07(payloads) = result else { + panic!("expected TracerPayloadCollection::V07"); + }; + assert_eq!(payloads[0].app_version, "1.2.3"); + } } From 00b025cd2e578338adf11ceca1a2c876682ecb0d Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Fri, 8 May 2026 15:16:38 -0400 Subject: [PATCH 4/7] normalize env tag in case span was skipped during normalization --- libdd-trace-utils/src/trace_utils.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libdd-trace-utils/src/trace_utils.rs b/libdd-trace-utils/src/trace_utils.rs index b8785d4f9d..f1b1bdf377 100644 --- a/libdd-trace-utils/src/trace_utils.rs +++ b/libdd-trace-utils/src/trace_utils.rs @@ -667,8 +667,12 @@ pub fn collect_pb_trace_chunks( // from an earlier trace. let root = &trace[root_span_index]; if tracer_payload_tags.env.is_empty() { - if let Some(v) = search_trace_for_field(root, trace, "env") { - tracer_payload_tags.env = v; + if let Some(mut v) = search_trace_for_field(root, trace, "env") { + // Normalize env tag in case the span it was pulled from was skipped during normalization + libdd_trace_normalization::normalize_utils::normalize_tag(&mut v); + if !v.is_empty() { + tracer_payload_tags.env = v; + } } } if tracer_payload_tags.app_version.is_empty() { From 963dadf3681d335ad27aa13aa4cde690613ff217 Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Fri, 8 May 2026 15:20:11 -0400 Subject: [PATCH 5/7] apply formatting --- libdd-trace-utils/src/trace_utils.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libdd-trace-utils/src/trace_utils.rs b/libdd-trace-utils/src/trace_utils.rs index f1b1bdf377..3b84afc387 100644 --- a/libdd-trace-utils/src/trace_utils.rs +++ b/libdd-trace-utils/src/trace_utils.rs @@ -668,7 +668,8 @@ pub fn collect_pb_trace_chunks( let root = &trace[root_span_index]; if tracer_payload_tags.env.is_empty() { if let Some(mut v) = search_trace_for_field(root, trace, "env") { - // Normalize env tag in case the span it was pulled from was skipped during normalization + // Normalize env tag in case the span it was pulled from was skipped during + // normalization libdd_trace_normalization::normalize_utils::normalize_tag(&mut v); if !v.is_empty() { tracer_payload_tags.env = v; From d18934e5b21705386abca238bd89420c7c85c66b Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Fri, 8 May 2026 15:56:42 -0400 Subject: [PATCH 6/7] add more unit tests --- libdd-trace-utils/src/trace_utils.rs | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/libdd-trace-utils/src/trace_utils.rs b/libdd-trace-utils/src/trace_utils.rs index 3b84afc387..4934c763fa 100644 --- a/libdd-trace-utils/src/trace_utils.rs +++ b/libdd-trace-utils/src/trace_utils.rs @@ -1404,4 +1404,40 @@ mod tests { }; assert_eq!(payloads[0].app_version, "1.2.3"); } + + #[test] + fn test_collect_pb_trace_chunks_normalizes_env() { + let mut root = create_test_span(1, 1, 0, 1, true); + root.meta.insert("env".to_string(), "PRODUCTION".to_string()); + + let result = collect_pb_trace_chunks( + vec![vec![root]], + &TracerHeaderTags::default(), + &mut tracer_payload::DefaultTraceChunkProcessor, + true, + ) + .unwrap(); + + let TracerPayloadCollection::V07(payloads) = result else { + panic!("expected TracerPayloadCollection::V07"); + }; + assert_eq!(payloads[0].env, "production"); + } + + #[test] + fn test_search_trace_for_field_skips_span_with_same_id_as_root() { + // A span with the same span_id as root is treated as the root and skipped + // in the child span search. Only the root spans own meta is checked for it. + let mut root = create_test_span(1, 1, 0, 1, true); + root.meta.remove("version"); + + // This span shares the same span_id as the root span, it should be skipped. + let mut duplicate = create_test_span(1, 1, 0, 1, false); + duplicate + .meta + .insert("version".to_string(), "should-not-appear".to_string()); + + let trace = vec![root.clone(), duplicate]; + assert_eq!(search_trace_for_field(&root, &trace, "version"), None); + } } From 551934a0e8ad951e84acd06b74f6f67439ffbce6 Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Mon, 11 May 2026 17:04:23 -0400 Subject: [PATCH 7/7] add more unit tests --- libdd-trace-utils/src/trace_utils.rs | 119 ++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 11 deletions(-) diff --git a/libdd-trace-utils/src/trace_utils.rs b/libdd-trace-utils/src/trace_utils.rs index 4934c763fa..0837e21bf9 100644 --- a/libdd-trace-utils/src/trace_utils.rs +++ b/libdd-trace-utils/src/trace_utils.rs @@ -1304,14 +1304,25 @@ mod tests { #[test] fn test_collect_pb_trace_chunks_searches_multiple_root_spans_for_fields() { - // First trace root span has no version. Second trace root span has a version. - // The second root span should populate the version field. - let first_root_span = create_test_span(1, 1, 0, 1, true); + // First trace root span has no fields. Second trace root span has all fields. + // The second root span should populate all fields. + let mut first_root_span = create_test_span(1, 1, 0, 1, true); + first_root_span.meta.remove("env"); + first_root_span.meta.remove("runtime-id"); let mut second_root_span = create_test_span(2, 3, 0, 1, true); second_root_span .meta .insert("version".to_string(), "1.2.3".to_string()); + second_root_span + .meta + .insert("env".to_string(), "prod".to_string()); + second_root_span + .meta + .insert("_dd.hostname".to_string(), "my-host".to_string()); + second_root_span + .meta + .insert("runtime-id".to_string(), "123".to_string()); let result = collect_pb_trace_chunks( vec![vec![first_root_span], vec![second_root_span]], @@ -1325,17 +1336,31 @@ mod tests { panic!("expected TracerPayloadCollection::V07"); }; assert_eq!(payloads[0].app_version, "1.2.3"); + assert_eq!(payloads[0].env, "prod"); + assert_eq!(payloads[0].hostname, "my-host"); + assert_eq!(payloads[0].runtime_id, "123"); } #[test] fn test_collect_pb_trace_chunks_searches_non_root_spans_for_fields() { - // Root span has no version. Child span has a version. - // The child span should populate the version field. - let root_span = create_test_span(1, 1, 0, 1, true); + // Root span has no fields. Child span has all fields. + // The child span should populate all fields. + let mut root_span = create_test_span(1, 1, 0, 1, true); + root_span.meta.remove("env"); + root_span.meta.remove("runtime-id"); let mut child_span = create_test_span(1, 2, 1, 1, false); child_span .meta .insert("version".to_string(), "1.2.3".to_string()); + child_span + .meta + .insert("env".to_string(), "prod".to_string()); + child_span + .meta + .insert("_dd.hostname".to_string(), "my-host".to_string()); + child_span + .meta + .insert("runtime-id".to_string(), "123".to_string()); let result = collect_pb_trace_chunks( vec![vec![root_span, child_span]], @@ -1349,21 +1374,42 @@ mod tests { panic!("expected TracerPayloadCollection::V07"); }; assert_eq!(payloads[0].app_version, "1.2.3"); + assert_eq!(payloads[0].env, "prod"); + assert_eq!(payloads[0].hostname, "my-host"); + assert_eq!(payloads[0].runtime_id, "123"); } #[test] fn test_collect_pb_trace_chunks_root_span_takes_priority_over_child() { - // Root span has a version. Child has a different version. - // The root span should populate the version field. + // Root span has all fields. Child has different values for all fields. + // The root span should populate all fields. let mut root_span = create_test_span(1, 1, 0, 1, true); root_span .meta .insert("version".to_string(), "root-version".to_string()); + root_span + .meta + .insert("env".to_string(), "root-env".to_string()); + root_span + .meta + .insert("_dd.hostname".to_string(), "root-host".to_string()); + root_span + .meta + .insert("runtime-id".to_string(), "root-runtime-id".to_string()); let mut child_span = create_test_span(1, 2, 1, 1, false); child_span .meta .insert("version".to_string(), "child-version".to_string()); + child_span + .meta + .insert("env".to_string(), "child-env".to_string()); + child_span + .meta + .insert("_dd.hostname".to_string(), "child-host".to_string()); + child_span + .meta + .insert("runtime-id".to_string(), "child-runtime-id".to_string()); let result = collect_pb_trace_chunks( vec![vec![root_span, child_span]], @@ -1377,19 +1423,38 @@ mod tests { panic!("expected TracerPayloadCollection::V07"); }; assert_eq!(payloads[0].app_version, "root-version"); + assert_eq!(payloads[0].env, "root-env"); + assert_eq!(payloads[0].hostname, "root-host"); + assert_eq!(payloads[0].runtime_id, "root-runtime-id"); } #[test] fn test_collect_pb_trace_chunks_skips_empty_root_span_value() { - // Root span has version: "". Child span has a non-empty version. - // The child span should populate the version field. + // Root span has empty values for all fields. Child span has non-empty values. + // The child span should populate all fields. let mut root_span = create_test_span(1, 1, 0, 1, true); root_span.meta.insert("version".to_string(), "".to_string()); + root_span.meta.insert("env".to_string(), "".to_string()); + root_span + .meta + .insert("_dd.hostname".to_string(), "".to_string()); + root_span + .meta + .insert("runtime-id".to_string(), "".to_string()); let mut child_span = create_test_span(1, 2, 1, 1, false); child_span .meta .insert("version".to_string(), "1.2.3".to_string()); + child_span + .meta + .insert("env".to_string(), "prod".to_string()); + child_span + .meta + .insert("_dd.hostname".to_string(), "my-host".to_string()); + child_span + .meta + .insert("runtime-id".to_string(), "123".to_string()); let result = collect_pb_trace_chunks( vec![vec![root_span, child_span]], @@ -1403,12 +1468,16 @@ mod tests { panic!("expected TracerPayloadCollection::V07"); }; assert_eq!(payloads[0].app_version, "1.2.3"); + assert_eq!(payloads[0].env, "prod"); + assert_eq!(payloads[0].hostname, "my-host"); + assert_eq!(payloads[0].runtime_id, "123"); } #[test] fn test_collect_pb_trace_chunks_normalizes_env() { let mut root = create_test_span(1, 1, 0, 1, true); - root.meta.insert("env".to_string(), "PRODUCTION".to_string()); + root.meta + .insert("env".to_string(), "PRODUCTION".to_string()); let result = collect_pb_trace_chunks( vec![vec![root]], @@ -1424,6 +1493,34 @@ mod tests { assert_eq!(payloads[0].env, "production"); } + #[test] + fn test_collect_pb_trace_chunks_skips_env_empty_after_normalization() { + // First root span has an env that normalizes to empty (all invalid characters). + // Second root span has an env should populate env fields. + let mut first_root_span = create_test_span(1, 1, 0, 1, true); + first_root_span + .meta + .insert("env".to_string(), "!!!".to_string()); + + let mut second_root_span = create_test_span(2, 3, 0, 1, true); + second_root_span + .meta + .insert("env".to_string(), "prod".to_string()); + + let result = collect_pb_trace_chunks( + vec![vec![first_root_span], vec![second_root_span]], + &TracerHeaderTags::default(), + &mut tracer_payload::DefaultTraceChunkProcessor, + true, + ) + .unwrap(); + + let TracerPayloadCollection::V07(payloads) = result else { + panic!("expected TracerPayloadCollection::V07"); + }; + assert_eq!(payloads[0].env, "prod"); + } + #[test] fn test_search_trace_for_field_skips_span_with_same_id_as_root() { // A span with the same span_id as root is treated as the root and skipped