bestool 2.1.1

BES Deployment tooling
Documentation
//! The alertd daemon: the thing that runs the healthchecks on a schedule.
//!
//! The checks themselves, and the machinery they're built from, live in the
//! `bestool-alertd` crate, so a consumer can harvest checks without pulling in
//! the HTTP server, the backup registry, or the service registration here.
//! This module is the composition: it wires a schedule, an HTTP server, and a
//! canopy client together.

use std::{fmt, sync::Arc, time::Duration};

use bestool_alertd::Redacted;

pub mod backup;
mod child_confinement;
pub mod commands;
mod context;
mod daemon;
pub mod doctor;
pub mod http_server;
mod metrics;
pub mod tasks;

#[cfg(windows)]
pub mod windows_service;

pub use backup::{BackupRegistry, BackupRunner, BackupTask, RunningBackup};
pub use daemon::run;
pub use tasks::{BackgroundTask, TaskContext, TaskEndpoint, TaskEndpointResponse};

/// Configuration for the alertd daemon
#[derive(Clone)]
pub struct DaemonConfig {
	/// Tamanu device key PEM, used as the client identity for canopy.
	///
	/// Held only long enough to build the canopy `reqwest::Client` at startup,
	/// then dropped. Wrapped in `Redacted` so debug-logging the config can't
	/// leak the key.
	pub device_key_pem: Option<Redacted<String>>,

	/// Whether to disable the HTTP server
	pub no_server: bool,

	/// HTTP server bind addresses
	pub server_addrs: Vec<std::net::SocketAddr>,

	/// Watchdog timeout duration
	///
	/// If no background task reports activity within this duration, the daemon
	/// will exit with an error so it can be restarted by the service manager.
	/// Set to `None` to disable the watchdog.
	pub watchdog_timeout: Option<Duration>,

	/// Background tasks to run on a schedule.
	///
	/// Each task ticks at its own `interval()`. Errors are logged but do not
	/// kill the daemon. Activity from each tick counts towards the watchdog.
	pub background_tasks: Vec<Arc<dyn BackgroundTask>>,

	/// Backup run registry, set when backups are compiled in. Surfaced via the
	/// daemon's status so an operator can see what's backing up right now.
	pub backups: Option<Arc<BackupRegistry>>,

	/// Handle to the doctor task's latest sweep, set when a doctor task is
	/// registered. Feeds the `/metrics` endpoint the per-check declared stats
	/// and the status census.
	pub metrics: Option<doctor::DoctorMetricsHandle>,
}

impl fmt::Debug for DaemonConfig {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		f.debug_struct("DaemonConfig")
			.field("device_key_pem", &self.device_key_pem)
			.field("no_server", &self.no_server)
			.field("server_addrs", &self.server_addrs)
			.field("watchdog_timeout", &self.watchdog_timeout)
			.field(
				"background_tasks",
				&self
					.background_tasks
					.iter()
					.map(|t| t.name())
					.collect::<Vec<_>>(),
			)
			.finish()
	}
}

impl DaemonConfig {
	pub fn new() -> Self {
		Self {
			device_key_pem: None,
			no_server: false,
			server_addrs: Vec::new(),
			watchdog_timeout: Some(Duration::from_secs(10 * 60)),
			background_tasks: Vec::new(),
			backups: None,
			metrics: None,
		}
	}

	pub fn with_task(mut self, task: Arc<dyn BackgroundTask>) -> Self {
		self.background_tasks.push(task);
		self
	}

	/// Attach the backup registry, so the daemon's status can list in-flight runs.
	pub fn with_backups(mut self, registry: Arc<BackupRegistry>) -> Self {
		self.backups = Some(registry);
		self
	}

	/// Attach the doctor metrics handle, so `/metrics` can serve per-check stats.
	pub fn with_metrics_handle(mut self, metrics: doctor::DoctorMetricsHandle) -> Self {
		self.metrics = Some(metrics);
		self
	}

	pub fn with_device_key_pem(mut self, pem: String) -> Self {
		self.device_key_pem = Some(Redacted(pem));
		self
	}

	pub fn with_no_server(mut self, no_server: bool) -> Self {
		self.no_server = no_server;
		self
	}

	pub fn with_server_addrs(mut self, server_addrs: Vec<std::net::SocketAddr>) -> Self {
		self.server_addrs = server_addrs;
		self
	}

	pub fn with_watchdog_timeout(mut self, watchdog_timeout: Option<Duration>) -> Self {
		self.watchdog_timeout = watchdog_timeout;
		self
	}
}

/// The running binary's version: the daemon's status line and the sweep payload
/// both report it, and both mean this binary rather than the checks crate.
pub const BINARY_VERSION: &str = env!("CARGO_PKG_VERSION");

impl Default for DaemonConfig {
	fn default() -> Self {
		Self::new()
	}
}

/// Base builder for the daemon's outbound HTTP clients. Call sites add their
/// own timeouts etc. Identifies as [`bestool_alertd::USER_AGENT`], which
/// carries the checks crate's version rather than this binary's.
pub fn http_builder() -> reqwest::ClientBuilder {
	reqwest::Client::builder().user_agent(bestool_alertd::USER_AGENT)
}

/// A built [`reqwest::Client`] from [`http_builder`].
pub fn http_client() -> reqwest::Client {
	http_builder()
		.build()
		.expect("failed to build alertd HTTP client")
}

/// Helper to format miette errors for logging without ANSI codes
pub(crate) struct LogError<'a>(pub &'a miette::Report);

impl fmt::Display for LogError<'_> {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		use miette::ReportHandler;

		let handler = miette::NarratableReportHandler::new();

		if let Err(e) = handler.debug(self.0.as_ref(), f) {
			write!(f, "{}: {}", self.0, e)
		} else {
			Ok(())
		}
	}
}