use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap as FxHashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
#[cfg(feature = "pyo3")]
use pyo3::PyResult;
#[cfg(feature = "service_mesh")]
use hyper;
use crate::core::{RiResult, RiError};
use crate::observability::{RiTracer, RiSpanKind, RiSpanStatus};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiHealthCheckConfig {
pub endpoint: String,
pub method: String,
pub timeout: Duration,
pub expected_status_code: u16,
pub expected_response_body: Option<String>,
pub headers: FxHashMap<String, String>,
}
impl Default for RiHealthCheckConfig {
fn default() -> Self {
Self {
endpoint: "/health".to_string(),
method: "GET".to_string(),
timeout: Duration::from_secs(5),
expected_status_code: 200,
expected_response_body: None,
headers: FxHashMap::default(),
}
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Debug, Clone)]
pub struct RiHealthCheckResult {
pub service_name: String,
pub endpoint: String,
pub is_healthy: bool,
pub status_code: Option<u16>,
pub response_time: Duration,
pub error_message: Option<String>,
pub timestamp: SystemTime,
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiHealthCheckResult {
fn get_service_name(&self) -> String {
self.service_name.clone()
}
fn get_endpoint(&self) -> String {
self.endpoint.clone()
}
fn get_is_healthy(&self) -> bool {
self.is_healthy
}
fn get_status_code(&self) -> Option<u16> {
self.status_code
}
fn get_response_time_ms(&self) -> u64 {
self.response_time.as_millis() as u64
}
fn get_error_message(&self) -> Option<String> {
self.error_message.clone()
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum RiHealthCheckType {
Http,
Tcp,
Grpc,
Custom,
}
#[async_trait]
pub trait RiHealthCheckProvider: Send + Sync {
async fn check_health(&self, endpoint: &str, config: &RiHealthCheckConfig) -> RiResult<RiHealthCheckResult>;
}
pub struct RiHttpHealthCheckProvider;
#[async_trait]
impl RiHealthCheckProvider for RiHttpHealthCheckProvider {
#[cfg(feature = "service_mesh")]
async fn check_health(&self, endpoint: &str, _config: &RiHealthCheckConfig) -> RiResult<RiHealthCheckResult> {
let start_time = SystemTime::now();
let client = hyper::Client::new();
let uri: hyper::Uri = endpoint.parse()
.map_err(|e| RiError::ServiceMesh(format!("Invalid URI: {e}")))?;
let req = hyper::Request::builder()
.method(_config.method.as_str())
.uri(uri)
.body(hyper::Body::empty())
.map_err(|e| RiError::ServiceMesh(format!("Failed to build request: {e}")))?;
match client.request(req).await {
Ok(response) => {
let status_code = response.status().as_u16();
let is_healthy = status_code == _config.expected_status_code;
let response_time = SystemTime::now().duration_since(start_time)
.unwrap_or(Duration::from_secs(0));
let error_message = if !is_healthy {
Some(format!("Expected status code {}, got {}", _config.expected_status_code, status_code))
} else {
None
};
Ok(RiHealthCheckResult {
service_name: "unknown".to_string(),
endpoint: endpoint.to_string(),
is_healthy,
status_code: Some(status_code),
response_time,
error_message,
timestamp: SystemTime::now(),
})
}
Err(e) => {
let response_time = SystemTime::now().duration_since(start_time)
.unwrap_or(Duration::from_secs(0));
Ok(RiHealthCheckResult {
service_name: "unknown".to_string(),
endpoint: endpoint.to_string(),
is_healthy: false,
status_code: None,
response_time,
error_message: Some(e.to_string()),
timestamp: SystemTime::now(),
})
}
}
}
#[cfg(not(feature = "service_mesh"))]
async fn check_health(&self, endpoint: &str, _config: &RiHealthCheckConfig) -> RiResult<RiHealthCheckResult> {
Ok(RiHealthCheckResult {
service_name: "unknown".to_string(),
endpoint: endpoint.to_string(),
is_healthy: true,
status_code: Some(_config.expected_status_code),
response_time: Duration::from_secs(0),
error_message: None,
timestamp: SystemTime::now(),
})
}
}
pub struct RiTcpHealthCheckProvider;
#[async_trait]
impl RiHealthCheckProvider for RiTcpHealthCheckProvider {
async fn check_health(&self, endpoint: &str, _config: &RiHealthCheckConfig) -> RiResult<RiHealthCheckResult> {
let start_time = SystemTime::now();
match tokio::net::TcpStream::connect(endpoint).await {
Ok(_) => {
let response_time = SystemTime::now().duration_since(start_time)
.unwrap_or(Duration::from_secs(0));
Ok(RiHealthCheckResult {
service_name: "unknown".to_string(),
endpoint: endpoint.to_string(),
is_healthy: true,
status_code: None,
response_time,
error_message: None,
timestamp: SystemTime::now(),
})
}
Err(e) => {
let response_time = SystemTime::now().duration_since(start_time)
.unwrap_or(Duration::from_secs(0));
Ok(RiHealthCheckResult {
service_name: "unknown".to_string(),
endpoint: endpoint.to_string(),
is_healthy: false,
status_code: None,
response_time,
error_message: Some(e.to_string()),
timestamp: SystemTime::now(),
})
}
}
}
}
pub struct RiGrpcHealthCheckProvider;
#[async_trait]
impl RiHealthCheckProvider for RiGrpcHealthCheckProvider {
async fn check_health(&self, endpoint: &str, _config: &RiHealthCheckConfig) -> RiResult<RiHealthCheckResult> {
let start_time = SystemTime::now();
match tokio::net::TcpStream::connect(endpoint).await {
Ok(_) => {
let response_time = SystemTime::now().duration_since(start_time)
.unwrap_or(Duration::from_secs(0));
Ok(RiHealthCheckResult {
service_name: "unknown".to_string(),
endpoint: endpoint.to_string(),
is_healthy: true,
status_code: None,
response_time,
error_message: None,
timestamp: SystemTime::now(),
})
}
Err(e) => {
let response_time = SystemTime::now().duration_since(start_time)
.unwrap_or(Duration::from_secs(0));
Ok(RiHealthCheckResult {
service_name: "unknown".to_string(),
endpoint: endpoint.to_string(),
is_healthy: false,
status_code: None,
response_time,
error_message: Some(e.to_string()),
timestamp: SystemTime::now(),
})
}
}
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub struct RiHealthChecker {
check_interval: Duration,
providers: Arc<RwLock<FxHashMap<RiHealthCheckType, Box<dyn RiHealthCheckProvider>>>>,
check_results: Arc<RwLock<FxHashMap<String, Vec<RiHealthCheckResult>>>>,
background_tasks: Arc<RwLock<Vec<JoinHandle<()>>>>,
tracer: Option<Arc<RiTracer>>,
}
impl RiHealthChecker {
pub fn new(check_interval: Duration) -> Self {
let mut providers: FxHashMap<RiHealthCheckType, Box<dyn RiHealthCheckProvider>> = FxHashMap::default();
providers.insert(RiHealthCheckType::Http, Box::new(RiHttpHealthCheckProvider));
providers.insert(RiHealthCheckType::Tcp, Box::new(RiTcpHealthCheckProvider));
providers.insert(RiHealthCheckType::Grpc, Box::new(RiGrpcHealthCheckProvider));
Self {
check_interval,
providers: Arc::new(RwLock::new(providers)),
check_results: Arc::new(RwLock::new(FxHashMap::default())),
background_tasks: Arc::new(RwLock::new(Vec::new())),
tracer: None,
}
}
pub fn with_tracer(mut self, tracer: Arc<RiTracer>) -> Self {
self.tracer = Some(tracer);
self
}
pub fn set_tracer(&mut self, tracer: Arc<RiTracer>) {
self.tracer = Some(tracer);
}
fn validate_endpoint_url(endpoint: &str) -> RiResult<()> {
if endpoint.is_empty() || endpoint.len() > 2048 {
return Err(RiError::ServiceMesh(
"Endpoint URL must be 1-2048 characters".to_string()
));
}
let parsed_url = url::Url::parse(endpoint)
.map_err(|e| RiError::ServiceMesh(format!("Invalid endpoint URL: {}", e)))?;
let scheme = parsed_url.scheme();
if scheme != "http" && scheme != "https" {
log::warn!(
"[Ri.HealthCheck] Blocked non-HTTP(S) endpoint: scheme={} url={}",
scheme, endpoint
);
return Err(RiError::ServiceMesh(
format!("Invalid URL scheme '{}'. Only HTTP and HTTPS are allowed for health checks.", scheme)
));
}
if let Some(host) = parsed_url.host_str() {
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
log::warn!(
"[Ri.HealthCheck] Health check endpoint points to localhost: {}",
endpoint
);
}
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
let is_private_or_link_local = match ip {
std::net::IpAddr::V4(ipv4) => ipv4.is_private() || ipv4.is_link_local(),
std::net::IpAddr::V6(ipv6) => ipv6.is_unicast_link_local(),
};
if ip.is_loopback() || is_private_or_link_local {
log::warn!(
"[Ri.HealthCheck] Health check endpoint points to private IP {}: {}",
ip, endpoint
);
}
}
}
if parsed_url.username() != "" || parsed_url.password().is_some() {
log::warn!(
"[Ri.HealthCheck] Health check endpoint URL contains credentials: {}",
endpoint.split('@').last().unwrap_or(endpoint)
);
}
Ok(())
}
pub async fn register_health_check(
&self,
service_name: &str,
endpoint: &str,
check_type: RiHealthCheckType,
config: RiHealthCheckConfig,
) -> RiResult<()> {
Self::validate_endpoint_url(endpoint)?;
let span_id = if let Some(tracer) = &self.tracer {
let span_id = tracer.start_span_from_context(
format!("health_check:{}", service_name),
RiSpanKind::Internal,
);
if let Some(ref sid) = span_id {
let _ = tracer.span_mut(sid, |span| {
span.set_attribute("service_name".to_string(), service_name.to_string());
span.set_attribute("endpoint".to_string(), endpoint.to_string());
span.set_attribute("check_type".to_string(), format!("{:?}", check_type));
});
}
span_id
} else {
None
};
let result = self.register_health_check_internal(service_name, endpoint, check_type, config).await;
if let (Some(tracer), Some(sid)) = (&self.tracer, span_id) {
let status = match &result {
Ok(_) => RiSpanStatus::Ok,
Err(e) => RiSpanStatus::Error(e.to_string()),
};
let _ = tracer.end_span(&sid, status);
}
result
}
async fn register_health_check_internal(
&self,
service_name: &str,
endpoint: &str,
check_type: RiHealthCheckType,
config: RiHealthCheckConfig,
) -> RiResult<()> {
let providers = self.providers.read().await;
let provider = providers.get(&check_type)
.ok_or_else(|| RiError::ServiceMesh(format!("Health check provider for {check_type:?} not found")))?;
let result = provider.check_health(endpoint, &config).await?;
let mut check_results = self.check_results.write().await;
let service_results = check_results.entry(service_name.to_string())
.or_insert_with(Vec::new);
service_results.push(result);
Ok(())
}
pub async fn start_health_check(&self, service_name: &str, endpoint: &str) -> RiResult<()> {
let mut tasks = self.background_tasks.write().await;
let service_name_clone = service_name.to_string();
let endpoint_clone = endpoint.to_string();
let check_interval = self.check_interval;
let providers = Arc::clone(&self.providers);
let check_results = Arc::clone(&self.check_results);
let check_type = if endpoint.starts_with("grpc://") || endpoint.starts_with("grpcs://") {
RiHealthCheckType::Grpc
} else if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
RiHealthCheckType::Http
} else {
RiHealthCheckType::Tcp
};
let task = tokio::spawn(async move {
let mut interval = tokio::time::interval(check_interval);
let config = RiHealthCheckConfig::default();
loop {
interval.tick().await;
let providers_guard = providers.read().await;
if let Some(provider) = providers_guard.get(&check_type) {
match provider.check_health(&endpoint_clone, &config).await {
Ok(result) => {
let mut results = check_results.write().await;
let service_results = results.entry(service_name_clone.clone())
.or_insert_with(Vec::new);
service_results.push(result);
if service_results.len() > 100 {
service_results.drain(0..service_results.len() - 100);
}
}
Err(e) => {
log::warn!("Health check failed for {endpoint_clone}: {e}");
}
}
}
}
});
tasks.push(task);
Ok(())
}
pub async fn stop_health_check(&self, service_name: &str, _endpoint: &str) -> RiResult<()> {
let mut results = self.check_results.write().await;
results.remove(service_name);
Ok(())
}
pub async fn start_health_check_with_type(
&self,
service_name: &str,
endpoint: &str,
check_type: RiHealthCheckType
) -> RiResult<()> {
let mut tasks = self.background_tasks.write().await;
let service_name_clone = service_name.to_string();
let endpoint_clone = endpoint.to_string();
let check_interval = self.check_interval;
let providers = Arc::clone(&self.providers);
let check_results = Arc::clone(&self.check_results);
let check_type_clone = check_type;
let task = tokio::spawn(async move {
let mut interval = tokio::time::interval(check_interval);
let config = RiHealthCheckConfig::default();
loop {
interval.tick().await;
let providers_guard = providers.read().await;
if let Some(provider) = providers_guard.get(&check_type_clone) {
match provider.check_health(&endpoint_clone, &config).await {
Ok(result) => {
let mut results = check_results.write().await;
let service_results = results.entry(service_name_clone.clone())
.or_insert_with(Vec::new);
service_results.push(result);
if service_results.len() > 100 {
service_results.drain(0..service_results.len() - 100);
}
}
Err(e) => {
log::warn!("Health check failed for {endpoint_clone}: {e}");
}
}
}
}
});
tasks.push(task);
Ok(())
}
pub async fn get_health_status(&self, service_name: &str) -> RiResult<Vec<RiHealthCheckResult>> {
let check_results = self.check_results.read().await;
let results = check_results.get(service_name)
.cloned()
.unwrap_or_default();
Ok(results)
}
pub async fn get_latest_health_status(&self, service_name: &str) -> RiResult<Option<RiHealthCheckResult>> {
let check_results = self.check_results.read().await;
let latest_result = check_results.get(service_name)
.and_then(|results| results.last().cloned());
Ok(latest_result)
}
pub async fn get_health_status_within(&self, service_name: &str, time_window: Duration) -> RiResult<Vec<RiHealthCheckResult>> {
let check_results = self.check_results.read().await;
let now = SystemTime::now();
let results = check_results.get(service_name)
.map(|results| {
results.iter()
.filter(|r| {
if let Ok(elapsed) = now.duration_since(r.timestamp) {
elapsed <= time_window
} else {
false
}
})
.cloned()
.collect()
})
.unwrap_or_default();
Ok(results)
}
pub async fn get_service_health_summary(&self, service_name: &str) -> RiResult<RiHealthSummary> {
let results = self.get_health_status(service_name).await?;
if results.is_empty() {
return Ok(RiHealthSummary {
service_name: service_name.to_string(),
total_checks: 0,
healthy_checks: 0,
unhealthy_checks: 0,
success_rate: 0.0,
average_response_time: Duration::from_secs(0),
last_check_time: None,
overall_status: RiHealthStatus::Unknown,
});
}
let total_checks = results.len();
let healthy_checks = results.iter().filter(|r| r.is_healthy).count();
let unhealthy_checks = total_checks - healthy_checks;
let success_rate = (healthy_checks as f64) / (total_checks as f64) * 100.0;
let total_response_time: Duration = results.iter()
.map(|r| r.response_time)
.sum();
let average_response_time = total_response_time / total_checks as u32;
let last_check_time = results.last().map(|r| r.timestamp);
let overall_status = if success_rate >= 80.0 {
RiHealthStatus::Healthy
} else if success_rate >= 50.0 {
RiHealthStatus::Degraded
} else {
RiHealthStatus::Unhealthy
};
Ok(RiHealthSummary {
service_name: service_name.to_string(),
total_checks,
healthy_checks,
unhealthy_checks,
success_rate,
average_response_time,
last_check_time,
overall_status,
})
}
pub async fn start_background_tasks(&self) -> RiResult<()> {
let check_results = Arc::clone(&self.check_results);
let cleanup_interval = self.check_interval * 10;
let cleanup_task = tokio::spawn(async move {
let mut interval = tokio::time::interval(cleanup_interval);
loop {
interval.tick().await;
let mut results = check_results.write().await;
let now = SystemTime::now();
let max_age = Duration::from_secs(3600);
for service_results in results.values_mut() {
service_results.retain(|result| {
now.duration_since(result.timestamp)
.map(|age| age < max_age)
.unwrap_or(false)
});
}
results.retain(|_, results| !results.is_empty());
}
});
let mut tasks = self.background_tasks.write().await;
tasks.push(cleanup_task);
log::info!("Background health check tasks started successfully");
Ok(())
}
pub async fn stop_background_tasks(&self) -> RiResult<()> {
let mut tasks = self.background_tasks.write().await;
for task in tasks.drain(..) {
task.abort();
}
Ok(())
}
pub async fn health_check(&self) -> RiResult<bool> {
Ok(true)
}
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiHealthChecker {
#[new]
fn py_new(check_interval: u64) -> PyResult<Self> {
Ok(Self::new(Duration::from_secs(check_interval)))
}
#[pyo3(name = "get_service_health_summary")]
fn get_service_health_summary_impl(&self, service_name: String) -> PyResult<RiHealthSummary> {
let rt = tokio::runtime::Runtime::new().map_err(|e| {
pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create runtime: {}", e))
})?;
rt.block_on(async {
self.get_service_health_summary(&service_name)
.await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to get health summary: {e}")))
})
}
#[pyo3(name = "start_health_check")]
fn start_health_check_impl(&self, service_name: String, endpoint: String) -> PyResult<()> {
let rt = tokio::runtime::Runtime::new().map_err(|e| {
pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create runtime: {}", e))
})?;
rt.block_on(async {
self.start_health_check(&service_name, &endpoint)
.await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to start health check: {e}")))
})
}
#[pyo3(name = "stop_health_check")]
fn stop_health_check_impl(&self, service_name: String, endpoint: String) -> PyResult<()> {
let rt = tokio::runtime::Runtime::new().map_err(|e| {
pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create runtime: {}", e))
})?;
rt.block_on(async {
self.stop_health_check(&service_name, &endpoint)
.await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to stop health check: {e}")))
})
}
#[pyo3(name = "get_health_status")]
fn get_health_status_impl(&self, service_name: String) -> PyResult<Vec<RiHealthCheckResult>> {
let rt = tokio::runtime::Runtime::new().map_err(|e| {
pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create runtime: {}", e))
})?;
rt.block_on(async {
self.get_health_status(&service_name)
.await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to get health status: {e}")))
})
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Debug, Clone)]
pub enum RiHealthStatus {
Healthy,
Degraded,
Unhealthy,
Unknown,
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Debug, Clone)]
pub struct RiHealthSummary {
pub service_name: String,
pub total_checks: usize,
pub healthy_checks: usize,
pub unhealthy_checks: usize,
pub success_rate: f64,
pub average_response_time: Duration,
pub last_check_time: Option<SystemTime>,
pub overall_status: RiHealthStatus,
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiHealthSummary {
fn get_service_name(&self) -> String {
self.service_name.clone()
}
fn get_total_checks(&self) -> usize {
self.total_checks
}
fn get_healthy_checks(&self) -> usize {
self.healthy_checks
}
fn get_unhealthy_checks(&self) -> usize {
self.unhealthy_checks
}
fn get_success_rate(&self) -> f64 {
self.success_rate
}
fn get_average_response_time_ms(&self) -> u64 {
self.average_response_time.as_millis() as u64
}
fn get_overall_status(&self) -> String {
match self.overall_status {
RiHealthStatus::Healthy => "Healthy".to_string(),
RiHealthStatus::Degraded => "Degraded".to_string(),
RiHealthStatus::Unhealthy => "Unhealthy".to_string(),
RiHealthStatus::Unknown => "Unknown".to_string(),
}
}
}