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