hub v0.24.0: dispatcher routes critical severity (+ nil-prefs crash guard)
ProcessEvent routed only warning/error; a critical-severity event was silently dropped. Now routes warning/error/critical, logs unrecognized severities, and guards a nil GetNotificationPrefs (which would panic/crash the hub). host_disk_critical emits its natural critical severity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
@@ -27,11 +27,15 @@ type Dispatcher struct {
|
||||
mu sync.Mutex
|
||||
opCooldowns map[string]time.Time // "customerID:eventType" → last operator notify
|
||||
custCooldowns map[string]time.Time // "customerID:eventType" → last customer notify
|
||||
|
||||
// sendEmailFn is the email sender, seam-injected so tests exercise routing without real HTTP.
|
||||
// Defaults to (*Dispatcher).sendEmail (Resend) in NewDispatcher.
|
||||
sendEmailFn func(to, subject, textBody string) error
|
||||
}
|
||||
|
||||
// NewDispatcher creates a new notification dispatcher.
|
||||
func NewDispatcher(s *store.Store, resendAPIKey, fromEmail, operatorEmail string, operatorOn bool, logger *log.Logger) *Dispatcher {
|
||||
return &Dispatcher{
|
||||
d := &Dispatcher{
|
||||
store: s,
|
||||
resendAPIKey: resendAPIKey,
|
||||
fromEmail: fromEmail,
|
||||
@@ -42,6 +46,20 @@ func NewDispatcher(s *store.Store, resendAPIKey, fromEmail, operatorEmail string
|
||||
opCooldowns: make(map[string]time.Time),
|
||||
custCooldowns: make(map[string]time.Time),
|
||||
}
|
||||
d.sendEmailFn = d.sendEmail
|
||||
return d
|
||||
}
|
||||
|
||||
// severityNotifies reports whether a severity triggers email notifications. warning / error / critical
|
||||
// notify; everything else (info, recovery/status, or an unrecognized value) does not. Pure → unit-tested.
|
||||
// (Before v0.24.0 a "critical" severity was silently dropped here — the host_disk-class bug.)
|
||||
func severityNotifies(severity string) bool {
|
||||
switch severity {
|
||||
case "warning", "error", "critical":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessEvent evaluates an event and sends notifications as appropriate.
|
||||
@@ -57,8 +75,13 @@ func (d *Dispatcher) ProcessEvent(customerID, eventType, severity, message, deta
|
||||
return
|
||||
}
|
||||
|
||||
// Only warning and error severity trigger notifications
|
||||
if severity != "warning" && severity != "error" {
|
||||
// warning / error / critical trigger notifications. "info" is an intentional non-notify (status/
|
||||
// recovery events). Anything else is UNRECOGNIZED — log it (don't silently drop), so a bad severity
|
||||
// surfaces instead of vanishing (the felhom-pve-class lesson: a critical event must never be lost).
|
||||
if !severityNotifies(severity) {
|
||||
if severity != "info" {
|
||||
d.logger.Printf("[WARN] Dispatcher: unrecognized severity %q for %s/%s — not routing", severity, customerID, eventType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,7 +102,7 @@ func (d *Dispatcher) sendTestEmail(customerID string) {
|
||||
subject := "[Felhom] Teszt értesítés"
|
||||
body := "Kedves Ügyfél!\n\nEz egy teszt értesítés a Felhom monitoring rendszerből.\nAz értesítések megfelelően működnek.\n\nÜdvözlettel,\nFelhom.eu monitoring"
|
||||
|
||||
if err := d.sendEmail(prefs.Email, subject, body); err != nil {
|
||||
if err := d.sendEmailFn(prefs.Email, subject, body); err != nil {
|
||||
d.logger.Printf("[ERROR] Test email to %s failed: %v", prefs.Email, err)
|
||||
d.store.LogNotification(customerID, "test", "info", "Teszt értesítés", "failed", err.Error(), "customer")
|
||||
return
|
||||
@@ -104,7 +127,7 @@ func (d *Dispatcher) processOperator(customerID, eventType, severity, message, d
|
||||
|
||||
subject, body := FormatOperatorEmail(customerID, eventType, severity, message, detailsJSON)
|
||||
|
||||
if err := d.sendEmail(d.operatorEmail, subject, body); err != nil {
|
||||
if err := d.sendEmailFn(d.operatorEmail, subject, body); err != nil {
|
||||
d.logger.Printf("[ERROR] Operator email failed for %s/%s: %v", customerID, eventType, err)
|
||||
d.store.LogNotification(customerID, eventType, severity, message, "failed", err.Error(), "operator")
|
||||
return
|
||||
@@ -119,9 +142,11 @@ func (d *Dispatcher) processCustomer(customerID, eventType, severity, message, d
|
||||
return
|
||||
}
|
||||
|
||||
// Load preferences
|
||||
// Load preferences. GetNotificationPrefs returns (nil, nil) for a customer with no notification row —
|
||||
// guard the nil BEFORE dereferencing (else an event for such a customer panics the dispatcher
|
||||
// goroutine and crashes the hub). No prefs / no email → no customer notification.
|
||||
prefs, err := d.store.GetNotificationPrefs(customerID)
|
||||
if err != nil || prefs.Email == "" {
|
||||
if err != nil || prefs == nil || prefs.Email == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,7 +173,7 @@ func (d *Dispatcher) processCustomer(customerID, eventType, severity, message, d
|
||||
|
||||
subject, body := FormatCustomerEmail(customerID, eventType, severity, message, detailsJSON)
|
||||
|
||||
if err := d.sendEmail(prefs.Email, subject, body); err != nil {
|
||||
if err := d.sendEmailFn(prefs.Email, subject, body); err != nil {
|
||||
d.logger.Printf("[ERROR] Customer email failed for %s/%s: %v", customerID, eventType, err)
|
||||
d.store.LogNotification(customerID, eventType, severity, message, "failed", err.Error(), "customer")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user