use anyhow::{Context, Result};
use reqwest::Client;
use serde::Deserialize;
use crate::Cli;
#[derive(Debug, Deserialize)]
struct GcResponse {
removed: u64,
message: String,
}
pub async fn handle_gc_multipart(
client: &Client,
cli: &Cli,
bucket: Option<&str>,
retention_hours: u64,
) -> Result<()> {
let mut url = format!(
"{}/api/admin/gc/multipart?retention_hours={}",
cli.endpoint, retention_hours
);
if let Some(b) = bucket {
url.push_str(&format!("&bucket={}", urlencoding_simple(b)));
}
let response = client
.post(&url)
.send()
.await
.context("Failed to reach /api/admin/gc/multipart endpoint")?;
let status = response.status();
if status.is_success() {
let gc: GcResponse = response
.json()
.await
.context("Server returned non-JSON response from gc endpoint")?;
println!("{}", gc.message);
println!("Removed: {}", gc.removed);
Ok(())
} else {
let body = response.text().await.unwrap_or_default();
Err(anyhow::anyhow!(
"GC endpoint returned HTTP {} — {}",
status,
body.lines().next().unwrap_or("(empty body)")
))
}
}
struct MetricFamily {
help: Option<String>,
type_hint: Option<String>,
name: String,
samples: Vec<String>,
}
pub async fn handle_show_metrics(client: &Client, cli: &Cli, filter: Option<&str>) -> Result<()> {
let url = format!("{}/metrics", cli.endpoint);
let response = client
.get(&url)
.send()
.await
.context("Failed to reach /metrics endpoint")?;
let status = response.status();
if !status.is_success() {
return Err(anyhow::anyhow!(
"Metrics endpoint returned HTTP {}",
status
));
}
let body = response
.text()
.await
.context("Failed to read metrics response body")?;
let families = parse_prometheus_text(&body);
let filter_lower = filter.map(|f| f.to_lowercase());
let mut printed = 0usize;
for family in &families {
if let Some(ref f) = filter_lower {
if !family.name.to_lowercase().contains(f.as_str()) {
continue;
}
}
if let Some(ref help) = family.help {
println!("# {}", help);
}
if let Some(ref type_hint) = family.type_hint {
println!("# TYPE {} {}", family.name, type_hint);
}
for sample in &family.samples {
println!("{}", sample);
}
println!(); printed += 1;
}
if printed == 0 {
if let Some(f) = filter {
println!("No metrics matching filter '{}'", f);
} else {
println!("(no metrics returned)");
}
} else {
println!("--- {} metric familie(s) shown ---", printed);
}
Ok(())
}
fn parse_prometheus_text(text: &str) -> Vec<MetricFamily> {
let mut families: Vec<MetricFamily> = Vec::new();
let mut current: Option<MetricFamily> = None;
for line in text.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("# HELP ") {
if let Some(f) = current.take() {
families.push(f);
}
let (name, help_text) = split_first_word(rest);
current = Some(MetricFamily {
help: Some(format!("HELP {} {}", name, help_text)),
type_hint: None,
name: name.to_string(),
samples: Vec::new(),
});
} else if let Some(rest) = line.strip_prefix("# TYPE ") {
let (name, type_str) = split_first_word(rest);
if let Some(ref mut f) = current {
if f.name == name || f.name.is_empty() {
f.type_hint = Some(type_str.to_string());
f.name = name.to_string();
}
} else {
current = Some(MetricFamily {
help: None,
type_hint: Some(type_str.to_string()),
name: name.to_string(),
samples: Vec::new(),
});
}
} else if line.starts_with('#') || line.is_empty() {
} else {
if let Some(ref mut f) = current {
f.samples.push(line.to_string());
} else {
let name = line
.split(|c: char| !c.is_alphanumeric() && c != '_')
.next()
.unwrap_or("unknown")
.to_string();
let mut anon = MetricFamily {
help: None,
type_hint: None,
name,
samples: Vec::new(),
};
anon.samples.push(line.to_string());
current = Some(anon);
}
}
}
if let Some(f) = current {
families.push(f);
}
families
}
fn split_first_word(s: &str) -> (&str, &str) {
let s = s.trim();
if let Some(pos) = s.find(|c: char| c.is_whitespace()) {
(s[..pos].trim(), s[pos..].trim())
} else {
(s, "")
}
}
fn urlencoding_simple(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => {
out.push('%');
out.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
out.push(char::from_digit((byte & 0xF) as u32, 16).unwrap_or('0'));
}
}
}
out
}