use crate::span::trace_utils::DroppedP0Stats;
use crate::span::v1::{AttributeValue, Span, TraceChunk};
use crate::span::{SpanText, TraceData};
use std::collections::{HashMap, HashSet};
use tracing::debug;
const TOP_LEVEL_KEY: &str = "_top_level";
const TRACER_TOP_LEVEL_KEY: &str = "_dd.top_level";
const MEASURED_KEY: &str = "_dd.measured";
const PARTIAL_VERSION_KEY: &str = "_dd.partial_version";
const SAMPLING_SINGLE_SPAN_MECHANISM: &str = "_dd.span_sampling.mechanism";
const SAMPLING_ANALYTICS_RATE_KEY: &str = "_dd1.sr.eausr";
fn attribute_as_f64<T: TraceData>(value: &AttributeValue<T>) -> Option<f64> {
match value {
AttributeValue::Float(v) => Some(*v),
AttributeValue::Int(v) => Some(*v as f64),
_ => None,
}
}
fn set_top_level_span<T: TraceData>(span: &mut Span<T>) {
span.attributes.insert(
T::Text::from_static_str(TOP_LEVEL_KEY),
AttributeValue::Float(1.0),
);
}
pub fn compute_top_level_span<T: TraceData>(trace: &mut [Span<T>]) {
let span_id_idx: HashMap<u64, usize> = trace
.iter()
.enumerate()
.map(|(i, span)| (span.span_id, i))
.collect();
let top_level: Vec<usize> = trace
.iter()
.enumerate()
.filter(|(_, span)| {
span.parent_id == 0
|| span_id_idx
.get(&span.parent_id)
.is_none_or(|&parent_idx| trace[parent_idx].service != span.service)
})
.map(|(i, _)| i)
.collect();
for i in top_level {
set_top_level_span(&mut trace[i]);
}
}
pub fn get_root_span_index<T: TraceData>(trace: &[Span<T>]) -> anyhow::Result<usize> {
if trace.is_empty() {
anyhow::bail!("Cannot find root span index in an empty trace.");
}
for (i, span) in trace.iter().enumerate().rev() {
if span.parent_id == 0 {
return Ok(i);
}
}
let span_ids: HashSet<_> = trace.iter().map(|span| span.span_id).collect();
let mut root_span_id = None;
for (i, span) in trace.iter().enumerate() {
if !span_ids.contains(&span.parent_id) {
if root_span_id.is_some() {
debug!("trace has multiple root spans");
}
root_span_id = Some(i);
}
}
Ok(match root_span_id {
Some(i) => i,
None => {
debug!("Could not find the root span for trace");
trace.len() - 1
}
})
}
pub fn has_top_level<T: TraceData>(span: &Span<T>) -> bool {
span.attributes
.get(TRACER_TOP_LEVEL_KEY)
.and_then(attribute_as_f64)
.is_some_and(|v| v == 1.0)
|| span
.attributes
.get(TOP_LEVEL_KEY)
.and_then(attribute_as_f64)
.is_some_and(|v| v == 1.0)
}
pub fn is_measured<T: TraceData>(span: &Span<T>) -> bool {
span.attributes
.get(MEASURED_KEY)
.and_then(attribute_as_f64)
.is_some_and(|v| v == 1.0)
}
pub fn is_partial_snapshot<T: TraceData>(span: &Span<T>) -> bool {
span.attributes
.get(PARTIAL_VERSION_KEY)
.and_then(attribute_as_f64)
.is_some_and(|v| v >= 0.0)
}
pub fn drop_chunks<T: TraceData>(traces: &mut Vec<TraceChunk<T>>) -> DroppedP0Stats {
let mut dropped_p0_traces = 0;
let mut dropped_p0_spans = 0;
traces.retain_mut(|chunk| {
if chunk.spans.iter().any(|s| s.error) {
return true;
}
let effective_priority = if chunk.dropped_trace {
chunk.priority.filter(|&p| p < 0).or(Some(-1))
} else {
chunk.priority
};
if effective_priority.is_none_or(|p| p > 0) {
return true;
}
let spans_before = chunk.spans.len();
chunk.spans.retain(|span| {
span.attributes
.get(SAMPLING_SINGLE_SPAN_MECHANISM)
.and_then(attribute_as_f64)
.is_some_and(|m| m == 8.0)
|| span.attributes.contains_key(SAMPLING_ANALYTICS_RATE_KEY)
});
dropped_p0_spans += spans_before - chunk.spans.len();
if chunk.spans.is_empty() {
dropped_p0_traces += 1;
return false;
}
true
});
DroppedP0Stats {
dropped_p0_traces,
dropped_p0_spans,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::span::v1::{SpanBytes, TraceChunkBytes};
fn create_test_span(is_top_level: bool) -> SpanBytes {
let mut span = SpanBytes {
service: "test-service".into(),
name: "test_name".into(),
resource: "test-resource".into(),
..Default::default()
};
if is_top_level {
span.attributes
.insert("_top_level".into(), AttributeValue::Float(1.0));
}
span
}
fn create_test_span_with_ids(span_id: u64, parent_id: u64) -> SpanBytes {
SpanBytes {
service: "test-service".into(),
name: "test_name".into(),
resource: "test-resource".into(),
span_id,
parent_id,
..Default::default()
}
}
#[test]
fn test_has_top_level() {
let top_level_span = create_test_span(true);
let not_top_level_span = create_test_span(false);
assert!(has_top_level(&top_level_span));
assert!(!has_top_level(¬_top_level_span));
}
#[test]
fn test_is_measured() {
let mut measured_span = create_test_span(true);
measured_span
.attributes
.insert(MEASURED_KEY.into(), AttributeValue::Float(1.0));
let not_measured_span = create_test_span(true);
assert!(is_measured(&measured_span));
assert!(!is_measured(¬_measured_span));
}
#[test]
fn test_is_partial_snapshot() {
let mut partial_span = create_test_span(false);
partial_span
.attributes
.insert(PARTIAL_VERSION_KEY.into(), AttributeValue::Int(2));
let not_partial_span = create_test_span(false);
assert!(is_partial_snapshot(&partial_span));
assert!(!is_partial_snapshot(¬_partial_span));
}
#[test]
fn test_compute_top_level() {
let mut span_with_different_service = create_test_span_with_ids(5, 2);
span_with_different_service.service = "another_service".into();
let mut trace = vec![
create_test_span_with_ids(1, 0),
create_test_span_with_ids(2, 1),
create_test_span_with_ids(4, 3),
span_with_different_service,
];
compute_top_level_span(trace.as_mut_slice());
let spans_marked_as_top_level: Vec<u64> = trace
.iter()
.filter_map(|span| has_top_level(span).then_some(span.span_id))
.collect();
assert_eq!(spans_marked_as_top_level, [1, 4, 5]);
}
#[test]
fn test_get_root_span_index_from_complete_trace() {
let trace = vec![
create_test_span_with_ids(1, 0),
create_test_span_with_ids(2, 1),
create_test_span_with_ids(3, 1),
];
assert_eq!(get_root_span_index(&trace).unwrap(), 0);
}
#[test]
fn test_get_root_span_index_root_last() {
let trace = vec![
create_test_span_with_ids(2, 1),
create_test_span_with_ids(3, 1),
create_test_span_with_ids(1, 0),
];
assert_eq!(get_root_span_index(&trace).unwrap(), 2);
}
#[test]
fn test_get_root_span_index_from_partial_trace() {
let trace = vec![
create_test_span_with_ids(1, 99),
create_test_span_with_ids(2, 1),
];
assert_eq!(get_root_span_index(&trace).unwrap(), 0);
}
#[test]
fn test_get_root_span_index_empty_trace_errors() {
let trace: Vec<SpanBytes> = vec![];
assert!(get_root_span_index(&trace).is_err());
}
fn chunk_with_spans(priority: Option<i32>, spans: Vec<SpanBytes>) -> TraceChunkBytes {
TraceChunkBytes {
priority,
spans,
..Default::default()
}
}
#[test]
fn test_drop_chunks() {
let chunk_with_priority = chunk_with_spans(
Some(1),
vec![
SpanBytes {
span_id: 1,
..Default::default()
},
SpanBytes {
span_id: 2,
parent_id: 1,
..Default::default()
},
],
);
let chunk_with_null_priority = chunk_with_spans(
Some(0),
vec![
SpanBytes {
span_id: 1,
..Default::default()
},
SpanBytes {
span_id: 2,
parent_id: 1,
..Default::default()
},
],
);
let chunk_without_priority = chunk_with_spans(
None,
vec![
SpanBytes {
span_id: 1,
..Default::default()
},
SpanBytes {
span_id: 2,
parent_id: 1,
..Default::default()
},
],
);
let chunk_with_negative_priority = chunk_with_spans(
Some(-1),
vec![
SpanBytes {
span_id: 1,
..Default::default()
},
SpanBytes {
span_id: 2,
parent_id: 1,
..Default::default()
},
],
);
let chunk_with_error = chunk_with_spans(
Some(0),
vec![
SpanBytes {
span_id: 1,
error: true,
..Default::default()
},
SpanBytes {
span_id: 2,
parent_id: 1,
..Default::default()
},
],
);
let chunk_with_a_single_span = chunk_with_spans(
Some(0),
vec![
SpanBytes {
span_id: 1,
..Default::default()
},
SpanBytes {
span_id: 2,
parent_id: 1,
attributes: vec![(
SAMPLING_SINGLE_SPAN_MECHANISM.into(),
AttributeValue::Float(8.0),
)]
.into(),
..Default::default()
},
],
);
let chunk_with_analyzed_span = chunk_with_spans(
Some(0),
vec![
SpanBytes {
span_id: 1,
..Default::default()
},
SpanBytes {
span_id: 2,
parent_id: 1,
attributes: vec![(
SAMPLING_ANALYTICS_RATE_KEY.into(),
AttributeValue::Float(1.0),
)]
.into(),
..Default::default()
},
],
);
let chunks_and_expected_sampled_spans = vec![
(chunk_with_priority, 2),
(chunk_with_null_priority, 0),
(chunk_without_priority, 2),
(chunk_with_negative_priority, 0),
(chunk_with_error, 2),
(chunk_with_a_single_span, 1),
(chunk_with_analyzed_span, 1),
];
for (chunk, expected_count) in chunks_and_expected_sampled_spans.into_iter() {
let mut traces = vec![chunk];
drop_chunks(&mut traces);
if expected_count == 0 {
assert!(traces.is_empty());
} else {
assert_eq!(traces[0].spans.len(), expected_count);
}
}
}
#[test]
fn test_drop_chunks_dropped_trace_overrides_missing_or_positive_priority() {
let mut dropped_without_priority = chunk_with_spans(
None,
vec![SpanBytes {
span_id: 1,
..Default::default()
}],
);
dropped_without_priority.dropped_trace = true;
let mut dropped_with_positive_priority = chunk_with_spans(
Some(1),
vec![SpanBytes {
span_id: 1,
..Default::default()
}],
);
dropped_with_positive_priority.dropped_trace = true;
for chunk in [dropped_without_priority, dropped_with_positive_priority] {
let mut traces = vec![chunk];
drop_chunks(&mut traces);
assert!(
traces.is_empty(),
"dropped_trace should reject the chunk regardless of priority"
);
}
}
#[test]
fn test_drop_chunks_dropped_trace_keeps_existing_negative_priority() {
let mut chunk = chunk_with_spans(
Some(-5),
vec![SpanBytes {
span_id: 1,
..Default::default()
}],
);
chunk.dropped_trace = true;
let mut traces = vec![chunk];
drop_chunks(&mut traces);
assert!(traces.is_empty());
}
}