use std::future::Future; use std::net::SocketAddr; use axum::{Router, serve}; use clap::Parser; use database::db::ServerPostgres; use eyre::{Context, Result, eyre}; use tokio::net::TcpListener; use tokio::signal; use tracing_subscriber::util::SubscriberInitExt; use crate::database::DbType; use crate::settings::Settings; mod database; mod handlers; mod metrics; mod router; mod settings; #[derive(Parser, Clone, Debug)] #[command(infer_subcommands = true)] pub(crate) enum Cmd { /// Start the server Start { /// The host address to bind #[clap(long)] host: Option, /// The port to bind #[clap(long, short)] port: Option, }, /// Print server example configuration DefaultConfig, } impl Cmd { async fn run(self) -> Result<()> { match self { Self::Start { host, port } => { let settings = Settings::new().wrap_err("could not load server settings")?; let host = host.as_ref().unwrap_or(&settings.host).clone(); let port = port.unwrap_or(settings.port); let addr = SocketAddr::new(host.parse()?, port); if settings.metrics.enable { tokio::spawn(launch_metrics_server( settings.metrics.host.clone(), settings.metrics.port, )); } match settings.db_settings.db_type() { DbType::Postgres => launch(settings, addr).await, DbType::Unknown => { Err(eyre!("db_uri must start with postgres:// or sqlite://")) } } } Self::DefaultConfig => { // TODO(@bpeetz): Add this back <2026-06-11> println!("TODO"); Ok(()) } } } } #[tokio::main] async fn main() -> Result<()> { if let Err(e) = tracing_subscriber::registry().try_init() { eprintln!("failed to initialize logging: {e}"); } Cmd::parse().run().await } #[cfg(target_family = "unix")] async fn shutdown_signal() { let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to register signal handler"); let mut interrupt = signal::unix::signal(signal::unix::SignalKind::interrupt()) .expect("failed to register signal handler"); tokio::select! { _ = term.recv() => {}, _ = interrupt.recv() => {}, }; eprintln!("Shutting down gracefully..."); } async fn launch(settings: Settings, addr: SocketAddr) -> Result<()> { launch_with_tcp_listener( settings, TcpListener::bind(addr) .await .context("could not connect to socket")?, shutdown_signal(), ) .await } async fn launch_with_tcp_listener( settings: Settings, listener: TcpListener, shutdown: impl Future + Send + 'static, ) -> Result<()> { let r = make_router(settings).await?; serve(listener, r.into_make_service()) .with_graceful_shutdown(shutdown) .await?; Ok(()) } // The separate listener means it's much easier to ensure metrics are not accidentally exposed to // the public. async fn launch_metrics_server(host: String, port: u16) -> Result<()> { let listener = TcpListener::bind((host, port)) .await .context("failed to bind metrics tcp")?; let recorder_handle = metrics::setup_metrics_recorder(); let router = Router::new().route( "/metrics", axum::routing::get(move || std::future::ready(recorder_handle.render())), ); serve(listener, router.into_make_service()) .with_graceful_shutdown(shutdown_signal()) .await?; Ok(()) } async fn make_router(settings: Settings) -> Result { let db = ServerPostgres::new(&settings.db_settings) .await .wrap_err_with(|| format!("failed to connect to db: {:?}", settings.db_settings))?; let r = router::router(db, settings); Ok(r) }