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 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
|
/// Manages notifications
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct PersistedStatus {
|
|
||||||
metric_statuses: HashMap<String, Status>,
|
|
||||||
metric_details: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manages status change tracking and notifications
|
|
||||||
pub struct NotificationManager {
|
pub struct NotificationManager {
|
||||||
config: NotificationConfig,
|
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 {
|
impl NotificationManager {
|
||||||
pub fn new(config: &NotificationConfig, hostname: &str) -> Result<Self, anyhow::Error> {
|
pub fn new(config: &NotificationConfig, _hostname: &str) -> Result<Self> {
|
||||||
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);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
hostname: hostname.to_string(),
|
|
||||||
metric_statuses,
|
|
||||||
metric_details,
|
|
||||||
status_file_path,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_direct_email(&mut self, subject: &str, body: &str) -> Result<()> {
|
||||||
/// 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> {
|
|
||||||
if !self.config.enabled {
|
if !self.config.enabled {
|
||||||
return Ok(());
|
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() {
|
if self.is_maintenance_mode() {
|
||||||
debug!(
|
debug!("Maintenance mode active, suppressing email notification");
|
||||||
"Maintenance mode active, suppressing notification for {}",
|
|
||||||
status_change.metric_name
|
|
||||||
);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hostname = gethostname::gethostname()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
// Add metric details to status change
|
let from_email = self.config.from_email.replace("{hostname}", &hostname);
|
||||||
status_change.details = Some(self.format_metric_details(metric));
|
|
||||||
|
let email_body = format!(
|
||||||
// For recovery notifications, include original problem details
|
"{}\n\n--\nCM Dashboard Agent\nGenerated at {}",
|
||||||
let is_recovery = status_change.new_status == Status::Ok;
|
body,
|
||||||
|
Utc::now().format("%Y-%m-%d %H:%M:%S %Z")
|
||||||
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")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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()
|
let email = Message::builder()
|
||||||
.from(from_email.parse()?)
|
.from(from_email.parse()?)
|
||||||
.to(self.config.to_email.parse()?)
|
.to(self.config.to_email.parse()?)
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.body(body)?;
|
.body(email_body)?;
|
||||||
|
|
||||||
let mailer = SmtpTransport::builder_dangerous(&self.config.smtp_host)
|
let mailer = SmtpTransport::unencrypted_localhost();
|
||||||
.port(self.config.smtp_port)
|
|
||||||
.build();
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_maintenance_mode(&self) -> bool {
|
||||||
/// Load status from disk
|
std::fs::metadata("/tmp/cm-maintenance").is_ok()
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// 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 std::time::Instant;
|
||||||
use tracing::{debug, info, error};
|
use tracing::{debug, info, error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::notifications::StatusChange;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -32,7 +31,6 @@ pub struct StatusChangeSummary {
|
|||||||
pub initial_status: Status,
|
pub initial_status: Status,
|
||||||
pub final_status: Status,
|
pub final_status: Status,
|
||||||
pub change_count: usize,
|
pub change_count: usize,
|
||||||
pub significant_change: bool, // true if needs notification
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -191,7 +189,7 @@ impl HostStatusManager {
|
|||||||
info!("Sending aggregated notification for {} service changes", aggregated.service_summaries.len());
|
info!("Sending aggregated notification for {} service changes", aggregated.service_summaries.len());
|
||||||
|
|
||||||
// Send aggregated notification
|
// 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);
|
error!("Failed to send aggregated notification: {}", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -220,7 +218,6 @@ impl HostStatusManager {
|
|||||||
initial_status: *initial_status,
|
initial_status: *initial_status,
|
||||||
final_status: *final_status,
|
final_status: *final_status,
|
||||||
change_count: *change_count,
|
change_count: *change_count,
|
||||||
significant_change,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,22 +248,11 @@ impl HostStatusManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send aggregated notification email
|
async fn send_aggregated_email(
|
||||||
async fn send_aggregated_notification(
|
|
||||||
&self,
|
&self,
|
||||||
aggregated: &AggregatedStatusChanges,
|
aggregated: &AggregatedStatusChanges,
|
||||||
notification_manager: &mut crate::notifications::NotificationManager,
|
notification_manager: &mut crate::notifications::NotificationManager,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> 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 mut summary_parts = Vec::new();
|
||||||
let critical_count = aggregated.service_summaries.iter().filter(|s| s.final_status == Status::Critical).count();
|
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();
|
let warning_count = aggregated.service_summaries.iter().filter(|s| s.final_status == Status::Warning).count();
|
||||||
@ -288,17 +274,10 @@ impl HostStatusManager {
|
|||||||
summary_parts.join(", ")
|
summary_parts.join(", ")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a dummy metric for the notification
|
let subject = format!("Status Alert: {}", summary_text);
|
||||||
let summary_metric = Metric {
|
let body = self.format_aggregated_details(aggregated);
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
/// Format details for aggregated notification
|
||||||
|
|||||||
@ -367,10 +367,8 @@ impl TuiApp {
|
|||||||
let mut has_warning = false;
|
let mut has_warning = false;
|
||||||
let mut has_pending = false;
|
let mut has_pending = false;
|
||||||
let mut ok_count = 0;
|
let mut ok_count = 0;
|
||||||
let mut total_count = 0;
|
|
||||||
|
|
||||||
for metric in &metrics {
|
for metric in &metrics {
|
||||||
total_count += 1;
|
|
||||||
match metric.status {
|
match metric.status {
|
||||||
Status::Critical => has_critical = true,
|
Status::Critical => has_critical = true,
|
||||||
Status::Warning => has_warning = true,
|
Status::Warning => has_warning = true,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user