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, )
70 .await
71 }
72
73 pub async fn create_incident_with_samples(
75 &self,
76 endpoint: String,
77 method: String,
78 incident_type: IncidentType,
79 severity: IncidentSeverity,
80 details: serde_json::Value,
81 budget_id: Option<String>,
82 workspace_id: Option<String>,
83 sync_cycle_id: Option<String>,
84 contract_diff_id: Option<String>,
85 before_sample: Option<serde_json::Value>,
86 after_sample: Option<serde_json::Value>,
87 ) -> DriftIncident {
88 let id = Uuid::new_v4().to_string();
89 let mut incident =
90 DriftIncident::new(id, endpoint, method, incident_type, severity, details);
91 incident.budget_id = budget_id;
92 incident.workspace_id = workspace_id;
93 incident.sync_cycle_id = sync_cycle_id;
94 incident.contract_diff_id = contract_diff_id;
95 incident.before_sample = before_sample;
96 incident.after_sample = after_sample;
97
98 self.store.store(incident.clone()).await;
99
100 self.trigger_webhooks("incident.created", &incident).await;
102
103 incident
104 }
105
106 pub async fn get_incident(&self, id: &str) -> Option<DriftIncident> {
108 self.store.get(id).await
109 }
110
111 pub async fn update_incident(&self, incident: DriftIncident) {
113 self.store.update(incident).await;
114 }
115
116 pub async fn acknowledge_incident(&self, id: &str) -> Option<DriftIncident> {
118 let mut incident = self.store.get(id).await?;
119 incident.acknowledge();
120 self.store.update(incident.clone()).await;
121 Some(incident)
122 }
123
124 pub async fn resolve_incident(&self, id: &str) -> Option<DriftIncident> {
126 let mut incident = self.store.get(id).await?;
127 incident.resolve();
128 self.store.update(incident.clone()).await;
129 Some(incident)
130 }
131
132 pub async fn close_incident(&self, id: &str) -> Option<DriftIncident> {
134 let mut incident = self.store.get(id).await?;
135 incident.close();
136 self.store.update(incident.clone()).await;
137 Some(incident)
138 }
139
140 pub async fn link_external_ticket(
142 &self,
143 id: &str,
144 ticket_id: String,
145 ticket_url: Option<String>,
146 ) -> Option<DriftIncident> {
147 let mut incident = self.store.get(id).await?;
148 incident.link_external_ticket(ticket_id, ticket_url);
149 self.store.update(incident.clone()).await;
150 Some(incident)
151 }
152
153 pub async fn query_incidents(&self, query: IncidentQuery) -> Vec<DriftIncident> {
155 self.store.query(query).await
156 }
157
158 pub async fn get_open_incidents(&self) -> Vec<DriftIncident> {
160 self.store.get_by_status(IncidentStatus::Open).await
161 }
162
163 pub async fn get_statistics(&self) -> HashMap<IncidentStatus, usize> {
165 self.store.count_by_status().await
166 }
167
168 pub async fn cleanup_old_incidents(&self, retention_days: u32) {
170 self.store.cleanup_old_resolved(retention_days).await;
171 }
172
173 async fn trigger_webhooks(&self, event_type: &str, incident: &DriftIncident) {
175 use crate::incidents::integrations::send_webhook;
176 use serde_json::json;
177
178 for webhook in &self.webhook_configs {
179 if !webhook.enabled {
180 continue;
181 }
182
183 if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
185 continue;
186 }
187
188 let payload = if webhook.url.contains("slack.com")
190 || webhook.url.contains("hooks.slack.com")
191 {
192 use crate::incidents::slack_formatter::format_slack_webhook;
194 format_slack_webhook(incident)
195 } else if webhook.url.contains("jira") || webhook.headers.contains_key("X-Jira-Project")
196 {
197 use crate::incidents::jira_formatter::format_jira_webhook;
199 format_jira_webhook(incident)
200 } else {
201 json!({
203 "event": event_type,
204 "incident": {
205 "id": incident.id,
206 "endpoint": incident.endpoint,
207 "method": incident.method,
208 "type": format!("{:?}", incident.incident_type),
209 "severity": format!("{:?}", incident.severity),
210 "status": format!("{:?}", incident.status),
211 "details": incident.details,
212 "created_at": incident.created_at,
213 }
214 })
215 };
216
217 let webhook_clone = webhook.clone();
219 tokio::spawn(async move {
220 if let Err(e) = send_webhook(&webhook_clone, &payload).await {
221 tracing::warn!("Failed to send webhook to {}: {}", webhook_clone.url, e);
222 }
223 });
224 }
225 }
226}