mockforge_ui/handlers/
failure_analysis.rs

1//! Failure analysis API handlers
2//!
3//! Provides endpoints for retrieving failure narratives and analyzing request failures.
4
5use 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
21/// In-memory storage for failure narratives
22/// In a production system, this would be persisted to a database
23type FailureStorage = Arc<RwLock<HashMap<String, StoredFailure>>>;
24
25/// Stored failure with narrative
26#[derive(Debug, Clone, Serialize, Deserialize)]
27struct StoredFailure {
28    /// Request ID
29    request_id: String,
30    /// Failure context
31    context: mockforge_core::FailureContext,
32    /// Generated narrative
33    narrative: Option<mockforge_core::FailureNarrative>,
34    /// Timestamp
35    timestamp: chrono::DateTime<chrono::Utc>,
36}
37
38/// Global failure storage (in-memory)
39static FAILURE_STORAGE: once_cell::sync::Lazy<FailureStorage> =
40    once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
41
42/// Request to analyze a failure
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AnalyzeFailureRequest {
45    /// Request method
46    pub method: String,
47    /// Request path
48    pub path: String,
49    /// Request headers
50    #[serde(default)]
51    pub headers: HashMap<String, String>,
52    /// Query parameters
53    #[serde(default)]
54    pub query_params: HashMap<String, String>,
55    /// Request body
56    pub body: Option<Value>,
57    /// Response status code (if available)
58    pub status_code: Option<u16>,
59    /// Response headers
60    #[serde(default)]
61    pub response_headers: HashMap<String, String>,
62    /// Response body
63    pub response_body: Option<Value>,
64    /// Duration in milliseconds
65    pub duration_ms: Option<u64>,
66    /// Error message
67    pub error_message: Option<String>,
68}
69
70/// Response from failure analysis
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct AnalyzeFailureResponse {
73    /// Request ID (generated)
74    pub request_id: String,
75    /// Failure context
76    pub context: mockforge_core::FailureContext,
77    /// Generated narrative
78    pub narrative: Option<mockforge_core::FailureNarrative>,
79    /// Error message (if analysis failed)
80    pub error: Option<String>,
81}
82
83/// Analyze a failure and generate a narrative
84///
85/// POST /api/v2/failures/analyze
86pub async fn analyze_failure(
87    Json(request): Json<AnalyzeFailureRequest>,
88) -> Result<ResponseJson<ApiResponse<AnalyzeFailureResponse>>, StatusCode> {
89    // Generate a request ID
90    let request_id = Uuid::new_v4().to_string();
91
92    // Create context collector
93    let collector = FailureContextCollector::new();
94
95    // Collect failure context
96    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![], // chaos_configs - would be populated from actual system
109            vec![], // consistency_rules - would be populated from actual system
110            None,   // contract_validation - would be populated from actual system
111            vec![], // behavioral_rules - would be populated from actual system
112            vec![], // hook_results - would be populated from actual system
113        )
114        .map_err(|e| {
115            tracing::error!("Failed to collect failure context: {}", e);
116            StatusCode::INTERNAL_SERVER_ERROR
117        })?;
118
119    // Generate narrative
120    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    // Store failure
131    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
153/// Get failure analysis by request ID
154///
155/// GET /api/v2/failures/{request_id}
156pub 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
172/// List recent failures
173///
174/// GET /api/v2/failures/recent
175pub async fn list_recent_failures(
176) -> Result<ResponseJson<ApiResponse<Vec<FailureSummary>>>, StatusCode> {
177    let storage = FAILURE_STORAGE.read().await;
178
179    // Get all failures, sorted by timestamp (most recent first)
180    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); // Limit to 50 most recent
195
196    Ok(ResponseJson(ApiResponse::success(failures)))
197}
198
199/// Failure summary for listing
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct FailureSummary {
202    /// Request ID
203    pub request_id: String,
204    /// HTTP method
205    pub method: String,
206    /// Request path
207    pub path: String,
208    /// Status code (if available)
209    pub status_code: Option<u16>,
210    /// Error message
211    pub error_message: Option<String>,
212    /// Timestamp
213    pub timestamp: chrono::DateTime<chrono::Utc>,
214    /// Whether a narrative was generated
215    pub has_narrative: bool,
216}