bestool_alertd/
http_server.rs

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