use std::fmt;
use std::future::Future;
use std::pin::Pin;
use axum::handler::Handler;
use axum::routing::MethodRouter;
use http::Method;
use kerniq_core::config::DEFAULT_MAX_BODY_SIZE;
use kerniq_core::{Error, Result, ServerConfig};
use kerniq_middleware::{BodyLimit, ConcurrencyLimit, Timeout};
use kerniq_routing::{Group, Middleware, MiddlewareStack, RouteInfo, Router, TowerLayer};
use kerniq_runtime::{BackgroundTasks, ServeOptions, Shutdown};
use tokio::net::TcpListener;
type Signal = Pin<Box<dyn Future<Output = ()> + Send>>;
type TaskStarter = Box<dyn FnOnce(&BackgroundTasks) + Send>;
#[must_use = "an App does nothing until it is run"]
pub struct App<S = ()> {
router: Router<S>,
state: S,
middleware: MiddlewareStack<()>,
config: ServerConfig,
custom_fallback: bool,
shutdown: Shutdown,
signal: Option<Signal>,
tasks: Vec<TaskStarter>,
#[cfg(feature = "observability")]
observability: Option<kerniq_observability::Observability>,
}
impl<S> fmt::Debug for App<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut d = f.debug_struct("App");
let d = d
.field("router", &self.router)
.field("middleware", &self.middleware)
.field("config", &self.config)
.field("background_tasks", &self.tasks.len());
#[cfg(feature = "observability")]
let d = d.field("observability", &self.observability);
d.finish_non_exhaustive()
}
}
impl Default for App<()> {
fn default() -> Self {
Self::new()
}
}
impl App<()> {
pub fn new() -> Self {
Self {
router: Router::new(),
state: (),
middleware: MiddlewareStack::default(),
config: ServerConfig::default(),
custom_fallback: false,
shutdown: Shutdown::new(),
signal: None,
tasks: Vec::new(),
#[cfg(feature = "observability")]
observability: None,
}
}
pub fn state<T>(self, state: T) -> App<T>
where
T: Clone + Send + Sync + 'static,
{
App {
router: self.router.with_state(()),
state,
middleware: self.middleware,
config: self.config,
custom_fallback: self.custom_fallback,
shutdown: self.shutdown,
signal: self.signal,
tasks: self.tasks,
#[cfg(feature = "observability")]
observability: self.observability,
}
}
}
macro_rules! delegate_methods {
($($(#[$doc:meta])* $name:ident;)*) => {
$(
$(#[$doc])*
pub fn $name<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
self.router = self.router.$name(path, handler);
self
}
)*
};
}
impl<S> App<S>
where
S: Clone + Send + Sync + 'static,
{
delegate_methods! {
get;
post;
put;
patch;
delete;
options;
head;
trace;
any;
}
pub fn on<H, T>(mut self, method: Method, path: &str, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
self.router = self.router.on(method, path, handler);
self
}
pub fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
self.router = self.router.route(path, method_router);
self
}
pub fn group(mut self, f: impl FnOnce(&mut Group<'_, S>)) -> Self {
self.router = self.router.group(f);
self
}
pub fn mount(mut self, path: &str, router: Router<S>) -> Self {
self.router = self.router.mount(path, router);
self
}
pub fn merge(mut self, router: Router<S>) -> Self {
self.router = self.router.merge(router);
self
}
pub fn mount_axum(mut self, path: &str, router: axum::Router<S>) -> Self {
self.router = self.router.mount_axum(path, router);
self
}
pub fn middleware<M: Middleware>(mut self, middleware: M) -> Self {
self.middleware.push(middleware);
self
}
pub fn layer<L>(self, layer: L) -> Self
where
TowerLayer<L>: Middleware,
{
self.middleware(TowerLayer(layer))
}
pub fn fallback<H, T>(mut self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
self.router = self.router.fallback(handler);
self.custom_fallback = true;
self
}
#[cfg(feature = "observability")]
pub fn observability(mut self, observability: kerniq_observability::Observability) -> Self {
self.observability = Some(observability);
self
}
pub fn config(mut self, config: ServerConfig) -> Self {
self.config = config;
self
}
pub fn server_config(&self) -> &ServerConfig {
&self.config
}
pub fn background_task<F, Fut>(mut self, name: &'static str, task: F) -> Self
where
F: FnOnce(Shutdown) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.tasks.push(Box::new(move |tasks: &BackgroundTasks| {
tasks.spawn(name, task)
}));
self
}
pub fn shutdown_signal<F>(mut self, signal: F) -> Self
where
F: Future<Output = ()> + Send + 'static,
{
self.signal = Some(Box::pin(signal));
self
}
pub fn shutdown_handle(&self) -> Shutdown {
self.shutdown.clone()
}
pub fn routes(&self) -> &[RouteInfo] {
self.router.routes()
}
pub fn into_axum_router(self) -> axum::Router {
self.into_parts().0
}
fn into_parts(self) -> (axum::Router, Lifecycle) {
let App {
router,
state,
middleware,
config,
custom_fallback,
shutdown,
signal,
tasks,
#[cfg(feature = "observability")]
observability,
} = self;
let router = router.into_axum();
let router = if custom_fallback {
router
} else {
router.fallback(not_found)
};
let router = router
.method_not_allowed_fallback(method_not_allowed)
.with_state(state);
let mut middleware = middleware;
if let Some(timeout) = config.request_timeout {
middleware.push(Timeout::new(timeout));
}
if let Some(max) = config.max_concurrent_requests {
middleware.push(ConcurrencyLimit::new(max));
}
if config.max_body_size != DEFAULT_MAX_BODY_SIZE {
middleware.push(BodyLimit::new(config.max_body_size));
}
let router = middleware.apply(router);
#[cfg(feature = "observability")]
let router = match &observability {
Some(observability) => attach_observability(router, observability),
None => router,
};
(
router,
Lifecycle {
config,
shutdown,
signal,
tasks,
#[cfg(feature = "observability")]
observability,
},
)
}
pub async fn run(self, address: impl AsRef<str>) -> Result<()> {
let listener = kerniq_runtime::bind(address.as_ref()).await?;
self.serve_inner(listener, false).await
}
pub async fn serve(self, listener: TcpListener) -> Result<()> {
self.serve_inner(listener, false).await
}
pub fn run_blocking(self, address: impl AsRef<str>) -> Result<()> {
let workers = self.config.workers;
let timeout = self.config.shutdown_timeout;
let address = address.as_ref().to_owned();
kerniq_runtime::runtime::block_on(workers, timeout, async move {
let listener = kerniq_runtime::bind(&address).await?;
self.serve_inner(listener, true).await
})?
}
async fn serve_inner(self, listener: TcpListener, owns_runtime: bool) -> Result<()> {
if !owns_runtime && self.config.workers.is_some() {
tracing::warn!(
"`ServerConfig::workers` is ignored by `run`/`serve` because the application owns \
the Tokio runtime; configure `#[tokio::main(worker_threads = N)]` or use `run_blocking`"
);
}
for route in self.router.routes() {
tracing::debug!(method = %route.method, path = %route.path, "route registered");
}
let (router, lifecycle) = self.into_parts();
let Lifecycle {
config,
shutdown,
signal,
tasks,
#[cfg(feature = "observability")]
observability,
} = lifecycle;
let background = BackgroundTasks::new(shutdown.clone());
#[cfg(feature = "observability")]
if let Some(observability) = &observability {
start_metrics_listener(observability, &background).await?;
}
for start in tasks {
start(&background);
}
let signal = signal.unwrap_or_else(|| Box::pin(kerniq_runtime::os_signal()));
let signal_task = tokio::spawn({
let shutdown = shutdown.clone();
async move {
tokio::select! {
() = signal => shutdown.trigger(),
() = shutdown.wait() => {}
}
}
});
let result = kerniq_runtime::serve(
listener,
router,
ServeOptions::from(&config),
shutdown.clone(),
)
.await;
shutdown.trigger();
signal_task.abort();
if !background.is_empty() {
background.shutdown(config.shutdown_timeout).await;
}
tracing::info!("kerniq server stopped");
#[cfg(feature = "observability")]
if let Some(observability) = observability {
let report = observability.shutdown().await;
if report.timed_out || !report.errors.is_empty() {
tracing::warn!(
duration = ?report.duration,
errors = ?report.errors,
"telemetry shutdown incomplete"
);
}
}
result.map_err(Error::from)
}
}
struct Lifecycle {
config: ServerConfig,
shutdown: Shutdown,
signal: Option<Signal>,
tasks: Vec<TaskStarter>,
#[cfg(feature = "observability")]
observability: Option<kerniq_observability::Observability>,
}
#[cfg(feature = "observability")]
fn attach_observability(
router: axum::Router,
observability: &kerniq_observability::Observability,
) -> axum::Router {
let router = observability.http_instrumentation().apply(router);
#[cfg(feature = "prometheus")]
if let Some(prometheus) = observability.prometheus()
&& prometheus.config().listen_address().is_none()
{
return router.merge(prometheus.router());
}
router
}
#[cfg(feature = "observability")]
#[allow(clippy::unused_async)]
async fn start_metrics_listener(
observability: &kerniq_observability::Observability,
background: &BackgroundTasks,
) -> Result<()> {
#[cfg(feature = "prometheus")]
if let Some(prometheus) = observability.prometheus()
&& let Some(address) = prometheus.config().listen_address()
{
let listener = kerniq_runtime::bind(&address.to_string()).await?;
let prometheus = prometheus.clone();
background.spawn("prometheus-metrics", move |shutdown: Shutdown| async move {
if let Err(err) = prometheus.serve(listener, shutdown.wait_owned()).await {
tracing::error!(error = %err, "Prometheus metrics listener failed");
}
});
}
let _ = (observability, background);
Ok(())
}
async fn not_found() -> Error {
Error::not_found("No route matches the requested path")
}
async fn method_not_allowed() -> Error {
Error::method_not_allowed("The route does not support this HTTP method")
}
impl<S> From<App<S>> for axum::Router
where
S: Clone + Send + Sync + 'static,
{
fn from(app: App<S>) -> Self {
app.into_axum_router()
}
}