use std::path::PathBuf;
use std::time::Duration;
use reqwest::StatusCode;
#[derive(Debug, Clone)]
pub struct PprofBundle {
pub dir: PathBuf,
pub profile_bytes: u64,
pub trace_bytes: u64,
pub seconds: u32,
}
impl PprofBundle {
pub fn summary(&self) -> String {
format!(
"pprof bundle ready · {}s sampling · profile {} · trace {} → {}",
self.seconds,
human_bytes(self.profile_bytes),
human_bytes(self.trace_bytes),
self.dir.display(),
)
}
}
pub async fn fetch_and_write(
base_url: &str,
auth_token: Option<String>,
seconds: u32,
dir: PathBuf,
) -> Result<PprofBundle, String> {
std::fs::create_dir_all(&dir).map_err(|e| format!("mkdir {}: {e}", dir.display()))?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(u64::from(seconds) + 30))
.build()
.map_err(|e| format!("client build: {e}"))?;
let base = base_url.trim_end_matches('/').to_string();
let profile_url = format!("{base}/debug/pprof/profile?seconds={seconds}");
let trace_url = format!("{base}/debug/pprof/trace?seconds={seconds}");
let (profile, trace) = tokio::join!(
fetch_one(&client, &profile_url, auth_token.as_deref()),
fetch_one(&client, &trace_url, auth_token.as_deref()),
);
let profile_bytes = profile.map_err(|e| format!("profile: {e}"))?;
let trace_bytes = trace.map_err(|e| format!("trace: {e}"))?;
let profile_path = dir.join("profile.pprof");
let trace_path = dir.join("trace.pprof");
std::fs::write(&profile_path, &profile_bytes).map_err(|e| format!("write profile: {e}"))?;
std::fs::write(&trace_path, &trace_bytes).map_err(|e| format!("write trace: {e}"))?;
Ok(PprofBundle {
dir,
profile_bytes: profile_bytes.len() as u64,
trace_bytes: trace_bytes.len() as u64,
seconds,
})
}
async fn fetch_one(
client: &reqwest::Client,
url: &str,
auth: Option<&str>,
) -> Result<Vec<u8>, String> {
let mut req = client.get(url);
if let Some(token) = auth {
req = req.bearer_auth(token);
}
let resp = req.send().await.map_err(|e| format!("GET {url}: {e}"))?;
let status = resp.status();
if status == StatusCode::NOT_FOUND {
return Err("404 — Bee's debug API isn't enabled. Add `--debug-api-enable=true` to your Bee start args (or set `debug-api-enable: true` in bee.yaml) and restart.".to_string());
}
if !status.is_success() {
return Err(format!("{url} returned HTTP {status}"));
}
let bytes = resp
.bytes()
.await
.map_err(|e| format!("read body of {url}: {e}"))?;
Ok(bytes.to_vec())
}
fn human_bytes(n: u64) -> String {
const KIB: u64 = 1 << 10;
const MIB: u64 = 1 << 20;
if n >= MIB {
format!("{:.1} MiB", n as f64 / MIB as f64)
} else if n >= KIB {
format!("{:.1} KiB", n as f64 / KIB as f64)
} else {
format!("{n} B")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_bytes_picks_unit() {
assert_eq!(human_bytes(0), "0 B");
assert_eq!(human_bytes(1023), "1023 B");
assert_eq!(human_bytes(2048), "2.0 KiB");
assert_eq!(human_bytes(2 * 1024 * 1024), "2.0 MiB");
}
#[test]
fn summary_line_includes_dir_and_byte_counts() {
let b = PprofBundle {
dir: PathBuf::from("/tmp/bee-tui-diagnostic-1234"),
profile_bytes: 12_345,
trace_bytes: 6_789,
seconds: 60,
};
let s = b.summary();
assert!(s.contains("60s sampling"));
assert!(s.contains("/tmp/bee-tui-diagnostic-1234"));
assert!(s.contains("KiB"));
}
}