Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions libdd-trace-utils/src/send_data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -482,11 +482,11 @@ 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",
let tracer_payload_tags = TracerPayloadTags {
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 {
Expand All @@ -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 {
Expand Down
204 changes: 165 additions & 39 deletions libdd-trace-utils/src/trace_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,34 @@ 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<'a> {
pub env: &'a str,
pub app_version: &'a str,
pub hostname: &'a str,
pub runtime_id: &'a str,
pub struct TracerPayloadTags {
pub env: String,
pub app_version: String,
pub hostname: String,
pub runtime_id: String,
}

/// 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<String> {
if let Some(v) = root.meta.get(field) {
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) {
if !v.is_empty() {
return Some(v.clone());
}
}
}
None
}

pub(crate) fn construct_trace_chunk(trace: Vec<pb::Span>) -> pb::TraceChunk {
Expand All @@ -284,16 +305,16 @@ pub(crate) fn construct_trace_chunk(trace: Vec<pb::Span>) -> pb::TraceChunk {
pub(crate) fn construct_tracer_payload(
chunks: Vec<pb::TraceChunk>,
tracer_tags: &TracerHeaderTags,
root_span_tags: RootSpanTags,
tracer_payload_tags: TracerPayloadTags,
) -> pb::TracerPayload {
pb::TracerPayload {
app_version: root_span_tags.app_version.to_string(),
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.to_string(),
runtime_id: root_span_tags.runtime_id.to_string(),
env: tracer_payload_tags.env,
runtime_id: tracer_payload_tags.runtime_id,
chunks,
hostname: root_span_tags.hostname.to_string(),
hostname: tracer_payload_tags.hostname,
language_version: tracer_tags.lang_version.to_string(),
tags: HashMap::new(),
tracer_version: tracer_tags.tracer_version.to_string(),
Expand Down Expand Up @@ -569,20 +590,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<T: TraceData>(
traces: Vec<Vec<crate::span::v04::Span<T>>>,
use_v05_format: bool,
Expand Down Expand Up @@ -617,8 +624,7 @@ pub fn collect_pb_trace_chunks<T: tracer_payload::TraceChunkProcessor>(
let mut trace_chunks: Vec<pb::TraceChunk> = 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();
let mut tracer_payload_tags = TracerPayloadTags::default();

for trace in traces.iter_mut() {
if is_agentless {
Expand Down Expand Up @@ -656,23 +662,40 @@ pub fn collect_pb_trace_chunks<T: tracer_payload::TraceChunkProcessor>(

trace_chunks.push(chunk);

if !gathered_root_span_tags {
gathered_root_span_tags = true;
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 is_agentless {
// 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(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() {
if let Some(v) = search_trace_for_field(root, trace, "version") {
tracer_payload_tags.app_version = v;
}
}
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 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),
]))
}

Expand Down Expand Up @@ -1278,4 +1301,107 @@ mod tests {
assert!(!span.meta.contains_key("aas.site.kind"));
assert!(!span.meta.contains_key("aas.site.type"));
}

#[test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I was look at pb:Span, and it looks like there's a bunch of fuzzing used to test that code. Do you think that a bolero check!() type test might be useful here? How well formed are trace payloads, generally?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect_pb_trace_chunks, where search_trace_for_field is called, is certainly complex enough to warrant a bolero test in general for everything that it's doing. But for what we want to test as part of this PR in search_trace_for_field - "best effort, first non-empty value wins from any span regardless of structural validity". It just means the bolero test can only assert "if a value is returned, it came from some span in the trace", which is already guaranteed from the other unit tests.

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");
}

#[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");
}
}
Loading