Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,640 @@
//! Health check endpoints and status monitoring
//!
//! This module provides comprehensive health monitoring including:
//! - Service health and uptime
//! - GPU health and memory monitoring
//! - Inference latency tracking with SLI/SLO
//! - Component status checks
//! - Model loading verification
use axum::Json;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Instant, SystemTime};
use crate::ApiResult;
/// Global latency tracking
static TOTAL_INFERENCE_TIME_NS: AtomicU64 = AtomicU64::new(0);
static TOTAL_INFERENCE_COUNT: AtomicU64 = AtomicU64::new(0);
static P99_LATENCY_NS: AtomicU64 = AtomicU64::new(0);
static MAX_LATENCY_NS: AtomicU64 = AtomicU64::new(0);
/// Health monitoring service
#[derive(Debug, Clone)]
pub struct HealthService {
/// Service start time for uptime calculation
start_time: SystemTime,
}
impl Default for HealthService {
fn default() -> Self {
Self::new()
}
}
impl HealthService {
/// Create a new health service
#[must_use]
pub fn new() -> Self {
Self {
start_time: SystemTime::now(),
}
}
/// Get current uptime in seconds
fn uptime_seconds(&self) -> u64 {
self.start_time.elapsed().unwrap_or_default().as_secs()
}
/// Get current memory usage using system APIs
fn memory_usage_bytes(&self) -> u64 {
#[cfg(target_os = "linux")]
{
// Read from /proc/self/status for accurate memory usage
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if line.starts_with("VmRSS:")
&& let Some(kb_str) = line.split_whitespace().nth(1)
&& let Ok(kb) = kb_str.parse::<u64>()
{
return kb * 1024; // Convert KB to bytes
}
}
}
}
#[cfg(target_os = "macos")]
{
// On macOS, we could use task_info() system call
// For simplicity, return a reasonable estimate
}
#[cfg(target_os = "windows")]
{
// On Windows, use wmic to get process memory info
if let Ok(output) = std::process::Command::new("wmic")
.args([
"process",
"where",
&format!("ProcessId={}", std::process::id()),
"get",
"WorkingSetSize",
"/value",
])
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
for line in stdout.lines() {
if let Some(value) = line.strip_prefix("WorkingSetSize=") {
if let Ok(bytes) = value.trim().parse::<u64>() {
return bytes;
}
}
}
}
}
}
// Fallback for unsupported platforms or when file reading fails
// Return a reasonable default memory estimate
64 * 1024 * 1024 // 64 MB default
}
/// Get number of active requests using atomic counter
fn active_requests(&self) -> u64 {
// In production, this would be tracked by middleware
// using an atomic counter or metrics system like Prometheus
static REQUEST_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
// This is a simplified implementation - in reality, we'd have:
// - Middleware that increments on request start
// - Middleware that decrements on request end
// - Proper request tracking with request IDs and timestamps
REQUEST_COUNTER.load(std::sync::atomic::Ordering::Relaxed)
}
/// Check component status with actual health checks
fn check_components(&self) -> ComponentStatus {
ComponentStatus {
inference_runtime: self.check_inference_runtime(),
model_registry: self.check_model_registry(),
cache_system: self.check_cache_system(),
}
}
/// Check inference runtime health
fn check_inference_runtime(&self) -> String {
// In production, this would:
// 1. Check if inference service is responding
// 2. Verify GPU/CPU availability
// 3. Check model loading status
// 4. Test with a simple inference request
// For now, check basic system health indicators
let memory_usage = self.memory_usage_bytes();
let max_memory = 16 * 1024 * 1024 * 1024; // 16GB threshold
if memory_usage > max_memory {
"degraded - high memory usage".to_string()
} else {
"operational".to_string()
}
}
/// Check model registry health
fn check_model_registry(&self) -> String {
// In production, this would:
// 1. Check database/storage connectivity
// 2. Verify model metadata accessibility
// 3. Check model file system health
// 4. Test model loading capabilities
// Simplified check - verify uptime
if self.uptime_seconds() > 5 {
"operational".to_string()
} else {
"starting".to_string()
}
}
/// Check cache system health
fn check_cache_system(&self) -> String {
// In production, this would:
// 1. Check Redis/Memcached connectivity
// 2. Verify cache hit rates
// 3. Check memory usage of cache
// 4. Test cache read/write operations
// Simplified check - assume healthy if service is running
"operational".to_string()
}
/// Check GPU health and availability
fn check_gpu_health(&self) -> GpuHealthStatus {
#[cfg(feature = "cuda")]
{
// Try to get GPU info using nvidia-smi or NVML
match std::process::Command::new("nvidia-smi")
.args([
"--query-gpu=name,memory.used,memory.total,temperature.gpu,utilization.gpu",
"--format=csv,noheader,nounits",
])
.output()
{
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split(", ").collect();
if parts.len() >= 5 {
let memory_used_mb = parts[1].parse::<u64>().unwrap_or(0);
let memory_total_mb = parts[2].parse::<u64>().unwrap_or(1);
let temperature = parts[3].parse::<u32>().unwrap_or(0);
let utilization = parts[4].parse::<u32>().unwrap_or(0);
let status = if temperature > 85 {
"degraded - high temperature".to_string()
} else if memory_used_mb as f64 / memory_total_mb as f64 > 0.95 {
"degraded - low memory".to_string()
} else {
"operational".to_string()
};
return GpuHealthStatus {
available: true,
device_count: 1,
status,
memory_used_mb,
memory_total_mb,
temperature_celsius: Some(temperature),
utilization_percent: Some(utilization),
};
}
}
_ => {}
}
}
// Fallback when CUDA not available or nvidia-smi fails
GpuHealthStatus {
available: false,
device_count: 0,
status: "not available".to_string(),
memory_used_mb: 0,
memory_total_mb: 0,
temperature_celsius: None,
utilization_percent: None,
}
}
/// Get inference latency metrics
fn get_latency_metrics(&self) -> LatencyMetrics {
let total_time = TOTAL_INFERENCE_TIME_NS.load(Ordering::Relaxed);
let count = TOTAL_INFERENCE_COUNT.load(Ordering::Relaxed);
let p99 = P99_LATENCY_NS.load(Ordering::Relaxed);
let max = MAX_LATENCY_NS.load(Ordering::Relaxed);
let avg_latency_ms = if count > 0 {
(total_time as f64 / count as f64) / 1_000_000.0
} else {
0.0
};
LatencyMetrics {
total_requests: count,
avg_latency_ms,
p99_latency_ms: p99 as f64 / 1_000_000.0,
max_latency_ms: max as f64 / 1_000_000.0,
slo_target_ms: 100.0, // 100ms SLO target
slo_compliance_percent: self.calculate_slo_compliance(),
}
}
/// Calculate SLO compliance percentage
fn calculate_slo_compliance(&self) -> f64 {
let p99 = P99_LATENCY_NS.load(Ordering::Relaxed) as f64 / 1_000_000.0;
let slo_target = 100.0; // 100ms
if p99 <= slo_target {
100.0
} else {
// Linear degradation above target
(slo_target / p99 * 100.0).min(100.0).max(0.0)
}
}
/// Check model loading status
fn check_model_status(&self) -> ModelHealthStatus {
// In production, this would check:
// 1. Number of loaded models
// 2. Model loading errors
// 3. Model memory usage
// 4. Last successful inference time
ModelHealthStatus {
models_loaded: 0, // Would be populated from model registry
models_available: 0,
last_load_time: None,
loading_errors: 0,
status: "ready".to_string(),
}
}
/// Generate health status
#[must_use]
pub fn get_health_status(&self) -> HealthStatus {
let gpu_status = self.check_gpu_health();
let latency = self.get_latency_metrics();
let model_status = self.check_model_status();
// Determine overall health based on all components
let overall_status = self.determine_overall_status(&gpu_status, &latency, &model_status);
HealthStatus {
status: overall_status,
timestamp: Utc::now(),
version: crate::VERSION.to_string(),
uptime_seconds: self.uptime_seconds(),
details: HealthDetails {
memory_usage_bytes: self.memory_usage_bytes(),
active_requests: self.active_requests(),
components: self.check_components(),
gpu: gpu_status,
latency,
models: model_status,
},
}
}
/// Determine overall health status based on all components
fn determine_overall_status(
&self,
gpu: &GpuHealthStatus,
latency: &LatencyMetrics,
_models: &ModelHealthStatus,
) -> String {
// Check for critical issues
if gpu.available && gpu.status.contains("degraded") {
return "degraded".to_string();
}
if latency.slo_compliance_percent < 90.0 {
return "degraded".to_string();
}
if self.memory_usage_bytes() > 14 * 1024 * 1024 * 1024 {
// >14GB RAM used
return "degraded".to_string();
}
"healthy".to_string()
}
}
/// Record an inference latency measurement
///
/// Call this after each inference request to update latency metrics.
pub fn record_inference_latency(latency_ns: u64) {
TOTAL_INFERENCE_TIME_NS.fetch_add(latency_ns, Ordering::Relaxed);
TOTAL_INFERENCE_COUNT.fetch_add(1, Ordering::Relaxed);
// Update max latency
let mut current_max = MAX_LATENCY_NS.load(Ordering::Relaxed);
while latency_ns > current_max {
match MAX_LATENCY_NS.compare_exchange_weak(
current_max,
latency_ns,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => current_max = actual,
}
}
// Simple P99 approximation (in production, use a proper histogram)
// This updates P99 if the new latency is higher than 99% of expected latencies
let count = TOTAL_INFERENCE_COUNT.load(Ordering::Relaxed);
if count > 100 {
let current_p99 = P99_LATENCY_NS.load(Ordering::Relaxed);
// Simple heuristic: update P99 toward the new value
if latency_ns > current_p99 {
let _ = P99_LATENCY_NS.compare_exchange(
current_p99,
(current_p99 * 99 + latency_ns) / 100,
Ordering::Relaxed,
Ordering::Relaxed,
);
}
}
}
/// Inference latency guard for automatic timing
///
/// Use this to automatically record inference latency when the guard is dropped.
/// ```ignore
/// let _timer = InferenceTimer::new();
/// // ... do inference ...
/// // latency is automatically recorded when _timer goes out of scope
/// ```
pub struct InferenceTimer {
start: Instant,
}
impl InferenceTimer {
/// Start a new inference timer
#[must_use]
pub fn new() -> Self {
Self {
start: Instant::now(),
}
}
}
impl Default for InferenceTimer {
fn default() -> Self {
Self::new()
}
}
impl Drop for InferenceTimer {
fn drop(&mut self) {
let elapsed = self.start.elapsed();
record_inference_latency(elapsed.as_nanos() as u64);
}
}
/// Health status response
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HealthStatus {
/// Service status
pub status: String,
/// Current timestamp
pub timestamp: DateTime<Utc>,
/// Service version
pub version: String,
/// Uptime in seconds
pub uptime_seconds: u64,
/// Additional details
pub details: HealthDetails,
}
/// Additional health details
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HealthDetails {
/// Memory usage in bytes
pub memory_usage_bytes: u64,
/// Number of active requests
pub active_requests: u64,
/// Service components status
pub components: ComponentStatus,
/// GPU health status
pub gpu: GpuHealthStatus,
/// Inference latency metrics
pub latency: LatencyMetrics,
/// Model loading status
pub models: ModelHealthStatus,
}
/// Component status details
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ComponentStatus {
/// Inference runtime status
pub inference_runtime: String,
/// Model registry status
pub model_registry: String,
/// Cache system status
pub cache_system: String,
}
/// GPU health status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GpuHealthStatus {
/// Whether GPU is available
pub available: bool,
/// Number of GPU devices
pub device_count: u32,
/// Overall GPU status
pub status: String,
/// GPU memory used in MB
pub memory_used_mb: u64,
/// Total GPU memory in MB
pub memory_total_mb: u64,
/// GPU temperature in Celsius
pub temperature_celsius: Option<u32>,
/// GPU utilization percentage
pub utilization_percent: Option<u32>,
}
/// Inference latency metrics
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LatencyMetrics {
/// Total number of inference requests
pub total_requests: u64,
/// Average latency in milliseconds
pub avg_latency_ms: f64,
/// 99th percentile latency in milliseconds
pub p99_latency_ms: f64,
/// Maximum latency in milliseconds
pub max_latency_ms: f64,
/// SLO target in milliseconds
pub slo_target_ms: f64,
/// SLO compliance percentage
pub slo_compliance_percent: f64,
}
/// Model health status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModelHealthStatus {
/// Number of models currently loaded
pub models_loaded: u32,
/// Number of models available to load
pub models_available: u32,
/// Last successful model load time
pub last_load_time: Option<DateTime<Utc>>,
/// Number of model loading errors
pub loading_errors: u32,
/// Overall model status
pub status: String,
}
/// Global health service instance (in a real app, this would be managed by DI)
static HEALTH_SERVICE: std::sync::LazyLock<HealthService> =
std::sync::LazyLock::new(HealthService::new);
/// Health check handler
pub async fn health_check() -> ApiResult<Json<HealthStatus>> {
// REFACTOR phase: Use structured health service
let health_status = HEALTH_SERVICE.get_health_status();
Ok(Json(health_status))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{Router, http::StatusCode, routing::get};
use axum_test::TestServer;
/// RED PHASE: This test should fail initially
#[tokio::test]
async fn test_health_check_endpoint() {
// Arrange: Create a test server with health check route
let app = Router::new().route("/health", get(health_check));
let server = TestServer::new(app).unwrap();
// Act: Make request to health endpoint
let response = server.get("/health").await;
// Assert: Should return 200 OK with proper health status
response.assert_status(StatusCode::OK);
let health_status: HealthStatus = response.json();
// Verify health status structure
assert_eq!(health_status.status, "healthy");
assert_eq!(health_status.version, crate::VERSION);
assert!(!health_status.timestamp.to_string().is_empty());
assert_eq!(
health_status.details.components.inference_runtime,
"operational"
);
assert_eq!(
health_status.details.components.model_registry,
"operational"
);
assert_eq!(health_status.details.components.cache_system, "operational");
}
#[tokio::test]
async fn test_health_status_serialization() {
// Create a sample health status
let status = HealthStatus {
status: "healthy".to_string(),
timestamp: Utc::now(),
version: "0.1.0".to_string(),
uptime_seconds: 3600,
details: HealthDetails {
memory_usage_bytes: 1024 * 1024 * 100, // 100MB
active_requests: 5,
components: ComponentStatus {
inference_runtime: "operational".to_string(),
model_registry: "operational".to_string(),
cache_system: "operational".to_string(),
},
gpu: GpuHealthStatus {
available: true,
device_count: 1,
status: "operational".to_string(),
memory_used_mb: 4096,
memory_total_mb: 24576,
temperature_celsius: Some(45),
utilization_percent: Some(75),
},
latency: LatencyMetrics {
total_requests: 1000,
avg_latency_ms: 25.5,
p99_latency_ms: 85.0,
max_latency_ms: 150.0,
slo_target_ms: 100.0,
slo_compliance_percent: 99.5,
},
models: ModelHealthStatus {
models_loaded: 3,
models_available: 5,
last_load_time: Some(Utc::now()),
loading_errors: 0,
status: "ready".to_string(),
},
},
};
// Should serialize to JSON without errors
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("healthy"));
assert!(json.contains("gpu"));
assert!(json.contains("latency"));
assert!(json.contains("models"));
// Should deserialize back properly
let deserialized: HealthStatus = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.status, status.status);
assert_eq!(deserialized.details.gpu.available, true);
assert_eq!(deserialized.details.latency.total_requests, 1000);
}
#[test]
fn test_inference_timer() {
// Record some test latencies
record_inference_latency(10_000_000); // 10ms
record_inference_latency(20_000_000); // 20ms
record_inference_latency(15_000_000); // 15ms
let service = HealthService::new();
let metrics = service.get_latency_metrics();
assert_eq!(metrics.total_requests, 3);
assert!(metrics.avg_latency_ms > 0.0);
}
#[test]
fn test_gpu_health_check() {
let service = HealthService::new();
let gpu_status = service.check_gpu_health();
// GPU may or may not be available depending on test environment
// Just verify the structure is valid
assert!(!gpu_status.status.is_empty());
}
#[test]
fn test_slo_compliance() {
let service = HealthService::new();
let compliance = service.calculate_slo_compliance();
// Should be between 0 and 100
assert!(compliance >= 0.0 && compliance <= 100.0);
}
}