cellstate_core/
health.rs

1//! Unified health check types
2//!
3//! This module provides unified health check types that can be used across
4//! different crates (API, LLM, etc.) for consistent health reporting.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Health status for a service or component.
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
12#[serde(rename_all = "snake_case")]
13pub enum HealthStatus {
14    /// Component is fully operational
15    Healthy,
16    /// Component is operational but degraded
17    Degraded,
18    /// Component is not operational
19    Unhealthy,
20    /// Health status is unknown (e.g., not yet checked)
21    Unknown,
22}
23
24/// Detailed health check result for a component.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
27pub struct HealthCheck {
28    /// Overall health status
29    pub status: HealthStatus,
30    /// Component name
31    pub component: String,
32    /// Detailed status message
33    pub message: Option<String>,
34    /// Response time in milliseconds (if applicable)
35    pub response_time_ms: Option<i64>,
36    /// Additional metadata
37    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
38    pub metadata: Option<HashMap<String, serde_json::Value>>,
39}
40
41impl HealthCheck {
42    /// Create a healthy check result.
43    pub fn healthy(component: impl Into<String>) -> Self {
44        Self {
45            status: HealthStatus::Healthy,
46            component: component.into(),
47            message: None,
48            response_time_ms: None,
49            metadata: None,
50        }
51    }
52
53    /// Create a degraded check result.
54    pub fn degraded(component: impl Into<String>, message: impl Into<String>) -> Self {
55        Self {
56            status: HealthStatus::Degraded,
57            component: component.into(),
58            message: Some(message.into()),
59            response_time_ms: None,
60            metadata: None,
61        }
62    }
63
64    /// Create an unhealthy check result.
65    pub fn unhealthy(component: impl Into<String>, message: impl Into<String>) -> Self {
66        Self {
67            status: HealthStatus::Unhealthy,
68            component: component.into(),
69            message: Some(message.into()),
70            response_time_ms: None,
71            metadata: None,
72        }
73    }
74
75    /// Set the response time.
76    pub fn with_response_time(mut self, ms: i64) -> Self {
77        self.response_time_ms = Some(ms);
78        self
79    }
80
81    /// Add metadata.
82    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
83        self.metadata
84            .get_or_insert_with(HashMap::new)
85            .insert(key.into(), value);
86        self
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn healthy_check_has_correct_status() {
96        let check = HealthCheck::healthy("db");
97        assert_eq!(check.status, HealthStatus::Healthy);
98        assert_eq!(check.component, "db");
99        assert!(check.message.is_none());
100    }
101
102    #[test]
103    fn degraded_check_has_message() {
104        let check = HealthCheck::degraded("cache", "high latency");
105        assert_eq!(check.status, HealthStatus::Degraded);
106        assert_eq!(check.message.as_deref(), Some("high latency"));
107    }
108
109    #[test]
110    fn unhealthy_check_has_message() {
111        let check = HealthCheck::unhealthy("db", "connection refused");
112        assert_eq!(check.status, HealthStatus::Unhealthy);
113        assert_eq!(check.message.as_deref(), Some("connection refused"));
114    }
115
116    #[test]
117    fn with_response_time_sets_field() {
118        let check = HealthCheck::healthy("db").with_response_time(42);
119        assert_eq!(check.response_time_ms, Some(42));
120    }
121
122    #[test]
123    fn with_metadata_adds_entries() {
124        let check = HealthCheck::healthy("db")
125            .with_metadata("version", serde_json::json!("15.2"))
126            .with_metadata("connections", serde_json::json!(10));
127        let meta = check.metadata.unwrap();
128        assert_eq!(meta.len(), 2);
129        assert_eq!(meta["version"], serde_json::json!("15.2"));
130    }
131
132    #[test]
133    fn health_status_serde_roundtrip() {
134        let statuses = vec![
135            HealthStatus::Healthy,
136            HealthStatus::Degraded,
137            HealthStatus::Unhealthy,
138            HealthStatus::Unknown,
139        ];
140        for s in statuses {
141            let json = serde_json::to_string(&s).unwrap();
142            let d: HealthStatus = serde_json::from_str(&json).unwrap();
143            assert_eq!(s, d);
144        }
145    }
146
147    #[test]
148    fn health_check_serde_roundtrip() {
149        let check = HealthCheck::healthy("db")
150            .with_response_time(5)
151            .with_metadata("pool_size", serde_json::json!(20));
152        let json = serde_json::to_string(&check).unwrap();
153        let d: HealthCheck = serde_json::from_str(&json).unwrap();
154        assert_eq!(check, d);
155    }
156}