feat(monitor): add alert pairing logic and display incident names

- Add pair_alerts() to pair consecutive Alerting→Normal state transitions
- Add parse_labels_from_text() to extract labels from alert text field
- Update ProcessedAlert struct with alertTime, resolveTime, isResolved, labels
- Display incident name in alert list items instead of incident ID
- Show parsed labels in alert row (excluding alertname)
This commit is contained in:
Syahdan 2025-12-29 06:07:13 +07:00
parent ea1c9105a7
commit 22aee93fb3
6 changed files with 165 additions and 129 deletions

View file

@ -1,19 +1,17 @@
use crate::{calculate_kpis, AppError, GrafanaAlert, Incident, KpiMetrics, ProcessedAlert};
use crate::{calculate_kpis, pair_alerts, AppError, GrafanaAlert, Incident, KpiMetrics, ProcessedAlert};
use std::fs;
#[tauri::command]
pub async fn load_alerts_from_file(path: String) -> Result<Vec<ProcessedAlert>, AppError> {
let contents = fs::read_to_string(&path)?;
let raw_alerts: Vec<GrafanaAlert> = serde_json::from_str(&contents)?;
let processed: Vec<ProcessedAlert> = raw_alerts.into_iter().map(ProcessedAlert::from).collect();
Ok(processed)
Ok(pair_alerts(raw_alerts))
}
#[tauri::command]
pub fn process_alerts_json(json_content: String) -> Result<Vec<ProcessedAlert>, AppError> {
let raw_alerts: Vec<GrafanaAlert> = serde_json::from_str(&json_content)?;
let processed: Vec<ProcessedAlert> = raw_alerts.into_iter().map(ProcessedAlert::from).collect();
Ok(processed)
Ok(pair_alerts(raw_alerts))
}
#[tauri::command]

View file

@ -98,44 +98,76 @@ pub struct ProcessedAlert {
pub alert_id: u32,
#[serde(default)]
pub alert_name: Option<String>,
pub new_state: String,
pub prev_state: String,
pub time: u64,
pub time_end: u64,
pub alert_time: u64,
pub resolve_time: Option<u64>,
pub text: String,
pub duration_ms: u64,
pub is_firing: bool,
pub is_resolved: bool,
pub is_invalid: bool,
pub attached_incident_id: Option<String>,
pub values: HashMap<String, f64>,
}
impl From<GrafanaAlert> for ProcessedAlert {
fn from(alert: GrafanaAlert) -> Self {
let duration_ms = if alert.time_end > alert.time {
alert.time_end - alert.time
} else {
0
};
pub fn pair_alerts(raw_alerts: Vec<GrafanaAlert>) -> Vec<ProcessedAlert> {
use std::collections::BTreeMap;
let is_firing = alert.new_state == "Alerting" || alert.new_state == "Firing";
let mut by_alert_id: BTreeMap<u32, Vec<GrafanaAlert>> = BTreeMap::new();
for alert in raw_alerts {
by_alert_id.entry(alert.alert_id).or_default().push(alert);
}
ProcessedAlert {
id: alert.id,
alert_id: alert.alert_id,
alert_name: alert.alert_name,
new_state: alert.new_state,
prev_state: alert.prev_state,
time: alert.time,
time_end: alert.time_end,
text: alert.text,
duration_ms,
is_firing,
is_invalid: false,
attached_incident_id: None,
values: alert.data.map(|d| d.values).unwrap_or_default(),
let mut result = Vec::new();
for (_alert_id, mut alerts) in by_alert_id {
alerts.sort_by_key(|a| a.time);
let mut i = 0;
while i < alerts.len() {
let current = &alerts[i];
if current.new_state == "Alerting" {
if i + 1 < alerts.len() && alerts[i + 1].new_state == "Normal" {
let resolve = &alerts[i + 1];
let duration_ms = resolve.time.saturating_sub(current.time);
result.push(ProcessedAlert {
id: current.id,
alert_id: current.alert_id,
alert_name: current.alert_name.clone(),
alert_time: current.time,
resolve_time: Some(resolve.time),
text: current.text.clone(),
duration_ms,
is_resolved: true,
is_invalid: false,
attached_incident_id: None,
values: current.data.clone().map(|d| d.values).unwrap_or_default(),
});
i += 2;
} else {
result.push(ProcessedAlert {
id: current.id,
alert_id: current.alert_id,
alert_name: current.alert_name.clone(),
alert_time: current.time,
resolve_time: None,
text: current.text.clone(),
duration_ms: 0,
is_resolved: false,
is_invalid: false,
attached_incident_id: None,
values: current.data.clone().map(|d| d.values).unwrap_or_default(),
});
i += 1;
}
} else {
i += 1;
}
}
}
result.sort_by(|a, b| b.alert_time.cmp(&a.alert_time));
result
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -176,19 +208,16 @@ pub fn calculate_kpis(alerts: &[ProcessedAlert], incidents: &[Incident]) -> KpiM
let overall_downtime_ms: u64 = alerts
.iter()
.filter(|a| a.is_firing && !a.is_invalid)
.filter(|a| !a.is_invalid)
.map(|a| a.duration_ms)
.sum();
let overall_downtime_formatted = format_duration(overall_downtime_ms);
let total_firing_alerts = alerts.iter().filter(|a| a.is_firing).count();
let invalid_alerts = alerts
.iter()
.filter(|a| a.is_firing && a.is_invalid)
.count();
let invalid_alert_ratio = if total_firing_alerts > 0 {
(invalid_alerts as f64 / total_firing_alerts as f64) * 100.0
let total_alerts = alerts.len();
let invalid_alerts = alerts.iter().filter(|a| a.is_invalid).count();
let invalid_alert_ratio = if total_alerts > 0 {
(invalid_alerts as f64 / total_alerts as f64) * 100.0
} else {
0.0
};
@ -200,7 +229,7 @@ pub fn calculate_kpis(alerts: &[ProcessedAlert], incidents: &[Incident]) -> KpiM
overall_downtime_ms,
overall_downtime_formatted,
invalid_alert_ratio,
total_firing_alerts,
total_firing_alerts: total_alerts,
invalid_alerts,
}
}