refactor(app): implement ports & adapters

* also move alertFormatter into internal actor
This commit is contained in:
2025-08-09 17:53:01 +02:00
parent 985ee54669
commit f53aef1821
13 changed files with 223 additions and 69 deletions

View File

@@ -1,54 +0,0 @@
use crate::{Severity, SystemMonitor, ThresholdViolation};
pub struct AlertFormatter;
impl AlertFormatter {
pub fn format_violation(&self, violation: &ThresholdViolation) -> String {
let emoji = match violation.severity {
Severity::Warning => "⚠️",
Severity::Critical => "🚨",
};
let level = match violation.severity {
Severity::Warning => "ALERTE",
Severity::Critical => "CRITIQUE",
};
format!(
"{} **{}** - {}\n\n**Valeur actuelle:** {:.2}%\n**Seuil:** {:.2}%",
emoji,
level,
self.get_message_for_metric(&violation.metric_name, &violation.severity),
violation.current_value,
violation.threshold
)
}
fn get_message_for_metric(&self, metric: &str, severity: &Severity) -> String {
match (metric, severity) {
("CPU", Severity::Warning) => "Utilisation CPU élevée détectée".to_string(),
("CPU", Severity::Critical) => "CPU en surchauffe - Intervention requise !".to_string(),
("Memory", Severity::Warning) => "Utilisation mémoire élevée".to_string(),
("Memory", Severity::Critical) => "Mémoire saturée - Risque de crash !".to_string(),
("Swap", Severity::Warning) => "Utilisation swap élevée".to_string(),
("Swap", Severity::Critical) => "Swap saturé - Performance dégradée !".to_string(),
("Disk", Severity::Warning) => "Espace disque faible".to_string(),
("Disk", Severity::Critical) => "Disque presque plein - Nettoyage urgent !".to_string(),
(metric, _) => format!("Problème détecté sur {metric}"),
}
}
pub fn format_summary(&self, monitor: &SystemMonitor) -> String {
let metrics = monitor.get_metrics();
let mut result = "📊 **Rapport Système**\n\n```\n".to_string();
for (key, value) in &metrics {
result.push_str(&format!("{}: {:.1}%\n", key, value));
}
result.push_str("```\n\n*Surveillance automatique*");
result
}
}

View File

