use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Span {
pub span_id: String,
pub parent_span_id: Option<String>,
pub trace_id: String,
pub service_name: String,
pub operation_name: String,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub duration_us: Option<i64>,
pub tags: HashMap<String, String>,
pub logs: Vec<SpanLog>,
pub status: SpanStatus,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanLog {
pub timestamp: DateTime<Utc>,
pub message: String,
pub fields: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpanStatus {
Ok,
Error,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trace {
pub trace_id: String,
pub spans: Vec<Span>,
pub root_span_id: String,
pub total_duration_us: i64,
pub service_count: usize,
pub span_count: usize,
pub error_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceContext {
pub trace_id: String,
pub span_id: String,
pub sampled: bool,
pub baggage: HashMap<String, String>,
}
impl TraceContext {
pub fn new() -> Self {
Self {
trace_id: Uuid::new_v4().to_string(),
span_id: Uuid::new_v4().to_string(),
sampled: true,
baggage: HashMap::new(),
}
}
pub fn child(&self) -> Self {
Self {
trace_id: self.trace_id.clone(),
span_id: Uuid::new_v4().to_string(),
sampled: self.sampled,
baggage: self.baggage.clone(),
}
}
pub fn inject(&self) -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert("x-trace-id".to_string(), self.trace_id.clone());
headers.insert("x-span-id".to_string(), self.span_id.clone());
headers.insert("x-sampled".to_string(), self.sampled.to_string());
headers
}
pub fn extract(headers: &HashMap<String, String>) -> Option<Self> {
let trace_id = headers.get("x-trace-id")?.clone();
let span_id = headers.get("x-span-id")?.clone();
let sampled = headers
.get("x-sampled")
.and_then(|s| s.parse().ok())
.unwrap_or(true);
Some(Self {
trace_id,
span_id,
sampled,
baggage: HashMap::new(),
})
}
}
impl Default for TraceContext {
fn default() -> Self {
Self::new()
}
}
impl Span {
pub fn new(
trace_id: String,
service_name: String,
operation_name: String,
parent_span_id: Option<String>,
) -> Self {
Self {
span_id: Uuid::new_v4().to_string(),
parent_span_id,
trace_id,
service_name,
operation_name,
start_time: Utc::now(),
end_time: None,
duration_us: None,
tags: HashMap::new(),
logs: Vec::new(),
status: SpanStatus::Ok,
error: None,
}
}
pub fn set_tag(&mut self, key: String, value: String) {
self.tags.insert(key, value);
}
pub fn log(&mut self, message: String, fields: HashMap<String, String>) {
self.logs.push(SpanLog {
timestamp: Utc::now(),
message,
fields,
});
}
pub fn finish(&mut self) {
let end_time = Utc::now();
self.end_time = Some(end_time);
self.duration_us = Some((end_time - self.start_time).num_microseconds().unwrap_or(0));
}
pub fn set_error(&mut self, error: String) {
self.status = SpanStatus::Error;
self.error = Some(error);
}
}
pub struct DistributedTracer {
service_name: String,
active_spans: Arc<RwLock<HashMap<String, Span>>>,
completed_traces: Arc<RwLock<HashMap<String, Trace>>>,
#[allow(dead_code)]
sampling_rate: f64,
}
impl DistributedTracer {
pub fn new(service_name: String, sampling_rate: f64) -> Self {
Self {
service_name,
active_spans: Arc::new(RwLock::new(HashMap::new())),
completed_traces: Arc::new(RwLock::new(HashMap::new())),
sampling_rate: sampling_rate.clamp(0.0, 1.0),
}
}
pub async fn start_span(&self, operation_name: String, context: Option<TraceContext>) -> Span {
let (trace_id, parent_span_id) = if let Some(ctx) = context {
(ctx.trace_id, Some(ctx.span_id))
} else {
(Uuid::new_v4().to_string(), None)
};
let span = Span::new(
trace_id,
self.service_name.clone(),
operation_name,
parent_span_id,
);
let mut active = self.active_spans.write().await;
active.insert(span.span_id.clone(), span.clone());
span
}
pub async fn finish_span(&self, mut span: Span) {
span.finish();
let mut active = self.active_spans.write().await;
active.remove(&span.span_id);
self.try_build_trace(&span).await;
}
async fn try_build_trace(&self, finished_span: &Span) {
let mut traces = self.completed_traces.write().await;
let trace = traces
.entry(finished_span.trace_id.clone())
.or_insert_with(|| Trace {
trace_id: finished_span.trace_id.clone(),
spans: Vec::new(),
root_span_id: finished_span.span_id.clone(),
total_duration_us: 0,
service_count: 0,
span_count: 0,
error_count: 0,
});
trace.spans.push(finished_span.clone());
trace.span_count = trace.spans.len();
let mut services = std::collections::HashSet::new();
let mut total_duration = 0i64;
let mut error_count = 0;
for span in &trace.spans {
services.insert(span.service_name.clone());
if let Some(duration) = span.duration_us {
total_duration = total_duration.max(duration);
}
if span.status == SpanStatus::Error {
error_count += 1;
}
}
trace.service_count = services.len();
trace.total_duration_us = total_duration;
trace.error_count = error_count;
}
pub async fn get_trace(&self, trace_id: &str) -> Option<Trace> {
let traces = self.completed_traces.read().await;
traces.get(trace_id).cloned()
}
pub async fn get_all_traces(&self) -> Vec<Trace> {
let traces = self.completed_traces.read().await;
traces.values().cloned().collect()
}
}
pub struct BottleneckAnalyzer;
impl BottleneckAnalyzer {
pub fn identify_bottlenecks(trace: &Trace, threshold_ms: i64) -> Vec<BottleneckReport> {
let threshold_us = threshold_ms * 1000;
let mut bottlenecks = Vec::new();
for span in &trace.spans {
if let Some(duration) = span.duration_us {
if duration > threshold_us {
bottlenecks.push(BottleneckReport {
span_id: span.span_id.clone(),
operation_name: span.operation_name.clone(),
service_name: span.service_name.clone(),
duration_ms: duration / 1000,
percentage: ((duration as f64 / trace.total_duration_us as f64) * 100.0)
as i32,
});
}
}
}
bottlenecks.sort_by(|a, b| b.duration_ms.cmp(&a.duration_ms));
bottlenecks
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BottleneckReport {
pub span_id: String,
pub operation_name: String,
pub service_name: String,
pub duration_ms: i64,
pub percentage: i32,
}
pub struct LatencyAnalyzer;
impl LatencyAnalyzer {
pub fn by_service(trace: &Trace) -> HashMap<String, ServiceLatency> {
let mut latency_by_service = HashMap::new();
for span in &trace.spans {
if let Some(duration) = span.duration_us {
let entry = latency_by_service
.entry(span.service_name.clone())
.or_insert_with(|| ServiceLatency {
service_name: span.service_name.clone(),
total_duration_us: 0,
span_count: 0,
avg_duration_us: 0,
min_duration_us: i64::MAX,
max_duration_us: 0,
});
entry.total_duration_us += duration;
entry.span_count += 1;
entry.min_duration_us = entry.min_duration_us.min(duration);
entry.max_duration_us = entry.max_duration_us.max(duration);
}
}
for latency in latency_by_service.values_mut() {
if latency.span_count > 0 {
latency.avg_duration_us = latency.total_duration_us / latency.span_count as i64;
}
}
latency_by_service
}
pub fn by_operation(trace: &Trace) -> HashMap<String, OperationLatency> {
let mut latency_by_operation = HashMap::new();
for span in &trace.spans {
if let Some(duration) = span.duration_us {
let entry = latency_by_operation
.entry(span.operation_name.clone())
.or_insert_with(|| OperationLatency {
operation_name: span.operation_name.clone(),
total_duration_us: 0,
span_count: 0,
avg_duration_us: 0,
p50_duration_us: 0,
p95_duration_us: 0,
p99_duration_us: 0,
durations: Vec::new(),
});
entry.total_duration_us += duration;
entry.span_count += 1;
entry.durations.push(duration);
}
}
for latency in latency_by_operation.values_mut() {
if latency.span_count > 0 {
latency.avg_duration_us = latency.total_duration_us / latency.span_count as i64;
let mut sorted = latency.durations.clone();
sorted.sort_unstable();
let p50_idx = (sorted.len() as f64 * 0.50) as usize;
let p95_idx = (sorted.len() as f64 * 0.95) as usize;
let p99_idx = (sorted.len() as f64 * 0.99) as usize;
latency.p50_duration_us = sorted.get(p50_idx).copied().unwrap_or(0);
latency.p95_duration_us = sorted.get(p95_idx).copied().unwrap_or(0);
latency.p99_duration_us = sorted.get(p99_idx).copied().unwrap_or(0);
}
}
latency_by_operation
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceLatency {
pub service_name: String,
pub total_duration_us: i64,
pub span_count: usize,
pub avg_duration_us: i64,
pub min_duration_us: i64,
pub max_duration_us: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationLatency {
pub operation_name: String,
pub total_duration_us: i64,
pub span_count: usize,
pub avg_duration_us: i64,
pub p50_duration_us: i64,
pub p95_duration_us: i64,
pub p99_duration_us: i64,
#[serde(skip)]
durations: Vec<i64>,
}
pub struct FlowVisualizer;
impl FlowVisualizer {
pub fn generate_tree(trace: &Trace) -> String {
let mut output = String::new();
let root_span = trace
.spans
.iter()
.find(|s| s.parent_span_id.is_none())
.or_else(|| trace.spans.first());
if let Some(root) = root_span {
Self::append_span(&mut output, root, &trace.spans, 0);
}
output
}
fn append_span(output: &mut String, span: &Span, all_spans: &[Span], indent: usize) {
let indent_str = " ".repeat(indent);
let duration_ms = span.duration_us.unwrap_or(0) / 1000;
let status_char = match span.status {
SpanStatus::Ok => "✓",
SpanStatus::Error => "✗",
SpanStatus::Cancelled => "⊘",
};
output.push_str(&format!(
"{}{} {} [{}ms] - {}\n",
indent_str, status_char, span.operation_name, duration_ms, span.service_name
));
for child_span in all_spans
.iter()
.filter(|s| s.parent_span_id.as_ref() == Some(&span.span_id))
{
Self::append_span(output, child_span, all_spans, indent + 1);
}
}
pub fn critical_path(trace: &Trace) -> Vec<String> {
let mut path = Vec::new();
let mut current = trace
.spans
.iter()
.max_by_key(|s| s.duration_us.unwrap_or(0));
while let Some(span) = current {
path.push(format!(
"{} ({}ms)",
span.operation_name,
span.duration_us.unwrap_or(0) / 1000
));
current = span
.parent_span_id
.as_ref()
.and_then(|pid| trace.spans.iter().find(|s| &s.span_id == pid));
}
path.reverse();
path
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trace_context_creation() {
let ctx = TraceContext::new();
assert!(!ctx.trace_id.is_empty());
assert!(!ctx.span_id.is_empty());
assert!(ctx.sampled);
}
#[test]
fn test_trace_context_child() {
let parent = TraceContext::new();
let child = parent.child();
assert_eq!(parent.trace_id, child.trace_id);
assert_ne!(parent.span_id, child.span_id);
}
#[test]
fn test_trace_context_inject_extract() {
let ctx = TraceContext::new();
let headers = ctx.inject();
let extracted = TraceContext::extract(&headers).unwrap();
assert_eq!(ctx.trace_id, extracted.trace_id);
assert_eq!(ctx.span_id, extracted.span_id);
}
#[test]
fn test_span_creation() {
let span = Span::new(
"trace-1".to_string(),
"test-service".to_string(),
"test-operation".to_string(),
None,
);
assert!(!span.span_id.is_empty());
assert_eq!(span.trace_id, "trace-1");
assert_eq!(span.service_name, "test-service");
assert_eq!(span.operation_name, "test-operation");
assert_eq!(span.status, SpanStatus::Ok);
}
#[test]
fn test_span_finish() {
let mut span = Span::new(
"trace-1".to_string(),
"test-service".to_string(),
"test-operation".to_string(),
None,
);
std::thread::sleep(std::time::Duration::from_millis(10));
span.finish();
assert!(span.end_time.is_some());
assert!(span.duration_us.is_some());
assert!(span.duration_us.unwrap() > 0);
}
#[tokio::test]
async fn test_distributed_tracer() {
let tracer = DistributedTracer::new("test-service".to_string(), 1.0);
let span = tracer.start_span("test-operation".to_string(), None).await;
let trace_id = span.trace_id.clone();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
tracer.finish_span(span).await;
let trace = tracer.get_trace(&trace_id).await;
assert!(trace.is_some());
let trace = trace.unwrap();
assert_eq!(trace.span_count, 1);
assert_eq!(trace.service_count, 1);
}
}