use std::{any::Any, collections::HashMap, net::Ipv4Addr, time::Duration};
use kynos::{
http,
middleware::{Observer, request_id::RequestId, trace::Trace},
prelude::*,
router::operation::Route,
server::Server,
};
const BUDGETS: &[(&str, Duration)] = &[
("list_users", Duration::from_millis(50)),
("rebuild_index", Duration::from_secs(2)),
];
struct Slo {
budgets: HashMap<&'static str, Duration>,
}
impl Slo {
fn new() -> Self {
Self {
budgets: BUDGETS.iter().copied().collect(),
}
}
}
impl<C> Observer<C> for Slo {
fn on_request(&self, request: &http::Request, route: Option<Route<'_>>, context: &C) {
let _ = context;
let Some(route) = route else {
tracing::debug!(target: "slo", path = %request.uri().path(), "unmatched");
return;
};
tracing::trace!(target: "slo", operation_id = route.operation_id(), "started");
}
fn on_response(&self, response: &http::Response, route: Option<Route<'_>>, elapsed: Duration) {
let Some(route) = route else { return };
let Some(budget) = self.budgets.get(route.operation_id()) else {
return;
};
if elapsed > *budget {
tracing::warn!(
target: "slo",
operation_id = route.operation_id(),
matched_path = route.path(),
status = response.status().as_u16(),
elapsed_ms = elapsed.as_millis(),
budget_ms = budget.as_millis(),
"over budget",
);
}
}
fn on_disconnect(&self, route: Option<Route<'_>>, elapsed: Duration) {
tracing::info!(
target: "slo",
operation_id = route.map_or("<unmatched>", |route| route.operation_id()),
matched_path = route.map_or("<unmatched>", |route| route.path()),
elapsed_ms = elapsed.as_millis(),
"client left before the response finished",
);
}
fn on_panic(&self, payload: &(dyn Any + Send), route: Option<Route<'_>>) {
let message = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("<non-string panic payload>");
tracing::error!(
target: "slo",
operation_id = route.map_or("<unmatched>", |route| route.operation_id()),
message,
"handler panicked",
);
}
}
#[kynos::get("/users")]
async fn list_users() -> NoContent {
tracing::info!(count = 0, "listed users");
NoContent
}
#[kynos::post("/index")]
async fn rebuild_index() -> NoContent {
tokio::time::sleep(Duration::from_secs(3)).await;
tracing::info!("index rebuilt");
NoContent
}
#[tokio::main]
async fn main() -> kynos::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let router = Router::<()>::new()
.intercept(RequestId::new().trust_client(false))
.observe(
Trace::new()
.level(tracing::Level::DEBUG)
.record_headers(&["x-request-id"]),
)
.observe(Slo::new())
.mount(kynos::routes![list_users, rebuild_index]);
let document = router.openapi()?;
println!("{}", document.to_json()?);
Server::new(router.build(())?)
.bind((Ipv4Addr::UNSPECIFIED, 3000))
.serve()
.await
}