1use core::time::Duration;
4
5use metrics_exporter_prometheus::BuildError;
6use tokio::net::TcpListener;
7
8#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum ServerError {
12 #[error(transparent)]
14 Listen(#[from] std::io::Error),
15
16 #[error("failed to initialize Prometheus metrics recorder: {0}")]
18 MetricsInit(#[source] BuildError),
19
20 #[error("failed to build tokio runtime: {0}")]
22 RuntimeBuild(#[source] std::io::Error),
23}
24
25pub struct ServerConfig {
27 pub host: String,
30 pub port: u16,
32}
33
34impl ServerConfig {
35 #[inline]
37 #[must_use]
38 pub fn new<Host>(host: Host, port: u16) -> Self
39 where
40 Host: Into<String>,
41 {
42 Self {
43 host: host.into(),
44 port,
45 }
46 }
47}
48
49#[inline]
63pub async fn bind(config: &ServerConfig) -> std::io::Result<TcpListener> {
64 TcpListener::bind((config.host.as_str(), config.port)).await
65}
66
67#[inline]
80pub async fn serve(config: ServerConfig) -> Result<(), ServerError> {
81 let listener = bind(&config).await?;
82 let bound_addr = listener.local_addr()?;
83
84 let handle = crate::metrics::install_global().map_err(ServerError::MetricsInit)?;
85
86 #[cfg(feature = "posthog")]
87 init_posthog().await;
88
89 let upkeep_task = spawn_upkeep(handle.clone());
90
91 tracing::info!(addr = %bound_addr, "docspec-http listening");
92
93 let server_result = axum::serve(listener, crate::router::router_with_metrics(handle))
94 .with_graceful_shutdown(shutdown_signal())
95 .await;
96
97 crate::telemetry::shutdown().await;
98 #[cfg(feature = "posthog")]
99 tracing::info!("posthog flush complete");
100
101 teardown_upkeep(upkeep_task).await;
102 server_result?;
103
104 Ok(())
105}
106
107#[inline]
109fn spawn_upkeep(
110 handle: metrics_exporter_prometheus::PrometheusHandle,
111) -> tokio::task::JoinHandle<()> {
112 tokio::spawn(async move {
113 let mut interval = tokio::time::interval(Duration::from_secs(5));
114 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
115 loop {
116 interval.tick().await;
117 handle.run_upkeep();
118 }
119 })
120}
121
122#[inline]
124async fn teardown_upkeep(task: tokio::task::JoinHandle<()>) {
125 task.abort();
126 match task.await {
127 Err(error) if error.is_cancelled() => {}
128 Err(error) => tracing::error!(%error, "metrics upkeep task failed during shutdown"),
129 Ok(()) => {}
130 }
131}
132
133#[cfg(feature = "posthog")]
141#[inline]
142async fn init_posthog() {
143 let client = crate::telemetry::init_posthog_client_from_env().await;
144 crate::telemetry::install_posthog_client(client);
145}
146
147#[inline]
149async fn shutdown_signal() {
150 use core::future;
151
152 use tokio::signal;
153
154 let ctrl_c = async {
155 if let Err(error) = signal::ctrl_c().await {
156 tracing::error!(%error, "failed to install Ctrl+C handler");
157 future::pending::<()>().await;
158 }
159 };
160
161 #[cfg(unix)]
162 let terminate = async {
163 match signal::unix::signal(signal::unix::SignalKind::terminate()) {
164 Ok(mut stream) => {
165 stream.recv().await;
166 }
167 Err(error) => {
168 tracing::error!(%error, "failed to install SIGTERM handler");
169 future::pending::<()>().await;
170 }
171 }
172 };
173
174 #[cfg(not(unix))]
175 let terminate = future::pending::<()>();
176
177 tokio::select! {
178 () = ctrl_c => {
179 tracing::info!("shutdown signal received, draining");
180 },
181 () = terminate => {
182 tracing::info!("shutdown signal received, draining");
183 },
184 }
185}