use anyhow::{Result, Context};
use colored::Colorize;
use tabled::{Table, Tabled, settings::Style};
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc, Duration};
use crate::config::Config;
use crate::commands::TraceCommands;
use crate::utils::{print_success, print_error, print_info};
#[derive(Tabled, Serialize, Deserialize)]
struct TraceRow {
#[tabled(rename = "Trace ID")]
trace_id: String,
#[tabled(rename = "Service")]
service: String,
#[tabled(rename = "Operation")]
operation: String,
#[tabled(rename = "Duration")]
duration: String,
#[tabled(rename = "Status")]
status: String,
#[tabled(rename = "Time")]
timestamp: String,
}
#[derive(Serialize, Deserialize)]
struct SpanDetail {
span_id: String,
parent_span_id: Option<String>,
operation: String,
service: String,
start_time: DateTime<Utc>,
duration_ms: u64,
status: String,
attributes: std::collections::HashMap<String, String>,
}
pub async fn handle_command(command: TraceCommands, config: &Config) -> Result<()> {
match command {
TraceCommands::List { service, range, limit } => {
list_traces(service, &range, limit, config).await?
}
TraceCommands::Get { trace_id } => {
get_trace_details(&trace_id, config).await?
}
TraceCommands::Search { query, range } => {
search_traces(&query, &range, config).await?
}
TraceCommands::Export { output, format, range } => {
export_traces(&output, &format, &range, config).await?
}
}
Ok(())
}
async fn list_traces(
service_filter: Option<String>,
range: &str,
limit: usize,
config: &Config,
) -> Result<()> {
let duration = parse_duration(range)?;
let since = Utc::now() - duration;
let traces = vec![
TraceRow {
trace_id: "a1b2c3d4e5f6".to_string(),
service: "user-service".to_string(),
operation: "GET /api/users/:id".to_string(),
duration: "45ms".to_string(),
status: "success".green().to_string(),
timestamp: "2024-01-15 10:23:45".to_string(),
},
TraceRow {
trace_id: "f6e5d4c3b2a1".to_string(),
service: "order-service".to_string(),
operation: "POST /api/orders".to_string(),
duration: "234ms".to_string(),
status: "error".red().to_string(),
timestamp: "2024-01-15 10:22:30".to_string(),
},
];
let filtered_traces: Vec<_> = if let Some(filter) = service_filter {
traces.into_iter()
.filter(|t| t.service.contains(&filter))
.take(limit)
.collect()
} else {
traces.into_iter().take(limit).collect()
};
if filtered_traces.is_empty() {
print_info(&format!("No traces found in the last {}", range));
} else {
println!("{}", format!("Traces (last {})", range).bold());
println!("{}", "=".repeat(80));
let table = Table::new(&filtered_traces)
.with(Style::modern())
.to_string();
println!("{}", table);
println!("\n{}: {} traces shown",
"Total".bold(),
filtered_traces.len()
);
}
Ok(())
}
async fn get_trace_details(trace_id: &str, config: &Config) -> Result<()> {
print_info(&format!("Fetching details for trace: {}", trace_id));
let spans = vec![
SpanDetail {
span_id: "span-1".to_string(),
parent_span_id: None,
operation: "HTTP GET /api/users/123".to_string(),
service: "gateway".to_string(),
start_time: Utc::now() - Duration::milliseconds(100),
duration_ms: 95,
status: "ok".to_string(),
attributes: [
("http.method".to_string(), "GET".to_string()),
("http.status_code".to_string(), "200".to_string()),
("user.id".to_string(), "123".to_string()),
].into_iter().collect(),
},
SpanDetail {
span_id: "span-2".to_string(),
parent_span_id: Some("span-1".to_string()),
operation: "database.query".to_string(),
service: "user-service".to_string(),
start_time: Utc::now() - Duration::milliseconds(80),
duration_ms: 45,
status: "ok".to_string(),
attributes: [
("db.statement".to_string(), "SELECT * FROM users WHERE id = ?".to_string()),
("db.type".to_string(), "postgresql".to_string()),
].into_iter().collect(),
},
];
println!("\n{}", "Trace Details".bold());
println!("{}", "=".repeat(80));
println!("{}: {}", "Trace ID".bold(), trace_id);
println!("{}: {} spans", "Total Spans".bold(), spans.len());
println!("{}: {}ms", "Total Duration".bold(), spans.iter().map(|s| s.duration_ms).max().unwrap_or(0));
println!("\n{}", "Span Tree".bold());
println!("{}", "-".repeat(80));
for (i, span) in spans.iter().enumerate() {
let indent = if span.parent_span_id.is_some() { " └─ " } else { "" };
println!("{}{} {} ({} ms) {}",
indent,
span.service.cyan(),
span.operation,
span.duration_ms,
if span.status == "ok" {
"✓".green().to_string()
} else {
"✗".red().to_string()
}
);
if !span.attributes.is_empty() {
let attr_indent = if span.parent_span_id.is_some() { " " } else { " " };
for (key, value) in &span.attributes {
println!("{}{}: {}", attr_indent, key.dimmed(), value);
}
}
}
Ok(())
}
async fn search_traces(query: &str, range: &str, config: &Config) -> Result<()> {
print_info(&format!("Searching traces for: {}", query));
let duration = parse_duration(range)?;
let since = Utc::now() - duration;
let results = vec![
TraceRow {
trace_id: "search-result-1".to_string(),
service: "user-service".to_string(),
operation: "GET /api/users/search".to_string(),
duration: "123ms".to_string(),
status: "success".green().to_string(),
timestamp: "2024-01-15 09:45:23".to_string(),
},
];
if results.is_empty() {
print_info(&format!("No traces found matching '{}' in the last {}", query, range));
} else {
println!("\n{}", format!("Search Results for '{}' (last {})", query, range).bold());
println!("{}", "=".repeat(80));
let table = Table::new(&results)
.with(Style::modern())
.to_string();
println!("{}", table);
println!("\n{}: {} traces found",
"Total".bold(),
results.len()
);
}
Ok(())
}
async fn export_traces(
output: &str,
format: &str,
range: &str,
config: &Config,
) -> Result<()> {
print_info(&format!("Exporting traces from the last {} to {}", range, output));
let duration = parse_duration(range)?;
let since = Utc::now() - duration;
let traces = vec![
TraceRow {
trace_id: "export-1".to_string(),
service: "user-service".to_string(),
operation: "GET /api/users".to_string(),
duration: "32ms".to_string(),
status: "success".to_string(),
timestamp: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
},
];
let content = match format {
"json" => serde_json::to_string_pretty(&traces)?,
"csv" => {
let mut wtr = csv::Writer::from_writer(vec![]);
for trace in &traces {
wtr.serialize(trace)?;
}
String::from_utf8(wtr.into_inner()?)?
}
_ => return Err(anyhow::anyhow!("Unsupported export format: {}", format)),
};
std::fs::write(output, content)?;
print_success(&format!(
"Exported {} traces to {} in {} format",
traces.len(),
output,
format
));
Ok(())
}
fn parse_duration(duration_str: &str) -> Result<Duration> {
let len = duration_str.len();
if len < 2 {
return Err(anyhow::anyhow!("Invalid duration format"));
}
let (num_str, unit) = duration_str.split_at(len - 1);
let num: i64 = num_str.parse()
.context("Invalid duration number")?;
let duration = match unit {
"s" => Duration::seconds(num),
"m" => Duration::minutes(num),
"h" => Duration::hours(num),
"d" => Duration::days(num),
_ => return Err(anyhow::anyhow!("Invalid duration unit. Use s, m, h, or d")),
};
Ok(duration)
}