use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "database")]
pub struct DatabasePoolHealth {
pub size: u32,
pub idle: usize,
pub max_size: u32,
pub min_size: u32,
pub healthy: bool,
pub utilization_percent: f32,
}
#[cfg(feature = "database")]
impl DatabasePoolHealth {
pub fn from_pool(pool: &sqlx::PgPool, config: &crate::config::DatabaseConfig) -> Self {
let size = pool.size();
let idle = pool.num_idle();
let max_size = config.max_connections;
let min_size = config.min_connections;
let utilization_percent = if max_size > 0 {
((size as f32 / max_size as f32) * 100.0).min(100.0)
} else {
0.0
};
let healthy = size < max_size;
Self {
size,
idle,
max_size,
min_size,
healthy,
utilization_percent,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "cache")]
pub struct RedisPoolHealth {
pub max_size: usize,
pub available: bool,
pub status: String,
}
#[cfg(feature = "cache")]
impl RedisPoolHealth {
pub fn from_pool(pool: &deadpool_redis::Pool, config: &crate::config::RedisConfig) -> Self {
let max_size = config.max_connections;
let status = pool.status();
let available = status.size > 0 || status.available > 0;
Self {
max_size,
available,
status: format!("size={}, available={}", status.size, status.available),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "turso")]
pub struct TursoDbHealth {
pub mode: String,
pub connected: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub local_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_url: Option<String>,
}
#[cfg(feature = "turso")]
impl TursoDbHealth {
pub fn from_config(config: &crate::config::TursoConfig, connected: bool) -> Self {
Self {
mode: format!("{:?}", config.mode).to_lowercase(),
connected,
local_path: config.path.as_ref().map(|p| p.display().to_string()),
remote_url: config.url.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "surrealdb")]
pub struct SurrealDbHealth {
pub url: String,
pub namespace: String,
pub database: String,
pub connected: bool,
}
#[cfg(feature = "surrealdb")]
impl SurrealDbHealth {
pub fn from_config(config: &crate::config::SurrealDbConfig, connected: bool) -> Self {
Self {
url: crate::surrealdb_backend::sanitize_url(&config.url),
namespace: config.namespace.clone(),
database: config.database.clone(),
connected,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "events")]
pub struct NatsClientHealth {
pub connected: bool,
pub server_url: String,
pub client_name: Option<String>,
}
#[cfg(feature = "events")]
impl NatsClientHealth {
pub fn from_client(client: &async_nats::Client, config: &crate::config::NatsConfig) -> Self {
Self {
connected: client.connection_state() == async_nats::connection::State::Connected,
server_url: config.url.clone(),
client_name: config.name.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg(feature = "clickhouse")]
pub struct ClickHouseHealth {
pub url: String,
pub database: String,
pub connected: bool,
}
#[cfg(feature = "clickhouse")]
impl ClickHouseHealth {
pub fn from_config(config: &crate::config::ClickHouseConfig, connected: bool) -> Self {
Self {
url: crate::clickhouse_backend::sanitize_url(&config.url),
database: config.database.clone(),
connected,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolHealthSummary {
#[cfg(feature = "database")]
#[serde(skip_serializing_if = "Option::is_none")]
pub database: Option<DatabasePoolHealth>,
#[cfg(feature = "cache")]
#[serde(skip_serializing_if = "Option::is_none")]
pub redis: Option<RedisPoolHealth>,
#[cfg(feature = "events")]
#[serde(skip_serializing_if = "Option::is_none")]
pub nats: Option<NatsClientHealth>,
#[cfg(feature = "turso")]
#[serde(skip_serializing_if = "Option::is_none")]
pub turso: Option<TursoDbHealth>,
#[cfg(feature = "surrealdb")]
#[serde(skip_serializing_if = "Option::is_none")]
pub surrealdb: Option<SurrealDbHealth>,
#[cfg(feature = "clickhouse")]
#[serde(skip_serializing_if = "Option::is_none")]
pub clickhouse: Option<ClickHouseHealth>,
pub healthy: bool,
}
impl PoolHealthSummary {
pub fn new() -> Self {
Self {
#[cfg(feature = "database")]
database: None,
#[cfg(feature = "cache")]
redis: None,
#[cfg(feature = "events")]
nats: None,
#[cfg(feature = "turso")]
turso: None,
#[cfg(feature = "surrealdb")]
surrealdb: None,
#[cfg(feature = "clickhouse")]
clickhouse: None,
healthy: true,
}
}
pub fn is_healthy(&self) -> bool {
let database_healthy = {
#[cfg(feature = "database")]
{
self.database.as_ref().is_none_or(|db| db.healthy)
}
#[cfg(not(feature = "database"))]
{
true
}
};
let cache_healthy = {
#[cfg(feature = "cache")]
{
self.redis.as_ref().is_none_or(|redis| redis.available)
}
#[cfg(not(feature = "cache"))]
{
true
}
};
let events_healthy = {
#[cfg(feature = "events")]
{
self.nats.as_ref().is_none_or(|nats| nats.connected)
}
#[cfg(not(feature = "events"))]
{
true
}
};
let turso_healthy = {
#[cfg(feature = "turso")]
{
self.turso.as_ref().is_none_or(|turso| turso.connected)
}
#[cfg(not(feature = "turso"))]
{
true
}
};
let surrealdb_healthy = {
#[cfg(feature = "surrealdb")]
{
self.surrealdb.as_ref().is_none_or(|s| s.connected)
}
#[cfg(not(feature = "surrealdb"))]
{
true
}
};
let clickhouse_healthy = {
#[cfg(feature = "clickhouse")]
{
self.clickhouse.as_ref().is_none_or(|ch| ch.connected)
}
#[cfg(not(feature = "clickhouse"))]
{
true
}
};
database_healthy
&& cache_healthy
&& events_healthy
&& turso_healthy
&& surrealdb_healthy
&& clickhouse_healthy
}
}
impl Default for PoolHealthSummary {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pool_health_summary_default_is_healthy() {
let summary = PoolHealthSummary::new();
assert!(summary.is_healthy(), "Empty summary with no backends should be healthy");
}
#[cfg(feature = "clickhouse")]
mod clickhouse_health_tests {
use super::*;
use crate::config::ClickHouseConfig;
fn test_config() -> ClickHouseConfig {
ClickHouseConfig {
url: "http://admin:secret@ch.internal:8123".to_string(),
database: "analytics".to_string(),
username: Some("admin".to_string()),
password: Some("secret".to_string()),
max_retries: 5,
retry_delay_secs: 2,
optional: false,
lazy_init: true,
}
}
#[test]
fn test_clickhouse_health_from_config_connected() {
let config = test_config();
let health = ClickHouseHealth::from_config(&config, true);
assert!(health.connected);
assert_eq!(health.database, "analytics");
assert!(!health.url.contains("admin"));
assert!(!health.url.contains("secret"));
assert!(health.url.contains("ch.internal:8123"));
}
#[test]
fn test_clickhouse_health_from_config_disconnected() {
let config = test_config();
let health = ClickHouseHealth::from_config(&config, false);
assert!(!health.connected);
assert_eq!(health.database, "analytics");
}
#[test]
fn test_clickhouse_health_serializes_to_json() {
let config = test_config();
let health = ClickHouseHealth::from_config(&config, true);
let json = serde_json::to_value(&health).unwrap();
assert_eq!(json["connected"], true);
assert_eq!(json["database"], "analytics");
assert!(json["url"].is_string());
let url_str = json["url"].as_str().unwrap();
assert!(!url_str.contains("admin"));
assert!(!url_str.contains("secret"));
}
#[test]
fn test_pool_health_summary_unhealthy_when_clickhouse_disconnected() {
let mut summary = PoolHealthSummary::new();
summary.clickhouse = Some(ClickHouseHealth {
url: "http://ch:8123".to_string(),
database: "default".to_string(),
connected: false,
});
assert!(
!summary.is_healthy(),
"Summary should be unhealthy when ClickHouse is disconnected"
);
}
#[test]
fn test_pool_health_summary_healthy_when_clickhouse_connected() {
let mut summary = PoolHealthSummary::new();
summary.clickhouse = Some(ClickHouseHealth {
url: "http://ch:8123".to_string(),
database: "default".to_string(),
connected: true,
});
assert!(
summary.is_healthy(),
"Summary should be healthy when ClickHouse is connected"
);
}
#[test]
fn test_pool_health_summary_healthy_when_clickhouse_none() {
let summary = PoolHealthSummary::new();
assert!(summary.is_healthy());
}
}
}