use crate::Decimal;
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
static ALERT_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum AlertSeverity {
Info,
Warning,
Error,
Critical,
}
impl fmt::Display for AlertSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => write!(f, "INFO"),
Self::Warning => write!(f, "WARNING"),
Self::Error => write!(f, "ERROR"),
Self::Critical => write!(f, "CRITICAL"),
}
}
}
impl AlertSeverity {
#[must_use]
pub fn all() -> &'static [AlertSeverity] {
&[Self::Info, Self::Warning, Self::Error, Self::Critical]
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum AlertType {
LargeLoss {
amount: Decimal,
threshold: Decimal,
},
DailyLossLimit {
current: Decimal,
limit: Decimal,
pct: Decimal,
},
PositionLimit {
current: Decimal,
limit: Decimal,
pct: Decimal,
},
MaxDrawdown {
drawdown: Decimal,
threshold: Decimal,
},
ConnectivityIssue {
exchange: String,
error: String,
},
HighLatency {
metric: String,
latency_ms: u64,
threshold_ms: u64,
},
StrategyError {
message: String,
},
CircuitBreakerTriggered {
reason: String,
},
OrderRejected {
reason: String,
order_details: String,
},
MarketCondition {
condition: String,
details: String,
},
Custom {
name: String,
message: String,
},
}
impl AlertType {
#[must_use]
pub fn type_key(&self) -> String {
match self {
Self::LargeLoss { .. } => "large_loss".to_string(),
Self::DailyLossLimit { .. } => "daily_loss_limit".to_string(),
Self::PositionLimit { .. } => "position_limit".to_string(),
Self::MaxDrawdown { .. } => "max_drawdown".to_string(),
Self::ConnectivityIssue { exchange, .. } => format!("connectivity_{}", exchange),
Self::HighLatency { metric, .. } => format!("high_latency_{}", metric),
Self::StrategyError { .. } => "strategy_error".to_string(),
Self::CircuitBreakerTriggered { .. } => "circuit_breaker".to_string(),
Self::OrderRejected { .. } => "order_rejected".to_string(),
Self::MarketCondition { condition, .. } => format!("market_{}", condition),
Self::Custom { name, .. } => format!("custom_{}", name),
}
}
#[must_use]
pub fn default_message(&self) -> String {
match self {
Self::LargeLoss { amount, threshold } => {
format!("Large loss detected: {} (threshold: {})", amount, threshold)
}
Self::DailyLossLimit {
current,
limit,
pct,
} => {
format!(
"Daily loss limit: {} / {} ({:.1}%)",
current,
limit,
pct * Decimal::from(100)
)
}
Self::PositionLimit {
current,
limit,
pct,
} => {
format!(
"Position limit: {} / {} ({:.1}%)",
current,
limit,
pct * Decimal::from(100)
)
}
Self::MaxDrawdown {
drawdown,
threshold,
} => {
format!(
"Max drawdown reached: {:.2}% (threshold: {:.2}%)",
drawdown * Decimal::from(100),
threshold * Decimal::from(100)
)
}
Self::ConnectivityIssue { exchange, error } => {
format!("Connectivity issue with {}: {}", exchange, error)
}
Self::HighLatency {
metric,
latency_ms,
threshold_ms,
} => {
format!(
"High latency on {}: {}ms (threshold: {}ms)",
metric, latency_ms, threshold_ms
)
}
Self::StrategyError { message } => {
format!("Strategy error: {}", message)
}
Self::CircuitBreakerTriggered { reason } => {
format!("Circuit breaker triggered: {}", reason)
}
Self::OrderRejected {
reason,
order_details,
} => {
format!("Order rejected: {} ({})", reason, order_details)
}
Self::MarketCondition { condition, details } => {
format!("Market condition {}: {}", condition, details)
}
Self::Custom { name, message } => {
format!("{}: {}", name, message)
}
}
}
}
impl fmt::Display for AlertType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.default_message())
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Alert {
pub id: String,
pub alert_type: AlertType,
pub severity: AlertSeverity,
pub message: String,
pub timestamp: u64,
pub acknowledged: bool,
}
impl Alert {
#[must_use]
pub fn new(
alert_type: AlertType,
severity: AlertSeverity,
message: String,
timestamp: u64,
) -> Self {
let counter = ALERT_COUNTER.fetch_add(1, Ordering::Relaxed);
let id = format!("{}-{}-{}", alert_type.type_key(), timestamp, counter);
Self {
id,
alert_type,
severity,
message,
timestamp,
acknowledged: false,
}
}
#[must_use]
pub fn with_default_message(
alert_type: AlertType,
severity: AlertSeverity,
timestamp: u64,
) -> Self {
let message = alert_type.default_message();
Self::new(alert_type, severity, message, timestamp)
}
pub fn acknowledge(&mut self) {
self.acknowledged = true;
}
#[must_use]
pub fn is_critical(&self) -> bool {
self.severity == AlertSeverity::Critical
}
#[must_use]
pub fn is_error_or_higher(&self) -> bool {
self.severity >= AlertSeverity::Error
}
}
impl fmt::Display for Alert {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {} - {}",
self.severity,
self.alert_type.type_key(),
self.message
)
}
}
pub trait AlertHandler: Send + Sync {
fn handle(&self, alert: &Alert);
fn accepts_severity(&self, _severity: AlertSeverity) -> bool {
true
}
fn name(&self) -> &str {
"AlertHandler"
}
}
#[derive(Debug)]
pub struct LogAlertHandler {
min_severity: AlertSeverity,
}
impl LogAlertHandler {
#[must_use]
pub fn new(min_severity: AlertSeverity) -> Self {
Self { min_severity }
}
#[must_use]
pub fn all() -> Self {
Self::new(AlertSeverity::Info)
}
}
impl Default for LogAlertHandler {
fn default() -> Self {
Self::new(AlertSeverity::Info)
}
}
impl AlertHandler for LogAlertHandler {
fn handle(&self, alert: &Alert) {
match alert.severity {
AlertSeverity::Info => {
eprintln!("[INFO] Alert {}: {}", alert.id, alert.message);
}
AlertSeverity::Warning => {
eprintln!("[WARN] Alert {}: {}", alert.id, alert.message);
}
AlertSeverity::Error => {
eprintln!("[ERROR] Alert {}: {}", alert.id, alert.message);
}
AlertSeverity::Critical => {
eprintln!("[CRITICAL] Alert {}: {}", alert.id, alert.message);
}
}
}
fn accepts_severity(&self, severity: AlertSeverity) -> bool {
severity >= self.min_severity
}
fn name(&self) -> &str {
"LogAlertHandler"
}
}
pub struct CallbackAlertHandler<F>
where
F: Fn(&Alert) + Send + Sync,
{
min_severity: AlertSeverity,
callback: F,
}
impl<F> CallbackAlertHandler<F>
where
F: Fn(&Alert) + Send + Sync,
{
pub fn new(min_severity: AlertSeverity, callback: F) -> Self {
Self {
min_severity,
callback,
}
}
}
impl<F> AlertHandler for CallbackAlertHandler<F>
where
F: Fn(&Alert) + Send + Sync,
{
fn handle(&self, alert: &Alert) {
(self.callback)(alert);
}
fn accepts_severity(&self, severity: AlertSeverity) -> bool {
severity >= self.min_severity
}
fn name(&self) -> &str {
"CallbackAlertHandler"
}
}
impl<F> fmt::Debug for CallbackAlertHandler<F>
where
F: Fn(&Alert) + Send + Sync,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CallbackAlertHandler")
.field("min_severity", &self.min_severity)
.finish()
}
}
#[derive(Debug)]
pub struct CollectingAlertHandler {
min_severity: AlertSeverity,
alerts: std::sync::Mutex<Vec<Alert>>,
}
impl CollectingAlertHandler {
#[must_use]
pub fn new(min_severity: AlertSeverity) -> Self {
Self {
min_severity,
alerts: std::sync::Mutex::new(Vec::new()),
}
}
#[must_use]
pub fn alerts(&self) -> Vec<Alert> {
self.alerts.lock().unwrap().clone()
}
#[must_use]
pub fn count(&self) -> usize {
self.alerts.lock().unwrap().len()
}
pub fn clear(&self) {
self.alerts.lock().unwrap().clear();
}
}
impl AlertHandler for CollectingAlertHandler {
fn handle(&self, alert: &Alert) {
self.alerts.lock().unwrap().push(alert.clone());
}
fn accepts_severity(&self, severity: AlertSeverity) -> bool {
severity >= self.min_severity
}
fn name(&self) -> &str {
"CollectingAlertHandler"
}
}
pub struct AlertManager {
handlers: Vec<Box<dyn AlertHandler>>,
alert_history: VecDeque<Alert>,
max_history: usize,
dedup_window_ms: u64,
recent_alerts: HashMap<String, u64>,
}
impl AlertManager {
#[must_use]
pub fn new(max_history: usize, dedup_window_ms: u64) -> Self {
Self {
handlers: Vec::new(),
alert_history: VecDeque::with_capacity(max_history),
max_history,
dedup_window_ms,
recent_alerts: HashMap::new(),
}
}
#[must_use]
pub fn with_defaults() -> Self {
Self::new(1000, 60_000)
}
pub fn add_handler(&mut self, handler: Box<dyn AlertHandler>) {
self.handlers.push(handler);
}
pub fn alert(&mut self, alert_type: AlertType, severity: AlertSeverity, timestamp: u64) {
let message = alert_type.default_message();
self.alert_with_message(alert_type, severity, message, timestamp);
}
pub fn alert_with_message(
&mut self,
alert_type: AlertType,
severity: AlertSeverity,
message: String,
timestamp: u64,
) {
let type_key = alert_type.type_key();
if let Some(&last_time) = self.recent_alerts.get(&type_key)
&& timestamp.saturating_sub(last_time) < self.dedup_window_ms
{
return;
}
let alert = Alert::new(alert_type, severity, message, timestamp);
for handler in &self.handlers {
if handler.accepts_severity(severity) {
handler.handle(&alert);
}
}
self.recent_alerts.insert(type_key, timestamp);
self.alert_history.push_back(alert);
while self.alert_history.len() > self.max_history {
self.alert_history.pop_front();
}
}
#[must_use]
pub fn get_recent_alerts(&self, count: usize) -> Vec<&Alert> {
self.alert_history.iter().rev().take(count).collect()
}
#[must_use]
pub fn get_alerts_by_severity(&self, severity: AlertSeverity) -> Vec<&Alert> {
self.alert_history
.iter()
.filter(|a| a.severity == severity)
.collect()
}
#[must_use]
pub fn get_alerts_at_or_above(&self, severity: AlertSeverity) -> Vec<&Alert> {
self.alert_history
.iter()
.filter(|a| a.severity >= severity)
.collect()
}
pub fn acknowledge(&mut self, alert_id: &str) -> bool {
for alert in &mut self.alert_history {
if alert.id == alert_id {
alert.acknowledged = true;
return true;
}
}
false
}
pub fn acknowledge_all(&mut self) {
for alert in &mut self.alert_history {
alert.acknowledged = true;
}
}
#[must_use]
pub fn unacknowledged_count(&self) -> usize {
self.alert_history
.iter()
.filter(|a| !a.acknowledged)
.count()
}
#[must_use]
pub fn get_unacknowledged(&self) -> Vec<&Alert> {
self.alert_history
.iter()
.filter(|a| !a.acknowledged)
.collect()
}
pub fn cleanup(&mut self, max_age_ms: u64, current_time: u64) {
let cutoff = current_time.saturating_sub(max_age_ms);
self.alert_history.retain(|a| a.timestamp >= cutoff);
self.recent_alerts.retain(|_, &mut ts| ts >= cutoff);
}
#[must_use]
pub fn history_count(&self) -> usize {
self.alert_history.len()
}
#[must_use]
pub fn handler_count(&self) -> usize {
self.handlers.len()
}
pub fn clear_history(&mut self) {
self.alert_history.clear();
self.recent_alerts.clear();
}
}
impl Default for AlertManager {
fn default() -> Self {
Self::with_defaults()
}
}
impl fmt::Debug for AlertManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AlertManager")
.field("handler_count", &self.handlers.len())
.field("history_count", &self.alert_history.len())
.field("max_history", &self.max_history)
.field("dedup_window_ms", &self.dedup_window_ms)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dec;
use std::sync::Arc;
#[test]
fn test_severity_ordering() {
assert!(AlertSeverity::Info < AlertSeverity::Warning);
assert!(AlertSeverity::Warning < AlertSeverity::Error);
assert!(AlertSeverity::Error < AlertSeverity::Critical);
}
#[test]
fn test_severity_display() {
assert_eq!(AlertSeverity::Info.to_string(), "INFO");
assert_eq!(AlertSeverity::Warning.to_string(), "WARNING");
assert_eq!(AlertSeverity::Error.to_string(), "ERROR");
assert_eq!(AlertSeverity::Critical.to_string(), "CRITICAL");
}
#[test]
fn test_severity_all() {
let all = AlertSeverity::all();
assert_eq!(all.len(), 4);
assert_eq!(all[0], AlertSeverity::Info);
assert_eq!(all[3], AlertSeverity::Critical);
}
#[test]
fn test_alert_type_key() {
let alert = AlertType::LargeLoss {
amount: dec!(100.0),
threshold: dec!(50.0),
};
assert_eq!(alert.type_key(), "large_loss");
let alert = AlertType::ConnectivityIssue {
exchange: "binance".to_string(),
error: "timeout".to_string(),
};
assert_eq!(alert.type_key(), "connectivity_binance");
}
#[test]
fn test_alert_type_default_message() {
let alert = AlertType::LargeLoss {
amount: dec!(100.0),
threshold: dec!(50.0),
};
let msg = alert.default_message();
assert!(msg.contains("100"));
assert!(msg.contains("50"));
}
#[test]
fn test_alert_type_display() {
let alert = AlertType::StrategyError {
message: "test error".to_string(),
};
let display = format!("{}", alert);
assert!(display.contains("test error"));
}
#[test]
fn test_alert_new() {
let alert = Alert::new(
AlertType::StrategyError {
message: "test".to_string(),
},
AlertSeverity::Error,
"Test message".to_string(),
1000,
);
assert!(alert.id.contains("strategy_error"));
assert_eq!(alert.severity, AlertSeverity::Error);
assert_eq!(alert.message, "Test message");
assert_eq!(alert.timestamp, 1000);
assert!(!alert.acknowledged);
}
#[test]
fn test_alert_with_default_message() {
let alert = Alert::with_default_message(
AlertType::CircuitBreakerTriggered {
reason: "max loss".to_string(),
},
AlertSeverity::Critical,
2000,
);
assert!(alert.message.contains("max loss"));
}
#[test]
fn test_alert_acknowledge() {
let mut alert = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
"Test".to_string(),
1000,
);
assert!(!alert.acknowledged);
alert.acknowledge();
assert!(alert.acknowledged);
}
#[test]
fn test_alert_is_critical() {
let critical = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Critical,
"Test".to_string(),
1000,
);
assert!(critical.is_critical());
let error = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Error,
"Test".to_string(),
1000,
);
assert!(!error.is_critical());
}
#[test]
fn test_alert_is_error_or_higher() {
let error = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Error,
"Test".to_string(),
1000,
);
assert!(error.is_error_or_higher());
let warning = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Warning,
"Test".to_string(),
1000,
);
assert!(!warning.is_error_or_higher());
}
#[test]
fn test_log_handler_accepts_severity() {
let handler = LogAlertHandler::new(AlertSeverity::Warning);
assert!(!handler.accepts_severity(AlertSeverity::Info));
assert!(handler.accepts_severity(AlertSeverity::Warning));
assert!(handler.accepts_severity(AlertSeverity::Error));
assert!(handler.accepts_severity(AlertSeverity::Critical));
}
#[test]
fn test_log_handler_all() {
let handler = LogAlertHandler::all();
assert!(handler.accepts_severity(AlertSeverity::Info));
}
#[test]
fn test_collecting_handler() {
let handler = CollectingAlertHandler::new(AlertSeverity::Info);
let alert = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
"Test".to_string(),
1000,
);
handler.handle(&alert);
handler.handle(&alert);
assert_eq!(handler.count(), 2);
let alerts = handler.alerts();
assert_eq!(alerts.len(), 2);
handler.clear();
assert_eq!(handler.count(), 0);
}
#[test]
fn test_manager_new() {
let manager = AlertManager::new(100, 60000);
assert_eq!(manager.history_count(), 0);
assert_eq!(manager.handler_count(), 0);
}
#[test]
fn test_manager_add_handler() {
let mut manager = AlertManager::new(100, 60000);
manager.add_handler(Box::new(LogAlertHandler::all()));
assert_eq!(manager.handler_count(), 1);
}
#[test]
fn test_manager_alert() {
let collector = Arc::new(CollectingAlertHandler::new(AlertSeverity::Info));
let mut manager = AlertManager::new(100, 60000);
struct ArcHandler(Arc<CollectingAlertHandler>);
impl AlertHandler for ArcHandler {
fn handle(&self, alert: &Alert) {
self.0.handle(alert);
}
}
manager.add_handler(Box::new(ArcHandler(Arc::clone(&collector))));
manager.alert(
AlertType::StrategyError {
message: "test".to_string(),
},
AlertSeverity::Error,
1000,
);
assert_eq!(manager.history_count(), 1);
assert_eq!(collector.count(), 1);
}
#[test]
fn test_manager_deduplication() {
let mut manager = AlertManager::new(100, 60000);
manager.alert(
AlertType::StrategyError {
message: "test".to_string(),
},
AlertSeverity::Error,
1000,
);
manager.alert(
AlertType::StrategyError {
message: "test2".to_string(),
},
AlertSeverity::Error,
2000,
);
assert_eq!(manager.history_count(), 1);
manager.alert(
AlertType::StrategyError {
message: "test3".to_string(),
},
AlertSeverity::Error,
70000,
);
assert_eq!(manager.history_count(), 2);
}
#[test]
fn test_manager_different_types_not_deduplicated() {
let mut manager = AlertManager::new(100, 60000);
manager.alert(
AlertType::StrategyError {
message: "test".to_string(),
},
AlertSeverity::Error,
1000,
);
manager.alert(
AlertType::CircuitBreakerTriggered {
reason: "test".to_string(),
},
AlertSeverity::Error,
1001,
);
assert_eq!(manager.history_count(), 2);
}
#[test]
fn test_manager_history_limit() {
let mut manager = AlertManager::new(3, 0);
for i in 0..5 {
manager.alert(
AlertType::Custom {
name: format!("test{}", i),
message: "msg".to_string(),
},
AlertSeverity::Info,
i as u64,
);
}
assert_eq!(manager.history_count(), 3);
}
#[test]
fn test_manager_get_recent_alerts() {
let mut manager = AlertManager::new(100, 0);
for i in 0..5 {
manager.alert(
AlertType::Custom {
name: format!("test{}", i),
message: "msg".to_string(),
},
AlertSeverity::Info,
i as u64,
);
}
let recent = manager.get_recent_alerts(2);
assert_eq!(recent.len(), 2);
}
#[test]
fn test_manager_get_alerts_by_severity() {
let mut manager = AlertManager::new(100, 0);
manager.alert(
AlertType::Custom {
name: "info".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
1,
);
manager.alert(
AlertType::Custom {
name: "error".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Error,
2,
);
manager.alert(
AlertType::Custom {
name: "error2".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Error,
3,
);
let errors = manager.get_alerts_by_severity(AlertSeverity::Error);
assert_eq!(errors.len(), 2);
}
#[test]
fn test_manager_acknowledge() {
let mut manager = AlertManager::new(100, 0);
manager.alert(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
1,
);
assert_eq!(manager.unacknowledged_count(), 1);
let alert_id = manager.get_recent_alerts(1)[0].id.clone();
assert!(manager.acknowledge(&alert_id));
assert_eq!(manager.unacknowledged_count(), 0);
}
#[test]
fn test_manager_acknowledge_all() {
let mut manager = AlertManager::new(100, 0);
for i in 0..3 {
manager.alert(
AlertType::Custom {
name: format!("test{}", i),
message: "msg".to_string(),
},
AlertSeverity::Info,
i as u64,
);
}
assert_eq!(manager.unacknowledged_count(), 3);
manager.acknowledge_all();
assert_eq!(manager.unacknowledged_count(), 0);
}
#[test]
fn test_manager_cleanup() {
let mut manager = AlertManager::new(100, 0);
manager.alert(
AlertType::Custom {
name: "old".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
1000,
);
manager.alert(
AlertType::Custom {
name: "new".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
5000,
);
assert_eq!(manager.history_count(), 2);
manager.cleanup(3000, 6000);
assert_eq!(manager.history_count(), 1);
}
#[test]
fn test_manager_clear_history() {
let mut manager = AlertManager::new(100, 0);
manager.alert(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
1,
);
assert_eq!(manager.history_count(), 1);
manager.clear_history();
assert_eq!(manager.history_count(), 0);
}
#[test]
fn test_manager_severity_filtering() {
let collector = Arc::new(CollectingAlertHandler::new(AlertSeverity::Error));
let mut manager = AlertManager::new(100, 0);
struct ArcHandler(Arc<CollectingAlertHandler>);
impl AlertHandler for ArcHandler {
fn handle(&self, alert: &Alert) {
self.0.handle(alert);
}
fn accepts_severity(&self, severity: AlertSeverity) -> bool {
self.0.accepts_severity(severity)
}
}
manager.add_handler(Box::new(ArcHandler(Arc::clone(&collector))));
manager.alert(
AlertType::Custom {
name: "info".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
1,
);
manager.alert(
AlertType::Custom {
name: "error".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Error,
2,
);
assert_eq!(manager.history_count(), 2);
assert_eq!(collector.count(), 1);
}
#[test]
fn test_callback_handler() {
use std::sync::atomic::{AtomicUsize, Ordering};
let count = Arc::new(AtomicUsize::new(0));
let count_clone = Arc::clone(&count);
let handler = CallbackAlertHandler::new(AlertSeverity::Info, move |_alert| {
count_clone.fetch_add(1, Ordering::Relaxed);
});
let alert = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
AlertSeverity::Info,
"Test".to_string(),
1000,
);
handler.handle(&alert);
handler.handle(&alert);
assert_eq!(count.load(Ordering::Relaxed), 2);
}
#[test]
fn test_alert_type_type_key_all_variants() {
assert_eq!(
AlertType::LargeLoss {
amount: dec!(100),
threshold: dec!(50)
}
.type_key(),
"large_loss"
);
assert_eq!(
AlertType::DailyLossLimit {
current: dec!(100),
limit: dec!(200),
pct: dec!(0.5)
}
.type_key(),
"daily_loss_limit"
);
assert_eq!(
AlertType::PositionLimit {
current: dec!(50),
limit: dec!(100),
pct: dec!(0.5)
}
.type_key(),
"position_limit"
);
assert_eq!(
AlertType::MaxDrawdown {
drawdown: dec!(0.1),
threshold: dec!(0.2)
}
.type_key(),
"max_drawdown"
);
assert_eq!(
AlertType::ConnectivityIssue {
exchange: "binance".to_string(),
error: "timeout".to_string()
}
.type_key(),
"connectivity_binance"
);
assert_eq!(
AlertType::HighLatency {
metric: "order".to_string(),
latency_ms: 500,
threshold_ms: 100
}
.type_key(),
"high_latency_order"
);
assert_eq!(
AlertType::StrategyError {
message: "error".to_string()
}
.type_key(),
"strategy_error"
);
assert_eq!(
AlertType::CircuitBreakerTriggered {
reason: "loss".to_string()
}
.type_key(),
"circuit_breaker"
);
assert_eq!(
AlertType::OrderRejected {
reason: "insufficient".to_string(),
order_details: "BUY 100".to_string()
}
.type_key(),
"order_rejected"
);
assert_eq!(
AlertType::MarketCondition {
condition: "volatile".to_string(),
details: "high vol".to_string()
}
.type_key(),
"market_volatile"
);
assert_eq!(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string()
}
.type_key(),
"custom_test"
);
}
#[test]
fn test_alert_type_default_message_all_variants() {
let msg = AlertType::LargeLoss {
amount: dec!(100),
threshold: dec!(50),
}
.default_message();
assert!(msg.contains("Large loss"));
let msg = AlertType::DailyLossLimit {
current: dec!(100),
limit: dec!(200),
pct: dec!(0.5),
}
.default_message();
assert!(msg.contains("Daily loss limit"));
let msg = AlertType::PositionLimit {
current: dec!(50),
limit: dec!(100),
pct: dec!(0.5),
}
.default_message();
assert!(msg.contains("Position limit"));
let msg = AlertType::MaxDrawdown {
drawdown: dec!(0.1),
threshold: dec!(0.2),
}
.default_message();
assert!(msg.contains("Max drawdown"));
let msg = AlertType::ConnectivityIssue {
exchange: "binance".to_string(),
error: "timeout".to_string(),
}
.default_message();
assert!(msg.contains("Connectivity issue"));
let msg = AlertType::HighLatency {
metric: "order".to_string(),
latency_ms: 500,
threshold_ms: 100,
}
.default_message();
assert!(msg.contains("High latency"));
let msg = AlertType::StrategyError {
message: "error".to_string(),
}
.default_message();
assert!(msg.contains("Strategy error"));
let msg = AlertType::CircuitBreakerTriggered {
reason: "loss".to_string(),
}
.default_message();
assert!(msg.contains("Circuit breaker"));
let msg = AlertType::OrderRejected {
reason: "insufficient".to_string(),
order_details: "BUY 100".to_string(),
}
.default_message();
assert!(msg.contains("Order rejected"));
let msg = AlertType::MarketCondition {
condition: "volatile".to_string(),
details: "high vol".to_string(),
}
.default_message();
assert!(msg.contains("Market condition"));
let msg = AlertType::Custom {
name: "test".to_string(),
message: "custom msg".to_string(),
}
.default_message();
assert!(msg.contains("test"));
}
#[test]
fn test_alert_display() {
let alert = Alert::new(
AlertType::StrategyError {
message: "test".to_string(),
},
AlertSeverity::Error,
"Test message".to_string(),
1000,
);
let display = format!("{}", alert);
assert!(display.contains("strategy_error"));
assert!(display.contains("Test message"));
}
#[test]
fn test_log_handler_all_severities() {
let handler = LogAlertHandler::all();
assert!(handler.accepts_severity(AlertSeverity::Info));
assert!(handler.accepts_severity(AlertSeverity::Warning));
assert!(handler.accepts_severity(AlertSeverity::Error));
assert!(handler.accepts_severity(AlertSeverity::Critical));
for severity in [
AlertSeverity::Info,
AlertSeverity::Warning,
AlertSeverity::Error,
AlertSeverity::Critical,
] {
let alert = Alert::new(
AlertType::Custom {
name: "test".to_string(),
message: "msg".to_string(),
},
severity,
"Test".to_string(),
1000,
);
handler.handle(&alert);
}
assert_eq!(handler.name(), "LogAlertHandler");
}
#[test]
fn test_log_handler_default() {
let handler = LogAlertHandler::default();
assert!(handler.accepts_severity(AlertSeverity::Info));
}
#[test]
fn test_callback_handler_accepts_severity() {
let handler = CallbackAlertHandler::new(AlertSeverity::Warning, |_| {});
assert!(!handler.accepts_severity(AlertSeverity::Info));
assert!(handler.accepts_severity(AlertSeverity::Warning));
assert!(handler.accepts_severity(AlertSeverity::Error));
assert!(handler.accepts_severity(AlertSeverity::Critical));
assert_eq!(handler.name(), "CallbackAlertHandler");
}
#[test]
fn test_callback_handler_debug() {
let handler = CallbackAlertHandler::new(AlertSeverity::Info, |_| {});
let debug = format!("{:?}", handler);
assert!(debug.contains("CallbackAlertHandler"));
}
#[test]
fn test_collecting_handler_accepts_severity() {
let handler = CollectingAlertHandler::new(AlertSeverity::Error);
assert!(!handler.accepts_severity(AlertSeverity::Info));
assert!(!handler.accepts_severity(AlertSeverity::Warning));
assert!(handler.accepts_severity(AlertSeverity::Error));
assert!(handler.accepts_severity(AlertSeverity::Critical));
assert_eq!(handler.name(), "CollectingAlertHandler");
}
#[test]
fn test_manager_acknowledge_nonexistent() {
let mut manager = AlertManager::new(100, 0);
assert!(!manager.acknowledge("nonexistent_id"));
}
}