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    pub async fn create_incident(
48        &self,
49        endpoint: String,
50        method: String,
51        incident_type: IncidentType,
52        severity: IncidentSeverity,
53        details: serde_json::Value,
54        budget_id: Option<String>,
55        workspace_id: Option<String>,
56    ) -> DriftIncident {
57        self.create_incident_with_samples(
58            endpoint,
59            method,
60            incident_type,
61            severity,
62            details,
63            budget_id,
64            workspace_id,
65            None, // sync_cycle_id
66            None, // contract_diff_id
67            None, // before_sample
68            None, // after_sample
69            None, // fitness_test_results
70            None, // affected_consumers
71            None, // protocol
72        )
73        .await
74    }
75
76    /// Create a new incident with before/after samples and traceability
77    pub async fn create_incident_with_samples(
78        &self,
79        endpoint: String,
80        method: String,
81        incident_type: IncidentType,
82        severity: IncidentSeverity,
83        details: serde_json::Value,
84        budget_id: Option<String>,
85        workspace_id: Option<String>,
86        sync_cycle_id: Option<String>,
87        contract_diff_id: Option<String>,
88        before_sample: Option<serde_json::Value>,
89        after_sample: Option<serde_json::Value>,
90        fitness_test_results: Option<Vec<crate::contract_drift::fitness::FitnessTestResult>>,
91        affected_consumers: Option<crate::contract_drift::consumer_mapping::ConsumerImpact>,
92        protocol: Option<crate::protocol_abstraction::Protocol>,
93    ) -> DriftIncident {
94        let id = Uuid::new_v4().to_string();
95        let mut incident =
96            DriftIncident::new(id, endpoint, method, incident_type, severity, details);
97        incident.budget_id = budget_id;
98        incident.workspace_id = workspace_id;
99        incident.sync_cycle_id = sync_cycle_id;
100        incident.contract_diff_id = contract_diff_id;
101        incident.before_sample = before_sample;
102        incident.after_sample = after_sample;
103        incident.fitness_test_results = fitness_test_results.unwrap_or_default();
104        incident.affected_consumers = affected_consumers;
105        incident.protocol = protocol;
106
107        self.store.store(incident.clone()).await;
108
109        // Trigger webhook notifications for incident.created event
110        self.trigger_webhooks("incident.created", &incident).await;
111
112        incident
113    }
114
115    /// Get an incident by ID
116    pub async fn get_incident(&self, id: &str) -> Option<DriftIncident> {
117        self.store.get(id).await
118    }
119
120    /// Update an incident
121    pub async fn update_incident(&self, incident: DriftIncident) {
122        self.store.update(incident).await;
123    }
124
125    /// Acknowledge an incident
126    pub async fn acknowledge_incident(&self, id: &str) -> Option<DriftIncident> {
127        let mut incident = self.store.get(id).await?;
128        incident.acknowledge();
129        self.store.update(incident.clone()).await;
130        Some(incident)
131    }
132
133    /// Resolve an incident
134    pub async fn resolve_incident(&self, id: &str) -> Option<DriftIncident> {
135        let mut incident = self.store.get(id).await?;
136        incident.resolve();
137        self.store.update(incident.clone()).await;
138        Some(incident)
139    }
140
141    /// Close an incident
142    pub async fn close_incident(&self, id: &str) -> Option<DriftIncident> {
143        let mut incident = self.store.get(id).await?;
144        incident.close();
145        self.store.update(incident.clone()).await;
146        Some(incident)
147    }
148
149    /// Link an external ticket to an incident
150    pub async fn link_external_ticket(
151        &self,
152        id: &str,
153        ticket_id: String,
154        ticket_url: Option<String>,
155    ) -> Option<DriftIncident> {
156        let mut incident = self.store.get(id).await?;
157        incident.link_external_ticket(ticket_id, ticket_url);
158        self.store.update(incident.clone()).await;
159        Some(incident)
160    }
161
162    /// Query incidents
163    pub async fn query_incidents(&self, query: IncidentQuery) -> Vec<DriftIncident> {
164        self.store.query(query).await
165    }
166
167    /// Get all open incidents
168    pub async fn get_open_incidents(&self) -> Vec<DriftIncident> {
169        self.store.get_by_status(IncidentStatus::Open).await
170    }
171
172    /// Get incident statistics
173    pub async fn get_statistics(&self) -> HashMap<IncidentStatus, usize> {
174        self.store.count_by_status().await
175    }
176
177    /// Clean up old resolved incidents
178    pub async fn cleanup_old_incidents(&self, retention_days: u32) {
179        self.store.cleanup_old_resolved(retention_days).await;
180    }
181
182    /// Trigger webhook notifications for an event
183    async fn trigger_webhooks(&self, event_type: &str, incident: &DriftIncident) {
184        use crate::incidents::integrations::send_webhook;
185        use serde_json::json;
186
187        for webhook in &self.webhook_configs {
188            if !webhook.enabled {
189                continue;
190            }
191
192            // Check if webhook is subscribed to this event
193            if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
194                continue;
195            }
196
197            // Determine webhook format based on URL or headers
198            let payload = if webhook.url.contains("slack.com")
199                || webhook.url.contains("hooks.slack.com")
200            {
201                // Format as Slack message
202                use crate::incidents::slack_formatter::format_slack_webhook;
203                format_slack_webhook(incident)
204            } else if webhook.url.contains("jira") || webhook.headers.contains_key("X-Jira-Project")
205            {
206                // Format as Jira webhook
207                use crate::incidents::jira_formatter::format_jira_webhook;
208                format_jira_webhook(incident)
209            } else {
210                // Generic webhook format
211                json!({
212                    "event": event_type,
213                    "incident": {
214                        "id": incident.id,
215                        "endpoint": incident.endpoint,
216                        "method": incident.method,
217                        "type": format!("{:?}", incident.incident_type),
218                        "severity": format!("{:?}", incident.severity),
219                        "status": format!("{:?}", incident.status),
220                        "details": incident.details,
221                        "created_at": incident.created_at,
222                    }
223                })
224            };
225
226            // Send webhook asynchronously (fire and forget)
227            let webhook_clone = webhook.clone();
228            tokio::spawn(async move {
229                if let Err(e) = send_webhook(&webhook_clone, &payload).await {
230                    tracing::warn!("Failed to send webhook to {}: {}", webhook_clone.url, e);
231                }
232            });
233        }
234    }
235}