use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use lazy_static::lazy_static;
use log::{error, warn, info};
use serde::{Deserialize, Serialize};
use crate::monitoring::blockchain_metrics;
lazy_static! {
static ref ALERTS: Mutex<BlockchainAlerts> = Mutex::new(BlockchainAlerts::new());
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum AlertSeverity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlertStatus {
Active,
Acknowledged,
Resolved,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alert {
id: String,
name: String,
description: String,
severity: AlertSeverity,
status: AlertStatus,
triggered_at: u64,
resolved_at: Option<u64>,
metric_name: String,
threshold: f64,
current_value: f64,
comparison_operator: String,
}
#[derive(Debug)]
pub struct BlockchainAlerts {
active_alerts: HashMap<String, Alert>,
alert_definitions: HashMap<String, AlertDefinition>,
last_check: Instant,
alert_history: Vec<Alert>,
}
#[derive(Debug, Clone)]
struct AlertDefinition {
name: String,
description_template: String,
metric_name: String,
threshold: f64,
comparison_operator: String,
severity: AlertSeverity,
duration_threshold_seconds: u64,
consecutive_readings: u32,
current_consecutive: u32,
first_exceeded_at: Option<Instant>,
}
impl BlockchainAlerts {
pub fn new() -> Self {
let mut alerts = Self {
active_alerts: HashMap::new(),
alert_definitions: HashMap::new(),
last_check: Instant::now(),
alert_history: Vec::new(),
};
alerts.define_default_alerts();
alerts
}
fn define_default_alerts(&mut self) {
self.add_alert_definition(
"segwit_adoption_low",
"SegWit Adoption Below Threshold",
"SegWit adoption percentage is below {threshold}% (currently {value}%)",
"segwit_percentage",
80.0,
"<",
AlertSeverity::Warning,
300, 3,
);
self.add_alert_definition(
"taproot_adoption_low",
"Taproot Adoption Below Threshold",
"Taproot adoption percentage is below {threshold}% (currently {value}%)",
"taproot_percentage",
5.0,
"<",
AlertSeverity::Warning,
300, 3,
);
self.add_alert_definition(
"high_error_rate",
"High Error Rate",
"Error rate is above {threshold}% (currently {value}%)",
"error_rates.connection_failure",
5.0,
">",
AlertSeverity::Error,
60, 2,
);
self.add_alert_definition(
"slow_block_propagation",
"Slow Block Propagation",
"Block propagation time is above {threshold}ms (currently {value}ms)",
"block_propagation_ms",
1000.0, ">",
AlertSeverity::Warning,
0, 1,
);
self.add_alert_definition(
"mempool_size_high",
"High Mempool Size",
"Mempool size is above {threshold}MB (currently {value}MB)",
"mempool_size_bytes",
50.0 * 1024.0 * 1024.0, ">",
AlertSeverity::Warning,
300, 3,
);
self.add_alert_definition(
"high_fee_rate",
"High Fee Rate",
"Average fee rate is above {threshold} sats/vB (currently {value} sats/vB)",
"avg_fee_rate_sats_per_vb",
100.0,
">",
AlertSeverity::Warning,
300, 2,
);
}
fn add_alert_definition(
&mut self,
id: &str,
name: &str,
description_template: &str,
metric_name: &str,
threshold: f64,
comparison_operator: &str,
severity: AlertSeverity,
duration_threshold_seconds: u64,
consecutive_readings: u32,
) {
self.alert_definitions.insert(
id.to_string(),
AlertDefinition {
name: name.to_string(),
description_template: description_template.to_string(),
metric_name: metric_name.to_string(),
threshold,
comparison_operator: comparison_operator.to_string(),
severity,
duration_threshold_seconds,
consecutive_readings,
current_consecutive: 0,
first_exceeded_at: None,
},
);
}
pub fn check_alerts(&mut self) {
self.last_check = Instant::now();
let metrics_json = blockchain_metrics::get_metrics_json();
let mut checks = Vec::new();
for (alert_id, definition) in &self.alert_definitions {
let metric_value = self.get_metric_value(&metrics_json, &definition.metric_name);
match metric_value {
Some(value) => {
let threshold_exceeded = self.is_threshold_exceeded(
value,
definition.threshold,
&definition.comparison_operator,
);
checks.push((alert_id.clone(), value, threshold_exceeded));
},
None => {
checks.push((alert_id.clone(), 0.0, false));
}
}
}
for (alert_id, value, threshold_exceeded) in checks {
let definition = match self.alert_definitions.get_mut(&alert_id) {
Some(def) => def,
None => continue,
};
if threshold_exceeded {
definition.current_consecutive += 1;
if definition.first_exceeded_at.is_none() {
definition.first_exceeded_at = Some(Instant::now());
}
let duration_exceeded = definition
.first_exceeded_at
.map(|time| time.elapsed().as_secs() >= definition.duration_threshold_seconds)
.unwrap_or(false);
if duration_exceeded && definition.current_consecutive >= definition.consecutive_readings {
let description = definition.description_template
.replace("{threshold}", &definition.threshold.to_string())
.replace("{value}", &value.to_string());
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let alert = Alert {
id: alert_id.clone(),
name: definition.name.clone(),
description: description.clone(),
severity: definition.severity,
status: AlertStatus::Active,
triggered_at: now,
resolved_at: None,
metric_name: definition.metric_name.clone(),
threshold: definition.threshold,
current_value: value,
comparison_operator: definition.comparison_operator.clone(),
};
self.active_alerts.insert(alert_id.clone(), alert.clone());
self.alert_history.push(alert);
if self.alert_history.len() > 100 {
self.alert_history.remove(0);
}
match definition.severity {
AlertSeverity::Info => info!("ALERT: {}", description),
AlertSeverity::Warning => warn!("ALERT: {}", description),
AlertSeverity::Error => error!("ALERT: {}", description),
AlertSeverity::Critical => error!("CRITICAL ALERT: {}", description),
}
}
} else {
definition.current_consecutive = 0;
definition.first_exceeded_at = None;
if let Some(mut alert) = self.active_alerts.remove(&alert_id) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
alert.status = AlertStatus::Resolved;
alert.resolved_at = Some(now);
let description = alert.description.clone();
self.alert_history.push(alert);
if self.alert_history.len() > 100 {
self.alert_history.remove(0);
}
info!("RESOLVED: {}", description);
}
}
}
}
fn get_metric_value(&self, metrics: &serde_json::Value, path: &str) -> Option<f64> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = metrics;
for part in &parts[0..parts.len() - 1] {
current = ¤t[*part];
if current.is_null() {
return None;
}
}
let last_part = parts.last()?;
let value = ¤t[*last_part];
if value.is_f64() {
Some(value.as_f64().unwrap())
} else if value.is_i64() {
Some(value.as_i64().unwrap() as f64)
} else if value.is_u64() {
Some(value.as_u64().unwrap() as f64)
} else {
None
}
}
fn is_threshold_exceeded(&self, value: f64, threshold: f64, operator: &str) -> bool {
match operator {
">" => value > threshold,
"<" => value < threshold,
">=" => value >= threshold,
"<=" => value <= threshold,
"==" => (value - threshold).abs() < f64::EPSILON,
"!=" => (value - threshold).abs() > f64::EPSILON,
_ => false,
}
}
pub fn acknowledge_alert(&mut self, alert_id: &str) {
if let Some(alert) = self.active_alerts.get_mut(alert_id) {
alert.status = AlertStatus::Acknowledged;
info!("Alert '{}' acknowledged", alert_id);
}
}
pub fn get_active_alerts(&self) -> Vec<&Alert> {
self.active_alerts.values().collect()
}
pub fn get_alert_history(&self) -> &Vec<Alert> {
&self.alert_history
}
}
pub fn check_alerts() {
let mut alerts: std::sync::MutexGuard<BlockchainAlerts> = ALERTS.lock().unwrap();
alerts.check_alerts();
}
pub fn acknowledge_alert(alert_id: &str) {
let mut alerts: std::sync::MutexGuard<BlockchainAlerts> = ALERTS.lock().unwrap();
let _ = alerts.acknowledge_alert(alert_id);
}
pub fn get_active_alerts() -> Vec<Alert> {
let alerts: std::sync::MutexGuard<BlockchainAlerts> = ALERTS.lock().unwrap();
alerts.get_active_alerts().iter().map(|a| (*a).clone()).collect()
}
pub fn get_alert_history() -> Vec<Alert> {
let alerts: std::sync::MutexGuard<BlockchainAlerts> = ALERTS.lock().unwrap();
alerts.get_alert_history().clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blockchain_alerts() {
let alerts = get_active_alerts();
assert!(alerts.is_empty(), "Should have no active alerts initially");
}
}