1use crate::trace_correlation::Span;
26use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, HashSet};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum VisualizationFormat {
32 JSON,
34 HTML,
36 SVG,
38 ASCII,
40 Mermaid,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct TimelineEntry {
47 pub span_id: String,
49 pub name: String,
51 pub start_offset_ms: u64,
53 pub duration_ms: u64,
55 pub depth: usize,
57 pub parent_id: Option<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct FlameGraphNode {
64 pub name: String,
66 pub value: u64,
68 pub children: Vec<FlameGraphNode>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct DependencyEdge {
75 pub from: String,
77 pub to: String,
79 pub call_count: usize,
81 pub total_duration_ms: u64,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct CriticalPathSegment {
88 pub span_id: String,
90 pub name: String,
92 pub duration_ms: u64,
94 pub cumulative_duration_ms: u64,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct TraceStatistics {
101 pub total_spans: usize,
103 pub total_duration_ms: u64,
105 pub max_depth: usize,
107 pub service_count: usize,
109 pub error_count: usize,
111 pub avg_span_duration_ms: f64,
113 pub duration_percentiles: (u64, u64, u64),
115}
116
117pub struct TraceVisualizer;
119
120impl TraceVisualizer {
121 pub fn new() -> Self {
123 Self
124 }
125
126 pub fn generate_timeline(
128 &self,
129 spans: &[Span],
130 format: VisualizationFormat,
131 ) -> Result<String, String> {
132 if spans.is_empty() {
133 return Err("No spans to visualize".to_string());
134 }
135
136 let entries = self.build_timeline_entries(spans)?;
137
138 match format {
139 VisualizationFormat::JSON => Ok(serde_json::to_string_pretty(&entries)
140 .map_err(|e| format!("JSON serialization failed: {}", e))?),
141 VisualizationFormat::HTML => Ok(self.timeline_to_html(&entries)),
142 VisualizationFormat::ASCII => Ok(self.timeline_to_ascii(&entries)),
143 VisualizationFormat::Mermaid => Ok(self.timeline_to_mermaid(&entries)),
144 _ => Err("Unsupported format for timeline".to_string()),
145 }
146 }
147
148 fn build_timeline_entries(&self, spans: &[Span]) -> Result<Vec<TimelineEntry>, String> {
150 if spans.is_empty() {
151 return Ok(Vec::new());
152 }
153
154 let trace_start = spans
156 .iter()
157 .map(|s| s.start_time)
158 .min()
159 .ok_or("No start time found")?;
160
161 let hierarchy = self.build_hierarchy(spans);
163
164 let depths = self.calculate_depths(spans, &hierarchy);
166
167 let mut entries: Vec<TimelineEntry> = spans
169 .iter()
170 .map(|span| {
171 let start_offset = span
172 .start_time
173 .duration_since(trace_start)
174 .unwrap_or_default()
175 .as_millis() as u64;
176
177 TimelineEntry {
178 span_id: span.span_id.clone(),
179 name: span.name.clone(),
180 start_offset_ms: start_offset,
181 duration_ms: span.duration_ms.unwrap_or(0),
182 depth: *depths.get(&span.span_id).unwrap_or(&0),
183 parent_id: span.parent_span_id.clone(),
184 }
185 })
186 .collect();
187
188 entries.sort_by_key(|e| e.start_offset_ms);
190
191 Ok(entries)
192 }
193
194 fn build_hierarchy(&self, spans: &[Span]) -> HashMap<String, Vec<String>> {
196 let mut hierarchy: HashMap<String, Vec<String>> = HashMap::new();
197
198 for span in spans {
199 if let Some(parent_id) = &span.parent_span_id {
200 hierarchy
201 .entry(parent_id.clone())
202 .or_default()
203 .push(span.span_id.clone());
204 }
205 }
206
207 hierarchy
208 }
209
210 fn calculate_depths(
212 &self,
213 spans: &[Span],
214 hierarchy: &HashMap<String, Vec<String>>,
215 ) -> HashMap<String, usize> {
216 let mut depths = HashMap::new();
217
218 let roots: Vec<_> = spans
220 .iter()
221 .filter(|s| s.parent_span_id.is_none())
222 .collect();
223
224 for root in roots {
226 self.calculate_depth_recursive(&root.span_id, 0, hierarchy, &mut depths);
227 }
228
229 depths
230 }
231
232 #[allow(clippy::only_used_in_recursion)]
234 fn calculate_depth_recursive(
235 &self,
236 span_id: &str,
237 depth: usize,
238 hierarchy: &HashMap<String, Vec<String>>,
239 depths: &mut HashMap<String, usize>,
240 ) {
241 depths.insert(span_id.to_string(), depth);
242
243 if let Some(children) = hierarchy.get(span_id) {
244 for child_id in children {
245 self.calculate_depth_recursive(child_id, depth + 1, hierarchy, depths);
246 }
247 }
248 }
249
250 fn timeline_to_html(&self, entries: &[TimelineEntry]) -> String {
252 let mut html = String::from("<html><head><style>\n");
253 html.push_str(".timeline { font-family: monospace; }\n");
254 html.push_str(".span { background: #4CAF50; color: white; padding: 2px 5px; margin: 2px; display: inline-block; }\n");
255 html.push_str("</style></head><body><div class='timeline'>\n");
256
257 for entry in entries {
258 let indent = " ".repeat(entry.depth);
259 html.push_str(&format!(
260 "{}<div class='span'>{} ({}ms)</div><br>\n",
261 indent, entry.name, entry.duration_ms
262 ));
263 }
264
265 html.push_str("</div></body></html>");
266 html
267 }
268
269 fn timeline_to_ascii(&self, entries: &[TimelineEntry]) -> String {
271 let mut ascii = String::new();
272
273 for entry in entries {
274 let indent = " ".repeat(entry.depth);
275 ascii.push_str(&format!(
276 "{}├─ {} ({}ms)\n",
277 indent, entry.name, entry.duration_ms
278 ));
279 }
280
281 ascii
282 }
283
284 fn timeline_to_mermaid(&self, entries: &[TimelineEntry]) -> String {
286 let mut mermaid = String::from(
287 "gantt\n title Trace Timeline\n dateFormat x\n axisFormat %L\n\n",
288 );
289
290 for entry in entries {
291 mermaid.push_str(&format!(
292 " {} : {}, {}ms\n",
293 entry.name, entry.start_offset_ms, entry.duration_ms
294 ));
295 }
296
297 mermaid
298 }
299
300 pub fn generate_flame_graph(&self, spans: &[Span]) -> Result<FlameGraphNode, String> {
302 if spans.is_empty() {
303 return Err("No spans to visualize".to_string());
304 }
305
306 let roots: Vec<_> = spans
308 .iter()
309 .filter(|s| s.parent_span_id.is_none())
310 .collect();
311
312 if roots.is_empty() {
313 return Err("No root spans found".to_string());
314 }
315
316 let root = roots[0];
318 Ok(self.build_flame_graph_node(root, spans))
319 }
320
321 #[allow(clippy::only_used_in_recursion)]
323 fn build_flame_graph_node(&self, span: &Span, all_spans: &[Span]) -> FlameGraphNode {
324 let children: Vec<FlameGraphNode> = all_spans
325 .iter()
326 .filter(|s| s.parent_span_id.as_ref() == Some(&span.span_id))
327 .map(|child| self.build_flame_graph_node(child, all_spans))
328 .collect();
329
330 FlameGraphNode {
331 name: span.name.clone(),
332 value: span.duration_ms.unwrap_or(0),
333 children,
334 }
335 }
336
337 pub fn generate_dependency_graph(&self, spans: &[Span]) -> Vec<DependencyEdge> {
339 let mut edges: HashMap<(String, String), DependencyEdge> = HashMap::new();
340
341 for span in spans {
342 if let Some(parent_id) = &span.parent_span_id {
343 if let Some(parent) = spans.iter().find(|s| &s.span_id == parent_id) {
345 let from = parent.name.clone();
346 let to = span.name.clone();
347 let key = (from.clone(), to.clone());
348
349 edges
350 .entry(key)
351 .and_modify(|e| {
352 e.call_count += 1;
353 e.total_duration_ms += span.duration_ms.unwrap_or(0);
354 })
355 .or_insert(DependencyEdge {
356 from,
357 to,
358 call_count: 1,
359 total_duration_ms: span.duration_ms.unwrap_or(0),
360 });
361 }
362 }
363 }
364
365 edges.into_values().collect()
366 }
367
368 pub fn find_critical_path(&self, spans: &[Span]) -> Result<Vec<CriticalPathSegment>, String> {
370 if spans.is_empty() {
371 return Err("No spans to analyze".to_string());
372 }
373
374 let roots: Vec<_> = spans
376 .iter()
377 .filter(|s| s.parent_span_id.is_none())
378 .collect();
379
380 if roots.is_empty() {
381 return Err("No root spans found".to_string());
382 }
383
384 let root = roots[0];
386 let path = self.find_critical_path_recursive(root, spans, 0);
387
388 Ok(path)
389 }
390
391 #[allow(clippy::only_used_in_recursion)]
393 fn find_critical_path_recursive(
394 &self,
395 span: &Span,
396 all_spans: &[Span],
397 cumulative: u64,
398 ) -> Vec<CriticalPathSegment> {
399 let duration = span.duration_ms.unwrap_or(0);
400 let new_cumulative = cumulative + duration;
401
402 let mut path = vec![CriticalPathSegment {
403 span_id: span.span_id.clone(),
404 name: span.name.clone(),
405 duration_ms: duration,
406 cumulative_duration_ms: new_cumulative,
407 }];
408
409 let longest_child = all_spans
411 .iter()
412 .filter(|s| s.parent_span_id.as_ref() == Some(&span.span_id))
413 .max_by_key(|s| s.duration_ms.unwrap_or(0));
414
415 if let Some(child) = longest_child {
416 path.extend(self.find_critical_path_recursive(child, all_spans, new_cumulative));
417 }
418
419 path
420 }
421
422 pub fn calculate_statistics(&self, spans: &[Span]) -> TraceStatistics {
424 if spans.is_empty() {
425 return TraceStatistics {
426 total_spans: 0,
427 total_duration_ms: 0,
428 max_depth: 0,
429 service_count: 0,
430 error_count: 0,
431 avg_span_duration_ms: 0.0,
432 duration_percentiles: (0, 0, 0),
433 };
434 }
435
436 let total_spans = spans.len();
437
438 let total_duration_ms = spans
440 .iter()
441 .filter(|s| s.parent_span_id.is_none())
442 .map(|s| s.duration_ms.unwrap_or(0))
443 .max()
444 .unwrap_or(0);
445
446 let hierarchy = self.build_hierarchy(spans);
448 let depths = self.calculate_depths(spans, &hierarchy);
449 let max_depth = depths.values().max().copied().unwrap_or(0);
450
451 let services: HashSet<_> = spans.iter().map(|s| s.name.as_str()).collect();
453 let service_count = services.len();
454
455 let error_count = spans
457 .iter()
458 .filter(|s| matches!(s.status, crate::trace_correlation::SpanStatus::Error))
459 .count();
460
461 let total_span_duration: u64 = spans.iter().map(|s| s.duration_ms.unwrap_or(0)).sum();
463 let avg_span_duration_ms = total_span_duration as f64 / total_spans as f64;
464
465 let mut durations: Vec<u64> = spans.iter().map(|s| s.duration_ms.unwrap_or(0)).collect();
467 durations.sort_unstable();
468
469 let p50 = durations[durations.len() / 2];
470 let p95 = durations[durations.len() * 95 / 100];
471 let p99 = durations[durations.len() * 99 / 100];
472
473 TraceStatistics {
474 total_spans,
475 total_duration_ms,
476 max_depth,
477 service_count,
478 error_count,
479 avg_span_duration_ms,
480 duration_percentiles: (p50, p95, p99),
481 }
482 }
483}
484
485impl Default for TraceVisualizer {
486 fn default() -> Self {
487 Self::new()
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::trace_correlation::{SpanKind, TraceContext};
495
496 fn create_test_spans() -> Vec<Span> {
497 let ctx = TraceContext::new("test");
498
499 let mut root = Span::new("root".to_string(), ctx.clone(), SpanKind::Server);
500 root.finish();
501
502 let mut child1 = Span::new("child1".to_string(), ctx.clone(), SpanKind::Internal)
503 .with_parent(root.span_id.clone());
504 child1.finish();
505
506 let mut child2 = Span::new("child2".to_string(), ctx, SpanKind::Internal)
507 .with_parent(root.span_id.clone());
508 child2.finish();
509
510 vec![root, child1, child2]
511 }
512
513 #[test]
514 fn test_visualization_format() {
515 let formats = [
516 VisualizationFormat::JSON,
517 VisualizationFormat::HTML,
518 VisualizationFormat::SVG,
519 VisualizationFormat::ASCII,
520 VisualizationFormat::Mermaid,
521 ];
522
523 assert_eq!(formats.len(), 5);
524 }
525
526 #[test]
527 fn test_timeline_entry_creation() {
528 let entry = TimelineEntry {
529 span_id: "span1".to_string(),
530 name: "operation".to_string(),
531 start_offset_ms: 0,
532 duration_ms: 100,
533 depth: 0,
534 parent_id: None,
535 };
536
537 assert_eq!(entry.span_id, "span1");
538 assert_eq!(entry.duration_ms, 100);
539 }
540
541 #[test]
542 fn test_trace_visualizer_creation() {
543 let _visualizer = TraceVisualizer::new();
544 }
546
547 #[test]
548 fn test_build_hierarchy() {
549 let visualizer = TraceVisualizer::new();
550 let spans = create_test_spans();
551
552 let hierarchy = visualizer.build_hierarchy(&spans);
553
554 assert!(hierarchy.contains_key(&spans[0].span_id));
555 assert_eq!(
556 hierarchy
557 .get(&spans[0].span_id)
558 .expect("should succeed")
559 .len(),
560 2
561 );
562 }
563
564 #[test]
565 fn test_calculate_depths() {
566 let visualizer = TraceVisualizer::new();
567 let spans = create_test_spans();
568
569 let hierarchy = visualizer.build_hierarchy(&spans);
570 let depths = visualizer.calculate_depths(&spans, &hierarchy);
571
572 assert_eq!(depths.get(&spans[0].span_id), Some(&0));
573 assert_eq!(depths.get(&spans[1].span_id), Some(&1));
574 assert_eq!(depths.get(&spans[2].span_id), Some(&1));
575 }
576
577 #[test]
578 fn test_build_timeline_entries() {
579 let visualizer = TraceVisualizer::new();
580 let spans = create_test_spans();
581
582 let entries = visualizer
583 .build_timeline_entries(&spans)
584 .expect("should succeed");
585
586 assert_eq!(entries.len(), 3);
587 assert_eq!(entries[0].depth, 0);
588 }
589
590 #[test]
591 fn test_generate_timeline_json() {
592 let visualizer = TraceVisualizer::new();
593 let spans = create_test_spans();
594
595 let result = visualizer.generate_timeline(&spans, VisualizationFormat::JSON);
596
597 assert!(result.is_ok());
598 let json = result.expect("should succeed");
599 assert!(json.contains("span_id"));
600 }
601
602 #[test]
603 fn test_generate_timeline_html() {
604 let visualizer = TraceVisualizer::new();
605 let spans = create_test_spans();
606
607 let result = visualizer.generate_timeline(&spans, VisualizationFormat::HTML);
608
609 assert!(result.is_ok());
610 let html = result.expect("should succeed");
611 assert!(html.contains("<html>"));
612 assert!(html.contains("timeline"));
613 }
614
615 #[test]
616 fn test_generate_timeline_ascii() {
617 let visualizer = TraceVisualizer::new();
618 let spans = create_test_spans();
619
620 let result = visualizer.generate_timeline(&spans, VisualizationFormat::ASCII);
621
622 assert!(result.is_ok());
623 let ascii = result.expect("should succeed");
624 assert!(ascii.contains("├─"));
625 }
626
627 #[test]
628 fn test_generate_timeline_mermaid() {
629 let visualizer = TraceVisualizer::new();
630 let spans = create_test_spans();
631
632 let result = visualizer.generate_timeline(&spans, VisualizationFormat::Mermaid);
633
634 assert!(result.is_ok());
635 let mermaid = result.expect("should succeed");
636 assert!(mermaid.contains("gantt"));
637 }
638
639 #[test]
640 fn test_generate_timeline_empty() {
641 let visualizer = TraceVisualizer::new();
642 let spans: Vec<Span> = vec![];
643
644 let result = visualizer.generate_timeline(&spans, VisualizationFormat::JSON);
645
646 assert!(result.is_err());
647 }
648
649 #[test]
650 fn test_generate_flame_graph() {
651 let visualizer = TraceVisualizer::new();
652 let spans = create_test_spans();
653
654 let result = visualizer.generate_flame_graph(&spans);
655
656 assert!(result.is_ok());
657 let root = result.expect("should succeed");
658 assert_eq!(root.name, "root");
659 assert_eq!(root.children.len(), 2);
660 }
661
662 #[test]
663 fn test_generate_flame_graph_empty() {
664 let visualizer = TraceVisualizer::new();
665 let spans: Vec<Span> = vec![];
666
667 let result = visualizer.generate_flame_graph(&spans);
668
669 assert!(result.is_err());
670 }
671
672 #[test]
673 fn test_generate_dependency_graph() {
674 let visualizer = TraceVisualizer::new();
675 let spans = create_test_spans();
676
677 let edges = visualizer.generate_dependency_graph(&spans);
678
679 assert_eq!(edges.len(), 2);
680 assert!(edges.iter().any(|e| e.from == "root" && e.to == "child1"));
681 assert!(edges.iter().any(|e| e.from == "root" && e.to == "child2"));
682 }
683
684 #[test]
685 fn test_find_critical_path() {
686 let visualizer = TraceVisualizer::new();
687 let spans = create_test_spans();
688
689 let result = visualizer.find_critical_path(&spans);
690
691 assert!(result.is_ok());
692 let path = result.expect("should succeed");
693 assert!(!path.is_empty());
694 assert_eq!(path[0].name, "root");
695 }
696
697 #[test]
698 fn test_find_critical_path_empty() {
699 let visualizer = TraceVisualizer::new();
700 let spans: Vec<Span> = vec![];
701
702 let result = visualizer.find_critical_path(&spans);
703
704 assert!(result.is_err());
705 }
706
707 #[test]
708 fn test_calculate_statistics() {
709 let visualizer = TraceVisualizer::new();
710 let spans = create_test_spans();
711
712 let stats = visualizer.calculate_statistics(&spans);
713
714 assert_eq!(stats.total_spans, 3);
715 assert_eq!(stats.max_depth, 1);
716 assert_eq!(stats.service_count, 3); assert_eq!(stats.error_count, 0);
718 }
719
720 #[test]
721 fn test_calculate_statistics_empty() {
722 let visualizer = TraceVisualizer::new();
723 let spans: Vec<Span> = vec![];
724
725 let stats = visualizer.calculate_statistics(&spans);
726
727 assert_eq!(stats.total_spans, 0);
728 assert_eq!(stats.total_duration_ms, 0);
729 }
730
731 #[test]
732 fn test_critical_path_segment() {
733 let segment = CriticalPathSegment {
734 span_id: "span1".to_string(),
735 name: "operation".to_string(),
736 duration_ms: 100,
737 cumulative_duration_ms: 100,
738 };
739
740 assert_eq!(segment.duration_ms, 100);
741 assert_eq!(segment.cumulative_duration_ms, 100);
742 }
743
744 #[test]
745 fn test_dependency_edge() {
746 let edge = DependencyEdge {
747 from: "service1".to_string(),
748 to: "service2".to_string(),
749 call_count: 5,
750 total_duration_ms: 500,
751 };
752
753 assert_eq!(edge.call_count, 5);
754 assert_eq!(edge.total_duration_ms, 500);
755 }
756
757 #[test]
758 fn test_flame_graph_node() {
759 let node = FlameGraphNode {
760 name: "root".to_string(),
761 value: 100,
762 children: vec![],
763 };
764
765 assert_eq!(node.name, "root");
766 assert_eq!(node.value, 100);
767 assert!(node.children.is_empty());
768 }
769
770 #[test]
771 fn test_timeline_to_html_format() {
772 let visualizer = TraceVisualizer::new();
773 let entries = vec![TimelineEntry {
774 span_id: "span1".to_string(),
775 name: "test".to_string(),
776 start_offset_ms: 0,
777 duration_ms: 100,
778 depth: 0,
779 parent_id: None,
780 }];
781
782 let html = visualizer.timeline_to_html(&entries);
783
784 assert!(html.contains("<html>"));
785 assert!(html.contains("test"));
786 assert!(html.contains("100ms"));
787 }
788
789 #[test]
790 fn test_timeline_to_ascii_format() {
791 let visualizer = TraceVisualizer::new();
792 let entries = vec![TimelineEntry {
793 span_id: "span1".to_string(),
794 name: "test".to_string(),
795 start_offset_ms: 0,
796 duration_ms: 100,
797 depth: 0,
798 parent_id: None,
799 }];
800
801 let ascii = visualizer.timeline_to_ascii(&entries);
802
803 assert!(ascii.contains("├─"));
804 assert!(ascii.contains("test"));
805 assert!(ascii.contains("100ms"));
806 }
807
808 #[test]
809 fn test_statistics_percentiles() {
810 let visualizer = TraceVisualizer::new();
811 let spans = create_test_spans();
812
813 let stats = visualizer.calculate_statistics(&spans);
814
815 let (p50, p95, p99) = stats.duration_percentiles;
816 assert!(p95 >= p50);
817 assert!(p99 >= p95);
818 }
819}