Skip to main content

kerniq_server/
builder.rs

1//! The [`App`] builder.
2
3use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6
7use axum::handler::Handler;
8use axum::routing::MethodRouter;
9use http::Method;
10use kerniq_core::config::DEFAULT_MAX_BODY_SIZE;
11use kerniq_core::{Error, Result, ServerConfig};
12use kerniq_middleware::{BodyLimit, ConcurrencyLimit, Timeout};
13use kerniq_routing::{Group, Middleware, MiddlewareStack, RouteInfo, Router, TowerLayer};
14use kerniq_runtime::{BackgroundTasks, ServeOptions, Shutdown};
15use tokio::net::TcpListener;
16
17type Signal = Pin<Box<dyn Future<Output = ()> + Send>>;
18type TaskStarter = Box<dyn FnOnce(&BackgroundTasks) + Send>;
19
20/// A Kerniq application: routes, state, middleware, configuration and
21/// lifecycle in one builder.
22///
23/// ```no_run
24/// use kerniq_server::prelude::*;
25///
26/// #[tokio::main]
27/// async fn main() -> Result<()> {
28///     App::new()
29///         .get("/", || async { "Hello from Kerniq" })
30///         .run("127.0.0.1:3000")
31///         .await
32/// }
33/// ```
34///
35/// # State
36///
37/// `App<S>` is parameterized by its state type. [`App::new`] creates an
38/// `App<()>`; [`App::state`] switches to `App<T>`, after which handlers can
39/// extract `State<T>`. Routes registered before `state` (which cannot use
40/// `State<T>`) are kept.
41///
42/// # Finalization
43///
44/// Nothing is assembled until the app is run or converted with
45/// [`App::into_axum_router`]. At that point:
46///
47/// 1. routes are combined into a single `axum::Router` (no runtime nesting),
48/// 2. the JSON 404/405 fallbacks are installed,
49/// 3. the state is provided,
50/// 4. middleware is applied in declaration order (first = outermost),
51///    followed by the middleware implied by [`ServerConfig`]
52///    (`request_timeout` → `max_concurrent_requests` → `max_body_size`,
53///    outermost to innermost).
54#[must_use = "an App does nothing until it is run"]
55pub struct App<S = ()> {
56    router: Router<S>,
57    state: S,
58    middleware: MiddlewareStack<()>,
59    config: ServerConfig,
60    custom_fallback: bool,
61    shutdown: Shutdown,
62    signal: Option<Signal>,
63    tasks: Vec<TaskStarter>,
64    #[cfg(feature = "observability")]
65    observability: Option<kerniq_observability::Observability>,
66}
67
68impl<S> fmt::Debug for App<S> {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        let mut d = f.debug_struct("App");
71        let d = d
72            .field("router", &self.router)
73            .field("middleware", &self.middleware)
74            .field("config", &self.config)
75            .field("background_tasks", &self.tasks.len());
76        #[cfg(feature = "observability")]
77        let d = d.field("observability", &self.observability);
78        d.finish_non_exhaustive()
79    }
80}
81
82impl Default for App<()> {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl App<()> {
89    /// Creates an application with no routes, no state and default
90    /// configuration.
91    pub fn new() -> Self {
92        Self {
93            router: Router::new(),
94            state: (),
95            middleware: MiddlewareStack::default(),
96            config: ServerConfig::default(),
97            custom_fallback: false,
98            shutdown: Shutdown::new(),
99            signal: None,
100            tasks: Vec::new(),
101            #[cfg(feature = "observability")]
102            observability: None,
103        }
104    }
105
106    /// Sets the application state, available to handlers via `State<T>`.
107    ///
108    /// The state is cloned for each request that extracts it, so it should
109    /// be cheap to clone: wrap expensive resources in `Arc` (connection
110    /// pools usually already are). Sub-states can be extracted with Axum's
111    /// `FromRef`.
112    ///
113    /// ```
114    /// use std::sync::Arc;
115    /// use kerniq_server::prelude::*;
116    ///
117    /// #[derive(Clone)]
118    /// struct AppState {
119    ///     service_name: Arc<str>,
120    /// }
121    ///
122    /// async fn name(State(state): State<AppState>) -> String {
123    ///     state.service_name.to_string()
124    /// }
125    ///
126    /// let app = App::new()
127    ///     .state(AppState { service_name: "customer-api".into() })
128    ///     .get("/name", name);
129    /// ```
130    pub fn state<T>(self, state: T) -> App<T>
131    where
132        T: Clone + Send + Sync + 'static,
133    {
134        App {
135            router: self.router.with_state(()),
136            state,
137            middleware: self.middleware,
138            config: self.config,
139            custom_fallback: self.custom_fallback,
140            shutdown: self.shutdown,
141            signal: self.signal,
142            tasks: self.tasks,
143            #[cfg(feature = "observability")]
144            observability: self.observability,
145        }
146    }
147}
148
149macro_rules! delegate_methods {
150    ($($(#[$doc:meta])* $name:ident;)*) => {
151        $(
152            $(#[$doc])*
153            pub fn $name<H, T>(mut self, path: &str, handler: H) -> Self
154            where
155                H: Handler<T, S>,
156                T: 'static,
157            {
158                self.router = self.router.$name(path, handler);
159                self
160            }
161        )*
162    };
163}
164
165impl<S> App<S>
166where
167    S: Clone + Send + Sync + 'static,
168{
169    delegate_methods! {
170        /// Registers a `GET` handler (also answers `HEAD`).
171        ///
172        /// Paths use `{name}` captures; `:name` is accepted and converted.
173        get;
174        /// Registers a `POST` handler.
175        post;
176        /// Registers a `PUT` handler.
177        put;
178        /// Registers a `PATCH` handler.
179        patch;
180        /// Registers a `DELETE` handler.
181        delete;
182        /// Registers an `OPTIONS` handler.
183        options;
184        /// Registers a `HEAD` handler.
185        head;
186        /// Registers a `TRACE` handler.
187        trace;
188        /// Registers a handler for every method.
189        any;
190    }
191
192    /// Registers a handler for an arbitrary standard method.
193    /// See [`Router::on`].
194    pub fn on<H, T>(mut self, method: Method, path: &str, handler: H) -> Self
195    where
196        H: Handler<T, S>,
197        T: 'static,
198    {
199        self.router = self.router.on(method, path, handler);
200        self
201    }
202
203    /// Registers an Axum `MethodRouter` (advanced use: per-route layers,
204    /// several methods at once). See [`Router::route`].
205    pub fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
206        self.router = self.router.route(path, method_router);
207        self
208    }
209
210    /// Registers several routes in a block. See [`Router::group`].
211    pub fn group(mut self, f: impl FnOnce(&mut Group<'_, S>)) -> Self {
212        self.router = self.router.group(f);
213        self
214    }
215
216    /// Mounts a Kerniq [`Router`] under `path` (combined with the router's
217    /// own prefix).
218    ///
219    /// ```
220    /// use kerniq_server::prelude::*;
221    /// # async fn list() {} async fn create() {} async fn accounts() {}
222    ///
223    /// let users = Router::new().get("/", list).post("/", create);
224    /// let app = App::new()
225    ///     .mount("/users", users)
226    ///     .mount("/accounts", Router::new().get("/", accounts));
227    /// ```
228    pub fn mount(mut self, path: &str, router: Router<S>) -> Self {
229        self.router = self.router.mount(path, router);
230        self
231    }
232
233    /// Merges a Kerniq [`Router`] at its own [prefix](Router::prefix).
234    ///
235    /// ```
236    /// use kerniq_server::prelude::*;
237    /// # async fn users() {}
238    ///
239    /// let api = Router::new()
240    ///     .prefix("/api/v1")
241    ///     .group(|r| { r.get("/users", users); });
242    /// let app = App::new().merge(api);
243    /// ```
244    pub fn merge(mut self, router: Router<S>) -> Self {
245        self.router = self.router.merge(router);
246        self
247    }
248
249    /// Mounts an existing [`axum::Router`] under `path` (`"/"` merges it at
250    /// the root). Its routes, layers and handlers are used as-is.
251    pub fn mount_axum(mut self, path: &str, router: axum::Router<S>) -> Self {
252        self.router = self.router.mount_axum(path, router);
253        self
254    }
255
256    /// Adds application-wide middleware. The first middleware added is the
257    /// outermost. Applies to every route and to the 404 fallback, regardless
258    /// of whether routes are registered before or after this call.
259    pub fn middleware<M: Middleware>(mut self, middleware: M) -> Self {
260        self.middleware.push(middleware);
261        self
262    }
263
264    /// Adds an application-wide Tower layer. Shares the ordered stack with
265    /// [`middleware`](Self::middleware).
266    pub fn layer<L>(self, layer: L) -> Self
267    where
268        TowerLayer<L>: Middleware,
269    {
270        self.middleware(TowerLayer(layer))
271    }
272
273    /// Replaces the default JSON `404 NOT_FOUND` fallback.
274    pub fn fallback<H, T>(mut self, handler: H) -> Self
275    where
276        H: Handler<T, S>,
277        T: 'static,
278    {
279        self.router = self.router.fallback(handler);
280        self.custom_fallback = true;
281        self
282    }
283
284    /// Enables observability (feature `observability`).
285    ///
286    /// ```no_run
287    /// use kerniq_server::prelude::*;
288    /// use kerniq_server::observability::prelude::*;
289    ///
290    /// #[tokio::main]
291    /// async fn main() -> Result<()> {
292    ///     let observability = Observability::builder()
293    ///         .service_name("payment-api")
294    ///         .metrics(MetricsConfig::new())
295    ///         .build()
296    ///         .await?;
297    ///
298    ///     App::new()
299    ///         .observability(observability)
300    ///         .get("/health", || async { "OK" })
301    ///         .run("0.0.0.0:3000")
302    ///         .await
303    /// }
304    /// ```
305    ///
306    /// At finalization:
307    ///
308    /// * HTTP instrumentation (metrics, a span per request, W3C context
309    ///   extraction) becomes the **outermost** middleware, so it measures
310    ///   everything inside it, including timeouts, rate limiting and the
311    ///   404 fallback;
312    /// * a Prometheus endpoint configured on the application listener is
313    ///   added *outside* all middleware, so scrapes are never instrumented,
314    ///   rate-limited or timed out; one configured with its own listener is
315    ///   served by a background task bound when the app starts.
316    ///
317    /// Under [`run`](Self::run)/[`serve`](Self::serve), telemetry is flushed
318    /// and shut down after connections and background tasks have drained,
319    /// bounded by the observability shutdown timeout. With
320    /// [`into_axum_router`](Self::into_axum_router) the application owns the
321    /// handle and calls `shutdown` itself.
322    #[cfg(feature = "observability")]
323    pub fn observability(mut self, observability: kerniq_observability::Observability) -> Self {
324        self.observability = Some(observability);
325        self
326    }
327
328    /// Sets the server configuration.
329    pub fn config(mut self, config: ServerConfig) -> Self {
330        self.config = config;
331        self
332    }
333
334    /// The current server configuration.
335    pub fn server_config(&self) -> &ServerConfig {
336        &self.config
337    }
338
339    /// Registers a background task started when the server starts. It
340    /// receives a [`Shutdown`] handle and should return promptly once it
341    /// triggers; the server waits up to `shutdown_timeout` for it.
342    ///
343    /// ```
344    /// use std::time::Duration;
345    /// use kerniq_server::prelude::*;
346    ///
347    /// let app = App::new().background_task("heartbeat", |shutdown: Shutdown| async move {
348    ///     let mut tick = tokio::time::interval(Duration::from_secs(30));
349    ///     loop {
350    ///         tokio::select! {
351    ///             _ = tick.tick() => tracing::info!("still alive"),
352    ///             () = shutdown.wait() => break,
353    ///         }
354    ///     }
355    /// });
356    /// ```
357    pub fn background_task<F, Fut>(mut self, name: &'static str, task: F) -> Self
358    where
359        F: FnOnce(Shutdown) -> Fut + Send + 'static,
360        Fut: Future<Output = ()> + Send + 'static,
361    {
362        self.tasks.push(Box::new(move |tasks: &BackgroundTasks| {
363            tasks.spawn(name, task)
364        }));
365        self
366    }
367
368    /// Replaces the default shutdown trigger (Ctrl+C / SIGTERM) with
369    /// `signal`. Shutdown can also be triggered at any time through
370    /// [`shutdown_handle`](Self::shutdown_handle).
371    pub fn shutdown_signal<F>(mut self, signal: F) -> Self
372    where
373        F: Future<Output = ()> + Send + 'static,
374    {
375        self.signal = Some(Box::pin(signal));
376        self
377    }
378
379    /// A handle that triggers or observes this app's shutdown. Store it in
380    /// state so long-lived handlers (WebSockets, streams) can finish when the
381    /// server stops.
382    pub fn shutdown_handle(&self) -> Shutdown {
383        self.shutdown.clone()
384    }
385
386    /// The registered routes (for diagnostics; mounted Axum routers appear
387    /// as a single opaque entry).
388    pub fn routes(&self) -> &[RouteInfo] {
389        self.router.routes()
390    }
391
392    /// Finalizes the application into a plain [`axum::Router`], with
393    /// fallbacks, state and all middleware applied.
394    ///
395    /// Use this to run Kerniq under `axum::serve`, embed it into a larger
396    /// Axum application, or test it with `tower::ServiceExt::oneshot`.
397    /// Lifecycle features (background tasks, shutdown signal, connection
398    /// limits) belong to [`App::run`] and are not part of the router.
399    pub fn into_axum_router(self) -> axum::Router {
400        self.into_parts().0
401    }
402
403    fn into_parts(self) -> (axum::Router, Lifecycle) {
404        let App {
405            router,
406            state,
407            middleware,
408            config,
409            custom_fallback,
410            shutdown,
411            signal,
412            tasks,
413            #[cfg(feature = "observability")]
414            observability,
415        } = self;
416
417        let router = router.into_axum();
418        let router = if custom_fallback {
419            router
420        } else {
421            router.fallback(not_found)
422        };
423        let router = router
424            .method_not_allowed_fallback(method_not_allowed)
425            .with_state(state);
426
427        let mut middleware = middleware;
428        if let Some(timeout) = config.request_timeout {
429            middleware.push(Timeout::new(timeout));
430        }
431        if let Some(max) = config.max_concurrent_requests {
432            middleware.push(ConcurrencyLimit::new(max));
433        }
434        // Axum already enforces a 2 MiB default; adding an identical layer
435        // would only cost an extra service boxing per request.
436        if config.max_body_size != DEFAULT_MAX_BODY_SIZE {
437            middleware.push(BodyLimit::new(config.max_body_size));
438        }
439        let router = middleware.apply(router);
440
441        #[cfg(feature = "observability")]
442        let router = match &observability {
443            Some(observability) => attach_observability(router, observability),
444            None => router,
445        };
446
447        (
448            router,
449            Lifecycle {
450                config,
451                shutdown,
452                signal,
453                tasks,
454                #[cfg(feature = "observability")]
455                observability,
456            },
457        )
458    }
459
460    /// Binds `address` and serves until shutdown. Requires a Tokio runtime
461    /// (for example `#[tokio::main]`).
462    ///
463    /// `address` is anything `tokio::net::TcpListener::bind` accepts as a
464    /// string: `"127.0.0.1:3000"`, `"0.0.0.0:8080"`, `"localhost:3000"`,
465    /// `"[::1]:3000"`. To use the configured address, pass
466    /// `config.address()`.
467    pub async fn run(self, address: impl AsRef<str>) -> Result<()> {
468        let listener = kerniq_runtime::bind(address.as_ref()).await?;
469        self.serve_inner(listener, false).await
470    }
471
472    /// Serves on an already-bound listener (useful for tests with port 0,
473    /// socket activation, or binding before dropping privileges).
474    pub async fn serve(self, listener: TcpListener) -> Result<()> {
475        self.serve_inner(listener, false).await
476    }
477
478    /// Like [`run`](Self::run), but creates and owns a multi-threaded Tokio
479    /// runtime (with `workers` threads, if configured).
480    ///
481    /// ```no_run
482    /// use kerniq_server::prelude::*;
483    ///
484    /// fn main() -> Result<()> {
485    ///     App::new()
486    ///         .get("/", || async { "OK" })
487    ///         .run_blocking("0.0.0.0:3000")
488    /// }
489    /// ```
490    ///
491    /// # Errors
492    ///
493    /// Returns an error (and does not start) if called from inside a Tokio
494    /// runtime: nested runtimes are not allowed. Use `.run(addr).await` there.
495    pub fn run_blocking(self, address: impl AsRef<str>) -> Result<()> {
496        let workers = self.config.workers;
497        let timeout = self.config.shutdown_timeout;
498        let address = address.as_ref().to_owned();
499        kerniq_runtime::runtime::block_on(workers, timeout, async move {
500            let listener = kerniq_runtime::bind(&address).await?;
501            self.serve_inner(listener, true).await
502        })?
503    }
504
505    async fn serve_inner(self, listener: TcpListener, owns_runtime: bool) -> Result<()> {
506        if !owns_runtime && self.config.workers.is_some() {
507            tracing::warn!(
508                "`ServerConfig::workers` is ignored by `run`/`serve` because the application owns \
509                 the Tokio runtime; configure `#[tokio::main(worker_threads = N)]` or use `run_blocking`"
510            );
511        }
512        for route in self.router.routes() {
513            tracing::debug!(method = %route.method, path = %route.path, "route registered");
514        }
515
516        let (router, lifecycle) = self.into_parts();
517        let Lifecycle {
518            config,
519            shutdown,
520            signal,
521            tasks,
522            #[cfg(feature = "observability")]
523            observability,
524        } = lifecycle;
525
526        let background = BackgroundTasks::new(shutdown.clone());
527        #[cfg(feature = "observability")]
528        if let Some(observability) = &observability {
529            start_metrics_listener(observability, &background).await?;
530        }
531        for start in tasks {
532            start(&background);
533        }
534
535        let signal = signal.unwrap_or_else(|| Box::pin(kerniq_runtime::os_signal()));
536        let signal_task = tokio::spawn({
537            let shutdown = shutdown.clone();
538            async move {
539                tokio::select! {
540                    () = signal => shutdown.trigger(),
541                    () = shutdown.wait() => {}
542                }
543            }
544        });
545
546        let result = kerniq_runtime::serve(
547            listener,
548            router,
549            ServeOptions::from(&config),
550            shutdown.clone(),
551        )
552        .await;
553
554        shutdown.trigger();
555        signal_task.abort();
556        if !background.is_empty() {
557            background.shutdown(config.shutdown_timeout).await;
558        }
559        tracing::info!("kerniq server stopped");
560        #[cfg(feature = "observability")]
561        if let Some(observability) = observability {
562            let report = observability.shutdown().await;
563            if report.timed_out || !report.errors.is_empty() {
564                tracing::warn!(
565                    duration = ?report.duration,
566                    errors = ?report.errors,
567                    "telemetry shutdown incomplete"
568                );
569            }
570        }
571        result.map_err(Error::from)
572    }
573}
574
575struct Lifecycle {
576    config: ServerConfig,
577    shutdown: Shutdown,
578    signal: Option<Signal>,
579    tasks: Vec<TaskStarter>,
580    #[cfg(feature = "observability")]
581    observability: Option<kerniq_observability::Observability>,
582}
583
584#[cfg(feature = "observability")]
585fn attach_observability(
586    router: axum::Router,
587    observability: &kerniq_observability::Observability,
588) -> axum::Router {
589    // Applied last, so it is the outermost layer on every route.
590    let router = observability.http_instrumentation().apply(router);
591    #[cfg(feature = "prometheus")]
592    if let Some(prometheus) = observability.prometheus()
593        && prometheus.config().listen_address().is_none()
594    {
595        // Merged after every layer: scrapes bypass all middleware.
596        return router.merge(prometheus.router());
597    }
598    router
599}
600
601#[cfg(feature = "observability")]
602#[allow(clippy::unused_async)]
603async fn start_metrics_listener(
604    observability: &kerniq_observability::Observability,
605    background: &BackgroundTasks,
606) -> Result<()> {
607    #[cfg(feature = "prometheus")]
608    if let Some(prometheus) = observability.prometheus()
609        && let Some(address) = prometheus.config().listen_address()
610    {
611        let listener = kerniq_runtime::bind(&address.to_string()).await?;
612        let prometheus = prometheus.clone();
613        background.spawn("prometheus-metrics", move |shutdown: Shutdown| async move {
614            if let Err(err) = prometheus.serve(listener, shutdown.wait_owned()).await {
615                tracing::error!(error = %err, "Prometheus metrics listener failed");
616            }
617        });
618    }
619    let _ = (observability, background);
620    Ok(())
621}
622
623async fn not_found() -> Error {
624    Error::not_found("No route matches the requested path")
625}
626
627async fn method_not_allowed() -> Error {
628    Error::method_not_allowed("The route does not support this HTTP method")
629}
630
631impl<S> From<App<S>> for axum::Router
632where
633    S: Clone + Send + Sync + 'static,
634{
635    fn from(app: App<S>) -> Self {
636        app.into_axum_router()
637    }
638}