Skip to main content

nika_cli/
trace.rs

1//! Trace subcommand handler
2
3use clap::Subcommand;
4use std::fs;
5use std::path::PathBuf;
6
7use nika_engine::error::NikaError;
8use nika_engine::Event;
9
10#[derive(Subcommand)]
11pub enum TraceAction {
12    /// List all traces
13    List {
14        /// Show only last N traces
15        #[arg(short, long)]
16        limit: Option<usize>,
17    },
18
19    /// Show details of a trace
20    Show {
21        /// Generation ID or partial match
22        id: String,
23    },
24
25    /// Export trace to file
26    Export {
27        /// Generation ID
28        id: String,
29        /// Output format (json, yaml)
30        #[arg(short, long, default_value = "json")]
31        format: String,
32        /// Output file (stdout if not specified)
33        #[arg(short, long)]
34        output: Option<PathBuf>,
35    },
36
37    /// Delete old traces
38    Clean {
39        /// Keep only last N traces
40        #[arg(short, long, default_value = "10")]
41        keep: usize,
42    },
43}
44
45pub fn handle_trace_command(action: TraceAction) -> Result<(), NikaError> {
46    match action {
47        TraceAction::List { limit } => {
48            let traces = nika_engine::list_traces()?;
49            let traces = match limit {
50                Some(n) => traces.into_iter().take(n).collect::<Vec<_>>(),
51                None => traces,
52            };
53
54            println!("Found {} traces:\n", traces.len());
55            println!("{:<30} {:>10} {:>20}", "GENERATION ID", "SIZE", "CREATED");
56            println!("{}", "-".repeat(62));
57
58            for trace in traces {
59                let size = if trace.size_bytes > 1024 * 1024 {
60                    format!("{:.1}MB", trace.size_bytes as f64 / 1024.0 / 1024.0)
61                } else if trace.size_bytes > 1024 {
62                    format!("{:.1}KB", trace.size_bytes as f64 / 1024.0)
63                } else {
64                    format!("{}B", trace.size_bytes)
65                };
66
67                let created = trace
68                    .created
69                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
70                    .map(|d| {
71                        chrono::DateTime::from_timestamp(d.as_secs() as i64, 0).map_or_else(
72                            || "unknown".to_string(),
73                            |dt| dt.format("%Y-%m-%d %H:%M").to_string(),
74                        )
75                    })
76                    .unwrap_or_else(|| "unknown".to_string());
77
78                println!("{:<30} {:>10} {:>20}", trace.generation_id, size, created);
79            }
80            Ok(())
81        }
82
83        TraceAction::Show { id } => {
84            let traces = nika_engine::list_traces()?;
85            let trace = traces
86                .iter()
87                .find(|t| t.generation_id.contains(&id))
88                .ok_or_else(|| NikaError::ValidationError {
89                    reason: format!("No trace matching '{id}'"),
90                })?;
91
92            let content = fs::read_to_string(&trace.path)?;
93            let events: Vec<Event> = content
94                .lines()
95                .filter_map(|line| serde_json::from_str(line).ok())
96                .collect();
97
98            println!("Trace: {}", trace.generation_id);
99            println!("Events: {}", events.len());
100            println!("Size: {} bytes\n", trace.size_bytes);
101
102            for event in events {
103                println!("[{:>6}ms] {:?}", event.timestamp_ms, event.kind);
104            }
105            Ok(())
106        }
107
108        TraceAction::Export { id, format, output } => {
109            let traces = nika_engine::list_traces()?;
110            let trace = traces
111                .iter()
112                .find(|t| t.generation_id.contains(&id))
113                .ok_or_else(|| NikaError::ValidationError {
114                    reason: format!("No trace matching '{id}'"),
115                })?;
116
117            let content = fs::read_to_string(&trace.path)?;
118            let events: Vec<Event> = content
119                .lines()
120                .filter_map(|line| serde_json::from_str(line).ok())
121                .collect();
122
123            let exported = match format.as_str() {
124                "json" => serde_json::to_string_pretty(&events)?,
125                "yaml" => nika_engine::serde_yaml::to_string(&events).map_err(|e| {
126                    NikaError::SerializationError {
127                        details: e.to_string(),
128                    }
129                })?,
130                other => {
131                    return Err(NikaError::ValidationError {
132                        reason: format!("Unknown format: {other}. Use 'json' or 'yaml'"),
133                    })
134                }
135            };
136
137            match output {
138                Some(path) => {
139                    fs::write(&path, &exported)?;
140                    println!("Exported {} events to {}", events.len(), path.display());
141                }
142                None => println!("{exported}"),
143            }
144            Ok(())
145        }
146
147        TraceAction::Clean { keep } => {
148            let traces = nika_engine::list_traces()?;
149            let to_delete: Vec<_> = traces.into_iter().skip(keep).collect();
150            let count = to_delete.len();
151
152            for trace in to_delete {
153                fs::remove_file(&trace.path)?;
154            }
155
156            println!("Deleted {count} old traces, kept {keep}");
157            Ok(())
158        }
159    }
160}