Skip to main content

bestool_alertd/
http_server.rs

1//! HTTP server for alertd daemon control and metrics.
2
3use std::{sync::Arc, time::Duration};
4
5use axum::{
6	Router,
7	routing::{get, post},
8};
9use jiff::Timestamp;
10use tokio::sync::mpsc;
11use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
12use tracing::{Level, error, info, warn};
13
14use crate::{EmailConfig, alert::InternalContext, events::EventManager, scheduler::Scheduler};
15
16mod endpoints;
17mod state;
18#[cfg(test)]
19mod test_utils;
20mod types;
21
22pub use endpoints::*;
23pub use state::ServerState;
24pub use types::*;
25
26#[expect(
27	clippy::too_many_arguments,
28	reason = "server startup needs all these pieces"
29)]
30pub async fn start_server(
31	reload_tx: mpsc::Sender<()>,
32	event_manager: Option<Arc<EventManager>>,
33	internal_context: Arc<InternalContext>,
34	email_config: Option<EmailConfig>,
35	dry_run: bool,
36	addrs: Vec<std::net::SocketAddr>,
37	scheduler: Arc<Scheduler>,
38	watchdog_timeout: Option<Duration>,
39) {
40	let started_at = Timestamp::now();
41	let pid = std::process::id();
42
43	let state = ServerState {
44		reload_tx,
45		started_at,
46		pid,
47		event_manager,
48		internal_context,
49		email_config,
50		dry_run,
51		scheduler,
52		watchdog_timeout,
53	};
54
55	let app = Router::new()
56		.route("/", get(handle_index))
57		.route("/reload", post(handle_reload))
58		.route("/alerts", get(handle_alerts).delete(handle_pause_alert))
59		.route("/targets", get(handle_targets))
60		.route("/validate", post(handle_validate))
61		.route("/metrics", get(handle_metrics))
62		.route("/status", get(handle_status))
63		.route("/health", get(handle_health))
64		.layer(
65			TraceLayer::new_for_http()
66				.make_span_with(
67					DefaultMakeSpan::new()
68						.level(Level::INFO)
69						.include_headers(false),
70				)
71				.on_request(|request: &axum::http::Request<_>, _span: &tracing::Span| {
72					info!(
73						method = %request.method(),
74						uri = %request.uri(),
75						"HTTP request"
76					);
77				})
78				.on_response(
79					DefaultOnResponse::new()
80						.level(Level::INFO)
81						.include_headers(false),
82				),
83		)
84		.with_state(Arc::new(state));
85
86	// Use default if no addresses provided
87	let addrs_to_try = if addrs.is_empty() {
88		vec![
89			"[::1]:8271".parse().unwrap(),
90			"127.0.0.1:8271".parse().unwrap(),
91		]
92	} else {
93		addrs
94	};
95
96	let mut listener = None;
97	let mut last_error = None;
98
99	// Try each address in order until one succeeds
100	for addr in &addrs_to_try {
101		match tokio::net::TcpListener::bind(addr).await {
102			Ok(l) => {
103				info!("HTTP server listening on http://{}", addr);
104				listener = Some(l);
105				break;
106			}
107			Err(e) => {
108				warn!("failed to bind HTTP server to {}: {}", addr, e);
109				last_error = Some(e);
110			}
111		}
112	}
113
114	let listener = match listener {
115		Some(l) => l,
116		None => {
117			if let Some(e) = last_error {
118				warn!("failed to bind HTTP server to any address: {}", e);
119			} else {
120				warn!("no addresses provided for HTTP server");
121			}
122			warn!("waiting 10 seconds before continuing without");
123			warn!("use --no-server to disable the HTTP server and this warning");
124
125			tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
126
127			info!("continuing without HTTP server");
128			return;
129		}
130	};
131
132	if let Err(e) = axum::serve(listener, app).await {
133		error!("HTTP server error: {}", e);
134	}
135}