use std::{path::PathBuf, sync::Arc};
use node_semver::Version;
use tokio_postgres::Client as PgClient;
use bestool_tamanu::{ApiServerKind, config::TamanuConfig};
use super::check::Check;
pub mod util;
pub mod caddy_version;
pub mod certificate_notification_errors;
pub mod db_connect;
pub mod db_version;
pub mod disk_free;
pub mod external_users;
pub mod fhir_job_errors;
pub mod fhir_jobs;
pub mod fhir_service_requests_unresolved;
pub mod http_errors;
pub mod ips_errors;
pub mod kopia_backup;
pub mod load;
pub mod memory;
pub mod migrations;
pub mod patient_communication_errors;
pub mod report_errors;
pub mod server_id;
pub mod sync_facility_stale;
pub mod sync_lookup;
pub mod sync_restart_loop;
pub mod sync_session_errors;
pub mod sync_sessions;
pub mod tailscale;
pub mod tamanu_found;
pub mod tamanu_http;
pub mod tamanu_service;
pub mod time_sync;
pub mod uptime;
pub mod version_drift;
#[derive(Clone)]
pub struct CheckContext {
pub tamanu_version: Version,
pub tamanu_root: PathBuf,
pub config: Arc<TamanuConfig>,
pub kind: ApiServerKind,
pub database_url: String,
pub db: Option<Arc<PgClient>>,
pub http_client: reqwest::Client,
}
pub fn fmt_db_error(err: &tokio_postgres::Error) -> String {
if let Some(db) = err.as_db_error() {
let mut s = format!("{}: {}", db.severity(), db.message());
if let Some(detail) = db.detail() {
s.push_str(" — ");
s.push_str(detail);
}
return s;
}
fmt_chain(err)
}
pub fn query_error_check(name: &'static str, err: &tokio_postgres::Error) -> Check {
let reason = fmt_db_error(err);
if err
.as_db_error()
.is_some_and(|db| db.code().code().starts_with("42"))
{
Check::broken(name, "healthcheck query broken", reason)
} else {
Check::fail(name, "query failed", reason)
}
}
pub fn fmt_chain<E: std::error::Error + ?Sized>(err: &E) -> String {
use std::error::Error;
let mut parts = vec![err.to_string()];
let mut src: Option<&dyn Error> = err.source();
while let Some(s) = src {
parts.push(s.to_string());
src = s.source();
}
parts.join(": ")
}
pub struct CheckEntry {
pub name: &'static str,
pub on_wire: bool,
pub run: fn(CheckContext) -> futures::future::BoxFuture<'static, Check>,
}
macro_rules! entry {
($name:literal, $module:ident) => {
CheckEntry {
name: $name,
on_wire: true,
run: |ctx| Box::pin($module::run(ctx)),
}
};
($name:literal, $module:ident, off_wire) => {
CheckEntry {
name: $name,
on_wire: false,
run: |ctx| Box::pin($module::run(ctx)),
}
};
}
pub fn all() -> Vec<CheckEntry> {
vec![
entry!("tamanu_found", tamanu_found),
entry!("db_connect", db_connect),
entry!("db_version", db_version),
entry!("server_id", server_id),
entry!("migrations", migrations),
entry!("disk_free", disk_free),
entry!("memory", memory),
entry!("load", load),
entry!("uptime", uptime),
entry!("time_sync", time_sync),
entry!("tamanu_http", tamanu_http),
entry!("caddy_version", caddy_version),
entry!("http_errors", http_errors),
entry!("tailscale", tailscale, off_wire),
entry!("tamanu_service", tamanu_service),
entry!("version_drift", version_drift),
entry!("external_users", external_users),
entry!("sync_sessions", sync_sessions),
entry!("fhir_jobs", fhir_jobs),
entry!("kopia_backup", kopia_backup),
entry!(
"certificate_notification_errors",
certificate_notification_errors
),
entry!("ips_errors", ips_errors),
entry!("patient_communication_errors", patient_communication_errors),
entry!("report_errors", report_errors),
entry!("fhir_job_errors", fhir_job_errors),
entry!("sync_session_errors", sync_session_errors),
entry!("sync_facility_stale", sync_facility_stale),
entry!("sync_lookup", sync_lookup),
entry!("sync_restart_loop", sync_restart_loop),
entry!(
"fhir_service_requests_unresolved",
fhir_service_requests_unresolved
),
]
}
#[cfg(test)]
pub mod test_support {
use std::sync::Arc;
use node_semver::Version;
use bestool_tamanu::{ApiServerKind, config::TamanuConfig};
use super::CheckContext;
fn central_config() -> TamanuConfig {
serde_json::from_value(serde_json::json!({
"db": { "name": "tamanu-central", "username": "u", "password": "p" },
}))
.expect("central test config should parse")
}
fn facility_config() -> TamanuConfig {
serde_json::from_value(serde_json::json!({
"db": { "name": "tamanu-facility", "username": "u", "password": "p" },
"serverFacilityIds": ["facility-1"],
}))
.expect("facility test config should parse")
}
async fn connect(db_name: &str) -> Option<Arc<tokio_postgres::Client>> {
let url = format!("postgresql://localhost/{db_name}");
match bestool_postgres::pool::connect_one(&url, "bestool-alertd-test").await {
Ok(client) => Some(Arc::new(client)),
Err(_) => None,
}
}
pub async fn central_ctx() -> Option<CheckContext> {
let db = connect("tamanu-central").await?;
Some(CheckContext {
tamanu_version: Version::parse("0.0.0").unwrap(),
tamanu_root: std::path::PathBuf::from("/nonexistent"),
config: Arc::new(central_config()),
kind: ApiServerKind::Central,
database_url: "postgresql://localhost/tamanu-central".into(),
db: Some(db),
http_client: reqwest::Client::new(),
})
}
pub fn facility_ctx() -> CheckContext {
CheckContext {
tamanu_version: Version::parse("0.0.0").unwrap(),
tamanu_root: std::path::PathBuf::from("/nonexistent"),
config: Arc::new(facility_config()),
kind: ApiServerKind::Facility,
database_url: "postgresql://localhost/tamanu-facility".into(),
db: None,
http_client: reqwest::Client::new(),
}
}
}
#[cfg(test)]
mod tests {
use serde_json::Value;
use super::{query_error_check, test_support::central_ctx};
use crate::doctor::check::CheckStatus;
async fn query_err(sql: &str) -> Option<tokio_postgres::Error> {
let ctx = central_ctx().await?;
let client = ctx.db.expect("central_ctx always has a client");
Some(
client
.query(sql, &[])
.await
.expect_err("query should error"),
)
}
#[tokio::test]
async fn schema_drift_is_broken() {
let Some(err) = query_err("SELECT nope FROM no_such_table_bestool_test").await else {
return;
};
let check = query_error_check("x", &err);
assert!(matches!(check.status, CheckStatus::Broken(_)));
assert_eq!(check.to_wire()["result"], Value::from("broken"));
}
#[tokio::test]
async fn syntax_error_is_broken() {
let Some(err) = query_err("SELECT FROM WHERE").await else {
return;
};
let check = query_error_check("x", &err);
assert!(matches!(check.status, CheckStatus::Broken(_)));
}
#[tokio::test]
async fn runtime_db_error_still_fails() {
let Some(err) = query_err("SELECT 1/0").await else {
return;
};
let check = query_error_check("x", &err);
assert!(check.status.is_fatal());
assert_eq!(check.to_wire()["result"], Value::from("failed"));
}
}