use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Span {
pub trace_id: String,
pub span_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_span_id: Option<String>,
pub name: String,
pub service: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<SpanKind>,
pub status: SpanStatus,
pub start_time: i64,
pub duration_us: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Vec<SpanEvent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpanEvent {
pub name: String,
pub timestamp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpanKind {
Server,
Client,
Producer,
Consumer,
Internal,
}
impl SpanKind {
pub fn parse(s: &str) -> Self {
match s.to_ascii_lowercase().as_str() {
"client" => Self::Client,
"server" => Self::Server,
"producer" => Self::Producer,
"consumer" => Self::Consumer,
_ => Self::Internal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpanStatus {
Ok,
Error,
Unset,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TraceDetail {
pub trace_id: String,
pub span_count: usize,
pub service_count: usize,
pub duration_us: i64,
pub services: Vec<String>,
pub spans: Vec<Span>,
}
impl TraceDetail {
pub fn from_spans(trace_id: String, mut spans: Vec<Span>) -> Self {
spans.sort_by(|a, b| {
a.start_time
.cmp(&b.start_time)
.then(b.duration_us.cmp(&a.duration_us))
});
let span_count = spans.len();
let mut services: Vec<String> = spans.iter().map(|s| s.service.clone()).collect();
services.sort();
services.dedup();
let service_count = services.len();
let duration_us = if let Some(root) = spans.iter().find(|s| s.parent_span_id.is_none()) {
root.duration_us
} else if !spans.is_empty() {
let earliest_us = spans
.iter()
.map(|s| s.start_time * 1_000_000)
.min()
.unwrap_or(0);
let latest_end_us = spans
.iter()
.map(|s| s.start_time * 1_000_000 + s.duration_us)
.max()
.unwrap_or(0);
latest_end_us - earliest_us
} else {
0
};
Self {
trace_id,
span_count,
service_count,
duration_us,
services,
spans,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_kind_serialization() {
assert_eq!(
serde_json::to_string(&SpanKind::Server).unwrap(),
r#""server""#
);
assert_eq!(
serde_json::to_string(&SpanKind::Client).unwrap(),
r#""client""#
);
assert_eq!(
serde_json::to_string(&SpanKind::Internal).unwrap(),
r#""internal""#
);
}
#[test]
fn test_span_status_serialization() {
assert_eq!(serde_json::to_string(&SpanStatus::Ok).unwrap(), r#""ok""#);
assert_eq!(
serde_json::to_string(&SpanStatus::Error).unwrap(),
r#""error""#
);
assert_eq!(
serde_json::to_string(&SpanStatus::Unset).unwrap(),
r#""unset""#
);
}
#[test]
fn test_trace_detail_from_spans() {
let spans = vec![
Span {
trace_id: "abc123".to_string(),
span_id: "span1".to_string(),
parent_span_id: None,
name: "GET /api".to_string(),
service: "gateway".to_string(),
kind: Some(SpanKind::Server),
status: SpanStatus::Ok,
start_time: 1000,
duration_us: 100_000, attributes: None,
events: None,
resource: None,
extensions: None,
},
Span {
trace_id: "abc123".to_string(),
span_id: "span2".to_string(),
parent_span_id: Some("span1".to_string()),
name: "SELECT".to_string(),
service: "db".to_string(),
kind: Some(SpanKind::Client),
status: SpanStatus::Ok,
start_time: 1000,
duration_us: 20_000, attributes: None,
events: None,
resource: None,
extensions: None,
},
];
let detail = TraceDetail::from_spans("abc123".to_string(), spans);
assert_eq!(detail.span_count, 2);
assert_eq!(detail.service_count, 2);
assert_eq!(detail.duration_us, 100_000); assert_eq!(detail.services, vec!["db", "gateway"]);
}
#[test]
fn test_trace_detail_no_root_span() {
let spans = vec![
Span {
trace_id: "abc".to_string(),
span_id: "s1".to_string(),
parent_span_id: Some("missing".to_string()),
name: "op1".to_string(),
service: "svc".to_string(),
kind: None,
status: SpanStatus::Ok,
start_time: 1000,
duration_us: 50_000,
attributes: None,
events: None,
resource: None,
extensions: None,
},
Span {
trace_id: "abc".to_string(),
span_id: "s2".to_string(),
parent_span_id: Some("missing".to_string()),
name: "op2".to_string(),
service: "svc".to_string(),
kind: None,
status: SpanStatus::Ok,
start_time: 1000,
duration_us: 80_000,
attributes: None,
events: None,
resource: None,
extensions: None,
},
];
let detail = TraceDetail::from_spans("abc".to_string(), spans);
assert_eq!(detail.duration_us, 80_000);
}
#[test]
fn test_trace_detail_empty_spans() {
let detail = TraceDetail::from_spans("empty".to_string(), vec![]);
assert_eq!(detail.span_count, 0);
assert_eq!(detail.duration_us, 0);
assert!(detail.services.is_empty());
}
#[test]
fn test_span_kind_parse_capitalized() {
assert_eq!(SpanKind::parse("Client"), SpanKind::Client);
assert_eq!(SpanKind::parse("Server"), SpanKind::Server);
assert_eq!(SpanKind::parse("Producer"), SpanKind::Producer);
assert_eq!(SpanKind::parse("Consumer"), SpanKind::Consumer);
}
#[test]
fn test_span_kind_parse_lowercase() {
assert_eq!(SpanKind::parse("client"), SpanKind::Client);
assert_eq!(SpanKind::parse("server"), SpanKind::Server);
assert_eq!(SpanKind::parse("producer"), SpanKind::Producer);
assert_eq!(SpanKind::parse("consumer"), SpanKind::Consumer);
assert_eq!(SpanKind::parse("internal"), SpanKind::Internal);
}
#[test]
fn test_span_kind_parse_unknown_defaults_to_internal() {
assert_eq!(SpanKind::parse("unknown"), SpanKind::Internal);
assert_eq!(SpanKind::parse(""), SpanKind::Internal);
}
}