diff options
Diffstat (limited to '')
| -rw-r--r-- | crates/server/src/main.rs | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs new file mode 100644 index 00000000..56c44d2d --- /dev/null +++ b/crates/server/src/main.rs @@ -0,0 +1,146 @@ +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<String>, + + /// The port to bind + #[clap(long, short)] + port: Option<u16>, + }, + + /// 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<Output = ()> + 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<Router, eyre::Error> { + 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) +} |
