1use axum::{
7 extract::{Query, State},
8 response::Json,
9};
10use chrono::Utc;
11use serde_json::{json, Value};
12use std::collections::HashMap;
13
14use crate::handlers::AdminState;
15use crate::models::*;
16
17#[derive(Debug, Clone, Default)]
19pub struct RequestMetrics {
20 pub total_requests: u64,
22 pub active_requests: u64,
24 pub average_response_time: f64,
26 pub requests_per_second: f64,
28 pub total_errors: u64,
30}
31
32pub async fn get_server_info(State(state): State<AdminState>) -> Json<Value> {
34 Json(json!({
35 "http_server": state.http_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
36 "ws_server": state.ws_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
37 "grpc_server": state.grpc_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
38 "graphql_server": state.graphql_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
39 "api_enabled": state.api_enabled,
40 }))
41}
42
43pub async fn get_health() -> Json<HealthCheck> {
45 Json(HealthCheck {
46 status: "healthy".to_string(),
47 services: HashMap::new(),
48 last_check: Utc::now(),
49 issues: Vec::new(),
50 })
51}
52
53pub async fn get_logs(
55 State(_state): State<AdminState>,
56 Query(params): Query<HashMap<String, String>>,
57) -> Json<ApiResponse<Vec<LogEntry>>> {
58 let limit = params.get("limit").and_then(|s| s.parse::<usize>().ok()).unwrap_or(100);
60
61 let method_filter = params.get("method").map(|s| s.to_string());
62 let path_filter = params.get("path").map(|s| s.to_string());
63 let status_filter = params.get("status").and_then(|s| s.parse::<u16>().ok());
64
65 let request_logs = if let Some(global_logger) = mockforge_core::get_global_logger() {
67 global_logger.get_recent_logs(Some(limit * 2)).await
68 } else {
69 Vec::new()
70 };
71
72 let mut log_entries: Vec<LogEntry> = request_logs
74 .into_iter()
75 .filter(|log| {
76 log.server_type == "HTTP"
78 })
79 .filter(|log| {
80 method_filter.as_ref().is_none_or(|filter| log.method == *filter)
82 })
83 .filter(|log| {
84 path_filter.as_ref().is_none_or(|filter| log.path.contains(filter))
86 })
87 .filter(|log| {
88 status_filter.is_none_or(|filter| log.status_code == filter)
90 })
91 .map(|log| LogEntry {
92 timestamp: log.timestamp,
93 status: log.status_code,
94 method: log.method,
95 url: log.path,
96 response_time: log.response_time_ms,
97 size: log.response_size_bytes,
98 })
99 .take(limit)
100 .collect();
101
102 log_entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
104
105 Json(ApiResponse::success(log_entries))
106}
107
108pub async fn get_metrics(State(state): State<AdminState>) -> Json<ApiResponse<SimpleMetricsData>> {
110 let metrics = state.metrics.read().await;
111
112 let total_errors: u64 = metrics.errors_by_endpoint.values().sum();
113 let error_rate = if metrics.total_requests > 0 {
114 total_errors as f64 / metrics.total_requests as f64
115 } else {
116 0.0
117 };
118
119 let average_response_time = if metrics.response_times.is_empty() {
120 0.0
121 } else {
122 metrics.response_times.iter().sum::<u64>() as f64 / metrics.response_times.len() as f64
123 };
124
125 Json(ApiResponse::success(SimpleMetricsData {
126 total_requests: metrics.total_requests,
127 active_requests: metrics.active_connections,
128 average_response_time,
129 error_rate,
130 }))
131}
132
133pub async fn update_latency(
135 State(state): State<AdminState>,
136 Json(config): Json<Value>,
137) -> Json<ApiResponse<String>> {
138 let base_ms = config.get("base_ms").and_then(|v| v.as_u64()).unwrap_or(50);
140 let jitter_ms = config.get("jitter_ms").and_then(|v| v.as_u64()).unwrap_or(20);
141 let tag_overrides = config
142 .get("tag_overrides")
143 .and_then(|v| v.as_object())
144 .map(|obj| obj.iter().filter_map(|(k, v)| v.as_u64().map(|val| (k.clone(), val))).collect())
145 .unwrap_or_default();
146
147 state.update_latency_config(base_ms, jitter_ms, tag_overrides).await;
149
150 tracing::info!("Updated latency profile: base_ms={}, jitter_ms={}", base_ms, jitter_ms);
151 Json(ApiResponse::success("Latency configuration updated".to_string()))
152}
153
154pub async fn update_faults(
156 State(state): State<AdminState>,
157 Json(config): Json<Value>,
158) -> Json<ApiResponse<String>> {
159 let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
161 let failure_rate = config.get("failure_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
162 let status_codes = config
163 .get("status_codes")
164 .and_then(|v| v.as_array())
165 .map(|arr| arr.iter().filter_map(|v| v.as_u64().map(|n| n as u16)).collect())
166 .unwrap_or_default();
167
168 state.update_fault_config(enabled, failure_rate, status_codes).await;
170
171 tracing::info!("Updated fault config: enabled={}, failure_rate={}", enabled, failure_rate);
172 Json(ApiResponse::success("Fault configuration updated".to_string()))
173}
174
175pub async fn update_proxy(
177 State(state): State<AdminState>,
178 Json(config): Json<Value>,
179) -> Json<ApiResponse<String>> {
180 let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
182 let upstream_url = config.get("upstream_url").and_then(|v| v.as_str()).map(|s| s.to_string());
183 let timeout_seconds = config.get("timeout_seconds").and_then(|v| v.as_u64()).unwrap_or(30);
184
185 state.update_proxy_config(enabled, upstream_url.clone(), timeout_seconds).await;
187
188 tracing::info!(
189 "Updated proxy config: enabled={}, upstream_url={:?}, timeout_seconds={}",
190 enabled,
191 upstream_url,
192 timeout_seconds
193 );
194 Json(ApiResponse::success("Proxy configuration updated".to_string()))
195}
196
197pub async fn clear_logs(State(_state): State<AdminState>) -> Json<ApiResponse<String>> {
199 if let Some(global_logger) = mockforge_core::get_global_logger() {
200 global_logger.clear_logs().await;
201 }
202 tracing::info!("Request logs cleared via admin UI");
203 Json(ApiResponse::success("Logs cleared".to_string()))
204}
205
206pub async fn restart_servers(State(state): State<AdminState>) -> Json<ApiResponse<String>> {
208 let current_status = state.get_restart_status().await;
210 if current_status.in_progress {
211 return Json(ApiResponse::error("Server restart already in progress".to_string()));
212 }
213
214 if let Err(e) = state
216 .initiate_restart("Manual restart requested via admin UI".to_string())
217 .await
218 {
219 return Json(ApiResponse::error(format!("Failed to initiate restart: {}", e)));
220 }
221
222 let state_clone = state.clone();
224 tokio::spawn(async move {
225 if let Err(e) = super::perform_server_restart(&state_clone).await {
226 tracing::error!("Server restart failed: {}", e);
227 state_clone.complete_restart(false).await;
228 } else {
229 tracing::info!("Server restart completed successfully");
230 state_clone.complete_restart(true).await;
231 }
232 });
233
234 tracing::info!("Server restart initiated via admin UI");
235 Json(ApiResponse::success(
236 "Server restart initiated. Please wait for completion.".to_string(),
237 ))
238}
239
240pub async fn get_restart_status(
242 State(state): State<AdminState>,
243) -> Json<ApiResponse<super::RestartStatus>> {
244 let status = state.get_restart_status().await;
245 Json(ApiResponse::success(status))
246}
247
248pub async fn get_config(State(state): State<AdminState>) -> Json<ApiResponse<Value>> {
250 let config = state.get_config().await;
251 Json(ApiResponse::success(serde_json::to_value(config).unwrap_or_else(|_| json!({}))))
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 fn create_test_state() -> AdminState {
259 AdminState::new(
260 None, None, None, None, false, 8080, None, None, None, None, None, None, None, None,
261 )
262 }
263
264 #[test]
267 fn test_request_metrics_default() {
268 let metrics = RequestMetrics::default();
269 assert_eq!(metrics.total_requests, 0);
270 assert_eq!(metrics.active_requests, 0);
271 assert_eq!(metrics.average_response_time, 0.0);
272 assert_eq!(metrics.requests_per_second, 0.0);
273 assert_eq!(metrics.total_errors, 0);
274 }
275
276 #[test]
277 fn test_request_metrics_creation() {
278 let metrics = RequestMetrics {
279 total_requests: 1000,
280 active_requests: 10,
281 average_response_time: 45.5,
282 requests_per_second: 25.0,
283 total_errors: 5,
284 };
285
286 assert_eq!(metrics.total_requests, 1000);
287 assert_eq!(metrics.active_requests, 10);
288 assert!((metrics.average_response_time - 45.5).abs() < 0.001);
289 assert!((metrics.requests_per_second - 25.0).abs() < 0.001);
290 assert_eq!(metrics.total_errors, 5);
291 }
292
293 #[test]
294 fn test_request_metrics_clone() {
295 let metrics = RequestMetrics {
296 total_requests: 500,
297 active_requests: 5,
298 average_response_time: 30.0,
299 requests_per_second: 10.0,
300 total_errors: 2,
301 };
302
303 let cloned = metrics.clone();
304 assert_eq!(cloned.total_requests, 500);
305 assert_eq!(cloned.active_requests, 5);
306 }
307
308 #[test]
309 fn test_request_metrics_debug() {
310 let metrics = RequestMetrics::default();
311 let debug_str = format!("{:?}", metrics);
312 assert!(debug_str.contains("RequestMetrics"));
313 assert!(debug_str.contains("total_requests"));
314 }
315
316 #[tokio::test]
319 async fn test_get_restart_status() {
320 let state = create_test_state();
321 let response = get_restart_status(State(state)).await;
322
323 assert!(response.0.success);
324 }
325
326 #[tokio::test]
327 async fn test_get_config() {
328 let state = create_test_state();
329 let response = get_config(State(state)).await;
330
331 assert!(response.0.success);
332 }
333
334 #[tokio::test]
335 async fn test_get_health() {
336 let response = get_health().await;
337
338 assert_eq!(response.0.status, "healthy");
339 assert!(response.0.issues.is_empty());
340 }
341
342 #[tokio::test]
343 async fn test_get_server_info() {
344 let state = create_test_state();
345 let response = get_server_info(State(state)).await;
346
347 assert!(response.0.is_object());
348 let obj = response.0.as_object().unwrap();
349 assert!(obj.contains_key("http_server"));
350 assert!(obj.contains_key("ws_server"));
351 assert!(obj.contains_key("grpc_server"));
352 assert!(obj.contains_key("graphql_server"));
353 assert!(obj.contains_key("api_enabled"));
354 }
355
356 #[tokio::test]
357 async fn test_get_server_info_disabled() {
358 let state = create_test_state();
359 let response = get_server_info(State(state)).await;
360
361 let obj = response.0.as_object().unwrap();
363 assert_eq!(obj.get("http_server").and_then(|v| v.as_str()), Some("disabled"));
364 assert_eq!(obj.get("ws_server").and_then(|v| v.as_str()), Some("disabled"));
365 }
366
367 #[tokio::test]
368 async fn test_get_metrics() {
369 let state = create_test_state();
370 let response = get_metrics(State(state)).await;
371
372 assert!(response.0.success);
373 }
374
375 #[tokio::test]
376 async fn test_get_logs_empty() {
377 let state = create_test_state();
378 let params = HashMap::new();
379 let response = get_logs(State(state), Query(params)).await;
380
381 assert!(response.0.success);
382 }
383
384 #[tokio::test]
385 async fn test_get_logs_with_limit() {
386 let state = create_test_state();
387 let mut params = HashMap::new();
388 params.insert("limit".to_string(), "10".to_string());
389
390 let response = get_logs(State(state), Query(params)).await;
391
392 assert!(response.0.success);
393 }
394
395 #[tokio::test]
396 async fn test_get_logs_with_method_filter() {
397 let state = create_test_state();
398 let mut params = HashMap::new();
399 params.insert("method".to_string(), "GET".to_string());
400
401 let response = get_logs(State(state), Query(params)).await;
402
403 assert!(response.0.success);
404 }
405
406 #[tokio::test]
407 async fn test_get_logs_with_path_filter() {
408 let state = create_test_state();
409 let mut params = HashMap::new();
410 params.insert("path".to_string(), "/api".to_string());
411
412 let response = get_logs(State(state), Query(params)).await;
413
414 assert!(response.0.success);
415 }
416
417 #[tokio::test]
418 async fn test_get_logs_with_status_filter() {
419 let state = create_test_state();
420 let mut params = HashMap::new();
421 params.insert("status".to_string(), "200".to_string());
422
423 let response = get_logs(State(state), Query(params)).await;
424
425 assert!(response.0.success);
426 }
427
428 #[tokio::test]
429 async fn test_clear_logs() {
430 let state = create_test_state();
431 let response = clear_logs(State(state)).await;
432
433 assert!(response.0.success);
434 assert!(response.0.data.is_some());
435 }
436
437 #[tokio::test]
438 async fn test_update_latency() {
439 let state = create_test_state();
440 let config = json!({
441 "base_ms": 100,
442 "jitter_ms": 20
443 });
444
445 let response = update_latency(State(state), Json(config)).await;
446
447 assert!(response.0.success);
448 }
449
450 #[tokio::test]
451 async fn test_update_latency_with_overrides() {
452 let state = create_test_state();
453 let config = json!({
454 "base_ms": 50,
455 "jitter_ms": 10,
456 "tag_overrides": {
457 "slow": 500,
458 "fast": 10
459 }
460 });
461
462 let response = update_latency(State(state), Json(config)).await;
463
464 assert!(response.0.success);
465 }
466
467 #[tokio::test]
468 async fn test_update_faults() {
469 let state = create_test_state();
470 let config = json!({
471 "enabled": true,
472 "failure_rate": 0.1,
473 "status_codes": [500, 503]
474 });
475
476 let response = update_faults(State(state), Json(config)).await;
477
478 assert!(response.0.success);
479 }
480
481 #[tokio::test]
482 async fn test_update_faults_disabled() {
483 let state = create_test_state();
484 let config = json!({
485 "enabled": false
486 });
487
488 let response = update_faults(State(state), Json(config)).await;
489
490 assert!(response.0.success);
491 }
492
493 #[tokio::test]
494 async fn test_update_proxy() {
495 let state = create_test_state();
496 let config = json!({
497 "enabled": true,
498 "upstream_url": "http://localhost:8000",
499 "timeout_seconds": 60
500 });
501
502 let response = update_proxy(State(state), Json(config)).await;
503
504 assert!(response.0.success);
505 }
506
507 #[tokio::test]
508 async fn test_update_proxy_disabled() {
509 let state = create_test_state();
510 let config = json!({
511 "enabled": false
512 });
513
514 let response = update_proxy(State(state), Json(config)).await;
515
516 assert!(response.0.success);
517 }
518
519 #[tokio::test]
520 async fn test_restart_servers() {
521 let state = create_test_state();
522 let response = restart_servers(State(state)).await;
523
524 assert!(response.0.success || response.0.error.is_some());
526 }
527}