Skip to main content

bestool_alertd/
lib.rs

1use std::{fmt, sync::Arc, time::Duration};
2
3pub use bestool_canopy as canopy;
4pub use bestool_canopy::Redacted;
5
6pub mod backup;
7mod child_confinement;
8pub mod commands;
9mod context;
10mod daemon;
11pub mod doctor;
12pub mod http_server;
13mod metrics;
14pub mod tasks;
15
16#[cfg(windows)]
17pub mod windows_service;
18
19pub use backup::{BackupRegistry, BackupRunner, BackupTask, RunningBackup};
20pub use context::InternalContext;
21pub use daemon::{RestartTrigger, run, run_with_shutdown};
22pub use tasks::{BackgroundTask, TaskContext, TaskEndpoint, TaskEndpointResponse};
23
24/// The version of the alertd library
25pub const VERSION: &str = env!("CARGO_PKG_VERSION");
26
27/// Base builder for alertd's outbound HTTP clients. Call sites add their own
28/// timeouts etc. Canopy sets its own User-Agent, so this one applies to alertd's
29/// other requests.
30pub fn http_builder() -> reqwest::ClientBuilder {
31	reqwest::Client::builder().user_agent(concat!("bestool-alertd/", env!("CARGO_PKG_VERSION")))
32}
33
34/// A built [`reqwest::Client`] from [`http_builder`].
35pub fn http_client() -> reqwest::Client {
36	http_builder()
37		.build()
38		.expect("failed to build alertd HTTP client")
39}
40/// Configuration for the alertd daemon
41#[derive(Clone)]
42pub struct DaemonConfig {
43	/// Database connection pool, opened by the caller.
44	///
45	/// Centralising pool creation at the caller lets `bestool alertd`
46	/// reuse the pool for one-off setup queries (kind detection, device key
47	/// lookup) instead of opening additional short-lived connections.
48	///
49	/// `None` on hosts with no Tamanu deployment (and therefore no database).
50	pub pg_pool: Option<bestool_postgres::pool::PgPool>,
51
52	/// Database connection URL, retained for redacted display.
53	pub database_url: Option<String>,
54
55	/// Tamanu device key PEM, used as the client identity for canopy.
56	///
57	/// Held only long enough to build the canopy `reqwest::Client` at startup,
58	/// then dropped. Wrapped in `Redacted` so debug-logging the config can't
59	/// leak the key.
60	pub device_key_pem: Option<Redacted<String>>,
61
62	/// Whether to disable the HTTP server
63	pub no_server: bool,
64
65	/// HTTP server bind addresses
66	pub server_addrs: Vec<std::net::SocketAddr>,
67
68	/// Watchdog timeout duration
69	///
70	/// If no background task reports activity within this duration, the daemon
71	/// will exit with an error so it can be restarted by the service manager.
72	/// Set to `None` to disable the watchdog.
73	pub watchdog_timeout: Option<Duration>,
74
75	/// Background tasks to run on a schedule.
76	///
77	/// Each task ticks at its own `interval()`. Errors are logged but do not
78	/// kill the daemon. Activity from each tick counts towards the watchdog.
79	pub background_tasks: Vec<Arc<dyn BackgroundTask>>,
80
81	/// Backup run registry, set when backups are compiled in. Surfaced via the
82	/// daemon's status so an operator can see what's backing up right now.
83	pub backups: Option<Arc<BackupRegistry>>,
84
85	/// Handle to the doctor task's latest sweep, set when a doctor task is
86	/// registered. Feeds the `/metrics` endpoint the per-check declared stats
87	/// and the status census.
88	pub metrics: Option<crate::doctor::DoctorMetricsHandle>,
89
90	/// Version of the running `bestool` binary, shown in the systemd status line.
91	///
92	/// Distinct from this crate's own [`VERSION`]: `bestool` and `bestool-alertd`
93	/// are versioned independently, so the caller threads in its own version.
94	pub binary_version: String,
95}
96
97impl fmt::Debug for DaemonConfig {
98	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99		f.debug_struct("DaemonConfig")
100			.field("database_url", &self.database_url)
101			.field("device_key_pem", &self.device_key_pem)
102			.field("binary_version", &self.binary_version)
103			.field("no_server", &self.no_server)
104			.field("server_addrs", &self.server_addrs)
105			.field("watchdog_timeout", &self.watchdog_timeout)
106			.field(
107				"background_tasks",
108				&self
109					.background_tasks
110					.iter()
111					.map(|t| t.name())
112					.collect::<Vec<_>>(),
113			)
114			.finish()
115	}
116}
117
118impl DaemonConfig {
119	pub fn new(
120		pg_pool: Option<bestool_postgres::pool::PgPool>,
121		database_url: Option<String>,
122	) -> Self {
123		Self {
124			pg_pool,
125			database_url,
126			device_key_pem: None,
127			no_server: false,
128			server_addrs: Vec::new(),
129			watchdog_timeout: Some(Duration::from_secs(10 * 60)),
130			background_tasks: Vec::new(),
131			backups: None,
132			metrics: None,
133			// Fallback only; the binary sets its own version via
134			// `with_binary_version`. This crate's version differs from bestool's.
135			binary_version: VERSION.to_string(),
136		}
137	}
138
139	/// Set the running binary's (bestool's) version for the status line.
140	pub fn with_binary_version(mut self, version: String) -> Self {
141		self.binary_version = version;
142		self
143	}
144
145	pub fn with_task(mut self, task: Arc<dyn BackgroundTask>) -> Self {
146		self.background_tasks.push(task);
147		self
148	}
149
150	/// Attach the backup registry, so the daemon's status can list in-flight runs.
151	pub fn with_backups(mut self, registry: Arc<BackupRegistry>) -> Self {
152		self.backups = Some(registry);
153		self
154	}
155
156	/// Attach the doctor metrics handle, so `/metrics` can serve per-check stats.
157	pub fn with_metrics_handle(mut self, metrics: crate::doctor::DoctorMetricsHandle) -> Self {
158		self.metrics = Some(metrics);
159		self
160	}
161
162	pub fn with_device_key_pem(mut self, pem: String) -> Self {
163		self.device_key_pem = Some(Redacted(pem));
164		self
165	}
166
167	pub fn with_no_server(mut self, no_server: bool) -> Self {
168		self.no_server = no_server;
169		self
170	}
171
172	pub fn with_server_addrs(mut self, server_addrs: Vec<std::net::SocketAddr>) -> Self {
173		self.server_addrs = server_addrs;
174		self
175	}
176
177	pub fn with_watchdog_timeout(mut self, watchdog_timeout: Option<Duration>) -> Self {
178		self.watchdog_timeout = watchdog_timeout;
179		self
180	}
181}
182
183/// Helper to format miette errors for logging without ANSI codes
184pub(crate) struct LogError<'a>(pub &'a miette::Report);
185
186impl fmt::Display for LogError<'_> {
187	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188		use miette::ReportHandler;
189
190		let handler = miette::NarratableReportHandler::new();
191
192		if let Err(e) = handler.debug(self.0.as_ref(), f) {
193			write!(f, "{}: {}", self.0, e)
194		} else {
195			Ok(())
196		}
197	}
198}