Skip to main content

bestool_alertd/
http_server.rs

1//! HTTP server for alertd daemon control and metrics.
2
3use std::{collections::HashMap, sync::Arc, time::Duration};
4
5use axum::{
6	Router,
7	routing::{get, post},
8};
9use jiff::Timestamp;
10use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
11use tracing::{Level, error, info, warn};
12
13use crate::{
14	context::InternalContext,
15	daemon::DaemonControl,
16	tasks::{BackgroundTask, TaskEndpointHandler},
17};
18
19mod endpoints;
20mod state;
21#[cfg(test)]
22mod test_utils;
23mod types;
24
25pub use endpoints::*;
26pub use state::ServerState;
27pub use types::*;
28
29pub async fn start_server(
30	internal_context: Arc<InternalContext>,
31	addrs: Vec<std::net::SocketAddr>,
32	watchdog_timeout: Option<Duration>,
33	background_tasks: &[Arc<dyn BackgroundTask>],
34	control: DaemonControl,
35	backups: Option<Arc<crate::BackupRegistry>>,
36) {
37	let started_at = Timestamp::now();
38	let pid = std::process::id();
39
40	let task_endpoints = collect_task_endpoints(background_tasks);
41
42	let state = ServerState {
43		started_at,
44		pid,
45		internal_context,
46		watchdog_timeout,
47		task_endpoints: Arc::new(task_endpoints),
48		control,
49		backups,
50	};
51
52	let app = Router::new()
53		.route("/", get(handle_index))
54		.route("/metrics", get(handle_metrics))
55		.route("/status", get(handle_status))
56		.route("/health", get(handle_health))
57		.route("/reload", post(handle_reload))
58		.route("/restart", post(handle_restart))
59		.route("/tasks/{task}/{endpoint}", get(handle_task_endpoint))
60		.layer(
61			TraceLayer::new_for_http()
62				.make_span_with(
63					DefaultMakeSpan::new()
64						.level(Level::INFO)
65						.include_headers(false),
66				)
67				.on_request(|request: &axum::http::Request<_>, _span: &tracing::Span| {
68					info!(
69						method = %request.method(),
70						uri = %request.uri(),
71						"HTTP request"
72					);
73				})
74				.on_response(
75					DefaultOnResponse::new()
76						.level(Level::INFO)
77						.include_headers(false),
78				),
79		)
80		.with_state(Arc::new(state));
81
82	// Use default if no addresses provided
83	let addrs_to_try = if addrs.is_empty() {
84		vec![
85			"[::1]:8271".parse().unwrap(),
86			"127.0.0.1:8271".parse().unwrap(),
87		]
88	} else {
89		addrs
90	};
91
92	let mut listener = None;
93	let mut last_error = None;
94
95	// Try each address in order until one succeeds
96	for addr in &addrs_to_try {
97		match tokio::net::TcpListener::bind(addr).await {
98			Ok(l) => {
99				info!("HTTP server listening on http://{}", addr);
100				listener = Some(l);
101				break;
102			}
103			Err(e) => {
104				warn!("failed to bind HTTP server to {}: {}", addr, e);
105				last_error = Some(e);
106			}
107		}
108	}
109
110	let listener = match listener {
111		Some(l) => l,
112		None => {
113			if let Some(e) = last_error {
114				warn!("failed to bind HTTP server to any address: {}", e);
115			} else {
116				warn!("no addresses provided for HTTP server");
117			}
118			warn!("waiting 10 seconds before continuing without");
119			warn!("use --no-server to disable the HTTP server and this warning");
120
121			tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
122
123			info!("continuing without HTTP server");
124			return;
125		}
126	};
127
128	if let Err(e) = axum::serve(listener, app).await {
129		error!("HTTP server error: {}", e);
130	}
131}
132
133fn collect_task_endpoints(
134	tasks: &[Arc<dyn BackgroundTask>],
135) -> HashMap<(String, String), TaskEndpointHandler> {
136	let mut map = HashMap::new();
137	for task in tasks {
138		let task_name = task.name();
139		for endpoint in task.http_endpoints() {
140			let key = (task_name.to_string(), endpoint.name.to_string());
141			if map.contains_key(&key) {
142				warn!(
143					task = task_name,
144					endpoint = endpoint.name,
145					"duplicate task endpoint name; later registration wins"
146				);
147			}
148			info!(
149				task = task_name,
150				endpoint = endpoint.name,
151				path = %format!("/tasks/{task_name}/{}", endpoint.name),
152				"mounting task endpoint"
153			);
154			map.insert(key, endpoint.handler);
155		}
156	}
157	map
158}