servers/http/jaeger/v3/
summary.rs1use std::collections::{BTreeMap, HashSet};
16
17use opentelemetry_proto::tonic::common::v1::any_value;
18use opentelemetry_proto::tonic::trace::v1::status::StatusCode;
19use opentelemetry_proto::tonic::trace::v1::{Span, TracesData};
20use serde_json::{Value, json};
21
22use crate::otlp::trace::KEY_SERVICE_NAME;
23
24pub fn summarize(traces: TracesData) -> Vec<Value> {
25 let mut by_trace: BTreeMap<&[u8], Vec<(&str, &Span)>> = BTreeMap::new();
26 for resource in &traces.resource_spans {
27 let service = resource
28 .resource
29 .as_ref()
30 .and_then(|resource| {
31 resource
32 .attributes
33 .iter()
34 .find(|attr| attr.key == KEY_SERVICE_NAME)
35 .and_then(|attr| attr.value.as_ref())
36 .and_then(|value| match &value.value {
37 Some(any_value::Value::StringValue(name)) => Some(name.as_str()),
38 _ => None,
39 })
40 })
41 .unwrap_or_default();
42 for scope in &resource.scope_spans {
43 for span in &scope.spans {
44 by_trace
45 .entry(&span.trace_id)
46 .or_default()
47 .push((service, span));
48 }
49 }
50 }
51 by_trace
52 .into_iter()
53 .map(|(trace_id, spans)| {
54 let ids: HashSet<_> = spans
55 .iter()
56 .map(|(_, span)| span.span_id.as_slice())
57 .collect();
58 let mut services: BTreeMap<&str, (usize, usize)> = BTreeMap::new();
59 let mut min_start = u64::MAX;
60 let mut max_end = 0;
61 let mut errors = 0;
62 let mut orphans = 0;
63 let mut root: Option<(&str, &Span)> = None;
64 for &(service, span) in &spans {
65 let stats = services.entry(service).or_default();
66 stats.0 += 1;
67 if span
68 .status
69 .as_ref()
70 .is_some_and(|status| status.code == StatusCode::Error as i32)
71 {
72 stats.1 += 1;
73 errors += 1;
74 }
75 min_start = min_start.min(span.start_time_unix_nano);
76 max_end = max_end.max(span.end_time_unix_nano);
77 if span.parent_span_id.iter().all(|byte| *byte == 0) {
78 if root.is_none_or(|(_, root)| {
79 span.start_time_unix_nano < root.start_time_unix_nano
80 }) {
81 root = Some((service, span));
82 }
83 } else if !ids.contains(span.parent_span_id.as_slice()) {
84 orphans += 1;
85 }
86 }
87 let (root_service, root_operation) =
88 root.map_or(("", ""), |(service, span)| (service, span.name.as_str()));
89 let services: Vec<_> = services
90 .into_iter()
91 .map(|(name, (spans, errors))| {
92 json!({
93 "name": name, "spanCount": spans, "errorSpanCount": errors,
94 })
95 })
96 .collect();
97 json!({
98 "traceId": hex::encode(trace_id),
99 "rootServiceName": root_service,
100 "rootOperationName": root_operation,
101 "minStartTimeUnixNano": min_start.to_string(),
102 "maxEndTimeUnixNano": max_end.to_string(),
103 "spanCount": spans.len(),
104 "errorSpanCount": errors,
105 "orphanSpanCount": orphans,
106 "services": services,
107 })
108 })
109 .collect()
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_summary_roots_and_orphans() {
118 let traces: TracesData = serde_json::from_value(json!({"resourceSpans":[
119 {"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"b"}}]},"scopeSpans":[{"spans":[
120 {"traceId":"00000000000000000000000000000001","spanId":"0000000000000001","name":"late-root","startTimeUnixNano":"20","endTimeUnixNano":"50"},
121 {"traceId":"00000000000000000000000000000001","spanId":"0000000000000002","parentSpanId":"0000000000000099","name":"orphan","startTimeUnixNano":"1","endTimeUnixNano":"60","status":{"code":2}},
122 {"traceId":"00000000000000000000000000000002","spanId":"0000000000000003","parentSpanId":"0000000000000001","startTimeUnixNano":"1","endTimeUnixNano":"2"}
123 ]}]},
124 {"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"a"}}]},"scopeSpans":[{"spans":[
125 {"traceId":"00000000000000000000000000000001","spanId":"0000000000000004","parentSpanId":"0000000000000000","name":"early-root","startTimeUnixNano":"10","endTimeUnixNano":"20"},
126 {"traceId":"00000000000000000000000000000001","spanId":"0000000000000005","parentSpanId":"0000000000000004","startTimeUnixNano":"11","endTimeUnixNano":"15","status":{"code":2}}
127 ]}]}
128 ]})).unwrap();
129 let summaries = summarize(traces);
130 assert_eq!(
131 summaries[0],
132 json!({
133 "traceId":"00000000000000000000000000000001", "rootServiceName":"a", "rootOperationName":"early-root",
134 "minStartTimeUnixNano":"1", "maxEndTimeUnixNano":"60", "spanCount":4, "errorSpanCount":2, "orphanSpanCount":1,
135 "services":[{"name":"a","spanCount":2,"errorSpanCount":1},{"name":"b","spanCount":2,"errorSpanCount":1}]
136 })
137 );
138 assert_eq!(summaries[1]["orphanSpanCount"], 1);
139 assert_eq!(summaries[1]["rootServiceName"], "");
140 assert_eq!(summaries[1]["rootOperationName"], "");
141 }
142}