1use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct Span {
18 pub trace_id: String,
20
21 pub span_id: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub parent_span_id: Option<String>,
27
28 pub name: String,
30
31 pub service: String,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub kind: Option<SpanKind>,
37
38 pub status: SpanStatus,
40
41 pub start_time: i64,
43
44 pub duration_us: i64,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub attributes: Option<BTreeMap<String, String>>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub events: Option<Vec<SpanEvent>>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub resource: Option<BTreeMap<String, String>>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub extensions: Option<BTreeMap<String, serde_json::Value>>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct SpanEvent {
70 pub name: String,
72
73 pub timestamp: i64,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub attributes: Option<BTreeMap<String, String>>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum SpanKind {
85 Server,
87 Client,
89 Producer,
91 Consumer,
93 Internal,
95}
96
97impl SpanKind {
98 pub fn parse(s: &str) -> Self {
106 match s.to_ascii_lowercase().as_str() {
107 "client" => Self::Client,
108 "server" => Self::Server,
109 "producer" => Self::Producer,
110 "consumer" => Self::Consumer,
111 _ => Self::Internal,
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum SpanStatus {
120 Ok,
122 Error,
124 Unset,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133pub struct TraceDetail {
134 pub trace_id: String,
136
137 pub span_count: usize,
139
140 pub service_count: usize,
142
143 pub duration_us: i64,
145
146 pub services: Vec<String>,
148
149 pub spans: Vec<Span>,
151}
152
153impl TraceDetail {
154 pub fn from_spans(trace_id: String, mut spans: Vec<Span>) -> Self {
162 spans.sort_by(|a, b| {
164 a.start_time
165 .cmp(&b.start_time)
166 .then(b.duration_us.cmp(&a.duration_us))
167 });
168
169 let span_count = spans.len();
170
171 let mut services: Vec<String> = spans.iter().map(|s| s.service.clone()).collect();
173 services.sort();
174 services.dedup();
175 let service_count = services.len();
176
177 let duration_us = if let Some(root) = spans.iter().find(|s| s.parent_span_id.is_none()) {
179 root.duration_us
180 } else if !spans.is_empty() {
181 let earliest_us = spans
183 .iter()
184 .map(|s| s.start_time * 1_000_000)
185 .min()
186 .unwrap_or(0);
187 let latest_end_us = spans
188 .iter()
189 .map(|s| s.start_time * 1_000_000 + s.duration_us)
190 .max()
191 .unwrap_or(0);
192 latest_end_us - earliest_us
193 } else {
194 0
195 };
196
197 Self {
198 trace_id,
199 span_count,
200 service_count,
201 duration_us,
202 services,
203 spans,
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_span_kind_serialization() {
214 assert_eq!(
215 serde_json::to_string(&SpanKind::Server).unwrap(),
216 r#""server""#
217 );
218 assert_eq!(
219 serde_json::to_string(&SpanKind::Client).unwrap(),
220 r#""client""#
221 );
222 assert_eq!(
223 serde_json::to_string(&SpanKind::Internal).unwrap(),
224 r#""internal""#
225 );
226 }
227
228 #[test]
229 fn test_span_status_serialization() {
230 assert_eq!(serde_json::to_string(&SpanStatus::Ok).unwrap(), r#""ok""#);
231 assert_eq!(
232 serde_json::to_string(&SpanStatus::Error).unwrap(),
233 r#""error""#
234 );
235 assert_eq!(
236 serde_json::to_string(&SpanStatus::Unset).unwrap(),
237 r#""unset""#
238 );
239 }
240
241 #[test]
242 fn test_trace_detail_from_spans() {
243 let spans = vec![
244 Span {
245 trace_id: "abc123".to_string(),
246 span_id: "span1".to_string(),
247 parent_span_id: None,
248 name: "GET /api".to_string(),
249 service: "gateway".to_string(),
250 kind: Some(SpanKind::Server),
251 status: SpanStatus::Ok,
252 start_time: 1000,
253 duration_us: 100_000, attributes: None,
255 events: None,
256 resource: None,
257 extensions: None,
258 },
259 Span {
260 trace_id: "abc123".to_string(),
261 span_id: "span2".to_string(),
262 parent_span_id: Some("span1".to_string()),
263 name: "SELECT".to_string(),
264 service: "db".to_string(),
265 kind: Some(SpanKind::Client),
266 status: SpanStatus::Ok,
267 start_time: 1000,
268 duration_us: 20_000, attributes: None,
270 events: None,
271 resource: None,
272 extensions: None,
273 },
274 ];
275
276 let detail = TraceDetail::from_spans("abc123".to_string(), spans);
277 assert_eq!(detail.span_count, 2);
278 assert_eq!(detail.service_count, 2);
279 assert_eq!(detail.duration_us, 100_000); assert_eq!(detail.services, vec!["db", "gateway"]);
281 }
282
283 #[test]
284 fn test_trace_detail_no_root_span() {
285 let spans = vec![
286 Span {
287 trace_id: "abc".to_string(),
288 span_id: "s1".to_string(),
289 parent_span_id: Some("missing".to_string()),
290 name: "op1".to_string(),
291 service: "svc".to_string(),
292 kind: None,
293 status: SpanStatus::Ok,
294 start_time: 1000,
295 duration_us: 50_000,
296 attributes: None,
297 events: None,
298 resource: None,
299 extensions: None,
300 },
301 Span {
302 trace_id: "abc".to_string(),
303 span_id: "s2".to_string(),
304 parent_span_id: Some("missing".to_string()),
305 name: "op2".to_string(),
306 service: "svc".to_string(),
307 kind: None,
308 status: SpanStatus::Ok,
309 start_time: 1000,
310 duration_us: 80_000,
311 attributes: None,
312 events: None,
313 resource: None,
314 extensions: None,
315 },
316 ];
317 let detail = TraceDetail::from_spans("abc".to_string(), spans);
318 assert_eq!(detail.duration_us, 80_000);
320 }
321
322 #[test]
323 fn test_trace_detail_empty_spans() {
324 let detail = TraceDetail::from_spans("empty".to_string(), vec![]);
325 assert_eq!(detail.span_count, 0);
326 assert_eq!(detail.duration_us, 0);
327 assert!(detail.services.is_empty());
328 }
329
330 #[test]
331 fn test_span_kind_parse_capitalized() {
332 assert_eq!(SpanKind::parse("Client"), SpanKind::Client);
333 assert_eq!(SpanKind::parse("Server"), SpanKind::Server);
334 assert_eq!(SpanKind::parse("Producer"), SpanKind::Producer);
335 assert_eq!(SpanKind::parse("Consumer"), SpanKind::Consumer);
336 }
337
338 #[test]
339 fn test_span_kind_parse_lowercase() {
340 assert_eq!(SpanKind::parse("client"), SpanKind::Client);
341 assert_eq!(SpanKind::parse("server"), SpanKind::Server);
342 assert_eq!(SpanKind::parse("producer"), SpanKind::Producer);
343 assert_eq!(SpanKind::parse("consumer"), SpanKind::Consumer);
344 assert_eq!(SpanKind::parse("internal"), SpanKind::Internal);
345 }
346
347 #[test]
348 fn test_span_kind_parse_unknown_defaults_to_internal() {
349 assert_eq!(SpanKind::parse("unknown"), SpanKind::Internal);
350 assert_eq!(SpanKind::parse(""), SpanKind::Internal);
351 }
352}