use clap::Subcommand;
use std::fs;
use std::path::PathBuf;
use nika::error::NikaError;
use nika::Event;
#[derive(Subcommand)]
pub enum TraceAction {
List {
#[arg(short, long)]
limit: Option<usize>,
},
Show {
id: String,
},
Export {
id: String,
#[arg(short, long, default_value = "json")]
format: String,
#[arg(short, long)]
output: Option<PathBuf>,
},
Clean {
#[arg(short, long, default_value = "10")]
keep: usize,
},
}
pub fn handle_trace_command(action: TraceAction) -> Result<(), NikaError> {
match action {
TraceAction::List { limit } => {
let traces = nika::list_traces()?;
let traces = match limit {
Some(n) => traces.into_iter().take(n).collect::<Vec<_>>(),
None => traces,
};
println!("Found {} traces:\n", traces.len());
println!("{:<30} {:>10} {:>20}", "GENERATION ID", "SIZE", "CREATED");
println!("{}", "-".repeat(62));
for trace in traces {
let size = if trace.size_bytes > 1024 * 1024 {
format!("{:.1}MB", trace.size_bytes as f64 / 1024.0 / 1024.0)
} else if trace.size_bytes > 1024 {
format!("{:.1}KB", trace.size_bytes as f64 / 1024.0)
} else {
format!("{}B", trace.size_bytes)
};
let created = trace
.created
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| {
chrono::DateTime::from_timestamp(d.as_secs() as i64, 0)
.map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
.unwrap_or_else(|| "unknown".to_string())
})
.unwrap_or_else(|| "unknown".to_string());
println!("{:<30} {:>10} {:>20}", trace.generation_id, size, created);
}
Ok(())
}
TraceAction::Show { id } => {
let traces = nika::list_traces()?;
let trace = traces
.iter()
.find(|t| t.generation_id.contains(&id))
.ok_or_else(|| NikaError::ValidationError {
reason: format!("No trace matching '{}'", id),
})?;
let content = fs::read_to_string(&trace.path)?;
let events: Vec<Event> = content
.lines()
.filter_map(|line| serde_json::from_str(line).ok())
.collect();
println!("Trace: {}", trace.generation_id);
println!("Events: {}", events.len());
println!("Size: {} bytes\n", trace.size_bytes);
for event in events {
println!("[{:>6}ms] {:?}", event.timestamp_ms, event.kind);
}
Ok(())
}
TraceAction::Export { id, format, output } => {
let traces = nika::list_traces()?;
let trace = traces
.iter()
.find(|t| t.generation_id.contains(&id))
.ok_or_else(|| NikaError::ValidationError {
reason: format!("No trace matching '{}'", id),
})?;
let content = fs::read_to_string(&trace.path)?;
let events: Vec<Event> = content
.lines()
.filter_map(|line| serde_json::from_str(line).ok())
.collect();
let exported = match format.as_str() {
"json" => serde_json::to_string_pretty(&events)?,
"yaml" => nika::serde_yaml::to_string(&events).map_err(|e| {
NikaError::SerializationError {
details: e.to_string(),
}
})?,
other => {
return Err(NikaError::ValidationError {
reason: format!("Unknown format: {}. Use 'json' or 'yaml'", other),
})
}
};
match output {
Some(path) => {
fs::write(&path, &exported)?;
println!("Exported {} events to {}", events.len(), path.display());
}
None => println!("{}", exported),
}
Ok(())
}
TraceAction::Clean { keep } => {
let traces = nika::list_traces()?;
let to_delete: Vec<_> = traces.into_iter().skip(keep).collect();
let count = to_delete.len();
for trace in to_delete {
fs::remove_file(&trace.path)?;
}
println!("Deleted {} old traces, kept {}", count, keep);
Ok(())
}
}
}