use crate::incidents::store::IncidentStore;
use crate::incidents::types::{
DriftIncident, IncidentQuery, IncidentSeverity, IncidentStatus, IncidentType,
};
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct IncidentManager {
store: Arc<IncidentStore>,
webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
}
impl IncidentManager {
pub fn new(store: Arc<IncidentStore>) -> Self {
Self {
store,
webhook_configs: Vec::new(),
}
}
pub fn new_with_webhooks(
store: Arc<IncidentStore>,
webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
) -> Self {
Self {
store,
webhook_configs,
}
}
pub fn add_webhook(&mut self, config: crate::incidents::integrations::WebhookConfig) {
self.webhook_configs.push(config);
}
#[allow(clippy::too_many_arguments)]
pub async fn create_incident(
&self,
endpoint: String,
method: String,
incident_type: IncidentType,
severity: IncidentSeverity,
details: serde_json::Value,
budget_id: Option<String>,
workspace_id: Option<String>,
) -> DriftIncident {
self.create_incident_with_samples(
endpoint,
method,
incident_type,
severity,
details,
budget_id,
workspace_id,
None, None, None, None, None, None, None, )
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn create_incident_with_samples(
&self,
endpoint: String,
method: String,
incident_type: IncidentType,
severity: IncidentSeverity,
details: serde_json::Value,
budget_id: Option<String>,
workspace_id: Option<String>,
sync_cycle_id: Option<String>,
contract_diff_id: Option<String>,
before_sample: Option<serde_json::Value>,
after_sample: Option<serde_json::Value>,
fitness_test_results: Option<Vec<crate::contract_drift::fitness::FitnessTestResult>>,
affected_consumers: Option<crate::contract_drift::consumer_mapping::ConsumerImpact>,
protocol: Option<crate::protocol_abstraction::Protocol>,
) -> DriftIncident {
let id = Uuid::new_v4().to_string();
let mut incident =
DriftIncident::new(id, endpoint, method, incident_type, severity, details);
incident.budget_id = budget_id;
incident.workspace_id = workspace_id;
incident.sync_cycle_id = sync_cycle_id;
incident.contract_diff_id = contract_diff_id;
incident.before_sample = before_sample;
incident.after_sample = after_sample;
incident.fitness_test_results = fitness_test_results.unwrap_or_default();
incident.affected_consumers = affected_consumers;
incident.protocol = protocol;
self.store.store(incident.clone()).await;
self.trigger_webhooks("incident.created", &incident).await;
incident
}
pub async fn get_incident(&self, id: &str) -> Option<DriftIncident> {
self.store.get(id).await
}
pub async fn update_incident(&self, incident: DriftIncident) {
self.store.update(incident).await;
}
pub async fn acknowledge_incident(&self, id: &str) -> Option<DriftIncident> {
let mut incident = self.store.get(id).await?;
incident.acknowledge();
self.store.update(incident.clone()).await;
Some(incident)
}
pub async fn resolve_incident(&self, id: &str) -> Option<DriftIncident> {
let mut incident = self.store.get(id).await?;
incident.resolve();
self.store.update(incident.clone()).await;
Some(incident)
}
pub async fn close_incident(&self, id: &str) -> Option<DriftIncident> {
let mut incident = self.store.get(id).await?;
incident.close();
self.store.update(incident.clone()).await;
Some(incident)
}
pub async fn link_external_ticket(
&self,
id: &str,
ticket_id: String,
ticket_url: Option<String>,
) -> Option<DriftIncident> {
let mut incident = self.store.get(id).await?;
incident.link_external_ticket(ticket_id, ticket_url);
self.store.update(incident.clone()).await;
Some(incident)
}
pub async fn query_incidents(&self, query: IncidentQuery) -> Vec<DriftIncident> {
self.store.query(query).await
}
pub async fn get_open_incidents(&self) -> Vec<DriftIncident> {
self.store.get_by_status(IncidentStatus::Open).await
}
pub async fn get_statistics(&self) -> HashMap<IncidentStatus, usize> {
self.store.count_by_status().await
}
pub async fn cleanup_old_incidents(&self, retention_days: u32) {
self.store.cleanup_old_resolved(retention_days).await;
}
async fn trigger_webhooks(&self, event_type: &str, incident: &DriftIncident) {
use crate::incidents::integrations::send_webhook;
use serde_json::json;
for webhook in &self.webhook_configs {
if !webhook.enabled {
continue;
}
if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
continue;
}
let payload = if webhook.url.contains("slack.com")
|| webhook.url.contains("hooks.slack.com")
{
use crate::incidents::slack_formatter::format_slack_webhook;
format_slack_webhook(incident)
} else if webhook.url.contains("jira") || webhook.headers.contains_key("X-Jira-Project")
{
use crate::incidents::jira_formatter::format_jira_webhook;
format_jira_webhook(incident)
} else {
json!({
"event": event_type,
"incident": {
"id": incident.id,
"endpoint": incident.endpoint,
"method": incident.method,
"type": format!("{:?}", incident.incident_type),
"severity": format!("{:?}", incident.severity),
"status": format!("{:?}", incident.status),
"details": incident.details,
"created_at": incident.created_at,
}
})
};
let webhook_clone = webhook.clone();
tokio::spawn(async move {
if let Err(e) = send_webhook(&webhook_clone, &payload).await {
tracing::warn!("Failed to send webhook to {}: {}", webhook_clone.url, e);
}
});
}
}
}