Skip to main content

things3_core/database/
health.rs

1//! Health check and metrics methods for `ThingsDatabase`.
2
3use crate::{
4    database::{
5        pool::{ComprehensiveHealthStatus, PoolHealthStatus, PoolMetrics},
6        stats::DatabaseStats,
7        ThingsDatabase,
8    },
9    error::{Result as ThingsResult, ThingsError},
10};
11use chrono::Utc;
12use tracing::{debug, error, instrument};
13
14impl ThingsDatabase {
15    /// Check if the database is connected
16    #[instrument]
17    pub async fn is_connected(&self) -> bool {
18        match sqlx::query("SELECT 1").fetch_one(&self.pool).await {
19            Ok(_) => {
20                debug!("Database connection is healthy");
21                true
22            }
23            Err(e) => {
24                error!("Database connection check failed: {}", e);
25                false
26            }
27        }
28    }
29
30    /// Get connection pool health status
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if the health check fails
35    #[instrument]
36    pub async fn get_pool_health(&self) -> ThingsResult<PoolHealthStatus> {
37        let pool_size = self.pool.size();
38        let idle_connections = self.pool.num_idle();
39        let active_connections = pool_size - u32::try_from(idle_connections).unwrap_or(0);
40
41        let is_healthy = self.is_connected().await;
42
43        Ok(PoolHealthStatus {
44            is_healthy,
45            pool_size,
46            active_connections,
47            idle_connections: u32::try_from(idle_connections).unwrap_or(0),
48            max_connections: self.config.max_connections,
49            min_connections: self.config.min_connections,
50            connection_timeout: self.config.connect_timeout,
51            idle_timeout: Some(self.config.idle_timeout),
52            max_lifetime: Some(self.config.max_lifetime),
53        })
54    }
55
56    /// Get detailed connection pool metrics
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the metrics collection fails
61    #[instrument]
62    pub async fn get_pool_metrics(&self) -> ThingsResult<PoolMetrics> {
63        let pool_size = self.pool.size();
64        let idle_connections = self.pool.num_idle();
65        let active_connections = pool_size - u32::try_from(idle_connections).unwrap_or(0);
66
67        let max_connections = self.config.max_connections;
68        let utilization_percentage = if max_connections > 0 {
69            (f64::from(active_connections) / f64::from(max_connections)) * 100.0
70        } else {
71            0.0
72        };
73
74        let start_time = std::time::Instant::now();
75        let is_connected = self.is_connected().await;
76        let response_time_ms = u64::try_from(start_time.elapsed().as_millis()).unwrap_or(0);
77
78        Ok(PoolMetrics {
79            pool_size,
80            active_connections,
81            idle_connections: u32::try_from(idle_connections).unwrap_or(0),
82            max_connections,
83            min_connections: self.config.min_connections,
84            utilization_percentage,
85            is_healthy: is_connected,
86            response_time_ms,
87            connection_timeout: self.config.connect_timeout,
88            idle_timeout: Some(self.config.idle_timeout),
89            max_lifetime: Some(self.config.max_lifetime),
90        })
91    }
92
93    /// Perform a comprehensive health check including pool and database
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if the health check fails
98    #[instrument]
99    pub async fn comprehensive_health_check(&self) -> ThingsResult<ComprehensiveHealthStatus> {
100        let pool_health = self.get_pool_health().await?;
101        let pool_metrics = self.get_pool_metrics().await?;
102        let db_stats = self.get_stats().await?;
103
104        let overall_healthy = pool_health.is_healthy && pool_metrics.is_healthy;
105
106        Ok(ComprehensiveHealthStatus {
107            overall_healthy,
108            pool_health,
109            pool_metrics,
110            database_stats: db_stats,
111            timestamp: Utc::now(),
112        })
113    }
114
115    /// Get database statistics
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if the database query fails
120    #[instrument]
121    pub async fn get_stats(&self) -> ThingsResult<DatabaseStats> {
122        let task_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMTask")
123            .fetch_one(&self.pool)
124            .await
125            .map_err(|e| ThingsError::unknown(format!("Failed to get task count: {e}")))?;
126
127        let project_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMTask WHERE type = 1")
128            .fetch_one(&self.pool)
129            .await
130            .map_err(|e| ThingsError::unknown(format!("Failed to get project count: {e}")))?;
131
132        let area_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMArea")
133            .fetch_one(&self.pool)
134            .await
135            .map_err(|e| ThingsError::unknown(format!("Failed to get area count: {e}")))?;
136
137        Ok(DatabaseStats {
138            task_count: task_count.try_into().unwrap_or(0),
139            project_count: project_count.try_into().unwrap_or(0),
140            area_count: area_count.try_into().unwrap_or(0),
141        })
142    }
143}