1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
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 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<()> {
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)
}
|