praxis_core/config/cluster/
health_check.rs1use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
32#[serde(rename_all = "lowercase")]
33pub enum HealthCheckType {
34 Http,
36 Tcp,
38 Grpc,
40}
41
42impl fmt::Display for HealthCheckType {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Http => f.write_str("http"),
46 Self::Tcp => f.write_str("tcp"),
47 Self::Grpc => f.write_str("grpc"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Deserialize, Serialize)]
78pub struct HealthCheckConfig {
79 #[serde(rename = "type")]
85 pub check_type: HealthCheckType,
86
87 #[serde(default = "default_path")]
89 pub path: String,
90
91 #[serde(default = "default_expected_status")]
93 pub expected_status: u16,
94
95 #[serde(default = "default_interval_ms")]
97 pub interval_ms: u64,
98
99 #[serde(default = "default_timeout_ms")]
101 pub timeout_ms: u64,
102
103 #[serde(default = "default_healthy_threshold")]
105 pub healthy_threshold: u32,
106
107 #[serde(default = "default_unhealthy_threshold")]
109 pub unhealthy_threshold: u32,
110}
111
112fn default_path() -> String {
114 "/".to_owned()
115}
116
117fn default_expected_status() -> u16 {
119 200
120}
121
122fn default_interval_ms() -> u64 {
124 5000
125}
126
127fn default_timeout_ms() -> u64 {
129 2000
130}
131
132fn default_healthy_threshold() -> u32 {
134 2
135}
136
137fn default_unhealthy_threshold() -> u32 {
139 3
140}
141
142#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn parse_full_config() {
152 let yaml = r#"
153type: http
154path: "/healthz"
155expected_status: 200
156interval_ms: 5000
157timeout_ms: 2000
158healthy_threshold: 2
159unhealthy_threshold: 3
160"#;
161 let hc: HealthCheckConfig = serde_yaml::from_str(yaml).unwrap();
162 assert_eq!(hc.check_type, HealthCheckType::Http, "type should be http");
163 assert_eq!(hc.path, "/healthz", "path mismatch");
164 assert_eq!(hc.expected_status, 200, "expected_status mismatch");
165 assert_eq!(hc.interval_ms, 5000, "interval_ms mismatch");
166 assert_eq!(hc.timeout_ms, 2000, "timeout_ms mismatch");
167 assert_eq!(hc.healthy_threshold, 2, "healthy_threshold mismatch");
168 assert_eq!(hc.unhealthy_threshold, 3, "unhealthy_threshold mismatch");
169 }
170
171 #[test]
172 fn defaults_applied() {
173 let yaml = "type: http\n";
174 let hc: HealthCheckConfig = serde_yaml::from_str(yaml).unwrap();
175 assert_eq!(hc.path, "/", "default path should be /");
176 assert_eq!(hc.expected_status, 200, "default expected_status should be 200");
177 assert_eq!(hc.interval_ms, 5000, "default interval_ms should be 5000");
178 assert_eq!(hc.timeout_ms, 2000, "default timeout_ms should be 2000");
179 assert_eq!(hc.healthy_threshold, 2, "default healthy_threshold should be 2");
180 assert_eq!(hc.unhealthy_threshold, 3, "default unhealthy_threshold should be 3");
181 }
182
183 #[test]
184 fn tcp_type_parses() {
185 let yaml = "type: tcp\n";
186 let hc: HealthCheckConfig = serde_yaml::from_str(yaml).unwrap();
187 assert_eq!(hc.check_type, HealthCheckType::Tcp, "type should be tcp");
188 }
189
190 #[test]
191 fn roundtrip_via_serde() {
192 let hc = HealthCheckConfig {
193 check_type: HealthCheckType::Http,
194 path: "/health".to_owned(),
195 expected_status: 204,
196 interval_ms: 10000,
197 timeout_ms: 3000,
198 healthy_threshold: 3,
199 unhealthy_threshold: 5,
200 };
201 let value = serde_yaml::to_value(&hc).unwrap();
202 let back: HealthCheckConfig = serde_yaml::from_value(value).unwrap();
203 assert_eq!(back.check_type, hc.check_type, "type should roundtrip");
204 assert_eq!(back.path, hc.path, "path should roundtrip");
205 assert_eq!(back.expected_status, hc.expected_status, "status should roundtrip");
206 assert_eq!(back.interval_ms, hc.interval_ms, "interval should roundtrip");
207 assert_eq!(back.timeout_ms, hc.timeout_ms, "timeout should roundtrip");
208 }
209
210 #[test]
211 fn unknown_type_rejected_by_serde() {
212 let yaml = "type: websocket\n";
213 let result: Result<HealthCheckConfig, _> = serde_yaml::from_str(yaml);
214 assert!(result.is_err(), "unknown type should be rejected by serde");
215 }
216
217 #[test]
218 fn custom_expected_status() {
219 let yaml = r#"
220type: http
221expected_status: 204
222"#;
223 let hc: HealthCheckConfig = serde_yaml::from_str(yaml).unwrap();
224 assert_eq!(hc.expected_status, 204, "custom expected_status should be 204");
225 }
226}