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