clnrm_core/cli/commands/
graph.rs

1//! Graph command - Visualize OpenTelemetry trace graphs
2//!
3//! Generates ASCII, DOT, JSON, or Mermaid visualizations of trace spans and their relationships.
4
5use crate::cli::types::GraphFormat;
6use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10use tracing::{debug, info};
11
12#[derive(Debug, Deserialize, Serialize)]
13struct Span {
14    #[serde(default)]
15    name: String,
16    #[serde(default)]
17    span_id: String,
18    #[serde(default)]
19    parent_span_id: Option<String>,
20    #[serde(default)]
21    trace_id: String,
22    #[serde(default)]
23    kind: String,
24}
25
26#[derive(Debug, Deserialize, Serialize)]
27struct TraceData {
28    #[serde(default)]
29    spans: Vec<Span>,
30}
31
32/// Visualize OpenTelemetry trace graph
33pub fn visualize_graph(
34    trace_path: &Path,
35    format: &GraphFormat,
36    highlight_missing: bool,
37    filter: Option<&str>,
38) -> Result<()> {
39    info!("Loading trace from {}", trace_path.display());
40
41    // Load trace data
42    let trace_data = load_trace_data(trace_path)?;
43
44    // Apply filter if provided
45    let spans = if let Some(filter_pattern) = filter {
46        trace_data
47            .spans
48            .into_iter()
49            .filter(|span| span.name.contains(filter_pattern))
50            .collect()
51    } else {
52        trace_data.spans
53    };
54
55    if spans.is_empty() {
56        println!("No spans found in trace");
57        return Ok(());
58    }
59
60    info!("Found {} span(s) to visualize", spans.len());
61
62    // Generate visualization based on format
63    match format {
64        GraphFormat::Ascii => {
65            let output = generate_ascii_tree(&spans, highlight_missing)?;
66            println!("{}", output);
67        }
68        GraphFormat::Dot => {
69            let output = generate_dot_graph(&spans)?;
70            println!("{}", output);
71        }
72        GraphFormat::Json => {
73            let output = generate_json_graph(&spans)?;
74            println!("{}", output);
75        }
76        GraphFormat::Mermaid => {
77            let output = generate_mermaid_diagram(&spans)?;
78            println!("{}", output);
79        }
80    }
81
82    Ok(())
83}
84
85/// Load trace data from file
86fn load_trace_data(path: &Path) -> Result<TraceData> {
87    let content = std::fs::read_to_string(path).map_err(|e| {
88        CleanroomError::io_error(format!(
89            "Failed to read trace file {}: {}",
90            path.display(),
91            e
92        ))
93    })?;
94
95    serde_json::from_str(&content).map_err(|e| {
96        CleanroomError::serialization_error(format!("Failed to parse trace JSON: {}", e))
97    })
98}
99
100/// Generate ASCII tree visualization
101fn generate_ascii_tree(spans: &[Span], highlight_missing: bool) -> Result<String> {
102    debug!("Generating ASCII tree visualization");
103
104    let mut output = String::new();
105    output.push_str("OpenTelemetry Trace Graph\n");
106    output.push_str("=========================\n\n");
107
108    // Build parent-child relationships
109    let mut children_map: HashMap<String, Vec<&Span>> = HashMap::new();
110    let mut root_spans = Vec::new();
111
112    for span in spans {
113        if let Some(parent_id) = &span.parent_span_id {
114            children_map
115                .entry(parent_id.clone())
116                .or_default()
117                .push(span);
118        } else {
119            root_spans.push(span);
120        }
121    }
122
123    // Render tree starting from root spans
124    for root in root_spans {
125        render_span_tree(
126            root,
127            &children_map,
128            &mut output,
129            "",
130            true,
131            highlight_missing,
132        );
133    }
134
135    if output.trim().is_empty() {
136        output.push_str("(no spans to display)\n");
137    }
138
139    Ok(output)
140}
141
142/// Recursively render span tree
143fn render_span_tree(
144    span: &Span,
145    children_map: &HashMap<String, Vec<&Span>>,
146    output: &mut String,
147    prefix: &str,
148    is_last: bool,
149    highlight_missing: bool,
150) {
151    // Render current span
152    let connector = if is_last { "└──" } else { "├──" };
153    output.push_str(&format!(
154        "{}{} {} ({})\n",
155        prefix, connector, span.name, span.kind
156    ));
157
158    // Render children
159    if let Some(children) = children_map.get(&span.span_id) {
160        let child_prefix = format!("{}{}   ", prefix, if is_last { " " } else { "│" });
161
162        for (idx, child) in children.iter().enumerate() {
163            let is_last_child = idx == children.len() - 1;
164            render_span_tree(
165                child,
166                children_map,
167                output,
168                &child_prefix,
169                is_last_child,
170                highlight_missing,
171            );
172        }
173    } else if highlight_missing && !children_map.is_empty() {
174        let child_prefix = format!("{}{}   ", prefix, if is_last { " " } else { "│" });
175        output.push_str(&format!("{}└── (no children)\n", child_prefix));
176    }
177}
178
179/// Generate DOT graph for Graphviz
180fn generate_dot_graph(spans: &[Span]) -> Result<String> {
181    debug!("Generating DOT graph");
182
183    let mut output = String::new();
184    output.push_str("digraph trace {\n");
185    output.push_str("  rankdir=TB;\n");
186    output.push_str("  node [shape=box, style=rounded];\n\n");
187
188    // Add nodes
189    for span in spans {
190        let label = format!("{}\\n{}", span.name, span.kind);
191        output.push_str(&format!("  \"{}\" [label=\"{}\"];\n", span.span_id, label));
192    }
193
194    output.push('\n');
195
196    // Add edges
197    for span in spans {
198        if let Some(parent_id) = &span.parent_span_id {
199            output.push_str(&format!("  \"{}\" -> \"{}\";\n", parent_id, span.span_id));
200        }
201    }
202
203    output.push_str("}\n");
204
205    Ok(output)
206}
207
208/// Generate JSON graph structure
209fn generate_json_graph(spans: &[Span]) -> Result<String> {
210    debug!("Generating JSON graph");
211
212    #[derive(Serialize)]
213    struct JsonGraph {
214        nodes: Vec<JsonNode>,
215        edges: Vec<JsonEdge>,
216    }
217
218    #[derive(Serialize)]
219    struct JsonNode {
220        id: String,
221        name: String,
222        kind: String,
223    }
224
225    #[derive(Serialize)]
226    struct JsonEdge {
227        source: String,
228        target: String,
229    }
230
231    let nodes: Vec<JsonNode> = spans
232        .iter()
233        .map(|span| JsonNode {
234            id: span.span_id.clone(),
235            name: span.name.clone(),
236            kind: span.kind.clone(),
237        })
238        .collect();
239
240    let edges: Vec<JsonEdge> = spans
241        .iter()
242        .filter_map(|span| {
243            span.parent_span_id.as_ref().map(|parent_id| JsonEdge {
244                source: parent_id.clone(),
245                target: span.span_id.clone(),
246            })
247        })
248        .collect();
249
250    let graph = JsonGraph { nodes, edges };
251
252    serde_json::to_string_pretty(&graph).map_err(|e| {
253        CleanroomError::serialization_error(format!("Failed to serialize JSON graph: {}", e))
254    })
255}
256
257/// Generate Mermaid diagram
258fn generate_mermaid_diagram(spans: &[Span]) -> Result<String> {
259    debug!("Generating Mermaid diagram");
260
261    let mut output = String::new();
262    output.push_str("```mermaid\n");
263    output.push_str("graph TD\n");
264
265    // Add nodes and edges
266    for span in spans {
267        let node_id = sanitize_mermaid_id(&span.span_id);
268        let label = format!("{}[{}]", node_id, span.name);
269        output.push_str(&format!("  {}\n", label));
270
271        if let Some(parent_id) = &span.parent_span_id {
272            let parent_node_id = sanitize_mermaid_id(parent_id);
273            output.push_str(&format!("  {} --> {}\n", parent_node_id, node_id));
274        }
275    }
276
277    output.push_str("```\n");
278
279    Ok(output)
280}
281
282/// Sanitize span ID for Mermaid
283fn sanitize_mermaid_id(id: &str) -> String {
284    id.chars()
285        .map(|c| if c.is_alphanumeric() { c } else { '_' })
286        .collect()
287}