1use crate::{
4 ab_testing::{ABTestConfig, ABTestingEngine, TestConclusion, VariantResults},
5 analytics::{ChaosAnalytics, TimeBucket},
6 auto_remediation::{RemediationConfig, RemediationEngine},
7 config::{
8 BulkheadConfig, ChaosConfig, CircuitBreakerConfig, FaultInjectionConfig, LatencyConfig,
9 RateLimitConfig, TrafficShapingConfig,
10 },
11 recommendations::{
12 Recommendation, RecommendationCategory, RecommendationEngine, RecommendationSeverity,
13 },
14 scenario_orchestrator::{OrchestratedScenario, ScenarioOrchestrator},
15 scenario_recorder::{RecordedScenario, ScenarioRecorder},
16 scenario_replay::{ReplayOptions, ReplaySpeed, ScenarioReplayEngine},
17 scenario_scheduler::{ScenarioScheduler, ScheduleType, ScheduledScenario},
18 scenarios::{ChaosScenario, PredefinedScenarios, ScenarioEngine},
19};
20use axum::{
21 extract::{Path, State},
22 http::StatusCode,
23 response::{IntoResponse, Json, Response},
24 routing::{delete, get, post, put},
25 Router,
26};
27use serde::{Deserialize, Serialize};
28use std::sync::Arc;
29use tokio::sync::RwLock;
30use tracing::info;
31
32#[derive(Clone)]
34pub struct ChaosApiState {
35 pub config: Arc<RwLock<ChaosConfig>>,
36 pub scenario_engine: Arc<ScenarioEngine>,
37 pub orchestrator: Arc<tokio::sync::RwLock<ScenarioOrchestrator>>,
38 pub analytics: Arc<ChaosAnalytics>,
39 pub recommendation_engine: Arc<RecommendationEngine>,
40 pub remediation_engine: Arc<RemediationEngine>,
41 pub ab_testing_engine: Arc<tokio::sync::RwLock<ABTestingEngine>>,
42 pub recorder: Arc<ScenarioRecorder>,
43 pub replay_engine: Arc<tokio::sync::RwLock<ScenarioReplayEngine>>,
44 pub scheduler: Arc<tokio::sync::RwLock<ScenarioScheduler>>,
45}
46
47pub fn create_chaos_api_router(config: ChaosConfig) -> (Router, Arc<RwLock<ChaosConfig>>) {
49 let config_arc = Arc::new(RwLock::new(config));
50 let scenario_engine = Arc::new(ScenarioEngine::new());
51 let orchestrator = Arc::new(tokio::sync::RwLock::new(ScenarioOrchestrator::new()));
52 let analytics = Arc::new(ChaosAnalytics::new());
53 let recommendation_engine = Arc::new(RecommendationEngine::new());
54 let remediation_engine = Arc::new(RemediationEngine::new());
55 let ab_testing_engine =
56 Arc::new(tokio::sync::RwLock::new(ABTestingEngine::new(analytics.clone())));
57 let recorder = Arc::new(ScenarioRecorder::new());
58 let replay_engine = Arc::new(tokio::sync::RwLock::new(ScenarioReplayEngine::new()));
59 let scheduler = Arc::new(tokio::sync::RwLock::new(ScenarioScheduler::new()));
60
61 let state = ChaosApiState {
62 config: config_arc.clone(),
63 scenario_engine,
64 orchestrator,
65 analytics,
66 recommendation_engine,
67 remediation_engine,
68 ab_testing_engine,
69 recorder,
70 replay_engine,
71 scheduler,
72 };
73
74 let router = Router::new()
75 .route("/api/chaos/config", get(get_config))
77 .route("/api/chaos/config", put(update_config))
78 .route("/api/chaos/config/latency", put(update_latency_config))
79 .route("/api/chaos/config/faults", put(update_fault_config))
80 .route("/api/chaos/config/rate-limit", put(update_rate_limit_config))
81 .route("/api/chaos/config/traffic", put(update_traffic_config))
82 .route("/api/chaos/config/circuit-breaker", put(update_circuit_breaker_config))
83 .route("/api/chaos/config/bulkhead", put(update_bulkhead_config))
84
85 .route("/api/chaos/protocols/grpc/status-codes", post(inject_grpc_status_codes))
87 .route("/api/chaos/protocols/grpc/stream-interruption", post(set_grpc_stream_interruption))
88 .route("/api/chaos/protocols/websocket/close-codes", post(inject_websocket_close_codes))
89 .route("/api/chaos/protocols/websocket/message-drop", post(set_websocket_message_drop))
90 .route("/api/chaos/protocols/websocket/message-corruption", post(set_websocket_message_corruption))
91 .route("/api/chaos/protocols/graphql/error-codes", post(inject_graphql_error_codes))
92 .route("/api/chaos/protocols/graphql/partial-data", post(set_graphql_partial_data))
93 .route("/api/chaos/protocols/graphql/resolver-latency", post(toggle_graphql_resolver_latency))
94
95 .route("/api/chaos/enable", post(enable_chaos))
97 .route("/api/chaos/disable", post(disable_chaos))
98 .route("/api/chaos/reset", post(reset_chaos))
99
100 .route("/api/chaos/scenarios", get(list_scenarios))
102 .route("/api/chaos/scenarios/predefined", get(list_predefined_scenarios))
103 .route("/api/chaos/scenarios/:name", post(start_scenario))
104 .route("/api/chaos/scenarios/:name", delete(stop_scenario))
105 .route("/api/chaos/scenarios", delete(stop_all_scenarios))
106
107 .route("/api/chaos/status", get(get_status))
109
110 .route("/api/chaos/recording/start", post(start_recording))
112 .route("/api/chaos/recording/stop", post(stop_recording))
113 .route("/api/chaos/recording/status", get(recording_status))
114 .route("/api/chaos/recording/export", post(export_recording))
115
116 .route("/api/chaos/replay/start", post(start_replay))
118 .route("/api/chaos/replay/pause", post(pause_replay))
119 .route("/api/chaos/replay/resume", post(resume_replay))
120 .route("/api/chaos/replay/stop", post(stop_replay))
121 .route("/api/chaos/replay/status", get(replay_status))
122
123 .route("/api/chaos/orchestration/start", post(start_orchestration))
125 .route("/api/chaos/orchestration/stop", post(stop_orchestration))
126 .route("/api/chaos/orchestration/status", get(orchestration_status))
127 .route("/api/chaos/orchestration/import", post(import_orchestration))
128
129 .route("/api/chaos/schedule", post(add_schedule))
131 .route("/api/chaos/schedule/:id", get(get_schedule))
132 .route("/api/chaos/schedule/:id", delete(remove_schedule))
133 .route("/api/chaos/schedule/:id/enable", post(enable_schedule))
134 .route("/api/chaos/schedule/:id/disable", post(disable_schedule))
135 .route("/api/chaos/schedules", get(list_schedules))
141
142 .route("/api/chaos/recommendations", get(get_recommendations))
144 .route("/api/chaos/recommendations/analyze", post(analyze_and_recommend))
145 .route("/api/chaos/recommendations/category/:category", get(get_recommendations_by_category))
146 .route("/api/chaos/recommendations/severity/:severity", get(get_recommendations_by_severity))
147 .route("/api/chaos/recommendations", delete(clear_recommendations))
148
149 .route("/api/chaos/remediation/config", get(get_remediation_config))
151 .route("/api/chaos/remediation/config", put(update_remediation_config))
152 .route("/api/chaos/remediation/process", post(process_remediation))
153 .route("/api/chaos/remediation/approve/:id", post(approve_remediation))
154 .route("/api/chaos/remediation/reject/:id", post(reject_remediation))
155 .route("/api/chaos/remediation/rollback/:id", post(rollback_remediation))
156 .route("/api/chaos/remediation/actions", get(get_remediation_actions))
157 .route("/api/chaos/remediation/actions/:id", get(get_remediation_action))
158 .route("/api/chaos/remediation/approvals", get(get_approval_queue))
159 .route("/api/chaos/remediation/effectiveness/:id", get(get_remediation_effectiveness))
160 .route("/api/chaos/remediation/stats", get(get_remediation_stats))
161
162 .route("/api/chaos/ab-tests", post(create_ab_test))
164 .route("/api/chaos/ab-tests", get(get_ab_tests))
165 .route("/api/chaos/ab-tests/:id", get(get_ab_test))
166 .route("/api/chaos/ab-tests/:id/start", post(start_ab_test))
167 .route("/api/chaos/ab-tests/:id/stop", post(stop_ab_test))
168 .route("/api/chaos/ab-tests/:id/pause", post(pause_ab_test))
169 .route("/api/chaos/ab-tests/:id/resume", post(resume_ab_test))
170 .route("/api/chaos/ab-tests/:id/record/:variant", post(record_ab_test_result))
171 .route("/api/chaos/ab-tests/:id", delete(delete_ab_test))
172 .route("/api/chaos/ab-tests/stats", get(get_ab_test_stats))
173
174 .with_state(state);
175
176 (router, config_arc)
177}
178
179async fn get_config(State(state): State<ChaosApiState>) -> Json<ChaosConfig> {
181 let config = state.config.read().await;
182 Json(config.clone())
183}
184
185async fn update_config(
187 State(state): State<ChaosApiState>,
188 Json(new_config): Json<ChaosConfig>,
189) -> Json<StatusResponse> {
190 let mut config = state.config.write().await;
191 *config = new_config;
192 info!("Chaos configuration updated");
193 Json(StatusResponse {
194 message: "Configuration updated".to_string(),
195 })
196}
197
198async fn update_latency_config(
200 State(state): State<ChaosApiState>,
201 Json(latency_config): Json<LatencyConfig>,
202) -> Json<StatusResponse> {
203 let mut config = state.config.write().await;
204 config.latency = Some(latency_config);
205 info!("Latency configuration updated");
206 Json(StatusResponse {
207 message: "Latency configuration updated".to_string(),
208 })
209}
210
211async fn update_fault_config(
213 State(state): State<ChaosApiState>,
214 Json(fault_config): Json<FaultInjectionConfig>,
215) -> Json<StatusResponse> {
216 let mut config = state.config.write().await;
217 config.fault_injection = Some(fault_config);
218 info!("Fault injection configuration updated");
219 Json(StatusResponse {
220 message: "Fault injection configuration updated".to_string(),
221 })
222}
223
224async fn update_rate_limit_config(
226 State(state): State<ChaosApiState>,
227 Json(rate_config): Json<RateLimitConfig>,
228) -> Json<StatusResponse> {
229 let mut config = state.config.write().await;
230 config.rate_limit = Some(rate_config);
231 info!("Rate limit configuration updated");
232 Json(StatusResponse {
233 message: "Rate limit configuration updated".to_string(),
234 })
235}
236
237async fn update_traffic_config(
239 State(state): State<ChaosApiState>,
240 Json(traffic_config): Json<TrafficShapingConfig>,
241) -> Json<StatusResponse> {
242 let mut config = state.config.write().await;
243 config.traffic_shaping = Some(traffic_config);
244 info!("Traffic shaping configuration updated");
245 Json(StatusResponse {
246 message: "Traffic shaping configuration updated".to_string(),
247 })
248}
249
250async fn update_circuit_breaker_config(
252 State(state): State<ChaosApiState>,
253 Json(cb_config): Json<CircuitBreakerConfig>,
254) -> Json<StatusResponse> {
255 let mut config = state.config.write().await;
256 config.circuit_breaker = Some(cb_config);
257 info!("Circuit breaker configuration updated");
258 Json(StatusResponse {
259 message: "Circuit breaker configuration updated".to_string(),
260 })
261}
262
263async fn update_bulkhead_config(
265 State(state): State<ChaosApiState>,
266 Json(bulkhead_config): Json<BulkheadConfig>,
267) -> Json<StatusResponse> {
268 let mut config = state.config.write().await;
269 config.bulkhead = Some(bulkhead_config);
270 info!("Bulkhead configuration updated");
271 Json(StatusResponse {
272 message: "Bulkhead configuration updated".to_string(),
273 })
274}
275
276async fn enable_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
278 let mut config = state.config.write().await;
279 config.enabled = true;
280 info!("Chaos engineering enabled");
281 Json(StatusResponse {
282 message: "Chaos engineering enabled".to_string(),
283 })
284}
285
286async fn disable_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
288 let mut config = state.config.write().await;
289 config.enabled = false;
290 info!("Chaos engineering disabled");
291 Json(StatusResponse {
292 message: "Chaos engineering disabled".to_string(),
293 })
294}
295
296async fn reset_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
298 let mut config = state.config.write().await;
299 *config = ChaosConfig::default();
300 state.scenario_engine.stop_all_scenarios();
301 info!("Chaos configuration reset to defaults");
302 Json(StatusResponse {
303 message: "Chaos configuration reset".to_string(),
304 })
305}
306
307async fn list_scenarios(State(state): State<ChaosApiState>) -> Json<Vec<ChaosScenario>> {
309 let scenarios = state.scenario_engine.get_active_scenarios();
310 Json(scenarios)
311}
312
313async fn list_predefined_scenarios() -> Json<Vec<PredefinedScenarioInfo>> {
315 Json(vec![
316 PredefinedScenarioInfo {
317 name: "network_degradation".to_string(),
318 description: "Simulates degraded network conditions with high latency and packet loss"
319 .to_string(),
320 tags: vec!["network".to_string(), "latency".to_string()],
321 },
322 PredefinedScenarioInfo {
323 name: "service_instability".to_string(),
324 description: "Simulates an unstable service with random errors and timeouts"
325 .to_string(),
326 tags: vec!["service".to_string(), "errors".to_string()],
327 },
328 PredefinedScenarioInfo {
329 name: "cascading_failure".to_string(),
330 description: "Simulates a cascading failure with multiple simultaneous issues"
331 .to_string(),
332 tags: vec!["critical".to_string(), "cascading".to_string()],
333 },
334 PredefinedScenarioInfo {
335 name: "peak_traffic".to_string(),
336 description: "Simulates peak traffic conditions with aggressive rate limiting"
337 .to_string(),
338 tags: vec!["traffic".to_string(), "load".to_string()],
339 },
340 PredefinedScenarioInfo {
341 name: "slow_backend".to_string(),
342 description: "Simulates a consistently slow backend service".to_string(),
343 tags: vec!["latency".to_string(), "performance".to_string()],
344 },
345 ])
346}
347
348async fn start_scenario(
350 State(state): State<ChaosApiState>,
351 Path(name): Path<String>,
352) -> Result<Json<StatusResponse>, ChaosApiError> {
353 let scenario = match name.as_str() {
354 "network_degradation" => PredefinedScenarios::network_degradation(),
355 "service_instability" => PredefinedScenarios::service_instability(),
356 "cascading_failure" => PredefinedScenarios::cascading_failure(),
357 "peak_traffic" => PredefinedScenarios::peak_traffic(),
358 "slow_backend" => PredefinedScenarios::slow_backend(),
359 _ => return Err(ChaosApiError::NotFound(format!("Scenario '{}' not found", name))),
360 };
361
362 state.scenario_engine.start_scenario(scenario.clone());
363
364 let mut config = state.config.write().await;
366 *config = scenario.chaos_config;
367
368 info!("Started scenario: {}", name);
369 Ok(Json(StatusResponse {
370 message: format!("Scenario '{}' started", name),
371 }))
372}
373
374async fn stop_scenario(
376 State(state): State<ChaosApiState>,
377 Path(name): Path<String>,
378) -> Result<Json<StatusResponse>, ChaosApiError> {
379 if state.scenario_engine.stop_scenario(&name) {
380 info!("Stopped scenario: {}", name);
381 Ok(Json(StatusResponse {
382 message: format!("Scenario '{}' stopped", name),
383 }))
384 } else {
385 Err(ChaosApiError::NotFound(format!("Scenario '{}' not found or not running", name)))
386 }
387}
388
389async fn stop_all_scenarios(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
391 state.scenario_engine.stop_all_scenarios();
392 info!("Stopped all scenarios");
393 Json(StatusResponse {
394 message: "All scenarios stopped".to_string(),
395 })
396}
397
398async fn get_status(State(state): State<ChaosApiState>) -> Json<ChaosStatus> {
400 let config = state.config.read().await;
401 let scenarios = state.scenario_engine.get_active_scenarios();
402
403 Json(ChaosStatus {
404 enabled: config.enabled,
405 active_scenarios: scenarios.iter().map(|s| s.name.clone()).collect(),
406 latency_enabled: config.latency.as_ref().is_some_and(|l| l.enabled),
407 fault_injection_enabled: config.fault_injection.as_ref().is_some_and(|f| f.enabled),
408 rate_limit_enabled: config.rate_limit.as_ref().is_some_and(|r| r.enabled),
409 traffic_shaping_enabled: config.traffic_shaping.as_ref().is_some_and(|t| t.enabled),
410 })
411}
412
413async fn inject_grpc_status_codes(
417 State(state): State<ChaosApiState>,
418 Json(req): Json<GrpcStatusCodesRequest>,
419) -> Json<StatusResponse> {
420 let mut config = state.config.write().await;
421
422 let mut http_errors = config
424 .fault_injection
425 .as_ref()
426 .map(|f| f.http_errors.clone())
427 .unwrap_or_default();
428
429 for code in &req.status_codes {
430 let http_code = match code {
432 3 => 400, 16 => 401, 7 => 403, 5 => 404, 8 => 429, 13 => 500, 12 => 501, 14 => 503, 4 => 504, _ => 500, };
443 if !http_errors.contains(&http_code) {
444 http_errors.push(http_code);
445 }
446 }
447
448 if let Some(fault_config) = &mut config.fault_injection {
449 fault_config.http_errors = http_errors;
450 fault_config.http_error_probability = req.probability;
451 }
452
453 info!("gRPC status codes configured: {:?}", &req.status_codes);
454 Json(StatusResponse {
455 message: "gRPC status codes configured".to_string(),
456 })
457}
458
459async fn set_grpc_stream_interruption(
461 State(state): State<ChaosApiState>,
462 Json(req): Json<ProbabilityRequest>,
463) -> Json<StatusResponse> {
464 let mut config = state.config.write().await;
465
466 if let Some(fault_config) = &mut config.fault_injection {
467 fault_config.partial_response_probability = req.probability;
468 }
469
470 info!("gRPC stream interruption probability set to {}", req.probability);
471 Json(StatusResponse {
472 message: "gRPC stream interruption configured".to_string(),
473 })
474}
475
476async fn inject_websocket_close_codes(
478 State(state): State<ChaosApiState>,
479 Json(req): Json<WebSocketCloseCodesRequest>,
480) -> Json<StatusResponse> {
481 let mut config = state.config.write().await;
482
483 let mut http_errors = config
484 .fault_injection
485 .as_ref()
486 .map(|f| f.http_errors.clone())
487 .unwrap_or_default();
488
489 for code in &req.close_codes {
490 let http_code = match code {
492 1002 => 400, 1001 => 408, 1008 => 429, 1011 => 500, _ => 500,
497 };
498 if !http_errors.contains(&http_code) {
499 http_errors.push(http_code);
500 }
501 }
502
503 if let Some(fault_config) = &mut config.fault_injection {
504 fault_config.http_errors = http_errors;
505 fault_config.http_error_probability = req.probability;
506 }
507
508 info!("WebSocket close codes configured: {:?}", &req.close_codes);
509 Json(StatusResponse {
510 message: "WebSocket close codes configured".to_string(),
511 })
512}
513
514async fn set_websocket_message_drop(
516 State(state): State<ChaosApiState>,
517 Json(req): Json<ProbabilityRequest>,
518) -> Json<StatusResponse> {
519 let mut config = state.config.write().await;
520
521 if let Some(traffic_config) = &mut config.traffic_shaping {
522 traffic_config.packet_loss_percent = req.probability * 100.0;
523 }
524
525 info!("WebSocket message drop probability set to {}", req.probability);
526 Json(StatusResponse {
527 message: "WebSocket message drop configured".to_string(),
528 })
529}
530
531async fn set_websocket_message_corruption(
533 State(state): State<ChaosApiState>,
534 Json(req): Json<ProbabilityRequest>,
535) -> Json<StatusResponse> {
536 let mut config = state.config.write().await;
537
538 if let Some(fault_config) = &mut config.fault_injection {
539 fault_config.partial_response_probability = req.probability;
540 }
541
542 info!("WebSocket message corruption probability set to {}", req.probability);
543 Json(StatusResponse {
544 message: "WebSocket message corruption configured".to_string(),
545 })
546}
547
548async fn inject_graphql_error_codes(
550 State(state): State<ChaosApiState>,
551 Json(req): Json<GraphQLErrorCodesRequest>,
552) -> Json<StatusResponse> {
553 let mut config = state.config.write().await;
554
555 let mut http_errors = config
556 .fault_injection
557 .as_ref()
558 .map(|f| f.http_errors.clone())
559 .unwrap_or_default();
560
561 for code in &req.error_codes {
562 let http_code = match code.as_str() {
563 "BAD_USER_INPUT" => 400,
564 "UNAUTHENTICATED" => 401,
565 "FORBIDDEN" => 403,
566 "NOT_FOUND" => 404,
567 "INTERNAL_SERVER_ERROR" => 500,
568 "SERVICE_UNAVAILABLE" => 503,
569 _ => 500,
570 };
571 if !http_errors.contains(&http_code) {
572 http_errors.push(http_code);
573 }
574 }
575
576 if let Some(fault_config) = &mut config.fault_injection {
577 fault_config.http_errors = http_errors;
578 fault_config.http_error_probability = req.probability;
579 }
580
581 info!("GraphQL error codes configured: {:?}", &req.error_codes);
582 Json(StatusResponse {
583 message: "GraphQL error codes configured".to_string(),
584 })
585}
586
587async fn set_graphql_partial_data(
589 State(state): State<ChaosApiState>,
590 Json(req): Json<ProbabilityRequest>,
591) -> Json<StatusResponse> {
592 let mut config = state.config.write().await;
593
594 if let Some(fault_config) = &mut config.fault_injection {
595 fault_config.partial_response_probability = req.probability;
596 }
597
598 info!("GraphQL partial data probability set to {}", req.probability);
599 Json(StatusResponse {
600 message: "GraphQL partial data configured".to_string(),
601 })
602}
603
604async fn toggle_graphql_resolver_latency(
606 State(state): State<ChaosApiState>,
607 Json(req): Json<EnableRequest>,
608) -> Json<StatusResponse> {
609 let mut config = state.config.write().await;
610
611 if let Some(latency_config) = &mut config.latency {
612 latency_config.enabled = req.enabled;
613 }
614
615 info!("GraphQL resolver latency {}", if req.enabled { "enabled" } else { "disabled" });
616 Json(StatusResponse {
617 message: format!(
618 "GraphQL resolver latency {}",
619 if req.enabled { "enabled" } else { "disabled" }
620 ),
621 })
622}
623
624#[derive(Debug, Serialize)]
627struct StatusResponse {
628 message: String,
629}
630
631#[derive(Debug, Serialize)]
632struct PredefinedScenarioInfo {
633 name: String,
634 description: String,
635 tags: Vec<String>,
636}
637
638#[derive(Debug, Serialize)]
639struct ChaosStatus {
640 enabled: bool,
641 active_scenarios: Vec<String>,
642 latency_enabled: bool,
643 fault_injection_enabled: bool,
644 rate_limit_enabled: bool,
645 traffic_shaping_enabled: bool,
646}
647
648#[derive(Debug, Deserialize)]
649struct GrpcStatusCodesRequest {
650 status_codes: Vec<i32>,
651 probability: f64,
652}
653
654#[derive(Debug, Deserialize)]
655struct WebSocketCloseCodesRequest {
656 close_codes: Vec<u16>,
657 probability: f64,
658}
659
660#[derive(Debug, Deserialize)]
661struct GraphQLErrorCodesRequest {
662 error_codes: Vec<String>,
663 probability: f64,
664}
665
666#[derive(Debug, Deserialize)]
667struct ProbabilityRequest {
668 probability: f64,
669}
670
671#[derive(Debug, Deserialize)]
672struct EnableRequest {
673 enabled: bool,
674}
675
676async fn start_recording(
680 State(state): State<ChaosApiState>,
681 Json(req): Json<StartRecordingRequest>,
682) -> Result<Json<StatusResponse>, ChaosApiError> {
683 let scenario = match req.scenario_name.as_str() {
685 "network_degradation" => PredefinedScenarios::network_degradation(),
686 "service_instability" => PredefinedScenarios::service_instability(),
687 "cascading_failure" => PredefinedScenarios::cascading_failure(),
688 "peak_traffic" => PredefinedScenarios::peak_traffic(),
689 "slow_backend" => PredefinedScenarios::slow_backend(),
690 _ => {
691 let active_scenarios = state.scenario_engine.get_active_scenarios();
693 active_scenarios
694 .into_iter()
695 .find(|s| s.name == req.scenario_name)
696 .ok_or_else(|| {
697 ChaosApiError::NotFound(format!("Scenario '{}' not found", req.scenario_name))
698 })?
699 }
700 };
701
702 match state.recorder.start_recording(scenario) {
704 Ok(_) => {
705 info!("Recording started for scenario: {}", req.scenario_name);
706 Ok(Json(StatusResponse {
707 message: format!("Recording started for scenario: {}", req.scenario_name),
708 }))
709 }
710 Err(err) => Err(ChaosApiError::NotFound(err)),
711 }
712}
713
714async fn stop_recording(
716 State(state): State<ChaosApiState>,
717) -> Result<Json<StatusResponse>, ChaosApiError> {
718 match state.recorder.stop_recording() {
719 Ok(recording) => {
720 info!(
721 "Recording stopped for scenario: {} ({} events)",
722 recording.scenario.name,
723 recording.events.len()
724 );
725 Ok(Json(StatusResponse {
726 message: format!(
727 "Recording stopped for scenario: {} ({} events, {}ms)",
728 recording.scenario.name,
729 recording.events.len(),
730 recording.total_duration_ms
731 ),
732 }))
733 }
734 Err(err) => Err(ChaosApiError::NotFound(err)),
735 }
736}
737
738async fn recording_status(State(state): State<ChaosApiState>) -> Json<RecordingStatusResponse> {
740 if let Some(recording) = state.recorder.get_current_recording() {
741 Json(RecordingStatusResponse {
742 is_recording: true,
743 scenario_name: Some(recording.scenario.name),
744 events_recorded: recording.events.len(),
745 })
746 } else {
747 Json(RecordingStatusResponse {
748 is_recording: false,
749 scenario_name: None,
750 events_recorded: 0,
751 })
752 }
753}
754
755async fn export_recording(
757 State(state): State<ChaosApiState>,
758 Json(req): Json<ExportRequest>,
759) -> Result<Json<StatusResponse>, ChaosApiError> {
760 if state.recorder.get_current_recording().is_some() {
762 return Err(ChaosApiError::NotFound(
763 "Cannot export while recording is in progress. Stop recording first.".to_string(),
764 ));
765 }
766
767 let recordings = state.recorder.get_recordings();
769 if recordings.is_empty() {
770 return Err(ChaosApiError::NotFound("No recordings available to export".to_string()));
771 }
772
773 let recording = recordings.last().unwrap();
774
775 match recording.save_to_file(&req.path) {
777 Ok(_) => {
778 info!("Recording exported to: {}", req.path);
779 Ok(Json(StatusResponse {
780 message: format!(
781 "Recording exported to: {} ({} events)",
782 req.path,
783 recording.events.len()
784 ),
785 }))
786 }
787 Err(err) => Err(ChaosApiError::NotFound(format!("Failed to export recording: {}", err))),
788 }
789}
790
791async fn start_replay(
793 State(state): State<ChaosApiState>,
794 Json(req): Json<StartReplayRequest>,
795) -> Result<Json<StatusResponse>, ChaosApiError> {
796 let recorded = RecordedScenario::load_from_file(&req.path)
798 .map_err(|e| ChaosApiError::NotFound(format!("Failed to load recording: {}", e)))?;
799
800 let speed = match req.speed {
802 Some(s) if s > 0.0 => ReplaySpeed::Custom(s),
803 Some(0.0) => ReplaySpeed::Fast,
804 _ => ReplaySpeed::RealTime,
805 };
806
807 let options = ReplayOptions {
808 speed,
809 loop_replay: req.loop_replay.unwrap_or(false),
810 skip_initial_delay: false,
811 event_type_filter: None,
812 };
813
814 let mut replay_engine = state.replay_engine.write().await;
816 match replay_engine.replay(recorded.clone(), options).await {
817 Ok(_) => {
818 info!("Replay started for scenario: {}", recorded.scenario.name);
819 Ok(Json(StatusResponse {
820 message: format!(
821 "Replay started for scenario: {} ({} events)",
822 recorded.scenario.name,
823 recorded.events.len()
824 ),
825 }))
826 }
827 Err(err) => Err(ChaosApiError::NotFound(err)),
828 }
829}
830
831async fn pause_replay(
833 State(state): State<ChaosApiState>,
834) -> Result<Json<StatusResponse>, ChaosApiError> {
835 let replay_engine = state.replay_engine.read().await;
836 match replay_engine.pause().await {
837 Ok(_) => {
838 info!("Replay paused");
839 Ok(Json(StatusResponse {
840 message: "Replay paused".to_string(),
841 }))
842 }
843 Err(err) => Err(ChaosApiError::NotFound(err)),
844 }
845}
846
847async fn resume_replay(
849 State(state): State<ChaosApiState>,
850) -> Result<Json<StatusResponse>, ChaosApiError> {
851 let replay_engine = state.replay_engine.read().await;
852 match replay_engine.resume().await {
853 Ok(_) => {
854 info!("Replay resumed");
855 Ok(Json(StatusResponse {
856 message: "Replay resumed".to_string(),
857 }))
858 }
859 Err(err) => Err(ChaosApiError::NotFound(err)),
860 }
861}
862
863async fn stop_replay(
865 State(state): State<ChaosApiState>,
866) -> Result<Json<StatusResponse>, ChaosApiError> {
867 let replay_engine = state.replay_engine.read().await;
868 match replay_engine.stop().await {
869 Ok(_) => {
870 info!("Replay stopped");
871 Ok(Json(StatusResponse {
872 message: "Replay stopped".to_string(),
873 }))
874 }
875 Err(err) => Err(ChaosApiError::NotFound(err)),
876 }
877}
878
879async fn replay_status(State(state): State<ChaosApiState>) -> Json<ReplayStatusResponse> {
881 let replay_engine = state.replay_engine.read().await;
882 if let Some(status) = replay_engine.get_status() {
883 Json(ReplayStatusResponse {
884 is_replaying: status.is_playing,
885 scenario_name: Some(status.scenario_name),
886 progress: status.progress,
887 })
888 } else {
889 Json(ReplayStatusResponse {
890 is_replaying: false,
891 scenario_name: None,
892 progress: 0.0,
893 })
894 }
895}
896
897async fn start_orchestration(
899 State(state): State<ChaosApiState>,
900 Json(req): Json<OrchestratedScenarioRequest>,
901) -> Result<Json<StatusResponse>, ChaosApiError> {
902 use crate::scenario_orchestrator::ScenarioStep;
903
904 let mut orchestrated = OrchestratedScenario::new(req.name.clone());
906
907 for step_value in req.steps {
909 let step = serde_json::from_value::<ScenarioStep>(step_value)
910 .map_err(|e| ChaosApiError::NotFound(format!("Invalid step: {}", e)))?;
911 orchestrated = orchestrated.add_step(step);
912 }
913
914 if req.parallel.unwrap_or(false) {
916 orchestrated = orchestrated.with_parallel_execution();
917 }
918
919 let mut orchestrator = state.orchestrator.write().await;
921 orchestrator
922 .execute(orchestrated.clone())
923 .await
924 .map_err(|e| ChaosApiError::NotFound(format!("Failed to start orchestration: {}", e)))?;
925
926 info!("Started orchestration '{}'", req.name);
927
928 Ok(Json(StatusResponse {
929 message: format!("Orchestration '{}' started successfully", req.name),
930 }))
931}
932
933async fn stop_orchestration(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
935 let orchestrator = state.orchestrator.read().await;
936
937 if orchestrator.is_running() {
938 info!("Orchestration stop requested");
941 Json(StatusResponse {
942 message: "Orchestration stop requested (will complete current step)".to_string(),
943 })
944 } else {
945 Json(StatusResponse {
946 message: "No orchestration currently running".to_string(),
947 })
948 }
949}
950
951async fn orchestration_status(
953 State(state): State<ChaosApiState>,
954) -> Json<OrchestrationStatusResponse> {
955 let orchestrator = state.orchestrator.read().await;
956
957 if let Some(status) = orchestrator.get_status() {
958 Json(OrchestrationStatusResponse {
959 is_running: status.is_running,
960 name: Some(status.name.clone()),
961 progress: status.progress,
962 })
963 } else {
964 Json(OrchestrationStatusResponse {
965 is_running: false,
966 name: None,
967 progress: 0.0,
968 })
969 }
970}
971
972async fn import_orchestration(
974 State(_state): State<ChaosApiState>,
975 Json(req): Json<ImportRequest>,
976) -> Result<Json<StatusResponse>, ChaosApiError> {
977 let orchestrated = if req.format == "json" {
979 OrchestratedScenario::from_json(&req.content)
980 .map_err(|e| ChaosApiError::NotFound(format!("Invalid JSON: {}", e)))?
981 } else if req.format == "yaml" {
982 OrchestratedScenario::from_yaml(&req.content)
983 .map_err(|e| ChaosApiError::NotFound(format!("Invalid YAML: {}", e)))?
984 } else {
985 return Err(ChaosApiError::NotFound(
986 "Unsupported format. Use 'json' or 'yaml'".to_string(),
987 ));
988 };
989
990 info!("Imported orchestration: {}", orchestrated.name);
991
992 Ok(Json(StatusResponse {
993 message: format!(
994 "Orchestration '{}' imported successfully ({} steps)",
995 orchestrated.name,
996 orchestrated.steps.len()
997 ),
998 }))
999}
1000
1001async fn add_schedule(
1003 State(state): State<ChaosApiState>,
1004 Json(req): Json<ScheduledScenarioRequest>,
1005) -> Result<Json<StatusResponse>, ChaosApiError> {
1006 let scenario = serde_json::from_value::<ChaosScenario>(req.scenario)
1008 .map_err(|e| ChaosApiError::NotFound(format!("Invalid scenario: {}", e)))?;
1009
1010 let schedule = serde_json::from_value::<ScheduleType>(req.schedule)
1012 .map_err(|e| ChaosApiError::NotFound(format!("Invalid schedule: {}", e)))?;
1013
1014 let scheduled = ScheduledScenario::new(req.id.clone(), scenario, schedule);
1016
1017 let scheduler = state.scheduler.read().await;
1019 scheduler.add_schedule(scheduled);
1020
1021 info!("Schedule '{}' added", req.id);
1022 Ok(Json(StatusResponse {
1023 message: format!("Schedule '{}' added", req.id),
1024 }))
1025}
1026
1027async fn get_schedule(
1029 State(state): State<ChaosApiState>,
1030 Path(id): Path<String>,
1031) -> Result<Json<ScheduledScenario>, ChaosApiError> {
1032 let scheduler = state.scheduler.read().await;
1033 match scheduler.get_schedule(&id) {
1034 Some(scheduled) => Ok(Json(scheduled)),
1035 None => Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id))),
1036 }
1037}
1038
1039async fn remove_schedule(
1041 State(state): State<ChaosApiState>,
1042 Path(id): Path<String>,
1043) -> Result<Json<StatusResponse>, ChaosApiError> {
1044 let scheduler = state.scheduler.read().await;
1045 match scheduler.remove_schedule(&id) {
1046 Some(_) => {
1047 info!("Schedule '{}' removed", id);
1048 Ok(Json(StatusResponse {
1049 message: format!("Schedule '{}' removed", id),
1050 }))
1051 }
1052 None => Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id))),
1053 }
1054}
1055
1056async fn enable_schedule(
1058 State(state): State<ChaosApiState>,
1059 Path(id): Path<String>,
1060) -> Result<Json<StatusResponse>, ChaosApiError> {
1061 let scheduler = state.scheduler.read().await;
1062 match scheduler.enable_schedule(&id) {
1063 Ok(_) => {
1064 info!("Schedule '{}' enabled", id);
1065 Ok(Json(StatusResponse {
1066 message: format!("Schedule '{}' enabled", id),
1067 }))
1068 }
1069 Err(err) => Err(ChaosApiError::NotFound(err)),
1070 }
1071}
1072
1073async fn disable_schedule(
1075 State(state): State<ChaosApiState>,
1076 Path(id): Path<String>,
1077) -> Result<Json<StatusResponse>, ChaosApiError> {
1078 let scheduler = state.scheduler.read().await;
1079 match scheduler.disable_schedule(&id) {
1080 Ok(_) => {
1081 info!("Schedule '{}' disabled", id);
1082 Ok(Json(StatusResponse {
1083 message: format!("Schedule '{}' disabled", id),
1084 }))
1085 }
1086 Err(err) => Err(ChaosApiError::NotFound(err)),
1087 }
1088}
1089
1090#[allow(dead_code)]
1110async fn trigger_schedule_by_path(
1111 State(state): State<ChaosApiState>,
1112 Path(id): Path<String>,
1113) -> Result<Json<StatusResponse>, ChaosApiError> {
1114 let scheduler = state.scheduler.read().await;
1115 let schedule_exists = scheduler.get_schedule(&id).is_some();
1116
1117 if !schedule_exists {
1118 return Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id)));
1119 }
1120
1121 let trigger_result = scheduler.trigger_now(&id).await;
1122
1123 match trigger_result {
1124 Ok(_) => {
1125 info!("Schedule '{}' triggered", id);
1126 Ok(Json(StatusResponse {
1127 message: format!("Schedule '{}' triggered", id),
1128 }))
1129 }
1130 Err(err) => Err(ChaosApiError::NotFound(err)),
1131 }
1132}
1133
1134async fn list_schedules(State(state): State<ChaosApiState>) -> Json<Vec<ScheduleSummary>> {
1136 let scheduler = state.scheduler.read().await;
1137 let schedules = scheduler.get_all_schedules();
1138 let summaries = schedules
1139 .into_iter()
1140 .map(|s| ScheduleSummary {
1141 id: s.id,
1142 scenario_name: s.scenario.name,
1143 enabled: s.enabled,
1144 next_execution: s.next_execution.map(|t| t.to_rfc3339()),
1145 })
1146 .collect();
1147 Json(summaries)
1148}
1149
1150#[derive(Debug, Deserialize)]
1153struct StartRecordingRequest {
1154 scenario_name: String,
1155}
1156
1157#[derive(Debug, Deserialize)]
1158struct ExportRequest {
1159 path: String,
1160}
1161
1162#[derive(Debug, Serialize)]
1163struct RecordingStatusResponse {
1164 is_recording: bool,
1165 scenario_name: Option<String>,
1166 events_recorded: usize,
1167}
1168
1169#[derive(Debug, Deserialize)]
1170struct StartReplayRequest {
1171 path: String,
1172 speed: Option<f64>,
1173 loop_replay: Option<bool>,
1174}
1175
1176#[derive(Debug, Serialize)]
1177struct ReplayStatusResponse {
1178 is_replaying: bool,
1179 scenario_name: Option<String>,
1180 progress: f64,
1181}
1182
1183#[derive(Debug, Deserialize)]
1184struct OrchestratedScenarioRequest {
1185 name: String,
1186 steps: Vec<serde_json::Value>,
1187 parallel: Option<bool>,
1188}
1189
1190#[derive(Debug, Serialize)]
1191struct OrchestrationStatusResponse {
1192 is_running: bool,
1193 name: Option<String>,
1194 progress: f64,
1195}
1196
1197#[derive(Debug, Deserialize)]
1198struct ImportRequest {
1199 content: String,
1200 format: String, }
1202
1203#[derive(Debug, Deserialize)]
1204struct ScheduledScenarioRequest {
1205 id: String,
1206 scenario: serde_json::Value,
1207 schedule: serde_json::Value,
1208}
1209
1210#[derive(Debug, Deserialize, Serialize)]
1211struct ScheduleSummary {
1212 id: String,
1213 scenario_name: String,
1214 enabled: bool,
1215 next_execution: Option<String>,
1216}
1217
1218async fn get_recommendations(
1222 State(state): State<ChaosApiState>,
1223) -> Json<Vec<crate::recommendations::Recommendation>> {
1224 Json(state.recommendation_engine.get_recommendations())
1225}
1226
1227async fn analyze_and_recommend(State(state): State<ChaosApiState>) -> Json<AnalyzeResponse> {
1229 use chrono::{Duration, Utc};
1230
1231 let end = Utc::now();
1233 let start = end - Duration::hours(24);
1234
1235 let buckets = state.analytics.get_metrics(start, end, TimeBucket::Hour);
1236 let impact = state.analytics.get_impact_analysis(start, end, TimeBucket::Hour);
1237
1238 let recommendations = state.recommendation_engine.analyze_and_recommend(&buckets, &impact);
1239
1240 Json(AnalyzeResponse {
1241 total_recommendations: recommendations.len(),
1242 high_priority: recommendations
1243 .iter()
1244 .filter(|r| {
1245 matches!(
1246 r.severity,
1247 RecommendationSeverity::High | RecommendationSeverity::Critical
1248 )
1249 })
1250 .count(),
1251 recommendations,
1252 })
1253}
1254
1255async fn get_recommendations_by_category(
1257 State(state): State<ChaosApiState>,
1258 Path(category): Path<String>,
1259) -> Result<Json<Vec<crate::recommendations::Recommendation>>, StatusCode> {
1260 let category = match category.as_str() {
1261 "latency" => RecommendationCategory::Latency,
1262 "fault_injection" => RecommendationCategory::FaultInjection,
1263 "rate_limit" => RecommendationCategory::RateLimit,
1264 "traffic_shaping" => RecommendationCategory::TrafficShaping,
1265 "circuit_breaker" => RecommendationCategory::CircuitBreaker,
1266 "bulkhead" => RecommendationCategory::Bulkhead,
1267 "scenario" => RecommendationCategory::Scenario,
1268 "coverage" => RecommendationCategory::Coverage,
1269 _ => return Err(StatusCode::BAD_REQUEST),
1270 };
1271
1272 Ok(Json(state.recommendation_engine.get_recommendations_by_category(category)))
1273}
1274
1275async fn get_recommendations_by_severity(
1277 State(state): State<ChaosApiState>,
1278 Path(severity): Path<String>,
1279) -> Result<Json<Vec<crate::recommendations::Recommendation>>, StatusCode> {
1280 let severity = match severity.as_str() {
1281 "info" => RecommendationSeverity::Info,
1282 "low" => RecommendationSeverity::Low,
1283 "medium" => RecommendationSeverity::Medium,
1284 "high" => RecommendationSeverity::High,
1285 "critical" => RecommendationSeverity::Critical,
1286 _ => return Err(StatusCode::BAD_REQUEST),
1287 };
1288
1289 Ok(Json(state.recommendation_engine.get_recommendations_by_severity(severity)))
1290}
1291
1292async fn clear_recommendations(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
1294 state.recommendation_engine.clear();
1295 Json(StatusResponse {
1296 message: "Recommendations cleared".to_string(),
1297 })
1298}
1299
1300#[derive(Debug, Serialize)]
1301struct AnalyzeResponse {
1302 total_recommendations: usize,
1303 high_priority: usize,
1304 recommendations: Vec<crate::recommendations::Recommendation>,
1305}
1306
1307async fn get_remediation_config(State(state): State<ChaosApiState>) -> Json<RemediationConfig> {
1311 Json(state.remediation_engine.get_config())
1312}
1313
1314async fn update_remediation_config(
1316 State(state): State<ChaosApiState>,
1317 Json(config): Json<RemediationConfig>,
1318) -> Json<StatusResponse> {
1319 state.remediation_engine.update_config(config);
1320 Json(StatusResponse {
1321 message: "Remediation configuration updated".to_string(),
1322 })
1323}
1324
1325#[derive(Debug, Deserialize)]
1326struct ProcessRemediationRequest {
1327 recommendation: Recommendation,
1328}
1329
1330async fn process_remediation(
1332 State(state): State<ChaosApiState>,
1333 Json(req): Json<ProcessRemediationRequest>,
1334) -> Result<Json<serde_json::Value>, StatusCode> {
1335 match state.remediation_engine.process_recommendation(&req.recommendation) {
1336 Ok(action_id) => Ok(Json(serde_json::json!({
1337 "success": true,
1338 "action_id": action_id,
1339 "message": "Recommendation processed"
1340 }))),
1341 Err(err) => Ok(Json(serde_json::json!({
1342 "success": false,
1343 "error": err
1344 }))),
1345 }
1346}
1347
1348#[derive(Debug, Deserialize)]
1349struct ApproveRequest {
1350 approver: String,
1351}
1352
1353async fn approve_remediation(
1355 State(state): State<ChaosApiState>,
1356 Path(id): Path<String>,
1357 Json(req): Json<ApproveRequest>,
1358) -> Result<Json<StatusResponse>, StatusCode> {
1359 match state.remediation_engine.approve_action(&id, &req.approver) {
1360 Ok(_) => Ok(Json(StatusResponse {
1361 message: format!("Action {} approved", id),
1362 })),
1363 Err(_err) => Err(StatusCode::BAD_REQUEST),
1364 }
1365}
1366
1367#[derive(Debug, Deserialize)]
1368struct RejectRequest {
1369 reason: String,
1370}
1371
1372async fn reject_remediation(
1374 State(state): State<ChaosApiState>,
1375 Path(id): Path<String>,
1376 Json(req): Json<RejectRequest>,
1377) -> Result<Json<StatusResponse>, StatusCode> {
1378 match state.remediation_engine.reject_action(&id, &req.reason) {
1379 Ok(_) => Ok(Json(StatusResponse {
1380 message: format!("Action {} rejected", id),
1381 })),
1382 Err(_err) => Err(StatusCode::BAD_REQUEST),
1383 }
1384}
1385
1386async fn rollback_remediation(
1388 State(state): State<ChaosApiState>,
1389 Path(id): Path<String>,
1390) -> Result<Json<StatusResponse>, StatusCode> {
1391 match state.remediation_engine.rollback_action(&id) {
1392 Ok(_) => Ok(Json(StatusResponse {
1393 message: format!("Action {} rolled back", id),
1394 })),
1395 Err(_err) => Err(StatusCode::BAD_REQUEST),
1396 }
1397}
1398
1399async fn get_remediation_actions(
1401 State(state): State<ChaosApiState>,
1402) -> Json<Vec<crate::auto_remediation::RemediationAction>> {
1403 Json(state.remediation_engine.get_active_actions())
1404}
1405
1406async fn get_remediation_action(
1408 State(state): State<ChaosApiState>,
1409 Path(id): Path<String>,
1410) -> Result<Json<crate::auto_remediation::RemediationAction>, StatusCode> {
1411 match state.remediation_engine.get_action(&id) {
1412 Some(action) => Ok(Json(action)),
1413 None => Err(StatusCode::NOT_FOUND),
1414 }
1415}
1416
1417async fn get_approval_queue(
1419 State(state): State<ChaosApiState>,
1420) -> Json<Vec<crate::auto_remediation::ApprovalRequest>> {
1421 Json(state.remediation_engine.get_approval_queue())
1422}
1423
1424async fn get_remediation_effectiveness(
1426 State(state): State<ChaosApiState>,
1427 Path(id): Path<String>,
1428) -> Result<Json<crate::auto_remediation::EffectivenessMetrics>, StatusCode> {
1429 match state.remediation_engine.get_effectiveness(&id) {
1430 Some(metrics) => Ok(Json(metrics)),
1431 None => Err(StatusCode::NOT_FOUND),
1432 }
1433}
1434
1435async fn get_remediation_stats(
1437 State(state): State<ChaosApiState>,
1438) -> Json<crate::auto_remediation::RemediationStats> {
1439 Json(state.remediation_engine.get_stats())
1440}
1441
1442async fn create_ab_test(
1446 State(state): State<ChaosApiState>,
1447 Json(config): Json<ABTestConfig>,
1448) -> Result<Json<serde_json::Value>, StatusCode> {
1449 let engine = state.ab_testing_engine.read().await;
1450 match engine.create_test(config) {
1451 Ok(test_id) => Ok(Json(serde_json::json!({
1452 "success": true,
1453 "test_id": test_id
1454 }))),
1455 Err(err) => Ok(Json(serde_json::json!({
1456 "success": false,
1457 "error": err
1458 }))),
1459 }
1460}
1461
1462async fn get_ab_tests(State(state): State<ChaosApiState>) -> Json<Vec<crate::ab_testing::ABTest>> {
1464 let engine = state.ab_testing_engine.read().await;
1465 Json(engine.get_all_tests())
1466}
1467
1468async fn get_ab_test(
1470 State(state): State<ChaosApiState>,
1471 Path(id): Path<String>,
1472) -> Result<Json<crate::ab_testing::ABTest>, StatusCode> {
1473 let engine = state.ab_testing_engine.read().await;
1474 match engine.get_test(&id) {
1475 Some(test) => Ok(Json(test)),
1476 None => Err(StatusCode::NOT_FOUND),
1477 }
1478}
1479
1480async fn start_ab_test(
1482 State(state): State<ChaosApiState>,
1483 Path(id): Path<String>,
1484) -> Result<Json<StatusResponse>, StatusCode> {
1485 let engine = state.ab_testing_engine.read().await;
1486 match engine.start_test(&id) {
1487 Ok(_) => Ok(Json(StatusResponse {
1488 message: format!("Test {} started", id),
1489 })),
1490 Err(_err) => Err(StatusCode::BAD_REQUEST),
1491 }
1492}
1493
1494async fn stop_ab_test(
1496 State(state): State<ChaosApiState>,
1497 Path(id): Path<String>,
1498) -> Result<Json<TestConclusion>, StatusCode> {
1499 let engine = state.ab_testing_engine.read().await;
1500 match engine.stop_test(&id) {
1501 Ok(conclusion) => Ok(Json(conclusion)),
1502 Err(_err) => Err(StatusCode::BAD_REQUEST),
1503 }
1504}
1505
1506async fn pause_ab_test(
1508 State(state): State<ChaosApiState>,
1509 Path(id): Path<String>,
1510) -> Result<Json<StatusResponse>, StatusCode> {
1511 let engine = state.ab_testing_engine.read().await;
1512 match engine.pause_test(&id) {
1513 Ok(_) => Ok(Json(StatusResponse {
1514 message: format!("Test {} paused", id),
1515 })),
1516 Err(_err) => Err(StatusCode::BAD_REQUEST),
1517 }
1518}
1519
1520async fn resume_ab_test(
1522 State(state): State<ChaosApiState>,
1523 Path(id): Path<String>,
1524) -> Result<Json<StatusResponse>, StatusCode> {
1525 let engine = state.ab_testing_engine.read().await;
1526 match engine.resume_test(&id) {
1527 Ok(_) => Ok(Json(StatusResponse {
1528 message: format!("Test {} resumed", id),
1529 })),
1530 Err(_err) => Err(StatusCode::BAD_REQUEST),
1531 }
1532}
1533
1534async fn record_ab_test_result(
1536 State(state): State<ChaosApiState>,
1537 Path((id, variant)): Path<(String, String)>,
1538 Json(results): Json<VariantResults>,
1539) -> Result<Json<StatusResponse>, StatusCode> {
1540 let engine = state.ab_testing_engine.read().await;
1541 match engine.record_variant_result(&id, &variant, results) {
1542 Ok(_) => Ok(Json(StatusResponse {
1543 message: format!("Results recorded for variant {}", variant),
1544 })),
1545 Err(_err) => Err(StatusCode::BAD_REQUEST),
1546 }
1547}
1548
1549async fn delete_ab_test(
1551 State(state): State<ChaosApiState>,
1552 Path(id): Path<String>,
1553) -> Result<Json<StatusResponse>, StatusCode> {
1554 let engine = state.ab_testing_engine.read().await;
1555 match engine.delete_test(&id) {
1556 Ok(_) => Ok(Json(StatusResponse {
1557 message: format!("Test {} deleted", id),
1558 })),
1559 Err(_err) => Err(StatusCode::BAD_REQUEST),
1560 }
1561}
1562
1563async fn get_ab_test_stats(
1565 State(state): State<ChaosApiState>,
1566) -> Json<crate::ab_testing::ABTestStats> {
1567 let engine = state.ab_testing_engine.read().await;
1568 Json(engine.get_stats())
1569}
1570
1571#[derive(Debug)]
1574enum ChaosApiError {
1575 NotFound(String),
1576}
1577
1578impl IntoResponse for ChaosApiError {
1579 fn into_response(self) -> Response {
1580 let (status, message) = match self {
1581 ChaosApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
1582 };
1583
1584 (status, Json(serde_json::json!({ "error": message }))).into_response()
1585 }
1586}