use owo_colors::OwoColorize;
use crate::client::{ApiClient, PrometheusData, PrometheusResponse};
use crate::config::PartiriConfig;
use crate::error::Result;
use crate::output::{colored_job_status, ctx, format_datetime, print_result, sparkline};
fn extract_points(data: &PrometheusData) -> Vec<(f64, f64)> {
data.result
.first()
.map(|r| {
r.values
.iter()
.filter_map(|(t, v)| v.parse::<f64>().ok().map(|val| (*t, val)))
.collect()
})
.unwrap_or_default()
}
fn format_span(seconds: f64) -> String {
let (value, unit) = if seconds < 60.0 {
(seconds.round(), "second")
} else if seconds < 3600.0 {
((seconds / 60.0).round(), "minute")
} else if seconds < 86400.0 {
((seconds / 3600.0).round(), "hour")
} else {
((seconds / 86400.0).round(), "day")
};
let n = value as i64;
if n == 1 {
format!("1 {}", unit)
} else {
format!("{} {}s", n, unit)
}
}
fn format_bytes_rate(bytes: f64) -> String {
if bytes >= 1_048_576.0 {
format!("{:.1} MB/s", bytes / 1_048_576.0)
} else if bytes >= 1024.0 {
format!("{:.0} KB/s", bytes / 1024.0)
} else {
format!("{:.0} B/s", bytes)
}
}
fn print_sparkline(label: &str, resp: &PrometheusResponse, fmt: fn(f64) -> String) {
print!(" {} ", label.bold());
let points = extract_points(&resp.data);
if points.is_empty() {
println!("{}", "no data".dimmed());
} else {
let last = points.last().unwrap().1;
let vals: Vec<f64> = points
.into_iter()
.rev()
.take(30)
.rev()
.map(|(_, v)| v)
.collect();
println!("{} {}", sparkline(&vals).cyan(), fmt(last).dimmed());
}
}
fn window_span(resp: &PrometheusResponse) -> Option<String> {
let points = extract_points(&resp.data);
let trimmed: Vec<(f64, f64)> = points.into_iter().rev().take(30).rev().collect();
match (trimmed.first(), trimmed.last()) {
(Some((t0, _)), Some((t1, _))) if t1 > t0 => Some(format_span(t1 - t0)),
_ => None,
}
}
fn format_bytes(bytes: f64) -> String {
if bytes >= 1_073_741_824.0 {
format!("{:.1} GB", bytes / 1_073_741_824.0)
} else if bytes >= 1_048_576.0 {
format!("{:.0} MB", bytes / 1_048_576.0)
} else if bytes >= 1024.0 {
format!("{:.0} KB", bytes / 1024.0)
} else {
format!("{:.0} B", bytes)
}
}
pub fn run(client: &ApiClient, config: &PartiriConfig) -> Result<()> {
let id = config.id_or_err()?;
let service = client.read_service(id)?;
let deploy_tag = config.deploy_tag.as_deref();
let cpu_resp = client.read_metrics_cpu(id, deploy_tag);
let mem_resp = client.read_metrics_memory(id, deploy_tag);
let net_resp = client.read_metrics_network(id, deploy_tag);
let jobs_resp = client.list_service_jobs(id);
if ctx().json {
let cpu_points = cpu_resp
.as_ref()
.ok()
.map(|r| extract_points(&r.data))
.unwrap_or_default();
let mem_points = mem_resp
.as_ref()
.ok()
.map(|r| extract_points(&r.data))
.unwrap_or_default();
let net_down = net_resp
.as_ref()
.ok()
.map(|r| extract_points(&r.download.data))
.unwrap_or_default();
let net_up = net_resp
.as_ref()
.ok()
.map(|r| extract_points(&r.upload.data))
.unwrap_or_default();
let mut jobs_json: Vec<serde_json::Value> = Vec::new();
if let Ok(mut jobs) = jobs_resp {
jobs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
for j in jobs.into_iter().take(10) {
jobs_json.push(serde_json::json!({
"type": j.job_type,
"status": j.status,
"deploy_ref": j.deploy_ref,
"created_at": j.created_at,
}));
}
}
print_result(&serde_json::json!({
"service": {
"id": service.id,
"name": service.name,
"deploy_type": service.deploy_type,
"runtime": service.runtime,
"deploy_tag": service.deploy_tag,
"external_sd_url": service.external_sd_url,
},
"metrics": {
"cpu": cpu_points,
"memory": mem_points,
"network_download": net_down,
"network_upload": net_up,
},
"jobs": jobs_json,
}));
return Ok(());
}
println!(
"\n {} {}\n",
service.name.bold(),
format!("({})", service.deploy_type).dimmed(),
);
let span = cpu_resp
.as_ref()
.ok()
.and_then(window_span)
.or_else(|| mem_resp.as_ref().ok().and_then(window_span))
.or_else(|| {
net_resp
.as_ref()
.ok()
.and_then(|r| window_span(&r.download))
});
if let Some(s) = span {
println!(" {}\n", format!("Last {} of activity", s).dimmed());
}
match cpu_resp {
Ok(r) => print_sparkline("CPU", &r, |v| format!("{:.3} cores", v)),
Err(_) => println!(" {} {}", "CPU".bold(), "unavailable".dimmed()),
}
match mem_resp {
Ok(r) => print_sparkline("RAM", &r, format_bytes),
Err(_) => println!(" {} {}", "RAM".bold(), "unavailable".dimmed()),
}
match net_resp {
Ok(r) => {
print_sparkline("NET↓", &r.download, format_bytes_rate);
print_sparkline("NET↑", &r.upload, format_bytes_rate);
}
Err(_) => println!(" {} {}", "NET".bold(), "unavailable".dimmed()),
}
println!();
println!(" {}", "Last job".bold());
match jobs_resp {
Ok(jobs) => match jobs.first() {
Some(job) => {
let ts = job
.created_at
.as_deref()
.map(format_datetime)
.unwrap_or_else(|| "—".to_string());
let ref_str = job
.deploy_ref
.as_deref()
.map(|r| format!(" {}", r.get(..7).unwrap_or(r).dimmed()))
.unwrap_or_default();
println!(
" {} {}{} {}",
ts.dimmed(),
job.job_type.bold(),
ref_str,
colored_job_status(&job.status),
);
}
None => println!(" {}", "No jobs found.".dimmed()),
},
Err(_) => println!(" {}", "unavailable".dimmed()),
}
println!();
Ok(())
}