mockforge_chaos/
resilience_api.rs

1//! API endpoints for resilience management
2
3use crate::resilience::{
4    BulkheadManager, BulkheadStats, CircuitBreakerManager, CircuitState, CircuitStats,
5};
6use axum::{
7    extract::{Path, State},
8    http::StatusCode,
9    response::IntoResponse,
10    routing::{get, post},
11    Json, Router,
12};
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15
16/// Circuit breaker state for API response
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CircuitBreakerStateResponse {
19    pub endpoint: String,
20    pub state: String,
21    pub stats: CircuitStatsResponse,
22}
23
24/// Circuit breaker statistics for API response
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CircuitStatsResponse {
27    pub total_requests: u64,
28    pub successful_requests: u64,
29    pub failed_requests: u64,
30    pub rejected_requests: u64,
31    pub consecutive_failures: u64,
32    pub consecutive_successes: u64,
33    pub success_rate: f64,
34    pub failure_rate: f64,
35}
36
37impl From<CircuitStats> for CircuitStatsResponse {
38    fn from(stats: CircuitStats) -> Self {
39        let success_rate = if stats.total_requests > 0 {
40            (stats.successful_requests as f64 / stats.total_requests as f64) * 100.0
41        } else {
42            0.0
43        };
44
45        let failure_rate = if stats.total_requests > 0 {
46            (stats.failed_requests as f64 / stats.total_requests as f64) * 100.0
47        } else {
48            0.0
49        };
50
51        Self {
52            total_requests: stats.total_requests,
53            successful_requests: stats.successful_requests,
54            failed_requests: stats.failed_requests,
55            rejected_requests: stats.rejected_requests,
56            consecutive_failures: stats.consecutive_failures,
57            consecutive_successes: stats.consecutive_successes,
58            success_rate,
59            failure_rate,
60        }
61    }
62}
63
64/// Bulkhead state for API response
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct BulkheadStateResponse {
67    pub service: String,
68    pub stats: BulkheadStatsResponse,
69}
70
71/// Bulkhead statistics for API response
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct BulkheadStatsResponse {
74    pub active_requests: u32,
75    pub queued_requests: u32,
76    pub total_requests: u64,
77    pub rejected_requests: u64,
78    pub timeout_requests: u64,
79    pub utilization_percent: f64,
80}
81
82impl BulkheadStatsResponse {
83    fn from_stats(stats: BulkheadStats, max_concurrent: u32) -> Self {
84        let utilization_percent = if max_concurrent > 0 {
85            (stats.active_requests as f64 / max_concurrent as f64) * 100.0
86        } else {
87            0.0
88        };
89
90        Self {
91            active_requests: stats.active_requests,
92            queued_requests: stats.queued_requests,
93            total_requests: stats.total_requests,
94            rejected_requests: stats.rejected_requests,
95            timeout_requests: stats.timeout_requests,
96            utilization_percent,
97        }
98    }
99}
100
101/// Shared state for resilience API
102#[derive(Clone)]
103pub struct ResilienceApiState {
104    pub circuit_breaker_manager: Arc<CircuitBreakerManager>,
105    pub bulkhead_manager: Arc<BulkheadManager>,
106}
107
108/// Get all circuit breaker states
109async fn get_all_circuit_breakers(State(state): State<ResilienceApiState>) -> impl IntoResponse {
110    let states = state.circuit_breaker_manager.get_all_states().await;
111    let mut responses = Vec::new();
112
113    for (endpoint, cb_state) in states {
114        let breaker = state.circuit_breaker_manager.get_breaker(&endpoint).await;
115        let stats = breaker.stats().await;
116
117        responses.push(CircuitBreakerStateResponse {
118            endpoint: endpoint.clone(),
119            state: format!("{:?}", cb_state),
120            stats: stats.into(),
121        });
122    }
123
124    Json(responses)
125}
126
127/// Get circuit breaker state for specific endpoint
128async fn get_circuit_breaker(
129    State(state): State<ResilienceApiState>,
130    Path(endpoint): Path<String>,
131) -> impl IntoResponse {
132    let breaker = state.circuit_breaker_manager.get_breaker(&endpoint).await;
133    let cb_state = breaker.state().await;
134    let stats = breaker.stats().await;
135
136    Json(CircuitBreakerStateResponse {
137        endpoint: endpoint.clone(),
138        state: format!("{:?}", cb_state),
139        stats: stats.into(),
140    })
141}
142
143/// Reset circuit breaker for specific endpoint
144async fn reset_circuit_breaker(
145    State(state): State<ResilienceApiState>,
146    Path(endpoint): Path<String>,
147) -> impl IntoResponse {
148    let breaker = state.circuit_breaker_manager.get_breaker(&endpoint).await;
149    breaker.reset().await;
150
151    (StatusCode::OK, "Circuit breaker reset")
152}
153
154/// Get all bulkhead states
155async fn get_all_bulkheads(State(state): State<ResilienceApiState>) -> impl IntoResponse {
156    let stats_map = state.bulkhead_manager.get_all_stats().await;
157    let mut responses = Vec::new();
158
159    for (service, stats) in stats_map {
160        let bulkhead = state.bulkhead_manager.get_bulkhead(&service).await;
161        let config = bulkhead.config().await;
162
163        responses.push(BulkheadStateResponse {
164            service: service.clone(),
165            stats: BulkheadStatsResponse::from_stats(stats, config.max_concurrent_requests),
166        });
167    }
168
169    Json(responses)
170}
171
172/// Get bulkhead state for specific service
173async fn get_bulkhead(
174    State(state): State<ResilienceApiState>,
175    Path(service): Path<String>,
176) -> impl IntoResponse {
177    let bulkhead = state.bulkhead_manager.get_bulkhead(&service).await;
178    let stats = bulkhead.stats().await;
179    let config = bulkhead.config().await;
180
181    Json(BulkheadStateResponse {
182        service: service.clone(),
183        stats: BulkheadStatsResponse::from_stats(stats, config.max_concurrent_requests),
184    })
185}
186
187/// Reset bulkhead statistics for specific service
188async fn reset_bulkhead(
189    State(state): State<ResilienceApiState>,
190    Path(service): Path<String>,
191) -> impl IntoResponse {
192    let bulkhead = state.bulkhead_manager.get_bulkhead(&service).await;
193    bulkhead.reset().await;
194
195    (StatusCode::OK, "Bulkhead statistics reset")
196}
197
198/// Get dashboard summary
199async fn get_dashboard_summary(State(state): State<ResilienceApiState>) -> impl IntoResponse {
200    let circuit_states = state.circuit_breaker_manager.get_all_states().await;
201    let bulkhead_stats = state.bulkhead_manager.get_all_stats().await;
202
203    let mut open_circuits = 0;
204    let mut half_open_circuits = 0;
205    let mut closed_circuits = 0;
206
207    for cb_state in circuit_states.values() {
208        match cb_state {
209            CircuitState::Open => open_circuits += 1,
210            CircuitState::HalfOpen => half_open_circuits += 1,
211            CircuitState::Closed => closed_circuits += 1,
212        }
213    }
214
215    let total_active_requests: u32 = bulkhead_stats.values().map(|s| s.active_requests).sum();
216    let total_queued_requests: u32 = bulkhead_stats.values().map(|s| s.queued_requests).sum();
217
218    let summary = serde_json::json!({
219        "circuit_breakers": {
220            "total": circuit_states.len(),
221            "open": open_circuits,
222            "half_open": half_open_circuits,
223            "closed": closed_circuits,
224        },
225        "bulkheads": {
226            "total": bulkhead_stats.len(),
227            "active_requests": total_active_requests,
228            "queued_requests": total_queued_requests,
229        },
230    });
231
232    Json(summary)
233}
234
235/// Create resilience API router
236pub fn create_resilience_router(state: ResilienceApiState) -> Router {
237    Router::new()
238        .route("/circuit-breakers", get(get_all_circuit_breakers))
239        .route("/circuit-breakers/:endpoint", get(get_circuit_breaker))
240        .route("/circuit-breakers/:endpoint/reset", post(reset_circuit_breaker))
241        .route("/bulkheads", get(get_all_bulkheads))
242        .route("/bulkheads/:service", get(get_bulkhead))
243        .route("/bulkheads/:service/reset", post(reset_bulkhead))
244        .route("/dashboard/summary", get(get_dashboard_summary))
245        .with_state(state)
246}