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
This commit is contained in:
rcourtman
2026-02-04 14:26:44 +00:00
parent 313df78cf7
commit 05266d9062
9 changed files with 255 additions and 176 deletions

View File

@@ -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

View File

@@ -2665,7 +2665,7 @@ function OverviewTab(props: {
</span>
<Show when={alert.node}>
<span class="text-xs text-gray-500 dark:text-gray-500">
on {alert.node}
on {alert.nodeDisplayName || alert.node}
</span>
</Show>
<Show when={alert.acknowledged}>
@@ -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 */}
<td class="p-1 sm:p-1.5 px-1 sm:px-2 text-gray-600 dark:text-gray-400 truncate">
{alert.node || '—'}
{alert.nodeDisplayName || alert.node || '—'}
</td>
{/* 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,

View File

@@ -1051,6 +1051,7 @@ export interface Alert {
resourceId: string;
resourceName: string;
node: string;
nodeDisplayName?: string;
instance: string;
message: string;
value: number;

View File

@@ -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{}{}

View File

@@ -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

View File

@@ -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")

View File

@@ -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
</tr>`,
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))
}

View File

@@ -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,

View File

@@ -43,6 +43,7 @@ type WebhookPayloadData struct {
ResourceName string
ResourceID string
Node string
NodeDisplayName string
Instance string
Message string
Value float64