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