Remove legacy notification code and fix all warnings
This commit is contained in:
parent
f4b5bb814d
commit
338c4457a5
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user