use std::net::{AddrParseError, SocketAddr};
#[cfg(feature = "metrics")]
use std::sync::Arc;
#[cfg(feature = "graceful-shutdown")]
use std::time::Duration;
use axum::Router;
use hwhkit_core::BuiltApplication;
use tokio::net::TcpListener;
use super::*;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ServeError {
#[error("invalid server.host/port: {0}")]
InvalidAddr(#[from] AddrParseError),
#[error("bind {addr} failed")]
Bind {
addr: SocketAddr,
#[source]
source: std::io::Error,
},
#[error("axum serve error")]
Serve(#[source] std::io::Error),
}
pub async fn run(built: BuiltApplication) -> Result<(), ServeError> {
let cfg = built.config().clone();
let addr: SocketAddr = format!("{}:{}", cfg.server.host, cfg.server.port).parse()?;
let listener = TcpListener::bind(addr)
.await
.map_err(|e| ServeError::Bind { addr, source: e })?;
tracing::info!(%addr, "hwhkit server listening");
run_with_listener(built, listener).await
}
pub async fn run_with_listener(
mut built: BuiltApplication,
listener: TcpListener,
) -> Result<(), ServeError> {
let cfg = built.config().clone();
let mut router = built.router().clone();
#[cfg(feature = "health-endpoints")]
if cfg.runtime.health.enabled {
router = router.merge(health::router(&cfg.runtime.health, built.health()));
}
#[cfg(feature = "version-endpoints")]
if cfg.runtime.info.enabled {
let version = version::default_version();
let info = version::InfoResponse {
service_name: cfg.observability.service_name.clone(),
environment: format!("{:?}", cfg.observability.environment).to_lowercase(),
build: hwhkit_buildinfo::current(),
initialized_integrations: built.initialized_integrations().to_vec(),
degraded_integrations: built.degraded_integrations().to_vec(),
};
router = router.merge(version::router(&cfg.runtime.info, info, version));
}
#[cfg(feature = "metrics")]
if cfg.runtime.metrics.enabled {
match metrics::install_recorder() {
Ok(handle) => {
router = router.merge(metrics::router(&cfg.runtime.metrics, handle.clone()));
router = router.layer(metrics::HttpMetricsLayer::new());
built.set_metrics_handle(Arc::new(handle));
}
Err(err) => {
tracing::warn!(error = %err, "metrics recorder install failed");
}
}
}
#[cfg(feature = "process-metrics")]
{
if cfg.runtime.metrics.enabled {
process_metrics::spawn(built.shutdown());
}
}
#[cfg(feature = "request-id")]
if cfg.runtime.request_id.enabled {
router = router.layer(request_id::RequestIdLayer::new(
&cfg.runtime.request_id.header,
));
}
#[cfg(feature = "middleware-bundle")]
{
router = middleware::apply(router, &cfg.runtime.middleware);
}
let serve_result = serve(listener, router, &built).await;
drain_providers(&built).await;
serve_result
}
async fn drain_providers(built: &BuiltApplication) {
for provider in built.providers().iter().rev() {
let key = provider.key();
match provider.shutdown(built.context()).await {
Ok(_) => tracing::info!(integration = %key, "shutdown ok"),
Err(err) => tracing::warn!(integration = %key, error = %err, "shutdown error"),
}
}
}
#[cfg(feature = "graceful-shutdown")]
async fn serve(
listener: TcpListener,
router: Router,
built: &BuiltApplication,
) -> Result<(), ServeError> {
let shutdown = built.shutdown();
shutdown::install(shutdown.clone());
let drain = Duration::from_secs(built.config().runtime.shutdown.max_drain_secs);
let trigger = shutdown.clone();
let serve_fut = axum::serve(listener, router).with_graceful_shutdown(async move {
trigger.cancelled().await;
tracing::info!(?drain, "shutdown signalled; bounding inflight drain");
});
match tokio::time::timeout(drain, serve_fut).await {
Ok(res) => res.map_err(ServeError::Serve),
Err(_) => {
tracing::warn!(?drain, "drain deadline elapsed; forcing shutdown");
Ok(())
}
}
}
#[cfg(not(feature = "graceful-shutdown"))]
async fn serve(
listener: TcpListener,
router: Router,
_built: &BuiltApplication,
) -> Result<(), ServeError> {
axum::serve(listener, router)
.await
.map_err(ServeError::Serve)
}