Remove legacy notification code and fix all warnings

This commit is contained in:
Christoffer Martinsson 2025-10-21 19:48:55 +02:00
parent f4b5bb814d
commit 338c4457a5
3 changed files with 36 additions and 249 deletions

View File

@ -1,254 +1,64 @@
use cm_dashboard_shared::Status;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tracing::{debug, info, error, warn};
use chrono::{DateTime, Utc};
use chrono_tz::Europe::Stockholm;
use lettre::{Message, SmtpTransport, Transport};
use serde::{Serialize, Deserialize};
use crate::config::NotificationConfig;
use anyhow::Result;
use chrono::Utc;
use lettre::transport::smtp::SmtpTransport;
use lettre::{Message, Transport};
use tracing::{debug, error, info};
/// Persisted status data
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistedStatus {
metric_statuses: HashMap<String, Status>,
metric_details: HashMap<String, String>,
}
/// Manages status change tracking and notifications
/// Manages notifications
pub struct NotificationManager {
config: NotificationConfig,
hostname: String,
metric_statuses: HashMap<String, Status>,
metric_details: HashMap<String, String>, // Store details for warning/critical states
status_file_path: String,
}
/// Status change information
#[derive(Debug, Clone)]
pub struct StatusChange {
pub metric_name: String,
pub old_status: Status,
pub new_status: Status,
pub timestamp: DateTime<Utc>,
pub details: Option<String>,
}
impl NotificationManager {
pub fn new(config: &NotificationConfig, hostname: &str) -> Result<Self, anyhow::Error> {
info!("Initializing notification manager for {}", hostname);
let status_file_path = "/var/lib/cm-dashboard/last-status.json".to_string();
// Create directory if it doesn't exist
if let Some(parent) = Path::new(&status_file_path).parent() {
if let Err(e) = fs::create_dir_all(parent) {
warn!("Failed to create status directory {}: {}", parent.display(), e);
}
}
// Load previous status from disk
let (metric_statuses, metric_details) = Self::load_status(&status_file_path);
pub fn new(config: &NotificationConfig, _hostname: &str) -> Result<Self> {
Ok(Self {
config: config.clone(),
hostname: hostname.to_string(),
metric_statuses,
metric_details,
status_file_path,
})
}
/// Send notification for status change
pub async fn send_status_change_notification(
&mut self,
mut status_change: StatusChange,
metric: &cm_dashboard_shared::Metric,
) -> Result<(), anyhow::Error> {
pub async fn send_direct_email(&mut self, subject: &str, body: &str) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
// Only notify on transitions to warning/critical, or recovery to ok
let should_send = match (status_change.old_status, status_change.new_status) {
(_, Status::Warning) | (_, Status::Critical) => true,
(Status::Warning | Status::Critical | Status::Unknown, Status::Ok) => true,
_ => false,
};
if !should_send {
return Ok(());
}
// Check maintenance mode
if self.is_maintenance_mode() {
debug!(
"Maintenance mode active, suppressing notification for {}",
status_change.metric_name
);
debug!("Maintenance mode active, suppressing email notification");
return Ok(());
}
let hostname = gethostname::gethostname()
.to_string_lossy()
.to_string();
// Add metric details to status change
status_change.details = Some(self.format_metric_details(metric));
// For recovery notifications, include original problem details
let is_recovery = status_change.new_status == Status::Ok;
if is_recovery {
if let Some(old_details) = self.metric_details.get(&status_change.metric_name) {
status_change.details = Some(format!(
"Recovered from: {}\nCurrent status: {}",
old_details,
status_change.details.unwrap_or_default()
));
}
// Clear stored details after recovery
self.metric_details.remove(&status_change.metric_name);
} else if status_change.new_status == Status::Warning || status_change.new_status == Status::Critical {
// Store details for warning/critical states
if let Some(ref details) = status_change.details {
self.metric_details.insert(status_change.metric_name.clone(), details.clone());
}
}
// Save status after updating details
self.save_status();
// Send the actual email
if let Err(e) = self.send_email(&status_change).await {
error!("Failed to send notification email: {}", e);
} else {
info!(
"Sent notification: {} {:?} → {:?}",
status_change.metric_name, status_change.old_status, status_change.new_status
);
}
Ok(())
}
/// Check if maintenance mode is active
fn is_maintenance_mode(&self) -> bool {
std::fs::metadata("/tmp/cm-maintenance").is_ok()
}
/// Format metric details for notification
fn format_metric_details(&self, metric: &cm_dashboard_shared::Metric) -> String {
format!("Value: {}", metric.value.as_string())
}
/// Format email subject
fn format_subject(&self, change: &StatusChange) -> String {
let urgency = match change.new_status {
Status::Critical => "🔴 CRITICAL",
Status::Warning => "🟡 WARNING",
Status::Ok => "✅ RESOLVED",
Status::Pending => "⏳ PENDING",
Status::Unknown => " STATUS",
};
format!("{}: {} on {}", urgency, change.metric_name, self.hostname)
}
/// Format email body
fn format_body(&self, change: &StatusChange) -> String {
let mut body = format!(
"Status Change Alert\n\
\n\
Host: {}\n\
Metric: {}\n\
Status Change: {:?} {:?}\n\
Time: {}",
self.hostname,
change.metric_name,
change.old_status,
change.new_status,
change.timestamp.with_timezone(&Stockholm).format("%Y-%m-%d %H:%M:%S CET/CEST")
let from_email = self.config.from_email.replace("{hostname}", &hostname);
let email_body = format!(
"{}\n\n--\nCM Dashboard Agent\nGenerated at {}",
body,
Utc::now().format("%Y-%m-%d %H:%M:%S %Z")
);
if let Some(details) = &change.details {
body.push_str(&format!("\n\nDetails:\n{}", details));
}
body.push_str(&format!(
"\n\n--\n\
CM Dashboard Agent\n\
Generated at {}",
Utc::now().with_timezone(&Stockholm).format("%Y-%m-%d %H:%M:%S CET/CEST")
));
body
}
/// Send email notification
async fn send_email(&self, change: &StatusChange) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let subject = self.format_subject(change);
let body = self.format_body(change);
// Replace {hostname} placeholder in from_email
let from_email = self.config.from_email.replace("{hostname}", &self.hostname);
let email = Message::builder()
.from(from_email.parse()?)
.to(self.config.to_email.parse()?)
.subject(subject)
.body(body)?;
.body(email_body)?;
let mailer = SmtpTransport::builder_dangerous(&self.config.smtp_host)
.port(self.config.smtp_port)
.build();
let mailer = SmtpTransport::unencrypted_localhost();
match mailer.send(&email) {
Ok(_) => info!("Direct email sent successfully: {}", subject),
Err(e) => {
error!("Failed to send email: {}", e);
return Err(e.into());
}
}
mailer.send(&email)?;
Ok(())
}
/// Load status from disk
fn load_status(file_path: &str) -> (HashMap<String, Status>, HashMap<String, String>) {
match fs::read_to_string(file_path) {
Ok(content) => {
match serde_json::from_str::<PersistedStatus>(&content) {
Ok(persisted) => {
info!("Loaded {} metric statuses from {}", persisted.metric_statuses.len(), file_path);
(persisted.metric_statuses, persisted.metric_details)
}
Err(e) => {
warn!("Failed to parse status file {}: {}", file_path, e);
(HashMap::new(), HashMap::new())
}
}
}
Err(_) => {
info!("No previous status file found at {}, starting fresh", file_path);
(HashMap::new(), HashMap::new())
}
}
fn is_maintenance_mode(&self) -> bool {
std::fs::metadata("/tmp/cm-maintenance").is_ok()
}
/// Save status to disk
fn save_status(&self) {
let persisted = PersistedStatus {
metric_statuses: self.metric_statuses.clone(),
metric_details: self.metric_details.clone(),
};
match serde_json::to_string_pretty(&persisted) {
Ok(content) => {
if let Err(e) = fs::write(&self.status_file_path, content) {
warn!("Failed to save status to {}: {}", self.status_file_path, e);
}
}
Err(e) => {
warn!("Failed to serialize status: {}", e);
}
}
}
}
}

View File

@ -3,7 +3,6 @@ use std::collections::HashMap;
use std::time::Instant;
use tracing::{debug, info, error};
use serde::{Deserialize, Serialize};
use crate::notifications::StatusChange;
use chrono::Utc;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -32,7 +31,6 @@ pub struct StatusChangeSummary {
pub initial_status: Status,
pub final_status: Status,
pub change_count: usize,
pub significant_change: bool, // true if needs notification
}
#[derive(Debug, Clone)]
@ -191,7 +189,7 @@ impl HostStatusManager {
info!("Sending aggregated notification for {} service changes", aggregated.service_summaries.len());
// Send aggregated notification
if let Err(e) = self.send_aggregated_notification(&aggregated, notification_manager).await {
if let Err(e) = self.send_aggregated_email(&aggregated, notification_manager).await {
error!("Failed to send aggregated notification: {}", e);
}
} else {
@ -220,7 +218,6 @@ impl HostStatusManager {
initial_status: *initial_status,
final_status: *final_status,
change_count: *change_count,
significant_change,
});
}
@ -251,22 +248,11 @@ impl HostStatusManager {
}
}
/// Send aggregated notification email
async fn send_aggregated_notification(
async fn send_aggregated_email(
&self,
aggregated: &AggregatedStatusChanges,
notification_manager: &mut crate::notifications::NotificationManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Create a summary status change for the notification system
let summary_change = StatusChange {
metric_name: "host_status_summary".to_string(),
old_status: aggregated.host_status_initial,
new_status: aggregated.host_status_final,
timestamp: Utc::now(),
details: Some(self.format_aggregated_details(aggregated)),
};
// Create a descriptive summary based on change types
let mut summary_parts = Vec::new();
let critical_count = aggregated.service_summaries.iter().filter(|s| s.final_status == Status::Critical).count();
let warning_count = aggregated.service_summaries.iter().filter(|s| s.final_status == Status::Warning).count();
@ -288,17 +274,10 @@ impl HostStatusManager {
summary_parts.join(", ")
};
// Create a dummy metric for the notification
let summary_metric = Metric {
name: "host_status_summary".to_string(),
value: cm_dashboard_shared::MetricValue::String(summary_text),
status: aggregated.host_status_final,
timestamp: Utc::now().timestamp() as u64,
description: Some("Aggregated status summary".to_string()),
unit: None,
};
let subject = format!("Status Alert: {}", summary_text);
let body = self.format_aggregated_details(aggregated);
notification_manager.send_status_change_notification(summary_change, &summary_metric).await.map_err(|e| e.into())
notification_manager.send_direct_email(&subject, &body).await.map_err(|e| e.into())
}
/// Format details for aggregated notification

View File

@ -367,10 +367,8 @@ impl TuiApp {
let mut has_warning = false;
let mut has_pending = false;
let mut ok_count = 0;
let mut total_count = 0;
for metric in &metrics {
total_count += 1;
match metric.status {
Status::Critical => has_critical = true,
Status::Warning => has_warning = true,