Health Check
Status: Verified | Idempotent: Yes | Coverage: 95%+
Run Command
cargo run --example api_model_health_check
Code
//! # Recipe: Model Health Check API
//!
//! Contract: contracts/recipe-iiur-v1.yaml
//! **Category**: API Integration
//! **Isolation Level**: Full
//! **Idempotency**: Guaranteed
//! **Dependencies**: None (default features)
//!
//! ## QA Checklist
//! 1. [x] `cargo run` succeeds (Exit Code 0)
//! 2. [x] `cargo test` passes
//! 3. [x] Deterministic output (Verified)
//! 4. [x] No temp files leaked
//! 5. [x] Memory usage stable
//! 6. [x] WASM compatible (N/A)
//! 7. [x] Clippy clean
//! 8. [x] Rustfmt standard
//! 9. [x] No `unwrap()` in logic
//! 10. [x] Proptests pass (100+ cases)
//!
//! ## Learning Objective
//! Health check endpoint for deployed model monitoring.
//!
//! ## Run Command
//! ```bash
//! cargo run --example api_model_health_check
//! ```
//!
//!
//! ## Format Variants
//! ```bash
//! apr serve model.apr # APR native format
//! apr serve model.gguf # GGUF (llama.cpp compatible)
//! apr serve model.safetensors # SafeTensors (HuggingFace)
//! ```
//! ## References
//! - Crankshaw, D. et al. (2017). *Clipper: A Low-Latency Online Prediction Serving System*. NSDI. arXiv:1612.03079
use apr_cookbook::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn main() -> Result<()> {
let mut ctx = RecipeContext::new("api_model_health_check")?;
println!("=== Recipe: {} ===", ctx.name());
println!("Model health check endpoint");
println!();
// Create mock model endpoints
let endpoints = vec![
ModelEndpoint {
name: "fraud-detector".to_string(),
url: "http://localhost:8080/v1/fraud".to_string(),
version: "1.2.0".to_string(),
},
ModelEndpoint {
name: "sentiment-analyzer".to_string(),
url: "http://localhost:8081/v1/sentiment".to_string(),
version: "2.0.1".to_string(),
},
ModelEndpoint {
name: "image-classifier".to_string(),
url: "http://localhost:8082/v1/classify".to_string(),
version: "1.0.0".to_string(),
},
];
ctx.record_metric("endpoints", endpoints.len() as i64);
// Run health checks
println!("Running health checks...");
println!();
let mut health_results = Vec::new();
for endpoint in &endpoints {
let result = check_health(endpoint);
health_results.push(result);
}
// Display results
println!("{:-<70}", "");
println!(
"{:<20} {:<10} {:<15} {:>10} {:>10}",
"Model", "Status", "Version", "Latency", "Memory"
);
println!("{:-<70}", "");
let mut healthy_count = 0;
for result in &health_results {
let status_str = if result.healthy {
"HEALTHY"
} else {
"UNHEALTHY"
};
if result.healthy {
healthy_count += 1;
}
println!(
"{:<20} {:<10} {:<15} {:>8}ms {:>8}MB",
result.name, status_str, result.version, result.latency_ms, result.memory_mb
);
}
println!("{:-<70}", "");
ctx.record_metric("healthy", i64::from(healthy_count));
ctx.record_metric(
"unhealthy",
health_results.len() as i64 - i64::from(healthy_count),
);
// Aggregate health
let aggregate = aggregate_health(&health_results);
println!();
println!("Aggregate Health:");
println!(
" Status: {}",
if aggregate.all_healthy {
"ALL HEALTHY"
} else {
"DEGRADED"
}
);
println!(
" Healthy: {}/{}",
aggregate.healthy_count, aggregate.total_count
);
println!(" Avg latency: {:.1}ms", aggregate.avg_latency_ms);
println!(" Total memory: {}MB", aggregate.total_memory_mb);
// Save health report
let report_path = ctx.path("health_report.json");
save_health_report(&report_path, &health_results, &aggregate)?;
println!();
println!("Health report saved to: {:?}", report_path);
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ModelEndpoint {
name: String,
url: String,
version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct HealthResult {
name: String,
healthy: bool,
version: String,
latency_ms: u32,
memory_mb: u32,
checks: HashMap<String, bool>,
}
#[derive(Debug, Serialize, Deserialize)]
struct AggregateHealth {
all_healthy: bool,
healthy_count: usize,
total_count: usize,
avg_latency_ms: f64,
total_memory_mb: u32,
}
fn check_health(endpoint: &ModelEndpoint) -> HealthResult {
// Deterministic mock health check based on endpoint name
let seed = hash_name_to_seed(&endpoint.name);
// Mock checks
let mut checks = HashMap::new();
checks.insert("model_loaded".to_string(), true);
checks.insert("memory_ok".to_string(), true);
checks.insert("inference_ok".to_string(), true);
checks.insert("dependencies_ok".to_string(), true);
// Deterministic latency and memory based on seed
let latency = 10 + (seed % 50) as u32;
let memory = 100 + (seed % 400) as u32;
HealthResult {
name: endpoint.name.clone(),
healthy: checks.values().all(|&v| v),
version: endpoint.version.clone(),
latency_ms: latency,
memory_mb: memory,
checks,
}
}
fn aggregate_health(results: &[HealthResult]) -> AggregateHealth {
let healthy_count = results.iter().filter(|r| r.healthy).count();
let total_latency: u32 = results.iter().map(|r| r.latency_ms).sum();
let total_memory: u32 = results.iter().map(|r| r.memory_mb).sum();
AggregateHealth {
all_healthy: healthy_count == results.len(),
healthy_count,
total_count: results.len(),
avg_latency_ms: if results.is_empty() {
0.0
} else {
f64::from(total_latency) / results.len() as f64
},
total_memory_mb: total_memory,
}
}
fn save_health_report(
path: &std::path::Path,
results: &[HealthResult],
aggregate: &AggregateHealth,
) -> Result<()> {
#[derive(Serialize)]
struct Report<'a> {
timestamp: u64,
results: &'a [HealthResult],
aggregate: &'a AggregateHealth,
}
let report = Report {
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
results,
aggregate,
};
let json = serde_json::to_string_pretty(&report)
.map_err(|e| CookbookError::Serialization(e.to_string()))?;
std::fs::write(path, json)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_check() {
let endpoint = ModelEndpoint {
name: "test-model".to_string(),
url: "http://localhost".to_string(),
version: "1.0.0".to_string(),
};
let result = check_health(&endpoint);
assert!(result.healthy);
assert_eq!(result.name, "test-model");
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_deterministic_health() {
let endpoint = ModelEndpoint {
name: "test".to_string(),
url: "http://localhost".to_string(),
version: "1.0.0".to_string(),
};
let r1 = check_health(&endpoint);
let r2 = check_health(&endpoint);
assert_eq!(r1.latency_ms, r2.latency_ms);
assert_eq!(r1.memory_mb, r2.memory_mb);
}
#[test]
fn test_aggregate_all_healthy() {
let results = vec![
HealthResult {
name: "m1".to_string(),
healthy: true,
version: "1.0".to_string(),
latency_ms: 10,
memory_mb: 100,
checks: HashMap::new(),
},
HealthResult {
name: "m2".to_string(),
healthy: true,
version: "1.0".to_string(),
latency_ms: 20,
memory_mb: 200,
checks: HashMap::new(),
},
];
let aggregate = aggregate_health(&results);
assert!(aggregate.all_healthy);
assert_eq!(aggregate.healthy_count, 2);
assert_eq!(aggregate.total_count, 2);
assert!((aggregate.avg_latency_ms - 15.0).abs() < 0.01);
assert_eq!(aggregate.total_memory_mb, 300);
}
#[test]
fn test_aggregate_partial_healthy() {
let results = vec![
HealthResult {
name: "m1".to_string(),
healthy: true,
version: "1.0".to_string(),
latency_ms: 10,
memory_mb: 100,
checks: HashMap::new(),
},
HealthResult {
name: "m2".to_string(),
healthy: false,
version: "1.0".to_string(),
latency_ms: 20,
memory_mb: 200,
checks: HashMap::new(),
},
];
let aggregate = aggregate_health(&results);
assert!(!aggregate.all_healthy);
assert_eq!(aggregate.healthy_count, 1);
}
#[test]
fn test_save_report() {
let ctx = RecipeContext::new("test_health_report").unwrap();
let path = ctx.path("report.json");
let results = vec![];
let aggregate = aggregate_health(&results);
save_health_report(&path, &results, &aggregate).unwrap();
assert!(path.exists());
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_aggregate_counts_match(n in 0usize..20) {
let results: Vec<_> = (0..n)
.map(|i| HealthResult {
name: format!("m{}", i),
healthy: true,
version: "1.0".to_string(),
latency_ms: 10,
memory_mb: 100,
checks: HashMap::new(),
})
.collect();
let aggregate = aggregate_health(&results);
prop_assert_eq!(aggregate.total_count, n);
prop_assert_eq!(aggregate.healthy_count, n);
}
#[test]
fn prop_total_memory_sums(memories in proptest::collection::vec(1u32..500, 1..10)) {
let results: Vec<_> = memories
.iter()
.enumerate()
.map(|(i, &mem)| HealthResult {
name: format!("m{}", i),
healthy: true,
version: "1.0".to_string(),
latency_ms: 10,
memory_mb: mem,
checks: HashMap::new(),
})
.collect();
let aggregate = aggregate_health(&results);
let expected: u32 = memories.iter().sum();
prop_assert_eq!(aggregate.total_memory_mb, expected);
}
}
}