@@ -1,10 +1,9 @@
use crate::resource_threshold::ResourceThreshold; use crate::resource_threshold::ResourceThreshold;
use std::collections::HashMap; use std::{collections::HashMap, fmt};
use sysinfo::{Disks, System}; use sysinfo::{Disks, System};
mod get_info; mod get_info;
pub mod resource_threshold; pub mod resource_threshold;
pub mod alert_formatter;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ThresholdViolation { pub struct ThresholdViolation {
@@ -20,6 +19,15 @@ pub enum Severity {
Critical, Critical,
} }
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Warning => write!(f, "Warning"),
Severity::Critical => write!(f, "Critical"),
}
}
}
pub struct SystemMonitor { pub struct SystemMonitor {
pub system: System, pub system: System,
pub disks: Disks, pub disks: Disks,

View File

@@ -0,0 +1,56 @@
use std::collections::HashMap;
use crate::app::ports::driven::{ForFormattingMessage, ForGettingViolationData};
pub struct AlertFormatter;
impl AlertFormatter {
fn get_message_for_metric(&self, metric: &str, level: &str) -> String {
match (metric, level) {
("CPU", "AVERTISSEMENT") => "Utilisation CPU élevée détectée".to_string(),
("CPU", "CRITIQUE") => "CPU trop sollicité - Intervention requise !".to_string(),
("Memory", "AVERTISSEMENT") => "Utilisation mémoire élevée".to_string(),
("Memory", "CRITIQUE") => "Mémoire saturée - Risque de crash !".to_string(),
("Swap", "AVERTISSEMENT") => "Utilisation du swap élevé".to_string(),
("Swap", "CRITIQUE") => "Swap saturé - Performance dégradée !".to_string(),
("Disk", "AVERTISSEMENT") => "Espace disque faible".to_string(),
("Disk", "CRITIQUE") => "Disque presque plein - Nettoyage urgent !".to_string(),
(metric, _) => format!("Problème de niveau '{}' détecté sur {}", level, metric),
}
}
}
impl ForFormattingMessage for AlertFormatter {
fn format_violation(&self, violation: Box<dyn ForGettingViolationData>) -> String {
let (emoji, level) = if violation.is_critical() {
("🚨", "CRITIQUE")
} else {
("⚠️", "AVERTISSEMENT")
};
format!(
"{} **{}** - {}\n\n**Valeur actuelle:** {:.2}%\n**Seuil:** {:.2}%",
emoji,
level,
self.get_message_for_metric(&violation.get_metric_name(), level),
violation.get_metric_value(),
violation.get_threshold()
)
}
fn format_summary(&self, metrics: &HashMap<String, f32>) -> String {
let mut result = "📊 **Rapport Système**\n\n```\n".to_string();
result.push_str(
&metrics
.iter()
.map(|(key, value)| format!("{}: {:.1}%", key, value))
.collect::<Vec<_>>()
.join("\n"),
);
result.push_str("\n```\n\n*Surveillance automatique*");
result
}
}

View File

@@ -0,0 +1 @@
pub mod violation;

View File

@@ -0,0 +1,35 @@
pub struct Violation {
name: String,
is_critical: bool,
metric_value: f32,
threshold: f32,
}
impl Violation {
pub fn new(name: String, is_critical: bool, metric_value: f32, threshold: f32) -> Self {
Violation {
name,
is_critical,
metric_value,
threshold,
}
}
}
impl crate::app::ports::driven::ForGettingViolationData for Violation {
fn get_metric_name(&self) -> String {
self.name.clone()
}
fn is_critical(&self) -> bool {
self.is_critical
}
fn get_metric_value(&self) -> f32 {
self.metric_value
}
fn get_threshold(&self) -> f32 {
self.threshold
}
}

View File

@@ -0,0 +1 @@
pub mod system_monitor;

View File

@@ -0,0 +1,40 @@
use crate::{
adapters::driven::for_getting_violation_data::violation::Violation,
app::ports::driven::ForMonitoringSystem,
};
pub struct SystemMonitorAdapter {
system_monitor: system_monitor::SystemMonitor,
}
impl SystemMonitorAdapter {
pub fn new(system_monitor: system_monitor::SystemMonitor) -> Self {
SystemMonitorAdapter { system_monitor }
}
fn threshold_violation_to_for_getting_violation_data(
&self,
threshold_violation: &system_monitor::ThresholdViolation,
) -> Box<dyn crate::app::ports::driven::ForGettingViolationData> {
Box::new(Violation::new(
threshold_violation.metric_name.clone(),
threshold_violation.severity == system_monitor::Severity::Critical,
threshold_violation.current_value,
threshold_violation.threshold,
))
}
}
impl ForMonitoringSystem for SystemMonitorAdapter {
fn check_thresholds(&self) -> Vec<Box<dyn crate::app::ports::driven::ForGettingViolationData>> {
let thresholds = self.system_monitor.check_thresholds();
thresholds
.into_iter()
.map(|t| self.threshold_violation_to_for_getting_violation_data(&t))
.collect()
}
fn get_metrics(&self) -> std::collections::HashMap<String, f32> {
self.system_monitor.get_metrics()
}
}

View File

@@ -0,0 +1 @@
pub mod discord_client;

View File

@@ -0,0 +1,17 @@
use crate::app::ports::driven::ForSendingNotification;
pub struct DiscordAdapter {
discord_client: discord_client::DiscordNotifier,
}
impl DiscordAdapter {
pub fn new(discord_client: discord_client::DiscordNotifier) -> Self {
DiscordAdapter { discord_client }
}
}
impl ForSendingNotification for DiscordAdapter {
fn send_notification(&self, message: &str) -> Result<(), Box<dyn std::error::Error>> {
self.discord_client.send_notification(message)
}
}

1
src/app/ports.rs Normal file
View File

@@ -0,0 +1 @@
pub mod driven;

22
src/app/ports/driven.rs Normal file
View File

@@ -0,0 +1,22 @@
use std::collections::HashMap;
pub trait ForSendingNotification {
fn send_notification(&self, message: &str) -> Result<(), Box<dyn std::error::Error>>;
}
pub trait ForMonitoringSystem {
fn check_thresholds(&self) -> Vec<Box<dyn ForGettingViolationData>>;
fn get_metrics(&self) -> HashMap<String, f32>;
}
pub trait ForFormattingMessage {
fn format_violation(&self, violation: Box<dyn ForGettingViolationData>) -> String;
fn format_summary(&self, metrics: &HashMap<String, f32>) -> String;
}
pub trait ForGettingViolationData {
fn is_critical(&self) -> bool;
fn get_metric_name(&self) -> String;
fn get_metric_value(&self) -> f32;
fn get_threshold(&self) -> f32;
}

View File

@@ -1,15 +1,17 @@
use discord_client::DiscordNotifier; use crate::app::ports::driven::{
use system_monitor::{SystemMonitor, alert_formatter::AlertFormatter}; ForFormattingMessage, ForMonitoringSystem, ForSendingNotification,
};
pub struct Service { pub struct Service {
notifier: DiscordNotifier, notifier: Box<dyn ForSendingNotification>,
monitor: SystemMonitor, monitor: Box<dyn ForMonitoringSystem>,
formatter: AlertFormatter, formatter: Box<dyn ForFormattingMessage>,
} }
pub fn new( pub fn new(
notifier: DiscordNotifier, notifier: Box<dyn ForSendingNotification>,
monitor: SystemMonitor, monitor: Box<dyn ForMonitoringSystem>,
formatter: AlertFormatter, formatter: Box<dyn ForFormattingMessage>,
) -> Service { ) -> Service {
Service { Service {
notifier, notifier,
@@ -23,14 +25,14 @@ impl Service {
// Check for threshold violations and send alerts // Check for threshold violations and send alerts
let violations = self.monitor.check_thresholds(); let violations = self.monitor.check_thresholds();
for violation in violations { for violation in violations {
let message = self.formatter.format_violation(&violation); let message = self.formatter.format_violation(violation);
self.notifier self.notifier
.send_notification(&message) .send_notification(&message)
.expect("Failed to send notification"); .expect("Failed to send notification");
} }
// Send a final notification with all system information // Send a final notification with all system information
let summary = self.formatter.format_summary(&self.monitor); let summary = self.formatter.format_summary(&self.monitor.get_metrics());
self.notifier self.notifier
.send_notification(&summary) .send_notification(&summary)
.expect("Failed to send final notification"); .expect("Failed to send final notification");

View File

@@ -3,9 +3,26 @@ use dotenvy::dotenv;
use system_monitor::SystemMonitor; use system_monitor::SystemMonitor;
mod app { mod app {
pub mod ports;
pub mod service; pub mod service;
} }
mod adapters {
pub mod driven {
pub mod for_getting_violation_data;
pub mod for_monitoring_system;
pub mod for_sending_notification;
}
}
mod actors {
pub mod driven {
pub mod for_formatting_message {
pub mod alert_formatter;
}
}
}
fn main() { fn main() {
dotenv().ok(); dotenv().ok();
@@ -14,12 +31,19 @@ fn main() {
"System Monitor".to_string(), "System Monitor".to_string(),
"https://cdn.shopify.com/s/files/1/0262/1423/6212/files/Lord_of_the_Rings_eye_of_Sauron_-_Ghtic.com_-_Blog.png?v=1579680018".to_string(), "https://cdn.shopify.com/s/files/1/0262/1423/6212/files/Lord_of_the_Rings_eye_of_Sauron_-_Ghtic.com_-_Blog.png?v=1579680018".to_string(),
); );
let monitor = let monitor =
SystemMonitor::new(system_monitor::resource_threshold::get_default_resource_thresholds()); SystemMonitor::new(system_monitor::resource_threshold::get_default_resource_thresholds());
let formatter = system_monitor::alert_formatter::AlertFormatter; let discord_adapter =
adapters::driven::for_sending_notification::discord_client::DiscordAdapter::new(notifier);
let system_monitor_adapter =
adapters::driven::for_monitoring_system::system_monitor::SystemMonitorAdapter::new(monitor);
let formatter = actors::driven::for_formatting_message::alert_formatter::AlertFormatter;
let service = app::service::new(notifier, monitor, formatter); let service = app::service::new(
Box::new(discord_adapter),
Box::new(system_monitor_adapter),
Box::new(formatter),
);
service.run(); service.run();
} }