1use axum::{
6 extract::{Json, Path},
7 http::StatusCode,
8 response::Json as ResponseJson,
9};
10use mockforge_core::failure_analysis::{FailureContextCollector, FailureNarrativeGenerator};
11use mockforge_core::intelligent_behavior::IntelligentBehaviorConfig;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15use std::sync::Arc;
16use tokio::sync::RwLock;
17use uuid::Uuid;
18
19use crate::models::ApiResponse;
20
21type FailureStorage = Arc<RwLock<HashMap<String, StoredFailure>>>;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27struct StoredFailure {
28 request_id: String,
30 context: mockforge_core::FailureContext,
32 narrative: Option<mockforge_core::FailureNarrative>,
34 timestamp: chrono::DateTime<chrono::Utc>,
36}
37
38static FAILURE_STORAGE: once_cell::sync::Lazy<FailureStorage> =
40 once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AnalyzeFailureRequest {
45 pub method: String,
47 pub path: String,
49 #[serde(default)]
51 pub headers: HashMap<String, String>,
52 #[serde(default)]
54 pub query_params: HashMap<String, String>,
55 pub body: Option<Value>,
57 pub status_code: Option<u16>,
59 #[serde(default)]
61 pub response_headers: HashMap<String, String>,
62 pub response_body: Option<Value>,
64 pub duration_ms: Option<u64>,
66 pub error_message: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct AnalyzeFailureResponse {
73 pub request_id: String,
75 pub context: mockforge_core::FailureContext,
77 pub narrative: Option<mockforge_core::FailureNarrative>,
79 pub error: Option<String>,
81}
82
83pub async fn analyze_failure(
87 Json(request): Json<AnalyzeFailureRequest>,
88) -> Result<ResponseJson<ApiResponse<AnalyzeFailureResponse>>, StatusCode> {
89 let request_id = Uuid::new_v4().to_string();
91
92 let collector = FailureContextCollector::new();
94
95 let context = collector
97 .collect_context_with_details(
98 &request.method,
99 &request.path,
100 request.headers,
101 request.query_params,
102 request.body,
103 request.status_code,
104 request.response_headers,
105 request.response_body,
106 request.duration_ms,
107 request.error_message,
108 vec![], vec![], None, vec![], vec![], )
114 .map_err(|e| {
115 tracing::error!("Failed to collect failure context: {}", e);
116 StatusCode::INTERNAL_SERVER_ERROR
117 })?;
118
119 let config = IntelligentBehaviorConfig::default();
121 let generator = FailureNarrativeGenerator::new(config);
122 let narrative = match generator.generate_narrative(&context).await {
123 Ok(narrative) => Some(narrative),
124 Err(e) => {
125 tracing::warn!("Failed to generate narrative: {}", e);
126 None
127 }
128 };
129
130 let stored = StoredFailure {
132 request_id: request_id.clone(),
133 context: context.clone(),
134 narrative: narrative.clone(),
135 timestamp: chrono::Utc::now(),
136 };
137
138 {
139 let mut storage = FAILURE_STORAGE.write().await;
140 storage.insert(request_id.clone(), stored);
141 }
142
143 let response = AnalyzeFailureResponse {
144 request_id,
145 context,
146 narrative,
147 error: None,
148 };
149
150 Ok(ResponseJson(ApiResponse::success(response)))
151}
152
153pub async fn get_failure_analysis(
157 Path(request_id): Path<String>,
158) -> Result<ResponseJson<ApiResponse<AnalyzeFailureResponse>>, StatusCode> {
159 let storage = FAILURE_STORAGE.read().await;
160 let stored = storage.get(&request_id).ok_or(StatusCode::NOT_FOUND)?;
161
162 let response = AnalyzeFailureResponse {
163 request_id: stored.request_id.clone(),
164 context: stored.context.clone(),
165 narrative: stored.narrative.clone(),
166 error: None,
167 };
168
169 Ok(ResponseJson(ApiResponse::success(response)))
170}
171
172pub async fn list_recent_failures(
176) -> Result<ResponseJson<ApiResponse<Vec<FailureSummary>>>, StatusCode> {
177 let storage = FAILURE_STORAGE.read().await;
178
179 let mut failures: Vec<_> = storage
181 .values()
182 .map(|f| FailureSummary {
183 request_id: f.request_id.clone(),
184 method: f.context.request.method.clone(),
185 path: f.context.request.path.clone(),
186 status_code: f.context.response.as_ref().map(|r| r.status_code),
187 error_message: f.context.error_message.clone(),
188 timestamp: f.timestamp,
189 has_narrative: f.narrative.is_some(),
190 })
191 .collect();
192
193 failures.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
194 failures.truncate(50); Ok(ResponseJson(ApiResponse::success(failures)))
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct FailureSummary {
202 pub request_id: String,
204 pub method: String,
206 pub path: String,
208 pub status_code: Option<u16>,
210 pub error_message: Option<String>,
212 pub timestamp: chrono::DateTime<chrono::Utc>,
214 pub has_narrative: bool,
216}