//! Local systemd daemon-health check storage. //! //! Stores the latest result per target the same way `scan_pipeline_checks` does, //! so `/status.json` renders it from the ledger rather than shelling out to //! `systemctl` on every viewer poll. The unit snapshots, host-wide failed units, //! and issue lines are each held as a JSON blob. use super::{Result, SqlitePool, SystemdCheckResult, SystemdUnitSnapshot}; use tracing::instrument; #[instrument(skip_all)] pub async fn insert_systemd_check(pool: &SqlitePool, result: &SystemdCheckResult) -> Result { let units = serde_json::to_string(&result.units).unwrap_or_default(); let failed_units = serde_json::to_string(&result.failed_units).unwrap_or_default(); let issues = serde_json::to_string(&result.issues).unwrap_or_default(); let row = sqlx::query( "INSERT INTO systemd_checks (target, status, units, failed_units, issues, checked_at, error) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind(&result.target) .bind(&result.status) .bind(&units) .bind(&failed_units) .bind(&issues) .bind(&result.checked_at) .bind(&result.error) .execute(pool) .await?; Ok(row.last_insert_rowid()) } /// The latest systemd check for a target, if one has been recorded. #[instrument(skip_all)] pub async fn get_latest_systemd_check( pool: &SqlitePool, target: &str, ) -> Result> { Ok(sqlx::query_as::<_, SystemdCheckRow>( "SELECT id, target, status, units, failed_units, issues, checked_at, error FROM systemd_checks WHERE target = ? ORDER BY id DESC LIMIT 1", ) .bind(target) .fetch_optional(pool) .await?) } #[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)] pub struct SystemdCheckRow { pub id: i64, pub target: String, pub status: String, /// JSON array of unit snapshots. Use [`Self::unit_list`]. pub units: String, /// JSON array of host-wide failed unit names. Use [`Self::failed_unit_list`]. pub failed_units: String, /// JSON array of the fired issue lines. Use [`Self::issue_list`]. pub issues: String, pub checked_at: String, pub error: Option, } impl SystemdCheckRow { /// Decode the stored unit snapshots, tolerating a malformed blob as empty. pub fn unit_list(&self) -> Vec { serde_json::from_str(&self.units).unwrap_or_default() } /// Decode the stored failed-unit names, tolerating a malformed blob as empty. pub fn failed_unit_list(&self) -> Vec { serde_json::from_str(&self.failed_units).unwrap_or_default() } /// Decode the stored issue lines, tolerating a malformed blob as empty. pub fn issue_list(&self) -> Vec { serde_json::from_str(&self.issues).unwrap_or_default() } }