mockforge_core/incidents/
manager.rs1use 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#[derive(Debug, Clone)]
15pub struct IncidentManager {
16 store: Arc<IncidentStore>,
17 webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
19}
20
21impl IncidentManager {
22 pub fn new(store: Arc<IncidentStore>) -> Self {
24 Self {
25 store,
26 webhook_configs: Vec::new(),
27 }
28 }
29
30 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 pub fn add_webhook(&mut self, config: crate::incidents::integrations::WebhookConfig) {
43 self.webhook_configs.push(config);
44 }
45
46 #[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, None, None, None, None, None, None, )
74 .await
75 }
76
77 #[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 self.trigger_webhooks("incident.created", &incident).await;
113
114 incident
115 }
116
117 pub async fn get_incident(&self, id: &str) -> Option<DriftIncident> {
119 self.store.get(id).await
120 }
121
122 pub async fn update_incident(&self, incident: DriftIncident) {
124 self.store.update(incident).await;
125 }
126
127 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 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 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 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 pub async fn query_incidents(&self, query: IncidentQuery) -> Vec<DriftIncident> {
166 self.store.query(query).await
167 }
168
169 pub async fn get_open_incidents(&self) -> Vec<DriftIncident> {
171 self.store.get_by_status(IncidentStatus::Open).await
172 }
173
174 pub async fn get_statistics(&self) -> HashMap<IncidentStatus, usize> {
176 self.store.count_by_status().await
177 }
178
179 pub async fn cleanup_old_incidents(&self, retention_days: u32) {
181 self.store.cleanup_old_resolved(retention_days).await;
182 }
183
184 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 if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
196 continue;
197 }
198
199 let payload = if webhook.url.contains("slack.com")
201 || webhook.url.contains("hooks.slack.com")
202 {
203 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 use crate::incidents::jira_formatter::format_jira_webhook;
210 format_jira_webhook(incident)
211 } else {
212 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 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}