use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use ullun::prelude::*;
#[derive(Default)]
struct Metrics {
requests: AtomicU64,
errors: AtomicU64,
in_flight: AtomicU64,
}
#[tokio::main]
async fn main() {
let metrics = Arc::new(Metrics::default());
let next_id = Arc::new(AtomicU64::new(1));
let request_id = move |req: Request, next: Next| {
let next_id = next_id.clone();
async move {
let id = req
.headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.filter(|v| !v.is_empty() && v.len() <= 64 && v.is_ascii())
.map(str::to_owned)
.unwrap_or_else(|| format!("req-{:08x}", next_id.fetch_add(1, Ordering::Relaxed)));
let mut res = match next.run(req).await {
Ok(res) => res,
Err(err) => Response::from_error(err),
};
res = res.with_header("x-request-id", id);
Ok(res)
}
};
let m = metrics.clone();
let measure = move |req: Request, next: Next| {
let m = m.clone();
async move {
m.requests.fetch_add(1, Ordering::Relaxed);
m.in_flight.fetch_add(1, Ordering::Relaxed);
let result = next.run(req).await;
m.in_flight.fetch_sub(1, Ordering::Relaxed);
let failed = match &result {
Ok(res) => res.status.as_u16() >= 400,
Err(_) => true,
};
if failed {
m.errors.fetch_add(1, Ordering::Relaxed);
}
result
}
};
let m = metrics.clone();
let metrics_endpoint = move |_req: Request| {
let m = m.clone();
async move {
let body = format!(
"# TYPE http_requests_total counter\nhttp_requests_total {}\n\
# TYPE http_errors_total counter\nhttp_errors_total {}\n\
# TYPE http_requests_in_flight gauge\nhttp_requests_in_flight {}\n",
m.requests.load(Ordering::Relaxed),
m.errors.load(Ordering::Relaxed),
m.in_flight.load(Ordering::Relaxed),
);
Ok(Response::text(body)
.with_header("content-type", "text/plain; version=0.0.4; charset=utf-8"))
}
};
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".into());
App::new()
.middleware(request_id)
.middleware(measure)
.get("/healthz", |_req: Request| async {
Ok(Response::json(serde_json::json!({ "status": "alive" })))
})
.get("/readyz", |_req: Request| async {
Ok(Response::json(serde_json::json!({ "status": "ready" })))
})
.get("/metrics", metrics_endpoint)
.get_with_params("/orders/{id}", |params: Params| async move {
let id: u64 = params
.get("id")?
.parse()
.map_err(|_| Error::bad_request("order id must be a number"))?;
if id == 0 {
return Err(Error::not_found("order not found"));
}
Ok(Response::json(
serde_json::json!({ "id": id, "status": "shipped" }),
))
})
.run(&format!("0.0.0.0:{port}"))
.await
.unwrap();
}