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