Skip to main content

libdd_trace_utils/span/
trace_utils_v1.rs

1// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4//! Trace-utils functionalities implementation for V1 spans.
5
6use crate::span::trace_utils::DroppedP0Stats;
7use crate::span::v1::{AttributeValue, Span, TraceChunk};
8use crate::span::{SpanText, TraceData};
9use std::collections::{HashMap, HashSet};
10use tracing::debug;
11
12/// Span attribute the mini agent must set for the backend to recognize top level span
13const TOP_LEVEL_KEY: &str = "_top_level";
14/// Span attribute the tracer sets to denote a top level span
15const TRACER_TOP_LEVEL_KEY: &str = "_dd.top_level";
16const MEASURED_KEY: &str = "_dd.measured";
17const PARTIAL_VERSION_KEY: &str = "_dd.partial_version";
18const SAMPLING_SINGLE_SPAN_MECHANISM: &str = "_dd.span_sampling.mechanism";
19const SAMPLING_ANALYTICS_RATE_KEY: &str = "_dd1.sr.eausr";
20
21/// Reads a numeric attribute (`Float` or `Int`), mirroring how v0.4's `metrics` map is read.
22fn attribute_as_f64<T: TraceData>(value: &AttributeValue<T>) -> Option<f64> {
23    match value {
24        AttributeValue::Float(v) => Some(*v),
25        AttributeValue::Int(v) => Some(*v as f64),
26        _ => None,
27    }
28}
29
30fn set_top_level_span<T: TraceData>(span: &mut Span<T>) {
31    span.attributes.insert(
32        T::Text::from_static_str(TOP_LEVEL_KEY),
33        AttributeValue::Float(1.0),
34    );
35}
36
37/// Updates all the spans top-level attribute.
38/// A span is considered top-level if:
39///   - it's a root span
40///   - OR its parent is unknown (other part of the code, distributed trace)
41///   - OR its parent belongs to another service (in that case it's a "local root" being the highest
42///     ancestor of other spans belonging to this service and attached to it).
43pub fn compute_top_level_span<T: TraceData>(trace: &mut [Span<T>]) {
44    let span_id_idx: HashMap<u64, usize> = trace
45        .iter()
46        .enumerate()
47        .map(|(i, span)| (span.span_id, i))
48        .collect();
49    let top_level: Vec<usize> = trace
50        .iter()
51        .enumerate()
52        .filter(|(_, span)| {
53            span.parent_id == 0
54                || span_id_idx
55                    .get(&span.parent_id)
56                    .is_none_or(|&parent_idx| trace[parent_idx].service != span.service)
57        })
58        .map(|(i, _)| i)
59        .collect();
60    for i in top_level {
61        set_top_level_span(&mut trace[i]);
62    }
63}
64
65/// Returns the index of the root span in `trace`.
66pub fn get_root_span_index<T: TraceData>(trace: &[Span<T>]) -> anyhow::Result<usize> {
67    if trace.is_empty() {
68        anyhow::bail!("Cannot find root span index in an empty trace.");
69    }
70
71    // Do a first pass to find if we have an obvious root span (starting from the end) since some
72    // clients put the root span last.
73    for (i, span) in trace.iter().enumerate().rev() {
74        if span.parent_id == 0 {
75            return Ok(i);
76        }
77    }
78
79    let span_ids: HashSet<_> = trace.iter().map(|span| span.span_id).collect();
80
81    let mut root_span_id = None;
82    for (i, span) in trace.iter().enumerate() {
83        // If a span's parent is not in the trace, it is a root
84        if !span_ids.contains(&span.parent_id) {
85            if root_span_id.is_some() {
86                debug!("trace has multiple root spans");
87            }
88            root_span_id = Some(i);
89        }
90    }
91    Ok(match root_span_id {
92        Some(i) => i,
93        None => {
94            debug!("Could not find the root span for trace");
95            trace.len() - 1
96        }
97    })
98}
99
100/// Returns true if the span has a top level key set
101pub fn has_top_level<T: TraceData>(span: &Span<T>) -> bool {
102    span.attributes
103        .get(TRACER_TOP_LEVEL_KEY)
104        .and_then(attribute_as_f64)
105        .is_some_and(|v| v == 1.0)
106        || span
107            .attributes
108            .get(TOP_LEVEL_KEY)
109            .and_then(attribute_as_f64)
110            .is_some_and(|v| v == 1.0)
111}
112
113/// Returns true if a span should be measured (i.e., it should get trace metrics calculated).
114pub fn is_measured<T: TraceData>(span: &Span<T>) -> bool {
115    span.attributes
116        .get(MEASURED_KEY)
117        .and_then(attribute_as_f64)
118        .is_some_and(|v| v == 1.0)
119}
120
121/// Returns true if the span is a partial snapshot.
122/// This kind of spans are partial images of long-running spans.
123/// When incomplete, a partial snapshot has an attribute _dd.partial_version which is a positive
124/// integer. This value usually increases each time a new version of the same span is sent by
125/// the tracer
126pub fn is_partial_snapshot<T: TraceData>(span: &Span<T>) -> bool {
127    span.attributes
128        .get(PARTIAL_VERSION_KEY)
129        .and_then(attribute_as_f64)
130        .is_some_and(|v| v >= 0.0)
131}
132
133/// Remove spans and chunks, only keeping the ones that may be sampled by the agent.
134///
135/// Unlike v0.4 (where sampling priority is a per-span metric), v1's `priority` is already a
136/// direct field on [`TraceChunk`], so it is read once per chunk instead of being searched for
137/// across the chunk's spans.
138///
139/// # Returns
140///
141/// The number of P0 traces and spans that were dropped.
142pub fn drop_chunks<T: TraceData>(traces: &mut Vec<TraceChunk<T>>) -> DroppedP0Stats {
143    let mut dropped_p0_traces = 0;
144    let mut dropped_p0_spans = 0;
145
146    traces.retain_mut(|chunk| {
147        // ErrorSampler
148        if chunk.spans.iter().any(|s| s.error) {
149            // We send chunks containing an error
150            return true;
151        }
152
153        // PrioritySampler and NoPrioritySampler
154        //
155        // `dropped_trace` signals the trace was rejected by samplers even when `priority` is
156        // absent or positive; treat it as an implicit negative priority (mirrors the
157        // v1-to-v0.4 encoder's handling of this flag), unless the chunk already carries a
158        // negative priority of its own.
159        let effective_priority = if chunk.dropped_trace {
160            chunk.priority.filter(|&p| p < 0).or(Some(-1))
161        } else {
162            chunk.priority
163        };
164        if effective_priority.is_none_or(|p| p > 0) {
165            // We send chunks with positive priority or no priority
166            return true;
167        }
168
169        // SingleSpanSampler and AnalyzedSpansSampler
170        // Keep only the spans sampled by single-span sampling or analyzed spans, even if the
171        // chunk is dropped.
172        let spans_before = chunk.spans.len();
173        chunk.spans.retain(|span| {
174            span.attributes
175                .get(SAMPLING_SINGLE_SPAN_MECHANISM)
176                .and_then(attribute_as_f64)
177                .is_some_and(|m| m == 8.0)
178                || span.attributes.contains_key(SAMPLING_ANALYTICS_RATE_KEY)
179        });
180        dropped_p0_spans += spans_before - chunk.spans.len();
181        if chunk.spans.is_empty() {
182            // If no spans were sampled we can drop the whole chunk
183            dropped_p0_traces += 1;
184            return false;
185        }
186        true
187    });
188
189    DroppedP0Stats {
190        dropped_p0_traces,
191        dropped_p0_spans,
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::span::v1::{SpanBytes, TraceChunkBytes};
199
200    fn create_test_span(is_top_level: bool) -> SpanBytes {
201        let mut span = SpanBytes {
202            service: "test-service".into(),
203            name: "test_name".into(),
204            resource: "test-resource".into(),
205            ..Default::default()
206        };
207        if is_top_level {
208            span.attributes
209                .insert("_top_level".into(), AttributeValue::Float(1.0));
210        }
211        span
212    }
213
214    fn create_test_span_with_ids(span_id: u64, parent_id: u64) -> SpanBytes {
215        SpanBytes {
216            service: "test-service".into(),
217            name: "test_name".into(),
218            resource: "test-resource".into(),
219            span_id,
220            parent_id,
221            ..Default::default()
222        }
223    }
224
225    #[test]
226    fn test_has_top_level() {
227        let top_level_span = create_test_span(true);
228        let not_top_level_span = create_test_span(false);
229        assert!(has_top_level(&top_level_span));
230        assert!(!has_top_level(&not_top_level_span));
231    }
232
233    #[test]
234    fn test_is_measured() {
235        let mut measured_span = create_test_span(true);
236        measured_span
237            .attributes
238            .insert(MEASURED_KEY.into(), AttributeValue::Float(1.0));
239        let not_measured_span = create_test_span(true);
240        assert!(is_measured(&measured_span));
241        assert!(!is_measured(&not_measured_span));
242    }
243
244    #[test]
245    fn test_is_partial_snapshot() {
246        let mut partial_span = create_test_span(false);
247        partial_span
248            .attributes
249            .insert(PARTIAL_VERSION_KEY.into(), AttributeValue::Int(2));
250        let not_partial_span = create_test_span(false);
251        assert!(is_partial_snapshot(&partial_span));
252        assert!(!is_partial_snapshot(&not_partial_span));
253    }
254
255    #[test]
256    fn test_compute_top_level() {
257        let mut span_with_different_service = create_test_span_with_ids(5, 2);
258        span_with_different_service.service = "another_service".into();
259        let mut trace = vec![
260            // Root span, should be marked as top-level
261            create_test_span_with_ids(1, 0),
262            // Should not be marked as top-level
263            create_test_span_with_ids(2, 1),
264            // No parent in local trace, should be marked as top-level
265            create_test_span_with_ids(4, 3),
266            // Parent belongs to another service, should be marked as top-level
267            span_with_different_service,
268        ];
269
270        compute_top_level_span(trace.as_mut_slice());
271
272        let spans_marked_as_top_level: Vec<u64> = trace
273            .iter()
274            .filter_map(|span| has_top_level(span).then_some(span.span_id))
275            .collect();
276        assert_eq!(spans_marked_as_top_level, [1, 4, 5]);
277    }
278
279    #[test]
280    fn test_get_root_span_index_from_complete_trace() {
281        let trace = vec![
282            create_test_span_with_ids(1, 0),
283            create_test_span_with_ids(2, 1),
284            create_test_span_with_ids(3, 1),
285        ];
286        assert_eq!(get_root_span_index(&trace).unwrap(), 0);
287    }
288
289    #[test]
290    fn test_get_root_span_index_root_last() {
291        let trace = vec![
292            create_test_span_with_ids(2, 1),
293            create_test_span_with_ids(3, 1),
294            create_test_span_with_ids(1, 0),
295        ];
296        assert_eq!(get_root_span_index(&trace).unwrap(), 2);
297    }
298
299    #[test]
300    fn test_get_root_span_index_from_partial_trace() {
301        // No span has parent_id == 0, but span 1's parent (99) isn't in the trace.
302        let trace = vec![
303            create_test_span_with_ids(1, 99),
304            create_test_span_with_ids(2, 1),
305        ];
306        assert_eq!(get_root_span_index(&trace).unwrap(), 0);
307    }
308
309    #[test]
310    fn test_get_root_span_index_empty_trace_errors() {
311        let trace: Vec<SpanBytes> = vec![];
312        assert!(get_root_span_index(&trace).is_err());
313    }
314
315    fn chunk_with_spans(priority: Option<i32>, spans: Vec<SpanBytes>) -> TraceChunkBytes {
316        TraceChunkBytes {
317            priority,
318            spans,
319            ..Default::default()
320        }
321    }
322
323    #[test]
324    fn test_drop_chunks() {
325        let chunk_with_priority = chunk_with_spans(
326            Some(1),
327            vec![
328                SpanBytes {
329                    span_id: 1,
330                    ..Default::default()
331                },
332                SpanBytes {
333                    span_id: 2,
334                    parent_id: 1,
335                    ..Default::default()
336                },
337            ],
338        );
339        let chunk_with_null_priority = chunk_with_spans(
340            Some(0),
341            vec![
342                SpanBytes {
343                    span_id: 1,
344                    ..Default::default()
345                },
346                SpanBytes {
347                    span_id: 2,
348                    parent_id: 1,
349                    ..Default::default()
350                },
351            ],
352        );
353        let chunk_without_priority = chunk_with_spans(
354            None,
355            vec![
356                SpanBytes {
357                    span_id: 1,
358                    ..Default::default()
359                },
360                SpanBytes {
361                    span_id: 2,
362                    parent_id: 1,
363                    ..Default::default()
364                },
365            ],
366        );
367        let chunk_with_negative_priority = chunk_with_spans(
368            Some(-1),
369            vec![
370                SpanBytes {
371                    span_id: 1,
372                    ..Default::default()
373                },
374                SpanBytes {
375                    span_id: 2,
376                    parent_id: 1,
377                    ..Default::default()
378                },
379            ],
380        );
381        let chunk_with_error = chunk_with_spans(
382            Some(0),
383            vec![
384                SpanBytes {
385                    span_id: 1,
386                    error: true,
387                    ..Default::default()
388                },
389                SpanBytes {
390                    span_id: 2,
391                    parent_id: 1,
392                    ..Default::default()
393                },
394            ],
395        );
396        let chunk_with_a_single_span = chunk_with_spans(
397            Some(0),
398            vec![
399                SpanBytes {
400                    span_id: 1,
401                    ..Default::default()
402                },
403                SpanBytes {
404                    span_id: 2,
405                    parent_id: 1,
406                    attributes: vec![(
407                        SAMPLING_SINGLE_SPAN_MECHANISM.into(),
408                        AttributeValue::Float(8.0),
409                    )]
410                    .into(),
411                    ..Default::default()
412                },
413            ],
414        );
415        let chunk_with_analyzed_span = chunk_with_spans(
416            Some(0),
417            vec![
418                SpanBytes {
419                    span_id: 1,
420                    ..Default::default()
421                },
422                SpanBytes {
423                    span_id: 2,
424                    parent_id: 1,
425                    attributes: vec![(
426                        SAMPLING_ANALYTICS_RATE_KEY.into(),
427                        AttributeValue::Float(1.0),
428                    )]
429                    .into(),
430                    ..Default::default()
431                },
432            ],
433        );
434
435        let chunks_and_expected_sampled_spans = vec![
436            (chunk_with_priority, 2),
437            (chunk_with_null_priority, 0),
438            (chunk_without_priority, 2),
439            (chunk_with_negative_priority, 0),
440            (chunk_with_error, 2),
441            (chunk_with_a_single_span, 1),
442            (chunk_with_analyzed_span, 1),
443        ];
444
445        for (chunk, expected_count) in chunks_and_expected_sampled_spans.into_iter() {
446            let mut traces = vec![chunk];
447            drop_chunks(&mut traces);
448
449            if expected_count == 0 {
450                assert!(traces.is_empty());
451            } else {
452                assert_eq!(traces[0].spans.len(), expected_count);
453            }
454        }
455    }
456
457    #[test]
458    fn test_drop_chunks_dropped_trace_overrides_missing_or_positive_priority() {
459        // `dropped_trace: true` should reject the chunk even though `priority` alone (absent or
460        // positive) would normally keep it.
461        let mut dropped_without_priority = chunk_with_spans(
462            None,
463            vec![SpanBytes {
464                span_id: 1,
465                ..Default::default()
466            }],
467        );
468        dropped_without_priority.dropped_trace = true;
469
470        let mut dropped_with_positive_priority = chunk_with_spans(
471            Some(1),
472            vec![SpanBytes {
473                span_id: 1,
474                ..Default::default()
475            }],
476        );
477        dropped_with_positive_priority.dropped_trace = true;
478
479        for chunk in [dropped_without_priority, dropped_with_positive_priority] {
480            let mut traces = vec![chunk];
481            drop_chunks(&mut traces);
482            assert!(
483                traces.is_empty(),
484                "dropped_trace should reject the chunk regardless of priority"
485            );
486        }
487    }
488
489    #[test]
490    fn test_drop_chunks_dropped_trace_keeps_existing_negative_priority() {
491        // dropped_trace shouldn't change behavior when the chunk already has a negative
492        // priority: the chunk is still rejected, same as without dropped_trace.
493        let mut chunk = chunk_with_spans(
494            Some(-5),
495            vec![SpanBytes {
496                span_id: 1,
497                ..Default::default()
498            }],
499        );
500        chunk.dropped_trace = true;
501
502        let mut traces = vec![chunk];
503        drop_chunks(&mut traces);
504        assert!(traces.is_empty());
505    }
506}