bestool_alertd/
http_server.rs1use 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 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 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}