use crate::aggregator::{Aggregator, EVENTS_RECEIVED, EVENTS_RESOLVED};
use crate::diff;
use crate::leak_report;
use std::io::Cursor;
use std::sync::{atomic::Ordering, Arc, Mutex};
use tiny_http::{Header, Method, Response, Server};
pub fn serve(port: u16, aggregator: Arc<Aggregator>) {
let server = Server::http(format!("127.0.0.1:{port}"))
.unwrap_or_else(|e| panic!("Cannot bind API port {port}: {e}"));
let baseline: Arc<Mutex<Option<Vec<crate::aggregator::LineStats>>>> =
Arc::new(Mutex::new(None));
eprintln!("[ferroalloc] API listening on http://127.0.0.1:{port}");
for request in server.incoming_requests() {
let url = request.url().to_string();
let (status, body) = match (request.method(), url.as_str()) {
(Method::Get, "/snapshot") => {
let json = serde_json::to_string(&aggregator.snapshot()).unwrap_or_default();
(200, json)
}
(Method::Get, "/leaks") => {
let json = serde_json::to_string(&aggregator.live_leaks()).unwrap_or_default();
(200, json)
}
(Method::Get, u) if u.starts_with("/leak-report") => {
let min_bytes = parse_query_param(u, "min_bytes").unwrap_or(0);
let report = leak_report::build(&aggregator, min_bytes);
let json = serde_json::to_string(&report).unwrap_or_default();
(200, json)
}
(Method::Post, "/baseline") => {
*baseline.lock().unwrap() = Some(aggregator.snapshot());
(200, r#"{"status":"baseline saved"}"#.to_string())
}
(Method::Get, "/diff") => {
let guard = baseline.lock().unwrap();
match guard.as_ref() {
Some(base) => {
let current = aggregator.snapshot();
let d = diff::compute(base, ¤t);
let json = serde_json::to_string(&d).unwrap_or_default();
(200, json)
}
None => (
400,
r#"{"error":"no baseline set — POST /baseline first"}"#.to_string(),
),
}
}
(Method::Post, "/reset") => {
aggregator.reset();
*baseline.lock().unwrap() = None;
(200, r#"{"status":"reset"}"#.to_string())
}
(Method::Get, "/health") => {
let body = format!(
r#"{{"status":"ok","events_received":{},"events_resolved":{}}}"#,
EVENTS_RECEIVED.load(Ordering::Relaxed),
EVENTS_RESOLVED.load(Ordering::Relaxed),
);
(200, body)
}
_ => (404, r#"{"error":"not found"}"#.to_string()),
};
let len = body.len();
let response = Response::new(
tiny_http::StatusCode(status),
vec![
Header::from_bytes("Content-Type", "application/json").unwrap(),
Header::from_bytes("Access-Control-Allow-Origin", "*").unwrap(),
Header::from_bytes("Content-Length", len.to_string().as_str()).unwrap(),
],
Cursor::new(body),
Some(len),
None,
);
let _ = request.respond(response);
}
}
fn parse_query_param(url: &str, key: &str) -> Option<usize> {
let query = url.split('?').nth(1)?;
for pair in query.split('&') {
let mut parts = pair.splitn(2, '=');
if parts.next()? == key {
return parts.next()?.parse().ok();
}
}
None
}