Skip to main content

bestool_alertd/
lib.rs

1use std::{fmt, time::Duration};
2
3mod alert;
4pub mod canopy;
5pub mod commands;
6mod daemon;
7mod events;
8mod glob_resolver;
9pub mod http_server;
10mod loader;
11mod metrics;
12
13pub mod scheduler;
14pub mod state_file;
15mod targets;
16pub mod templates;
17
18#[cfg(windows)]
19pub mod windows_service;
20
21pub use alert::{AlertDefinition, AlwaysSend, InternalContext, TicketSource, WhenChanged};
22pub use daemon::{run, run_with_shutdown, run_with_shutdown_and_reload};
23pub use events::EventType;
24pub use targets::{
25	AlertTargets, ExternalTarget, ResolvedTarget, SendTarget, TargetConnection, TargetEmail,
26};
27
28/// The version of the alertd library
29pub const VERSION: &str = env!("CARGO_PKG_VERSION");
30
31/// Email server configuration
32#[derive(Debug, Clone)]
33pub struct EmailConfig {
34	pub from: String,
35	pub mailgun_api_key: String,
36	pub mailgun_domain: String,
37}
38
39/// Configuration for the alertd daemon
40#[derive(Clone)]
41pub struct DaemonConfig {
42	/// Glob patterns for directories/files containing alert definitions
43	///
44	/// Patterns are resolved to directories and files, and watched for changes.
45	/// On occasion, patterns are re-evaluated to pick up newly created paths.
46	pub alert_globs: Vec<String>,
47
48	/// Database connection URL
49	pub database_url: String,
50
51	/// Email server configuration
52	pub email: Option<EmailConfig>,
53
54	/// Tamanu device key PEM, used as the client identity for canopy targets.
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	/// Tamanu version of the install this daemon is alerting for. Sent in the
62	/// `X-Version` header on every canopy request — canopy rejects requests
63	/// without one.
64	pub tamanu_version: String,
65
66	/// Whether to perform a dry run (execute all alerts once and quit)
67	pub dry_run: bool,
68
69	/// Whether to disable the HTTP server
70	pub no_server: bool,
71
72	/// HTTP server bind addresses
73	pub server_addrs: Vec<std::net::SocketAddr>,
74
75	/// Watchdog timeout duration
76	///
77	/// If no alert task reports activity within this duration, the daemon
78	/// will exit with an error so it can be restarted by the service manager.
79	/// Set to `None` to disable the watchdog.
80	pub watchdog_timeout: Option<Duration>,
81}
82
83impl fmt::Debug for DaemonConfig {
84	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85		f.debug_struct("DaemonConfig")
86			.field("alert_globs", &self.alert_globs)
87			.field("database_url", &self.database_url)
88			.field("email", &self.email)
89			.field("device_key_pem", &self.device_key_pem)
90			.field("tamanu_version", &self.tamanu_version)
91			.field("dry_run", &self.dry_run)
92			.field("no_server", &self.no_server)
93			.field("server_addrs", &self.server_addrs)
94			.field("watchdog_timeout", &self.watchdog_timeout)
95			.finish()
96	}
97}
98
99/// Wraps a sensitive value so its `Debug` output doesn't leak the contents.
100#[derive(Clone)]
101pub struct Redacted<T>(pub T);
102
103impl<T> fmt::Debug for Redacted<T> {
104	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105		f.write_str("<redacted>")
106	}
107}
108
109impl<T> std::ops::Deref for Redacted<T> {
110	type Target = T;
111	fn deref(&self) -> &T {
112		&self.0
113	}
114}
115
116impl DaemonConfig {
117	pub fn new(alert_globs: Vec<String>, database_url: String, tamanu_version: String) -> Self {
118		Self {
119			alert_globs,
120			database_url,
121			email: None,
122			device_key_pem: None,
123			tamanu_version,
124			dry_run: false,
125			no_server: false,
126			server_addrs: Vec::new(),
127			watchdog_timeout: Some(Duration::from_secs(10 * 60)),
128		}
129	}
130
131	pub fn with_email(mut self, email: EmailConfig) -> Self {
132		self.email = Some(email);
133		self
134	}
135
136	pub fn with_device_key_pem(mut self, pem: String) -> Self {
137		self.device_key_pem = Some(Redacted(pem));
138		self
139	}
140
141	pub fn with_dry_run(mut self, dry_run: bool) -> Self {
142		self.dry_run = dry_run;
143		self
144	}
145
146	pub fn with_no_server(mut self, no_server: bool) -> Self {
147		self.no_server = no_server;
148		self
149	}
150
151	pub fn with_server_addrs(mut self, server_addrs: Vec<std::net::SocketAddr>) -> Self {
152		self.server_addrs = server_addrs;
153		self
154	}
155
156	pub fn with_watchdog_timeout(mut self, watchdog_timeout: Option<Duration>) -> Self {
157		self.watchdog_timeout = watchdog_timeout;
158		self
159	}
160}
161
162/// Helper to format miette errors for logging without ANSI codes
163pub(crate) struct LogError<'a>(pub &'a miette::Report);
164
165impl fmt::Display for LogError<'_> {
166	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167		use miette::ReportHandler;
168
169		let handler = miette::NarratableReportHandler::new();
170
171		if let Err(e) = handler.debug(self.0.as_ref(), f) {
172			write!(f, "{}: {}", self.0, e)
173		} else {
174			Ok(())
175		}
176	}
177}