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}