Skip to main content

libdd_trace_utils/span/
trace_utils.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4//! Trace-utils functionalities implementation for tinybytes based spans
5
6use tracing::debug;
7
8use super::{v04::Span, SpanText, TraceData};
9use std::collections::{HashMap, HashSet};
10
11/// Span metric the mini agent must set for the backend to recognize top level span
12const TOP_LEVEL_KEY: &str = "_top_level";
13/// Span metric the tracer sets to denote a top level span
14const TRACER_TOP_LEVEL_KEY: &str = "_dd.top_level";
15const MEASURED_KEY: &str = "_dd.measured";
16const PARTIAL_VERSION_KEY: &str = "_dd.partial_version";
17
18fn set_top_level_span<T>(span: &mut Span<T>)
19where
20    T: TraceData,
21{
22    span.metrics
23        .insert(T::Text::from_static_str(TOP_LEVEL_KEY), 1.0);
24}
25
26/// Updates all the spans top-level attribute.
27/// A span is considered top-level if:
28///   - it's a root span
29///   - OR its parent is unknown (other part of the code, distributed trace)
30///   - OR its parent belongs to another service (in that case it's a "local root" being the highest
31///     ancestor of other spans belonging to this service and attached to it).
32pub fn compute_top_level_span<T>(trace: &mut [Span<T>])
33where
34    T: TraceData,
35{
36    let mut span_id_idx: HashMap<u64, usize> = HashMap::new();
37    for (i, span) in trace.iter().enumerate() {
38        span_id_idx.insert(span.span_id, i);
39    }
40    for span_idx in 0..trace.len() {
41        let parent_id = trace[span_idx].parent_id;
42        if parent_id == 0 {
43            set_top_level_span(&mut trace[span_idx]);
44            continue;
45        }
46        match span_id_idx.get(&parent_id).map(|i| &trace[*i].service) {
47            Some(parent_span_service) => {
48                if !(parent_span_service == &trace[span_idx].service) {
49                    // parent is not in the same service
50                    set_top_level_span(&mut trace[span_idx])
51                }
52            }
53            None => {
54                // span has no parent in chunk
55                set_top_level_span(&mut trace[span_idx])
56            }
57        }
58    }
59}
60
61pub fn get_root_span_index<T>(trace: &[Span<T>]) -> anyhow::Result<usize>
62where
63    T: TraceData,
64{
65    if trace.is_empty() {
66        anyhow::bail!("Cannot find root span index in an empty trace.");
67    }
68
69    // Do a first pass to find if we have an obvious root span (starting from the end) since some
70    // clients put the root span last.
71    for (i, span) in trace.iter().enumerate().rev() {
72        if span.parent_id == 0 {
73            return Ok(i);
74        }
75    }
76
77    let span_ids: HashSet<_> = trace.iter().map(|span| span.span_id).collect();
78
79    let mut root_span_id = None;
80    for (i, span) in trace.iter().enumerate() {
81        // If a span's parent is not in the trace, it is a root
82        if !span_ids.contains(&span.parent_id) {
83            if root_span_id.is_some() {
84                debug!(
85                    trace_id = &trace[0].trace_id,
86                    "trace has multiple root spans"
87                );
88            }
89            root_span_id = Some(i);
90        }
91    }
92    Ok(match root_span_id {
93        Some(i) => i,
94        None => {
95            debug!(
96                trace_id = &trace[0].trace_id,
97                "Could not find the root span for trace"
98            );
99            trace.len() - 1
100        }
101    })
102}
103
104/// Return true if the span has a top level key set
105pub fn has_top_level<T: TraceData>(span: &Span<T>) -> bool {
106    span.metrics
107        .get(TRACER_TOP_LEVEL_KEY)
108        .is_some_and(|v| *v == 1.0)
109        || span.metrics.get(TOP_LEVEL_KEY).is_some_and(|v| *v == 1.0)
110}
111
112/// Returns true if a span should be measured (i.e., it should get trace metrics calculated).
113pub fn is_measured<T: TraceData>(span: &Span<T>) -> bool {
114    span.metrics.get(MEASURED_KEY).is_some_and(|v| *v == 1.0)
115}
116
117/// Returns true if the span is a partial snapshot.
118/// This kind of spans are partial images of long-running spans.
119/// When incomplete, a partial snapshot has a metric _dd.partial_version which is a positive
120/// integer. The metric usually increases each time a new version of the same span is sent by
121/// the tracer
122pub fn is_partial_snapshot<T: TraceData>(span: &Span<T>) -> bool {
123    span.metrics
124        .get(PARTIAL_VERSION_KEY)
125        .is_some_and(|v| *v >= 0.0)
126}
127
128pub struct DroppedP0Stats {
129    pub dropped_p0_traces: usize,
130    pub dropped_p0_spans: usize,
131}
132
133// Keys used for sampling
134const SAMPLING_PRIORITY_KEY: &str = "_sampling_priority_v1";
135const SAMPLING_SINGLE_SPAN_MECHANISM: &str = "_dd.span_sampling.mechanism";
136const SAMPLING_ANALYTICS_RATE_KEY: &str = "_dd1.sr.eausr";
137
138/// Remove spans and chunks from a TraceCollection only keeping the ones that may be sampled by
139/// the agent.
140///
141/// # Returns
142///
143/// A tuple containing the dropped p0 stats, the first value correspond the amount of traces
144/// dropped and the latter to the spans dropped.
145///
146/// # Trace-level attributes
147/// Some attributes related to the whole trace are stored in the root span of the chunk.
148pub fn drop_chunks<T>(traces: &mut Vec<Vec<Span<T>>>) -> DroppedP0Stats
149where
150    T: TraceData,
151{
152    let mut dropped_p0_traces = 0;
153    let mut dropped_p0_spans = 0;
154
155    traces.retain_mut(|chunk| {
156        // ErrorSampler
157        if chunk.iter().any(|s| s.error == 1) {
158            // We send chunks containing an error
159            return true;
160        }
161
162        // PrioritySampler and NoPrioritySampler
163        let chunk_priority = chunk
164            .iter()
165            .find_map(|s| s.metrics.get(SAMPLING_PRIORITY_KEY));
166        if chunk_priority.is_none_or(|p| *p > 0.0) {
167            // We send chunks with positive priority or no priority
168            return true;
169        }
170
171        // SingleSpanSampler and AnalyzedSpansSampler
172        // List of spans to keep even if the chunk is dropped
173        let mut sampled_indexes = Vec::new();
174        for (index, span) in chunk.iter().enumerate() {
175            if span
176                .metrics
177                .get(SAMPLING_SINGLE_SPAN_MECHANISM)
178                .is_some_and(|m| *m == 8.0)
179                || span.metrics.contains_key(SAMPLING_ANALYTICS_RATE_KEY)
180            {
181                // We send spans sampled by single-span sampling or analyzed spans
182                sampled_indexes.push(index);
183            }
184        }
185        dropped_p0_spans += chunk.len() - sampled_indexes.len();
186        if sampled_indexes.is_empty() {
187            // If no spans were sampled we can drop the whole chunk
188            dropped_p0_traces += 1;
189            return false;
190        }
191        let sampled_spans = sampled_indexes
192            .iter()
193            .map(|i| std::mem::take(&mut chunk[*i]))
194            .collect();
195        *chunk = sampled_spans;
196        true
197    });
198
199    DroppedP0Stats {
200        dropped_p0_traces,
201        dropped_p0_spans,
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::span::v04::{SpanBytes, VecMap};
209
210    fn create_test_span(
211        trace_id: u64,
212        span_id: u64,
213        parent_id: u64,
214        start: i64,
215        is_top_level: bool,
216    ) -> SpanBytes {
217        let mut span = SpanBytes {
218            trace_id: trace_id as u128,
219            span_id,
220            service: "test-service".into(),
221            name: "test_name".into(),
222            resource: "test-resource".into(),
223            parent_id,
224            start,
225            duration: 5,
226            error: 0,
227            meta: vec![
228                ("service".into(), "test-service".into()),
229                ("env".into(), "test-env".into()),
230                ("runtime-id".into(), "test-runtime-id-value".into()),
231            ]
232            .into(),
233            metrics: VecMap::new(),
234            r#type: "".into(),
235            meta_struct: VecMap::new(),
236            span_links: vec![],
237            span_events: vec![],
238        };
239        if is_top_level {
240            span.metrics.insert("_top_level".into(), 1.0);
241            span.meta
242                .insert("_dd.origin".into(), "cloudfunction".into());
243            span.meta.insert("origin".into(), "cloudfunction".into());
244            span.meta
245                .insert("functionname".into(), "dummy_function_name".into());
246        }
247        span
248    }
249
250    #[test]
251    fn test_has_top_level() {
252        let top_level_span = create_test_span(123, 1234, 12, 1, true);
253        let not_top_level_span = create_test_span(123, 1234, 12, 1, false);
254        assert!(has_top_level(&top_level_span));
255        assert!(!has_top_level(&not_top_level_span));
256    }
257
258    #[test]
259    fn test_is_measured() {
260        let mut measured_span = create_test_span(123, 1234, 12, 1, true);
261        measured_span.metrics.insert(MEASURED_KEY.into(), 1.0);
262        let not_measured_span = create_test_span(123, 1234, 12, 1, true);
263        assert!(is_measured(&measured_span));
264        assert!(!is_measured(&not_measured_span));
265    }
266
267    #[test]
268    fn test_compute_top_level() {
269        let mut span_with_different_service = create_test_span(123, 5, 2, 1, false);
270        span_with_different_service.service = "another_service".into();
271        let mut trace = vec![
272            // Root span, should be marked as top-level
273            create_test_span(123, 1, 0, 1, false),
274            // Should not be marked as top-level
275            create_test_span(123, 2, 1, 1, false),
276            // No parent in local trace, should be marked as
277            // top-level
278            create_test_span(123, 4, 3, 1, false),
279            // Parent belongs to another service, should be marked
280            // as top-level
281            span_with_different_service,
282        ];
283
284        compute_top_level_span(trace.as_mut_slice());
285
286        let spans_marked_as_top_level: Vec<u64> = trace
287            .iter()
288            .filter_map(|span| {
289                if has_top_level(span) {
290                    Some(span.span_id)
291                } else {
292                    None
293                }
294            })
295            .collect();
296        assert_eq!(spans_marked_as_top_level, [1, 4, 5])
297    }
298
299    #[test]
300    fn test_drop_chunks() {
301        let chunk_with_priority = vec![
302            SpanBytes {
303                span_id: 1,
304                metrics: vec![
305                    (SAMPLING_PRIORITY_KEY.into(), 1.0),
306                    (TRACER_TOP_LEVEL_KEY.into(), 1.0),
307                ]
308                .into(),
309                ..Default::default()
310            },
311            SpanBytes {
312                span_id: 2,
313                parent_id: 1,
314                ..Default::default()
315            },
316        ];
317        let chunk_with_null_priority = vec![
318            SpanBytes {
319                span_id: 1,
320                metrics: vec![
321                    (SAMPLING_PRIORITY_KEY.into(), 0.0),
322                    (TRACER_TOP_LEVEL_KEY.into(), 1.0),
323                ]
324                .into(),
325                ..Default::default()
326            },
327            SpanBytes {
328                span_id: 2,
329                parent_id: 1,
330                ..Default::default()
331            },
332        ];
333        let chunk_without_priority = vec![
334            SpanBytes {
335                span_id: 1,
336                metrics: vec![(TRACER_TOP_LEVEL_KEY.into(), 1.0)].into(),
337                ..Default::default()
338            },
339            SpanBytes {
340                span_id: 2,
341                parent_id: 1,
342                ..Default::default()
343            },
344        ];
345        let chunk_with_multiple_top_level = vec![
346            SpanBytes {
347                span_id: 1,
348                metrics: vec![
349                    (SAMPLING_PRIORITY_KEY.into(), -1.0),
350                    (TRACER_TOP_LEVEL_KEY.into(), 1.0),
351                ]
352                .into(),
353                ..Default::default()
354            },
355            SpanBytes {
356                span_id: 2,
357                parent_id: 1,
358                ..Default::default()
359            },
360            SpanBytes {
361                span_id: 4,
362                parent_id: 3,
363                metrics: vec![(TRACER_TOP_LEVEL_KEY.into(), 1.0)].into(),
364                ..Default::default()
365            },
366        ];
367        let chunk_with_error = vec![
368            SpanBytes {
369                span_id: 1,
370                error: 1,
371                metrics: vec![
372                    (SAMPLING_PRIORITY_KEY.into(), 0.0),
373                    (TRACER_TOP_LEVEL_KEY.into(), 1.0),
374                ]
375                .into(),
376                ..Default::default()
377            },
378            SpanBytes {
379                span_id: 2,
380                parent_id: 1,
381                ..Default::default()
382            },
383        ];
384        let chunk_with_a_single_span = vec![
385            SpanBytes {
386                span_id: 1,
387                metrics: vec![
388                    (SAMPLING_PRIORITY_KEY.into(), 0.0),
389                    (TRACER_TOP_LEVEL_KEY.into(), 1.0),
390                ]
391                .into(),
392                ..Default::default()
393            },
394            SpanBytes {
395                span_id: 2,
396                parent_id: 1,
397                metrics: vec![(SAMPLING_SINGLE_SPAN_MECHANISM.into(), 8.0)].into(),
398                ..Default::default()
399            },
400        ];
401        let chunk_with_analyzed_span = vec![
402            SpanBytes {
403                span_id: 1,
404                metrics: vec![
405                    (SAMPLING_PRIORITY_KEY.into(), 0.0),
406                    (TRACER_TOP_LEVEL_KEY.into(), 1.0),
407                ]
408                .into(),
409                ..Default::default()
410            },
411            SpanBytes {
412                span_id: 2,
413                parent_id: 1,
414                metrics: vec![(SAMPLING_ANALYTICS_RATE_KEY.into(), 1.0)].into(),
415                ..Default::default()
416            },
417        ];
418
419        let chunks_and_expected_sampled_spans = vec![
420            (chunk_with_priority, 2),
421            (chunk_with_null_priority, 0),
422            (chunk_without_priority, 2),
423            (chunk_with_multiple_top_level, 0),
424            (chunk_with_error, 2),
425            (chunk_with_a_single_span, 1),
426            (chunk_with_analyzed_span, 1),
427        ];
428
429        for (chunk, expected_count) in chunks_and_expected_sampled_spans.into_iter() {
430            let mut traces = vec![chunk];
431            drop_chunks(&mut traces);
432
433            if expected_count == 0 {
434                assert!(traces.is_empty());
435            } else {
436                assert_eq!(traces[0].len(), expected_count);
437            }
438        }
439    }
440}