aboutsummaryrefslogtreecommitdiffstats
path: root/crates/server/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/server/src/main.rs146
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)
+}