clnrm_core/cli/commands/
graph.rs1use 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
32pub 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 let trace_data = load_trace_data(trace_path)?;
43
44 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 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
85fn 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
100fn 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 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 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
142fn 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 let connector = if is_last { "└──" } else { "├──" };
153 output.push_str(&format!(
154 "{}{} {} ({})\n",
155 prefix, connector, span.name, span.kind
156 ));
157
158 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
179fn 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 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 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
208fn 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
257fn 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 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
282fn sanitize_mermaid_id(id: &str) -> String {
284 id.chars()
285 .map(|c| if c.is_alphanumeric() { c } else { '_' })
286 .collect()
287}