1use bytes::Bytes;
8use http::{Response, StatusCode};
9use http_body_util::Full;
10use serde::Serialize;
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14
15use sentinel_config::{BuiltinHandler, Config};
16
17pub struct BuiltinHandlerState {
19 start_time: Instant,
21 version: String,
23 instance_id: String,
25}
26
27impl BuiltinHandlerState {
28 pub fn new(version: String, instance_id: String) -> Self {
30 Self {
31 start_time: Instant::now(),
32 version,
33 instance_id,
34 }
35 }
36
37 pub fn uptime(&self) -> Duration {
39 self.start_time.elapsed()
40 }
41
42 pub fn uptime_string(&self) -> String {
44 let uptime = self.uptime();
45 let secs = uptime.as_secs();
46 let days = secs / 86400;
47 let hours = (secs % 86400) / 3600;
48 let mins = (secs % 3600) / 60;
49 let secs = secs % 60;
50
51 if days > 0 {
52 format!("{}d {}h {}m {}s", days, hours, mins, secs)
53 } else if hours > 0 {
54 format!("{}h {}m {}s", hours, mins, secs)
55 } else if mins > 0 {
56 format!("{}m {}s", mins, secs)
57 } else {
58 format!("{}s", secs)
59 }
60 }
61}
62
63#[derive(Debug, Serialize)]
65pub struct StatusResponse {
66 pub status: &'static str,
68 pub version: String,
70 pub uptime: String,
72 pub uptime_secs: u64,
74 pub instance_id: String,
76 pub timestamp: String,
78}
79
80#[derive(Debug, Serialize)]
82pub struct HealthResponse {
83 pub status: &'static str,
85 pub timestamp: String,
87}
88
89#[derive(Debug, Clone, Default)]
91pub struct UpstreamHealthSnapshot {
92 pub upstreams: HashMap<String, UpstreamStatus>,
94}
95
96#[derive(Debug, Clone, Serialize)]
98pub struct UpstreamStatus {
99 pub id: String,
101 pub load_balancing: String,
103 pub targets: Vec<TargetStatus>,
105}
106
107#[derive(Debug, Clone, Serialize)]
109pub struct TargetStatus {
110 pub address: String,
112 pub weight: u32,
114 pub status: TargetHealthStatus,
116 pub failure_rate: Option<f64>,
118 pub last_error: Option<String>,
120}
121
122#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
124#[serde(rename_all = "lowercase")]
125pub enum TargetHealthStatus {
126 Healthy,
128 Unhealthy,
130 Unknown,
132}
133
134pub fn execute_handler(
136 handler: BuiltinHandler,
137 state: &BuiltinHandlerState,
138 request_id: &str,
139 config: Option<Arc<Config>>,
140 upstreams: Option<UpstreamHealthSnapshot>,
141) -> Response<Full<Bytes>> {
142 match handler {
143 BuiltinHandler::Status => status_handler(state, request_id),
144 BuiltinHandler::Health => health_handler(request_id),
145 BuiltinHandler::Metrics => metrics_handler(request_id),
146 BuiltinHandler::NotFound => not_found_handler(request_id),
147 BuiltinHandler::Config => config_handler(config, request_id),
148 BuiltinHandler::Upstreams => upstreams_handler(upstreams, request_id),
149 }
150}
151
152fn status_handler(state: &BuiltinHandlerState, request_id: &str) -> Response<Full<Bytes>> {
154 let response = StatusResponse {
155 status: "ok",
156 version: state.version.clone(),
157 uptime: state.uptime_string(),
158 uptime_secs: state.uptime().as_secs(),
159 instance_id: state.instance_id.clone(),
160 timestamp: chrono::Utc::now().to_rfc3339(),
161 };
162
163 let body = serde_json::to_vec_pretty(&response).unwrap_or_else(|_| {
164 b"{\"status\":\"ok\"}".to_vec()
165 });
166
167 Response::builder()
168 .status(StatusCode::OK)
169 .header("Content-Type", "application/json; charset=utf-8")
170 .header("X-Request-Id", request_id)
171 .header("Cache-Control", "no-cache, no-store, must-revalidate")
172 .body(Full::new(Bytes::from(body)))
173 .expect("static response builder with valid headers cannot fail")
174}
175
176fn health_handler(request_id: &str) -> Response<Full<Bytes>> {
178 let response = HealthResponse {
179 status: "healthy",
180 timestamp: chrono::Utc::now().to_rfc3339(),
181 };
182
183 let body = serde_json::to_vec(&response).unwrap_or_else(|_| {
184 b"{\"status\":\"healthy\"}".to_vec()
185 });
186
187 Response::builder()
188 .status(StatusCode::OK)
189 .header("Content-Type", "application/json; charset=utf-8")
190 .header("X-Request-Id", request_id)
191 .header("Cache-Control", "no-cache, no-store, must-revalidate")
192 .body(Full::new(Bytes::from(body)))
193 .expect("static response builder with valid headers cannot fail")
194}
195
196fn metrics_handler(request_id: &str) -> Response<Full<Bytes>> {
198 let metrics = format!(
201 "# HELP sentinel_up Sentinel proxy is up and running\n\
202 # TYPE sentinel_up gauge\n\
203 sentinel_up 1\n\
204 # HELP sentinel_build_info Build information\n\
205 # TYPE sentinel_build_info gauge\n\
206 sentinel_build_info{{version=\"{}\"}} 1\n",
207 env!("CARGO_PKG_VERSION")
208 );
209
210 Response::builder()
211 .status(StatusCode::OK)
212 .header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
213 .header("X-Request-Id", request_id)
214 .body(Full::new(Bytes::from(metrics)))
215 .expect("static response builder with valid headers cannot fail")
216}
217
218fn not_found_handler(request_id: &str) -> Response<Full<Bytes>> {
220 let body = serde_json::json!({
221 "error": "Not Found",
222 "status": 404,
223 "message": "The requested resource could not be found.",
224 "request_id": request_id,
225 "timestamp": chrono::Utc::now().to_rfc3339(),
226 });
227
228 let body_bytes = serde_json::to_vec_pretty(&body).unwrap_or_else(|_| {
229 b"{\"error\":\"Not Found\",\"status\":404}".to_vec()
230 });
231
232 Response::builder()
233 .status(StatusCode::NOT_FOUND)
234 .header("Content-Type", "application/json; charset=utf-8")
235 .header("X-Request-Id", request_id)
236 .body(Full::new(Bytes::from(body_bytes)))
237 .expect("static response builder with valid headers cannot fail")
238}
239
240fn config_handler(config: Option<Arc<Config>>, request_id: &str) -> Response<Full<Bytes>> {
245 let body = match &config {
246 Some(cfg) => {
247 let response = serde_json::json!({
251 "timestamp": chrono::Utc::now().to_rfc3339(),
252 "request_id": request_id,
253 "config": {
254 "server": &cfg.server,
255 "listeners": cfg.listeners.iter().map(|l| {
256 serde_json::json!({
257 "id": l.id,
258 "address": l.address,
259 "protocol": l.protocol,
260 "default_route": l.default_route,
261 "request_timeout_secs": l.request_timeout_secs,
262 "keepalive_timeout_secs": l.keepalive_timeout_secs,
263 "tls_enabled": l.tls.is_some(),
265 })
266 }).collect::<Vec<_>>(),
267 "routes": cfg.routes.iter().map(|r| {
268 serde_json::json!({
269 "id": r.id,
270 "priority": r.priority,
271 "matches": r.matches,
272 "upstream": r.upstream,
273 "service_type": r.service_type,
274 "builtin_handler": r.builtin_handler,
275 "filters": r.filters,
276 "waf_enabled": r.waf_enabled,
277 })
278 }).collect::<Vec<_>>(),
279 "upstreams": cfg.upstreams.iter().map(|(id, u)| {
280 serde_json::json!({
281 "id": id,
282 "targets": u.targets.iter().map(|t| {
283 serde_json::json!({
284 "address": t.address,
285 "weight": t.weight,
286 })
287 }).collect::<Vec<_>>(),
288 "load_balancing": u.load_balancing,
289 "health_check": u.health_check.as_ref().map(|h| {
290 serde_json::json!({
291 "interval_secs": h.interval_secs,
292 "timeout_secs": h.timeout_secs,
293 "healthy_threshold": h.healthy_threshold,
294 "unhealthy_threshold": h.unhealthy_threshold,
295 })
296 }),
297 "tls_enabled": u.tls.is_some(),
299 })
300 }).collect::<Vec<_>>(),
301 "agents": cfg.agents.iter().map(|a| {
302 serde_json::json!({
303 "id": a.id,
304 "agent_type": a.agent_type,
305 "timeout_ms": a.timeout_ms,
306 })
307 }).collect::<Vec<_>>(),
308 "filters": cfg.filters.keys().collect::<Vec<_>>(),
309 "waf": cfg.waf.as_ref().map(|w| {
310 serde_json::json!({
311 "mode": w.mode,
312 "engine": w.engine,
313 "audit_log": w.audit_log,
314 })
315 }),
316 "limits": &cfg.limits,
317 }
318 });
319
320 serde_json::to_vec_pretty(&response).unwrap_or_else(|e| {
321 serde_json::to_vec(&serde_json::json!({
322 "error": "Failed to serialize config",
323 "message": e.to_string(),
324 })).unwrap_or_default()
325 })
326 }
327 None => {
328 serde_json::to_vec_pretty(&serde_json::json!({
329 "error": "Configuration unavailable",
330 "status": 503,
331 "message": "Config manager not available",
332 "request_id": request_id,
333 "timestamp": chrono::Utc::now().to_rfc3339(),
334 })).unwrap_or_default()
335 }
336 };
337
338 let status = if config.is_some() {
339 StatusCode::OK
340 } else {
341 StatusCode::SERVICE_UNAVAILABLE
342 };
343
344 Response::builder()
345 .status(status)
346 .header("Content-Type", "application/json; charset=utf-8")
347 .header("X-Request-Id", request_id)
348 .header("Cache-Control", "no-cache, no-store, must-revalidate")
349 .body(Full::new(Bytes::from(body)))
350 .expect("static response builder with valid headers cannot fail")
351}
352
353fn upstreams_handler(
357 snapshot: Option<UpstreamHealthSnapshot>,
358 request_id: &str,
359) -> Response<Full<Bytes>> {
360 let body = match snapshot {
361 Some(data) => {
362 let mut total_healthy = 0;
364 let mut total_unhealthy = 0;
365 let mut total_unknown = 0;
366
367 for upstream in data.upstreams.values() {
368 for target in &upstream.targets {
369 match target.status {
370 TargetHealthStatus::Healthy => total_healthy += 1,
371 TargetHealthStatus::Unhealthy => total_unhealthy += 1,
372 TargetHealthStatus::Unknown => total_unknown += 1,
373 }
374 }
375 }
376
377 let response = serde_json::json!({
378 "timestamp": chrono::Utc::now().to_rfc3339(),
379 "request_id": request_id,
380 "summary": {
381 "total_upstreams": data.upstreams.len(),
382 "total_targets": total_healthy + total_unhealthy + total_unknown,
383 "healthy": total_healthy,
384 "unhealthy": total_unhealthy,
385 "unknown": total_unknown,
386 },
387 "upstreams": data.upstreams.values().collect::<Vec<_>>(),
388 });
389
390 serde_json::to_vec_pretty(&response).unwrap_or_else(|e| {
391 serde_json::to_vec(&serde_json::json!({
392 "error": "Failed to serialize upstreams",
393 "message": e.to_string(),
394 })).unwrap_or_default()
395 })
396 }
397 None => {
398 serde_json::to_vec_pretty(&serde_json::json!({
400 "timestamp": chrono::Utc::now().to_rfc3339(),
401 "request_id": request_id,
402 "summary": {
403 "total_upstreams": 0,
404 "total_targets": 0,
405 "healthy": 0,
406 "unhealthy": 0,
407 "unknown": 0,
408 },
409 "upstreams": [],
410 "message": "No upstreams configured",
411 })).unwrap_or_default()
412 }
413 };
414
415 Response::builder()
416 .status(StatusCode::OK)
417 .header("Content-Type", "application/json; charset=utf-8")
418 .header("X-Request-Id", request_id)
419 .header("Cache-Control", "no-cache, no-store, must-revalidate")
420 .body(Full::new(Bytes::from(body)))
421 .expect("static response builder with valid headers cannot fail")
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn test_status_handler() {
430 let state = BuiltinHandlerState::new(
431 "0.1.0".to_string(),
432 "test-instance".to_string(),
433 );
434
435 let response = status_handler(&state, "test-request-id");
436 assert_eq!(response.status(), StatusCode::OK);
437
438 let content_type = response.headers().get("Content-Type").unwrap();
439 assert_eq!(content_type, "application/json; charset=utf-8");
440 }
441
442 #[test]
443 fn test_health_handler() {
444 let response = health_handler("test-request-id");
445 assert_eq!(response.status(), StatusCode::OK);
446 }
447
448 #[test]
449 fn test_metrics_handler() {
450 let response = metrics_handler("test-request-id");
451 assert_eq!(response.status(), StatusCode::OK);
452
453 let content_type = response.headers().get("Content-Type").unwrap();
454 assert!(content_type.to_str().unwrap().contains("text/plain"));
455 }
456
457 #[test]
458 fn test_not_found_handler() {
459 let response = not_found_handler("test-request-id");
460 assert_eq!(response.status(), StatusCode::NOT_FOUND);
461 }
462
463 #[test]
464 fn test_config_handler_with_config() {
465 let config = Arc::new(Config::default_for_testing());
466 let response = config_handler(Some(config), "test-request-id");
467 assert_eq!(response.status(), StatusCode::OK);
468
469 let content_type = response.headers().get("Content-Type").unwrap();
470 assert_eq!(content_type, "application/json; charset=utf-8");
471 }
472
473 #[test]
474 fn test_config_handler_without_config() {
475 let response = config_handler(None, "test-request-id");
476 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
477 }
478
479 #[test]
480 fn test_upstreams_handler_with_data() {
481 let mut upstreams = HashMap::new();
482 upstreams.insert(
483 "backend".to_string(),
484 UpstreamStatus {
485 id: "backend".to_string(),
486 load_balancing: "round_robin".to_string(),
487 targets: vec![
488 TargetStatus {
489 address: "10.0.0.1:8080".to_string(),
490 weight: 1,
491 status: TargetHealthStatus::Healthy,
492 failure_rate: Some(0.0),
493 last_error: None,
494 },
495 TargetStatus {
496 address: "10.0.0.2:8080".to_string(),
497 weight: 1,
498 status: TargetHealthStatus::Unhealthy,
499 failure_rate: Some(0.8),
500 last_error: Some("connection refused".to_string()),
501 },
502 ],
503 },
504 );
505
506 let snapshot = UpstreamHealthSnapshot { upstreams };
507 let response = upstreams_handler(Some(snapshot), "test-request-id");
508 assert_eq!(response.status(), StatusCode::OK);
509
510 let content_type = response.headers().get("Content-Type").unwrap();
511 assert_eq!(content_type, "application/json; charset=utf-8");
512 }
513
514 #[test]
515 fn test_upstreams_handler_no_upstreams() {
516 let response = upstreams_handler(None, "test-request-id");
517 assert_eq!(response.status(), StatusCode::OK);
518 }
519
520 #[test]
521 fn test_uptime_formatting() {
522 let state = BuiltinHandlerState::new(
523 "0.1.0".to_string(),
524 "test".to_string(),
525 );
526
527 let uptime = state.uptime_string();
529 assert!(!uptime.is_empty());
530 }
531}