clnrm_core/cli/commands/
diff.rs

1//! Diff command for trace comparison
2//!
3//! Compares two OpenTelemetry traces to detect regressions.
4
5use crate::error::{CleanroomError, Result};
6use std::path::Path;
7
8/// Result of trace comparison
9#[derive(Debug, Clone)]
10pub struct DiffResult {
11    /// Number of spans added
12    pub added_count: usize,
13    /// Number of spans removed
14    pub removed_count: usize,
15    /// Number of spans modified
16    pub modified_count: usize,
17    /// Added spans
18    pub added: Vec<String>,
19    /// Removed spans
20    pub removed: Vec<String>,
21    /// Modified spans
22    pub modified: Vec<String>,
23}
24
25/// Compare two traces
26pub fn diff_traces(
27    baseline: &Path,
28    current: &Path,
29    format: &str,
30    only_changes: bool,
31) -> Result<DiffResult> {
32    // Read baseline and current traces
33    let baseline_content = std::fs::read_to_string(baseline)
34        .map_err(|e| CleanroomError::io_error(format!("Failed to read baseline: {}", e)))?;
35
36    let current_content = std::fs::read_to_string(current)
37        .map_err(|e| CleanroomError::io_error(format!("Failed to read current: {}", e)))?;
38
39    // Parse JSON traces
40    let baseline_json: serde_json::Value =
41        serde_json::from_str(&baseline_content).map_err(|e| {
42            CleanroomError::serialization_error(format!("Failed to parse baseline JSON: {}", e))
43        })?;
44
45    let current_json: serde_json::Value = serde_json::from_str(&current_content).map_err(|e| {
46        CleanroomError::serialization_error(format!("Failed to parse current JSON: {}", e))
47    })?;
48
49    // Extract span names
50    let baseline_spans = extract_span_names(&baseline_json);
51    let current_spans = extract_span_names(&current_json);
52
53    // Compute differences
54    let added: Vec<String> = current_spans
55        .iter()
56        .filter(|s| !baseline_spans.contains(s))
57        .cloned()
58        .collect();
59
60    let removed: Vec<String> = baseline_spans
61        .iter()
62        .filter(|s| !current_spans.contains(s))
63        .cloned()
64        .collect();
65
66    // For now, we don't detect modifications (would need deeper analysis)
67    let modified = Vec::new();
68
69    let result = DiffResult {
70        added_count: added.len(),
71        removed_count: removed.len(),
72        modified_count: modified.len(),
73        added,
74        removed,
75        modified,
76    };
77
78    // Display results
79    match format {
80        "json" => {
81            let json = serde_json::json!({
82                "added_count": result.added_count,
83                "removed_count": result.removed_count,
84                "modified_count": result.modified_count,
85                "added": result.added,
86                "removed": result.removed,
87                "modified": result.modified,
88            });
89            println!(
90                "{}",
91                serde_json::to_string_pretty(&json).map_err(|e| {
92                    CleanroomError::serialization_error(format!("Failed to serialize JSON: {}", e))
93                })?
94            );
95        }
96        _ => {
97            // Human-readable format
98            if result.added_count > 0 {
99                println!("Added spans ({}):", result.added_count);
100                for span in &result.added {
101                    println!("  + {}", span);
102                }
103            }
104
105            if result.removed_count > 0 {
106                println!("Removed spans ({}):", result.removed_count);
107                for span in &result.removed {
108                    println!("  - {}", span);
109                }
110            }
111
112            if result.modified_count > 0 {
113                println!("Modified spans ({}):", result.modified_count);
114                for span in &result.modified {
115                    println!("  ~ {}", span);
116                }
117            }
118
119            if !only_changes
120                || result.added_count + result.removed_count + result.modified_count == 0
121            {
122                println!(
123                    "\nSummary: {} added, {} removed, {} modified",
124                    result.added_count, result.removed_count, result.modified_count
125                );
126            }
127        }
128    }
129
130    Ok(result)
131}
132
133/// Extract span names from JSON trace
134fn extract_span_names(json: &serde_json::Value) -> Vec<String> {
135    let mut spans = Vec::new();
136
137    if let Some(array) = json.as_array() {
138        for item in array {
139            if let Some(name) = item.get("name").and_then(|n| n.as_str()) {
140                spans.push(name.to_string());
141            }
142        }
143    } else if let Some(obj) = json.as_object() {
144        if let Some(name) = obj.get("name").and_then(|n| n.as_str()) {
145            spans.push(name.to_string());
146        }
147
148        // Recursively extract from nested structures
149        for (_, value) in obj {
150            spans.extend(extract_span_names(value));
151        }
152    }
153
154    spans
155}