Skip to main content

mockforge_core/incidents/
manager.rs

1//! Incident manager for creating and managing drift incidents
2//!
3//! This module provides high-level functionality for incident lifecycle management.
4
5use crate::incidents::store::IncidentStore;
6use crate::incidents::types::{
7    DriftIncident, IncidentQuery, IncidentSeverity, IncidentStatus, IncidentType,
8};
9use std::collections::HashMap;
10use std::sync::Arc;
11use uuid::Uuid;
12
13/// Manager for drift incidents
14#[derive(Debug, Clone)]
15pub struct IncidentManager {
16    store: Arc<IncidentStore>,
17    /// Webhook configurations for incident notifications
18    webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
19}
20
21impl IncidentManager {
22    /// Create a new incident manager
23    pub fn new(store: Arc<IncidentStore>) -> Self {
24        Self {
25            store,
26            webhook_configs: Vec::new(),
27        }
28    }
29
30    /// Create a new incident manager with webhook configurations
31    pub fn new_with_webhooks(
32        store: Arc<IncidentStore>,
33        webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
34    ) -> Self {
35        Self {
36            store,
37            webhook_configs,
38        }
39    }
40
41    /// Add webhook configuration
42    pub fn add_webhook(&mut self, config: crate::incidents::integrations::WebhookConfig) {
43        self.webhook_configs.push(config);
44    }
45
46    /// Create a new incident from drift result
47    #[allow(clippy::too_many_arguments)]
48    pub async fn create_incident(
49        &self,
50        endpoint: String,
51        method: String,
52        incident_type: IncidentType,
53        severity: IncidentSeverity,
54        details: serde_json::Value,
55        budget_id: Option<String>,
56        workspace_id: Option<String>,
57    ) -> DriftIncident {
58        self.create_incident_with_samples(
59            endpoint,
60            method,
61            incident_type,
62            severity,
63            details,
64            budget_id,
65            workspace_id,
66            None, // sync_cycle_id
67            None, // contract_diff_id
68            None, // before_sample
69            None, // after_sample
70            None, // fitness_test_results
71            None, // affected_consumers
72            None, // protocol
73        )
74        .await
75    }
76
77    /// Create a new incident with before/after samples and traceability
78    #[allow(clippy::too_many_arguments)]
79    pub async fn create_incident_with_samples(
80        &self,
81        endpoint: String,
82        method: String,
83        incident_type: IncidentType,
84        severity: IncidentSeverity,
85        details: serde_json::Value,
86        budget_id: Option<String>,
87        workspace_id: Option<String>,
88        sync_cycle_id: Option<String>,
89        contract_diff_id: Option<String>,
90        before_sample: Option<serde_json::Value>,
91        after_sample: Option<serde_json::Value>,
92        fitness_test_results: Option<Vec<crate::contract_drift::fitness::FitnessTestResult>>,
93        affected_consumers: Option<crate::contract_drift::consumer_mapping::ConsumerImpact>,
94        protocol: Option<crate::protocol_abstraction::Protocol>,
95    ) -> DriftIncident {
96        let id = Uuid::new_v4().to_string();
97        let mut incident =
98            DriftIncident::new(id, endpoint, method, incident_type, severity, details);
99        incident.budget_id = budget_id;
100        incident.workspace_id = workspace_id;
101        incident.sync_cycle_id = sync_cycle_id;
102        incident.contract_diff_id = contract_diff_id;
103        incident.before_sample = before_sample;
104        incident.after_sample = after_sample;
105        incident.fitness_test_results = fitness_test_results.unwrap_or_default();
106        incident.affected_consumers = affected_consumers;
107        incident.protocol = protocol;
108
109        self.store.store(incident.clone()).await;
110
111        // Trigger webhook notifications for incident.created event
112        self.trigger_webhooks("incident.created", &incident).await;
113
114        incident
115    }
116
117    /// Get an incident by ID
118    pub async fn get_incident(&self, id: &str) -> Option<DriftIncident> {
119        self.store.get(id).await
120    }
121
122    /// Update an incident
123    pub async fn update_incident(&self, incident: DriftIncident) {
124        self.store.update(incident).await;
125    }
126
127    /// Acknowledge an incident
128    pub async fn acknowledge_incident(&self, id: &str) -> Option<DriftIncident> {
129        let mut incident = self.store.get(id).await?;
130        incident.acknowledge();
131        self.store.update(incident.clone()).await;
132        Some(incident)
133    }
134
135    /// Resolve an incident
136    pub async fn resolve_incident(&self, id: &str) -> Option<DriftIncident> {
137        let mut incident = self.store.get(id).await?;
138        incident.resolve();
139        self.store.update(incident.clone()).await;
140        Some(incident)
141    }
142
143    /// Close an incident
144    pub async fn close_incident(&self, id: &str) -> Option<DriftIncident> {
145        let mut incident = self.store.get(id).await?;
146        incident.close();
147        self.store.update(incident.clone()).await;
148        Some(incident)
149    }
150
151    /// Link an external ticket to an incident
152    pub async fn link_external_ticket(
153        &self,
154        id: &str,
155        ticket_id: String,
156        ticket_url: Option<String>,
157    ) -> Option<DriftIncident> {
158        let mut incident = self.store.get(id).await?;
159        incident.link_external_ticket(ticket_id, ticket_url);
160        self.store.update(incident.clone()).await;
161        Some(incident)
162    }
163
164    /// Query incidents
165    pub async fn query_incidents(&self, query: IncidentQuery) -> Vec<DriftIncident> {
166        self.store.query(query).await
167    }
168
169    /// Get all open incidents
170    pub async fn get_open_incidents(&self) -> Vec<DriftIncident> {
171        self.store.get_by_status(IncidentStatus::Open).await
172    }
173
174    /// Get incident statistics
175    pub async fn get_statistics(&self) -> HashMap<IncidentStatus, usize> {
176        self.store.count_by_status().await
177    }
178
179    /// Clean up old resolved incidents
180    pub async fn cleanup_old_incidents(&self, retention_days: u32) {
181        self.store.cleanup_old_resolved(retention_days).await;
182    }
183
184    /// Trigger webhook notifications for an event
185    async fn trigger_webhooks(&self, event_type: &str, incident: &DriftIncident) {
186        use crate::incidents::integrations::send_webhook;
187        use serde_json::json;
188
189        for webhook in &self.webhook_configs {
190            if !webhook.enabled {
191                continue;
192            }
193
194            // Check if webhook is subscribed to this event
195            if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
196                continue;
197            }
198
199            // Determine webhook format based on URL or headers
200            let payload = if webhook.url.contains("slack.com")
201                || webhook.url.contains("hooks.slack.com")
202            {
203                // Format as Slack message
204                use crate::incidents::slack_formatter::format_slack_webhook;
205                format_slack_webhook(incident)
206            } else if webhook.url.contains("jira") || webhook.headers.contains_key("X-Jira-Project")
207            {
208                // Format as Jira webhook
209                use crate::incidents::jira_formatter::format_jira_webhook;
210                format_jira_webhook(incident)
211            } else {
212                // Generic webhook format
213                json!({
214                    "event": event_type,
215                    "incident": {
216                        "id": incident.id,
217                        "endpoint": incident.endpoint,
218                        "method": incident.method,
219                        "type": format!("{:?}", incident.incident_type),
220                        "severity": format!("{:?}", incident.severity),
221                        "status": format!("{:?}", incident.status),
222                        "details": incident.details,
223                        "created_at": incident.created_at,
224                    }
225                })
226            };
227
228            // Send webhook asynchronously (fire and forget)
229            let webhook_clone = webhook.clone();
230            tokio::spawn(async move {
231                if let Err(e) = send_webhook(&webhook_clone, &payload).await {
232                    tracing::warn!("Failed to send webhook to {}: {}", webhook_clone.url, e);
233                }
234            });
235        }
236    }
237}