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