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 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, None, None, None, None, None, None, )
73 .await
74 }
75
76 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 self.trigger_webhooks("incident.created", &incident).await;
111
112 incident
113 }
114
115 pub async fn get_incident(&self, id: &str) -> Option<DriftIncident> {
117 self.store.get(id).await
118 }
119
120 pub async fn update_incident(&self, incident: DriftIncident) {
122 self.store.update(incident).await;
123 }
124
125 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 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 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 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 pub async fn query_incidents(&self, query: IncidentQuery) -> Vec<DriftIncident> {
164 self.store.query(query).await
165 }
166
167 pub async fn get_open_incidents(&self) -> Vec<DriftIncident> {
169 self.store.get_by_status(IncidentStatus::Open).await
170 }
171
172 pub async fn get_statistics(&self) -> HashMap<IncidentStatus, usize> {
174 self.store.count_by_status().await
175 }
176
177 pub async fn cleanup_old_incidents(&self, retention_days: u32) {
179 self.store.cleanup_old_resolved(retention_days).await;
180 }
181
182 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 if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
194 continue;
195 }
196
197 let payload = if webhook.url.contains("slack.com")
199 || webhook.url.contains("hooks.slack.com")
200 {
201 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 use crate::incidents::jira_formatter::format_jira_webhook;
208 format_jira_webhook(incident)
209 } else {
210 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 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}