From 05266d9062ff15b4719860b466e299b4e520558b Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 4 Feb 2026 14:26:44 +0000 Subject: [PATCH] Show node display name in alerts instead of raw Proxmox node name Alerts previously showed the raw Proxmox node name (e.g., "on pve") even when users configured a display name (e.g., "SPACEX") via Settings or the host agent --hostname flag. This affected the alert UI, email notifications, and webhook payloads. Add NodeDisplayName field to the alert chain: cache display names in the alert Manager (populated by CheckNode/CheckHost on every poll), resolve them at alert creation via preserveAlertState, refresh on metric updates, and enrich at read time in GetActiveAlerts. Update models.Alert, the syncAlertsToState conversion, email templates, Apprise body text, webhook payloads, and all frontend rendering paths. Related to #1188 --- .../Alerts/InvestigateAlertButton.tsx | 2 +- frontend-modern/src/pages/Alerts.tsx | 8 +- frontend-modern/src/types/api.ts | 1 + internal/alerts/alerts.go | 339 +++++++++++------- internal/models/models.go | 29 +- internal/monitoring/monitor.go | 29 +- internal/notifications/email_template.go | 17 +- internal/notifications/notifications.go | 5 +- internal/notifications/webhook_enhanced.go | 1 + 9 files changed, 255 insertions(+), 176 deletions(-) diff --git a/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx b/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx index cf0aaea18..265e4123f 100644 --- a/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx +++ b/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx @@ -54,7 +54,7 @@ export function InvestigateAlertButton(props: InvestigateAlertButtonProps) { **Current Value:** ${formatAlertValue(props.alert.value, props.alert.type)} **Threshold:** ${formatAlertValue(props.alert.threshold, props.alert.type)} **Duration:** ${durationStr} -${props.alert.node ? `**Node:** ${props.alert.node}` : ''} +${props.alert.node ? `**Node:** ${props.alert.nodeDisplayName || props.alert.node}` : ''} Please: 1. Identify the root cause diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 6f5a498b9..d59e69b10 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -2665,7 +2665,7 @@ function OverviewTab(props: { - on {alert.node} + on {alert.nodeDisplayName || alert.node} @@ -4740,6 +4740,7 @@ function HistoryTab(props: { resourceType: string; resourceId?: string; node?: string; + nodeDisplayName?: string; severity: string; // warning, critical for alerts; severity for findings // Aliases for backward compat with existing rendering code level: string; // same as severity @@ -4767,6 +4768,7 @@ function HistoryTab(props: { resourceType: getResourceType(alert.resourceName, alert.metadata), resourceId: alert.resourceId, node: alert.node, + nodeDisplayName: alert.nodeDisplayName, severity: alert.level, level: alert.level, type: alert.type, @@ -4795,6 +4797,7 @@ function HistoryTab(props: { resourceType: getResourceType(alert.resourceName, alert.metadata), resourceId: alert.resourceId, node: alert.node, + nodeDisplayName: alert.nodeDisplayName, severity: alert.level, level: alert.level, type: alert.type, @@ -5816,7 +5819,7 @@ function HistoryTab(props: { {/* Node */} - {alert.node || '—'} + {alert.nodeDisplayName || alert.node || '—'} {/* Actions */} @@ -5854,6 +5857,7 @@ function HistoryTab(props: { resourceId: alert.resourceId || '', resourceName: alert.resourceName, node: alert.node || '', + nodeDisplayName: alert.nodeDisplayName, instance: '', message: alert.message || '', value: 0, diff --git a/frontend-modern/src/types/api.ts b/frontend-modern/src/types/api.ts index d844fbf27..6b04ef60a 100644 --- a/frontend-modern/src/types/api.ts +++ b/frontend-modern/src/types/api.ts @@ -1051,6 +1051,7 @@ export interface Alert { resourceId: string; resourceName: string; node: string; + nodeDisplayName?: string; instance: string; message: string; value: number; diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index 25e72ff3d..4aec26cf5 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -82,22 +82,23 @@ func normalizePoweredOffSeverity(level AlertLevel) AlertLevel { // Alert represents an active alert type Alert struct { - ID string `json:"id"` - Type string `json:"type"` // cpu, memory, disk, etc. - Level AlertLevel `json:"level"` - ResourceID string `json:"resourceId"` // guest or node ID - ResourceName string `json:"resourceName"` - Node string `json:"node"` - Instance string `json:"instance"` - Message string `json:"message"` - Value float64 `json:"value"` - Threshold float64 `json:"threshold"` - StartTime time.Time `json:"startTime"` - LastSeen time.Time `json:"lastSeen"` - Acknowledged bool `json:"acknowledged"` - AckTime *time.Time `json:"ackTime,omitempty"` - AckUser string `json:"ackUser,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + ID string `json:"id"` + Type string `json:"type"` // cpu, memory, disk, etc. + Level AlertLevel `json:"level"` + ResourceID string `json:"resourceId"` // guest or node ID + ResourceName string `json:"resourceName"` + Node string `json:"node"` + NodeDisplayName string `json:"nodeDisplayName,omitempty"` + Instance string `json:"instance"` + Message string `json:"message"` + Value float64 `json:"value"` + Threshold float64 `json:"threshold"` + StartTime time.Time `json:"startTime"` + LastSeen time.Time `json:"lastSeen"` + Acknowledged bool `json:"acknowledged"` + AckTime *time.Time `json:"ackTime,omitempty"` + AckUser string `json:"ackUser,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` // Notification tracking LastNotified *time.Time `json:"lastNotified,omitempty"` // Last time notification was sent // Escalation tracking @@ -539,6 +540,10 @@ type Manager struct { // When a host agent is running on a Proxmox node, we prefer the host agent // alerts and suppress the node alerts to avoid duplicate monitoring. hostAgentHostnames map[string]struct{} // Normalized hostnames (lowercase) + // Node display name cache: maps raw node/host name → user-configured display name. + // Populated by CheckNode and CheckHost so that checkMetric (and direct alert + // creation sites) can resolve display names without signature changes. + nodeDisplayNames map[string]string // License checking for Pro-only alert features hasProFeature func(feature string) bool @@ -594,6 +599,7 @@ func NewManagerWithDataDir(dataDir string) *Manager { flappingActive: make(map[string]bool), cleanupStop: make(chan struct{}), hostAgentHostnames: make(map[string]struct{}), + nodeDisplayNames: make(map[string]string), config: AlertConfig{ Enabled: true, ActivationState: ActivationPending, @@ -2614,6 +2620,9 @@ func (m *Manager) CheckGuest(guest interface{}, instanceName string) { // CheckNode checks a node against thresholds func (m *Manager) CheckNode(node models.Node) { + // Cache display name so all alerts (including guest alerts on this node) can resolve it. + m.updateNodeDisplayName(node.Name, node.DisplayName) + m.mu.RLock() if !m.config.Enabled { m.mu.RUnlock() @@ -2752,6 +2761,29 @@ func (m *Manager) hasHostAgentForNode(nodeName string) bool { return exists } +// updateNodeDisplayName caches the display name for a node/host so alerts +// can resolve it without needing the full model object. +func (m *Manager) updateNodeDisplayName(name, displayName string) { + name = strings.TrimSpace(name) + if name == "" { + return + } + displayName = strings.TrimSpace(displayName) + m.mu.Lock() + if displayName != "" && displayName != name { + m.nodeDisplayNames[name] = displayName + } else { + delete(m.nodeDisplayNames, name) + } + m.mu.Unlock() +} + +// resolveNodeDisplayName returns the cached display name for a node, or empty +// string if none is set. Caller must hold m.mu (read or write). +func (m *Manager) resolveNodeDisplayName(node string) string { + return m.nodeDisplayNames[node] +} + func hostResourceID(hostID string) string { trimmed := strings.TrimSpace(hostID) if trimmed == "" { @@ -2846,6 +2878,9 @@ func (m *Manager) CheckHost(host models.Host) { m.RegisterHostAgentHostname(host.Hostname) } + // Cache display name so host alerts show the user-configured name. + m.updateNodeDisplayName(host.Hostname, host.DisplayName) + // Fresh telemetry marks the host as online and clears offline tracking. m.HandleHostOnline(host) @@ -3075,19 +3110,20 @@ func (m *Manager) CheckHost(host models.Host) { m.mu.Lock() if _, exists := m.activeAlerts[alertID]; !exists { alert := &Alert{ - ID: alertID, - Type: "raid", - Level: AlertLevelCritical, - ResourceID: raidResourceID, - ResourceName: raidName, - Node: nodeName, - Instance: instanceName, - Message: msg, - Value: float64(array.FailedDevices), - Threshold: 0, - StartTime: time.Now(), - LastSeen: time.Now(), - Metadata: raidMetadata, + ID: alertID, + Type: "raid", + Level: AlertLevelCritical, + ResourceID: raidResourceID, + ResourceName: raidName, + Node: nodeName, + NodeDisplayName: m.resolveNodeDisplayName(nodeName), + Instance: instanceName, + Message: msg, + Value: float64(array.FailedDevices), + Threshold: 0, + StartTime: time.Now(), + LastSeen: time.Now(), + Metadata: raidMetadata, } m.preserveAlertState(alertID, alert) m.activeAlerts[alertID] = alert @@ -6188,19 +6224,20 @@ func (m *Manager) checkMetric(resourceID, resourceName, node, instance, resource alertMetadata["monitorOnly"] = monitorOnly alert := &Alert{ - ID: alertID, - Type: metricType, - Level: AlertLevelWarning, - ResourceID: resourceID, - ResourceName: resourceName, - Node: node, - Instance: instance, - Message: message, - Value: value, - Threshold: threshold.Trigger, - StartTime: alertStartTime, - LastSeen: time.Now(), - Metadata: alertMetadata, + ID: alertID, + Type: metricType, + Level: AlertLevelWarning, + ResourceID: resourceID, + ResourceName: resourceName, + Node: node, + NodeDisplayName: m.resolveNodeDisplayName(node), + Instance: instance, + Message: message, + Value: value, + Threshold: threshold.Trigger, + StartTime: alertStartTime, + LastSeen: time.Now(), + Metadata: alertMetadata, } // Set level based on how much over threshold @@ -6282,6 +6319,10 @@ func (m *Manager) checkMetric(resourceID, resourceName, node, instance, resource // Update existing alert existingAlert.LastSeen = time.Now() existingAlert.Value = value + // Keep display name current (handles upgrades and renames). + if dn := m.resolveNodeDisplayName(existingAlert.Node); dn != "" { + existingAlert.NodeDisplayName = dn + } if existingAlert.Metadata == nil { existingAlert.Metadata = map[string]interface{}{} } @@ -6578,6 +6619,11 @@ func (m *Manager) preserveAlertState(alertID string, updated *Alert) { return } + // Auto-resolve node display name if not already set. + if updated.NodeDisplayName == "" && updated.Node != "" { + updated.NodeDisplayName = m.resolveNodeDisplayName(updated.Node) + } + existing, exists := m.activeAlerts[alertID] if exists && existing != nil { // Preserve the original start time so duration calculations are correct @@ -6633,7 +6679,13 @@ func (m *Manager) GetActiveAlerts() []Alert { alerts := make([]Alert, 0, len(m.activeAlerts)) for _, alert := range m.activeAlerts { - alerts = append(alerts, *alert) + a := *alert + // Ensure display name is current (handles upgrades, renames, and + // alerts created before the cache was populated). + if dn := m.resolveNodeDisplayName(a.Node); dn != "" { + a.NodeDisplayName = dn + } + alerts = append(alerts, a) } return alerts } @@ -6776,18 +6828,19 @@ func (m *Manager) checkNodeOffline(node models.Node) { // Create new offline alert after confirmation alert := &Alert{ - ID: alertID, - Type: "connectivity", - Level: AlertLevelCritical, // Node offline is always critical - ResourceID: node.ID, - ResourceName: node.Name, - Node: node.Name, - Instance: node.Instance, - Message: fmt.Sprintf("Node '%s' is offline", node.Name), - Value: 0, // Not applicable for offline status - Threshold: 0, // Not applicable for offline status - StartTime: time.Now(), - Acknowledged: false, + ID: alertID, + Type: "connectivity", + Level: AlertLevelCritical, // Node offline is always critical + ResourceID: node.ID, + ResourceName: node.Name, + Node: node.Name, + NodeDisplayName: m.resolveNodeDisplayName(node.Name), + Instance: node.Instance, + Message: fmt.Sprintf("Node '%s' is offline", node.Name), + Value: 0, // Not applicable for offline status + Threshold: 0, // Not applicable for offline status + StartTime: time.Now(), + Acknowledged: false, } m.preserveAlertState(alertID, alert) @@ -7142,18 +7195,19 @@ func (m *Manager) checkPMGQueueDepths(pmg models.PMGInstance, defaults PMGThresh alert.Level = level } else { alert := &Alert{ - ID: alertID, - Type: "queue-depth", - Level: level, - ResourceID: pmg.ID, - ResourceName: pmg.Name, - Node: pmg.Host, - Instance: pmg.Name, - Message: fmt.Sprintf("PMG %s has %d total messages in queue (threshold: %d)", pmg.Name, totalQueue, threshold), - Value: float64(totalQueue), - Threshold: float64(threshold), - StartTime: time.Now(), - LastSeen: time.Now(), + ID: alertID, + Type: "queue-depth", + Level: level, + ResourceID: pmg.ID, + ResourceName: pmg.Name, + Node: pmg.Host, + NodeDisplayName: m.resolveNodeDisplayName(pmg.Host), + Instance: pmg.Name, + Message: fmt.Sprintf("PMG %s has %d total messages in queue (threshold: %d)", pmg.Name, totalQueue, threshold), + Value: float64(totalQueue), + Threshold: float64(threshold), + StartTime: time.Now(), + LastSeen: time.Now(), } m.activeAlerts[alertID] = alert m.dispatchAlert(alert, true) @@ -7196,18 +7250,19 @@ func (m *Manager) checkPMGQueueDepths(pmg models.PMGInstance, defaults PMGThresh alert.Level = level } else { alert := &Alert{ - ID: alertID, - Type: "queue-deferred", - Level: level, - ResourceID: pmg.ID, - ResourceName: pmg.Name, - Node: pmg.Host, - Instance: pmg.Name, - Message: fmt.Sprintf("PMG %s has %d deferred messages (threshold: %d)", pmg.Name, totalDeferred, threshold), - Value: float64(totalDeferred), - Threshold: float64(threshold), - StartTime: time.Now(), - LastSeen: time.Now(), + ID: alertID, + Type: "queue-deferred", + Level: level, + ResourceID: pmg.ID, + ResourceName: pmg.Name, + Node: pmg.Host, + NodeDisplayName: m.resolveNodeDisplayName(pmg.Host), + Instance: pmg.Name, + Message: fmt.Sprintf("PMG %s has %d deferred messages (threshold: %d)", pmg.Name, totalDeferred, threshold), + Value: float64(totalDeferred), + Threshold: float64(threshold), + StartTime: time.Now(), + LastSeen: time.Now(), } m.activeAlerts[alertID] = alert m.dispatchAlert(alert, true) @@ -7250,18 +7305,19 @@ func (m *Manager) checkPMGQueueDepths(pmg models.PMGInstance, defaults PMGThresh alert.Level = level } else { alert := &Alert{ - ID: alertID, - Type: "queue-hold", - Level: level, - ResourceID: pmg.ID, - ResourceName: pmg.Name, - Node: pmg.Host, - Instance: pmg.Name, - Message: fmt.Sprintf("PMG %s has %d held messages (threshold: %d)", pmg.Name, totalHold, threshold), - Value: float64(totalHold), - Threshold: float64(threshold), - StartTime: time.Now(), - LastSeen: time.Now(), + ID: alertID, + Type: "queue-hold", + Level: level, + ResourceID: pmg.ID, + ResourceName: pmg.Name, + Node: pmg.Host, + NodeDisplayName: m.resolveNodeDisplayName(pmg.Host), + Instance: pmg.Name, + Message: fmt.Sprintf("PMG %s has %d held messages (threshold: %d)", pmg.Name, totalHold, threshold), + Value: float64(totalHold), + Threshold: float64(threshold), + StartTime: time.Now(), + LastSeen: time.Now(), } m.activeAlerts[alertID] = alert m.dispatchAlert(alert, true) @@ -7330,18 +7386,19 @@ func (m *Manager) checkPMGOldestMessage(pmg models.PMGInstance, defaults PMGThre // Create new alert alert := &Alert{ - ID: alertID, - Type: "message-age", - Level: level, - ResourceID: pmg.ID, - ResourceName: pmg.Name, - Node: pmg.Host, - Instance: pmg.Name, - Message: fmt.Sprintf("PMG %s has messages queued for %d minutes (threshold: %d minutes)", pmg.Name, oldestMinutes, threshold), - Value: float64(oldestMinutes), - Threshold: float64(threshold), - StartTime: time.Now(), - LastSeen: time.Now(), + ID: alertID, + Type: "message-age", + Level: level, + ResourceID: pmg.ID, + ResourceName: pmg.Name, + Node: pmg.Host, + NodeDisplayName: m.resolveNodeDisplayName(pmg.Host), + Instance: pmg.Name, + Message: fmt.Sprintf("PMG %s has messages queued for %d minutes (threshold: %d minutes)", pmg.Name, oldestMinutes, threshold), + Value: float64(oldestMinutes), + Threshold: float64(threshold), + StartTime: time.Now(), + LastSeen: time.Now(), } m.activeAlerts[alertID] = alert @@ -7577,18 +7634,19 @@ func (m *Manager) createOrUpdateNodeAlert(alertID string, pmg models.PMGInstance // Create new alert alert := &Alert{ - ID: alertID, - Type: alertType, - Level: level, - ResourceID: pmg.ID, - ResourceName: pmg.Name, - Node: nodeName, - Instance: pmg.Name, - Message: message, - Value: value, - Threshold: threshold, - StartTime: time.Now(), - LastSeen: time.Now(), + ID: alertID, + Type: alertType, + Level: level, + ResourceID: pmg.ID, + ResourceName: pmg.Name, + Node: nodeName, + NodeDisplayName: m.resolveNodeDisplayName(nodeName), + Instance: pmg.Name, + Message: message, + Value: value, + Threshold: threshold, + StartTime: time.Now(), + LastSeen: time.Now(), } m.activeAlerts[alertID] = alert @@ -7758,18 +7816,19 @@ func (m *Manager) checkQuarantineMetric(pmg models.PMGInstance, metricType strin // Create new alert alert := &Alert{ - ID: alertID, - Type: fmt.Sprintf("quarantine-%s", metricType), - Level: level, - ResourceID: pmg.ID, - ResourceName: pmg.Name, - Node: pmg.Host, - Instance: pmg.Name, - Message: message, - Value: float64(current), - Threshold: float64(threshold), - StartTime: time.Now(), - LastSeen: time.Now(), + ID: alertID, + Type: fmt.Sprintf("quarantine-%s", metricType), + Level: level, + ResourceID: pmg.ID, + ResourceName: pmg.Name, + Node: pmg.Host, + NodeDisplayName: m.resolveNodeDisplayName(pmg.Host), + Instance: pmg.Name, + Message: message, + Value: float64(current), + Threshold: float64(threshold), + StartTime: time.Now(), + LastSeen: time.Now(), } m.activeAlerts[alertID] = alert @@ -8066,18 +8125,19 @@ func (m *Manager) checkAnomalyMetric(pmg models.PMGInstance, tracker *pmgAnomaly pmg.Name, metricName, current, ratio, baseline) alert := &Alert{ - ID: alertID, - Type: fmt.Sprintf("anomaly-%s", metricName), - Level: level, - ResourceID: pmg.ID, - ResourceName: pmg.Name, - Node: pmg.Host, - Instance: pmg.Name, - Message: message, - Value: current, - Threshold: baseline, - StartTime: now, - LastSeen: now, + ID: alertID, + Type: fmt.Sprintf("anomaly-%s", metricName), + Level: level, + ResourceID: pmg.ID, + ResourceName: pmg.Name, + Node: pmg.Host, + NodeDisplayName: m.resolveNodeDisplayName(pmg.Host), + Instance: pmg.Name, + Message: message, + Value: current, + Threshold: baseline, + StartTime: now, + LastSeen: now, } m.activeAlerts[alertID] = alert @@ -9653,6 +9713,7 @@ func (m *Manager) CheckDiskHealth(instance, node string, disk proxmox.Disk) { existing.ResourceID = resourceID existing.ResourceName = resourceName existing.Node = node + existing.NodeDisplayName = m.resolveNodeDisplayName(node) existing.Instance = instance if existing.Metadata == nil { existing.Metadata = map[string]interface{}{} diff --git a/internal/models/models.go b/internal/models/models.go index f60a37ac9..f93a6ecc0 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -42,20 +42,21 @@ type State struct { // Alert represents an active alert (simplified for State) type Alert struct { - ID string `json:"id"` - Type string `json:"type"` - Level string `json:"level"` - ResourceID string `json:"resourceId"` - ResourceName string `json:"resourceName"` - Node string `json:"node"` - Instance string `json:"instance"` - Message string `json:"message"` - Value float64 `json:"value"` - Threshold float64 `json:"threshold"` - StartTime time.Time `json:"startTime"` - Acknowledged bool `json:"acknowledged"` - AckTime *time.Time `json:"ackTime,omitempty"` - AckUser string `json:"ackUser,omitempty"` + ID string `json:"id"` + Type string `json:"type"` + Level string `json:"level"` + ResourceID string `json:"resourceId"` + ResourceName string `json:"resourceName"` + Node string `json:"node"` + NodeDisplayName string `json:"nodeDisplayName,omitempty"` + Instance string `json:"instance"` + Message string `json:"message"` + Value float64 `json:"value"` + Threshold float64 `json:"threshold"` + StartTime time.Time `json:"startTime"` + Acknowledged bool `json:"acknowledged"` + AckTime *time.Time `json:"ackTime,omitempty"` + AckUser string `json:"ackUser,omitempty"` } // ResolvedAlert represents a recently resolved alert diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index 7115d64a9..76d9a186e 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -4792,20 +4792,21 @@ func (m *Monitor) syncAlertsToState() { modelAlerts := make([]models.Alert, 0, len(activeAlerts)) for _, alert := range activeAlerts { modelAlerts = append(modelAlerts, models.Alert{ - ID: alert.ID, - Type: alert.Type, - Level: string(alert.Level), - ResourceID: alert.ResourceID, - ResourceName: alert.ResourceName, - Node: alert.Node, - Instance: alert.Instance, - Message: alert.Message, - Value: alert.Value, - Threshold: alert.Threshold, - StartTime: alert.StartTime, - Acknowledged: alert.Acknowledged, - AckTime: alert.AckTime, - AckUser: alert.AckUser, + ID: alert.ID, + Type: alert.Type, + Level: string(alert.Level), + ResourceID: alert.ResourceID, + ResourceName: alert.ResourceName, + Node: alert.Node, + NodeDisplayName: alert.NodeDisplayName, + Instance: alert.Instance, + Message: alert.Message, + Value: alert.Value, + Threshold: alert.Threshold, + StartTime: alert.StartTime, + Acknowledged: alert.Acknowledged, + AckTime: alert.AckTime, + AckUser: alert.AckUser, }) if alert.Acknowledged && logging.IsLevelEnabled(zerolog.DebugLevel) { log.Debug().Str("alertID", alert.ID).Interface("ackTime", alert.AckTime).Msg("Syncing acknowledged alert") diff --git a/internal/notifications/email_template.go b/internal/notifications/email_template.go index d1493a46d..adcf21c30 100644 --- a/internal/notifications/email_template.go +++ b/internal/notifications/email_template.go @@ -27,6 +27,15 @@ func titleCase(s string) string { return result.String() } +// alertNodeDisplay returns the display name for an alert's node, falling back +// to the raw node name if no display name is set. +func alertNodeDisplay(alert *alerts.Alert) string { + if alert.NodeDisplayName != "" { + return alert.NodeDisplayName + } + return alert.Node +} + // EmailTemplate generates a professional HTML email template for alerts func EmailTemplate(alertList []*alerts.Alert, isSingle bool) (subject, htmlBody, textBody string) { if isSingle && len(alertList) == 1 { @@ -162,7 +171,7 @@ func singleAlertTemplate(alert *alerts.Alert) (subject, htmlBody, textBody strin formatMetricThreshold(alert.Type, alert.Threshold), alert.ResourceID, alertType, - alert.Node, + alertNodeDisplay(alert), alert.Instance, alert.StartTime.Format("Jan 2, 2006 at 3:04 PM"), formatDuration(time.Since(alert.StartTime)), @@ -194,7 +203,7 @@ View alerts and configure settings in your Pulse dashboard.`, formatMetricValue(alert.Type, alert.Value), formatMetricThreshold(alert.Type, alert.Threshold), alert.Message, - alert.Node, + alertNodeDisplay(alert), alert.Instance, alert.StartTime.Format("Jan 2, 2006 at 3:04 PM"), formatDuration(time.Since(alert.StartTime)), @@ -255,7 +264,7 @@ func groupedAlertTemplate(alertList []*alerts.Alert) (subject, htmlBody, textBod `, levelColor, alert.ResourceName, - alert.Type, alert.Node, + alert.Type, alertNodeDisplay(alert), levelColor, alert.Level, formatMetricValue(alert.Type, alert.Value), formatMetricThreshold(alert.Type, alert.Threshold), formatDuration(time.Since(alert.StartTime)), @@ -364,7 +373,7 @@ func groupedAlertTemplate(alertList []*alerts.Alert) (subject, htmlBody, textBod textBuilder.WriteString(fmt.Sprintf("\n%d. %s (%s)\n", i+1, alert.ResourceName, alert.ResourceID)) textBuilder.WriteString(fmt.Sprintf(" Level: %s | Type: %s\n", strings.ToUpper(string(alert.Level)), alert.Type)) textBuilder.WriteString(fmt.Sprintf(" Value: %s (Threshold: %s)\n", formatMetricValue(alert.Type, alert.Value), formatMetricThreshold(alert.Type, alert.Threshold))) - textBuilder.WriteString(fmt.Sprintf(" Node: %s | Started: %s ago\n", alert.Node, formatDuration(time.Since(alert.StartTime)))) + textBuilder.WriteString(fmt.Sprintf(" Node: %s | Started: %s ago\n", alertNodeDisplay(alert), formatDuration(time.Since(alert.StartTime)))) textBuilder.WriteString(fmt.Sprintf(" Message: %s\n", alert.Message)) } diff --git a/internal/notifications/notifications.go b/internal/notifications/notifications.go index 5c9af98f3..3e4d43c14 100644 --- a/internal/notifications/notifications.go +++ b/internal/notifications/notifications.go @@ -1138,7 +1138,7 @@ func buildApprisePayload(alertList []*alerts.Alert, publicURL string) (string, s bodyBuilder.WriteString(fmt.Sprintf("[%s] %s", strings.ToUpper(string(alert.Level)), alert.ResourceName)) bodyBuilder.WriteString(fmt.Sprintf(" — value %.2f (threshold %.2f)\n", alert.Value, alert.Threshold)) if alert.Node != "" { - bodyBuilder.WriteString(fmt.Sprintf("Node: %s\n", alert.Node)) + bodyBuilder.WriteString(fmt.Sprintf("Node: %s\n", alertNodeDisplay(alert))) } if alert.Instance != "" && alert.Instance != alert.Node { bodyBuilder.WriteString(fmt.Sprintf("Instance: %s\n", alert.Instance)) @@ -1201,7 +1201,7 @@ func buildResolvedNotificationContent(alertList []*alerts.Alert, resolvedAt time bodyBuilder.WriteString("\n") if alert.Node != "" { bodyBuilder.WriteString("Node: ") - bodyBuilder.WriteString(alert.Node) + bodyBuilder.WriteString(alertNodeDisplay(alert)) bodyBuilder.WriteString("\n") } if alert.Instance != "" && alert.Instance != alert.Node { @@ -2228,6 +2228,7 @@ func (n *NotificationManager) prepareWebhookData(alert *alerts.Alert, customFiel ResourceName: alert.ResourceName, ResourceID: alert.ResourceID, Node: alert.Node, + NodeDisplayName: alertNodeDisplay(alert), Instance: instance, Message: alert.Message, Value: roundedValue, diff --git a/internal/notifications/webhook_enhanced.go b/internal/notifications/webhook_enhanced.go index ebcb1133e..d1e96581b 100644 --- a/internal/notifications/webhook_enhanced.go +++ b/internal/notifications/webhook_enhanced.go @@ -43,6 +43,7 @@ type WebhookPayloadData struct { ResourceName string ResourceID string Node string + NodeDisplayName string Instance string Message string Value float64