attempt to address: Discord webhooks, backup types, storage duplicates, alert issues

- Added service field to WebhookConfig to identify Discord webhooks
- Use Discord-specific template when sending Discord webhooks
- Fixed backup type detection for PBS backups (vm/ct)
- Fixed shared storage duplicate IDs across instances
- Fixed alert acknowledge/clear response format to match frontend expectations
This commit is contained in:
Pulse Monitor 2025-08-09 22:27:10 +00:00
parent 9b72c26994
commit a368d3b3c9
5 changed files with 186 additions and 39 deletions

View file

@ -133,7 +133,7 @@ const UnifiedBackups: Component = () => {
backupType: 'remote',
vmid: parseInt(backup.vmid) || 0,
name: backup.comment || '',
type: backup.backupType === 'vm' ? 'VM' : 'LXC',
type: (backup.backupType === 'vm' || backup.backupType === 'VM') ? 'VM' : 'LXC',
node: backup.instance || 'PBS',
backupTime: backupTimeSeconds,
backupName: backupName,

View file

@ -118,7 +118,7 @@ func (h *AlertHandlers) AcknowledgeAlert(w http.ResponseWriter, r *http.Request)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// ClearAlert manually clears an alert
@ -134,7 +134,7 @@ func (h *AlertHandlers) ClearAlert(w http.ResponseWriter, r *http.Request) {
h.monitor.GetAlertManager().ClearAlert(alertID)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// HandleAlerts routes alert requests to appropriate handlers

View file

@ -1076,12 +1076,15 @@ func (m *Monitor) pollStorage(ctx context.Context, instanceName string, client P
// Use appropriate node name
nodeID := node.Node
storageID := fmt.Sprintf("%s-%s-%s", instanceName, nodeID, storage.Storage)
if shared {
nodeID = "shared"
// Use a consistent ID for shared storage across all instances
storageID = fmt.Sprintf("shared-%s", storage.Storage)
}
modelStorage := models.Storage{
ID: fmt.Sprintf("%s-%s-%s", instanceName, nodeID, storage.Storage),
ID: storageID,
Name: storage.Storage,
Node: nodeID,
Instance: instanceName,
@ -1500,9 +1503,9 @@ func (m *Monitor) pollStorageBackups(ctx context.Context, instanceName string, c
backupType = "vztmpl"
} else if content.Content == "iso" {
backupType = "iso"
} else if strings.Contains(content.Volid, "qemu") {
} else if strings.Contains(content.Volid, "qemu") || strings.Contains(content.Volid, "/vm/") {
backupType = "qemu"
} else if strings.Contains(content.Volid, "lxc") {
} else if strings.Contains(content.Volid, "lxc") || strings.Contains(content.Volid, "/ct/") {
backupType = "lxc"
}

View file

@ -8,6 +8,7 @@ import (
"net/smtp"
"strings"
"sync"
"text/template"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
@ -69,6 +70,7 @@ type WebhookConfig struct {
Method string `json:"method"`
Headers map[string]string `json:"headers"`
Enabled bool `json:"enabled"`
Service string `json:"service"` // discord, slack, teams, etc.
}
// NewNotificationManager creates a new notification manager
@ -353,23 +355,65 @@ func (n *NotificationManager) sendEmailWithContent(subject, body string, config
// sendGroupedWebhook sends a grouped webhook notification
func (n *NotificationManager) sendGroupedWebhook(webhook WebhookConfig, alertList []*alerts.Alert) {
// Create webhook payload
payload := map[string]interface{}{
"alerts": alertList,
"count": len(alertList),
"timestamp": time.Now().Unix(),
"source": "pulse-monitoring",
"grouped": true,
}
var jsonData []byte
var err error
jsonData, err := json.Marshal(payload)
if err != nil {
log.Error().
Err(err).
Str("webhook", webhook.Name).
Int("alertCount", len(alertList)).
Msg("Failed to marshal grouped webhook payload")
return
// For Discord, send individual embeds for each alert
if webhook.Service == "discord" && len(alertList) > 0 {
// For simplicity, send the first alert with a note about others
// Discord webhooks work better with single embeds
alert := alertList[0]
// Convert to enhanced webhook to use template
enhanced := EnhancedWebhookConfig{
WebhookConfig: webhook,
Service: "discord",
}
// Get Discord template
templates := GetWebhookTemplates()
for _, tmpl := range templates {
if tmpl.Service == "discord" {
enhanced.PayloadTemplate = tmpl.PayloadTemplate
break
}
}
// Modify message if multiple alerts
if len(alertList) > 1 {
alert.Message = fmt.Sprintf("%s (and %d more alerts)", alert.Message, len(alertList)-1)
}
// Prepare data and generate payload
data := n.prepareWebhookData(alert, nil)
jsonData, err = n.generatePayloadFromTemplate(enhanced.PayloadTemplate, data)
if err != nil {
log.Error().
Err(err).
Str("webhook", webhook.Name).
Int("alertCount", len(alertList)).
Msg("Failed to generate Discord payload for grouped alerts")
return
}
} else {
// Use generic payload for other services
payload := map[string]interface{}{
"alerts": alertList,
"count": len(alertList),
"timestamp": time.Now().Unix(),
"source": "pulse-monitoring",
"grouped": true,
}
jsonData, err = json.Marshal(payload)
if err != nil {
log.Error().
Err(err).
Str("webhook", webhook.Name).
Int("alertCount", len(alertList)).
Msg("Failed to marshal grouped webhook payload")
return
}
}
// Send using same request logic
@ -434,27 +478,121 @@ func (n *NotificationManager) sendWebhookRequest(webhook WebhookConfig, jsonData
// sendWebhook sends a webhook notification
func (n *NotificationManager) sendWebhook(webhook WebhookConfig, alert *alerts.Alert) {
// Create webhook payload
payload := map[string]interface{}{
"alert": alert,
"timestamp": time.Now().Unix(),
"source": "pulse-monitoring",
}
var jsonData []byte
var err error
jsonData, err := json.Marshal(payload)
if err != nil {
log.Error().
Err(err).
Str("webhook", webhook.Name).
Str("alertID", alert.ID).
Msg("Failed to marshal webhook payload")
return
// Check if this is a Discord webhook and use the proper template
if webhook.Service == "discord" {
// Convert to enhanced webhook to use template
enhanced := EnhancedWebhookConfig{
WebhookConfig: webhook,
Service: "discord",
}
// Get Discord template
templates := GetWebhookTemplates()
for _, tmpl := range templates {
if tmpl.Service == "discord" {
enhanced.PayloadTemplate = tmpl.PayloadTemplate
break
}
}
// Prepare data and generate payload
data := n.prepareWebhookData(alert, nil)
jsonData, err = n.generatePayloadFromTemplate(enhanced.PayloadTemplate, data)
if err != nil {
log.Error().
Err(err).
Str("webhook", webhook.Name).
Str("alertID", alert.ID).
Msg("Failed to generate Discord payload")
return
}
} else {
// Use generic payload for other services
payload := map[string]interface{}{
"alert": alert,
"timestamp": time.Now().Unix(),
"source": "pulse-monitoring",
}
jsonData, err = json.Marshal(payload)
if err != nil {
log.Error().
Err(err).
Str("webhook", webhook.Name).
Str("alertID", alert.ID).
Msg("Failed to marshal webhook payload")
return
}
}
// Send using common request logic
n.sendWebhookRequest(webhook, jsonData, fmt.Sprintf("alert-%s", alert.ID))
}
// prepareWebhookData prepares data for template rendering
func (n *NotificationManager) prepareWebhookData(alert *alerts.Alert, customFields map[string]interface{}) WebhookPayloadData {
duration := time.Since(alert.StartTime)
return WebhookPayloadData{
ID: alert.ID,
Level: string(alert.Level),
Type: alert.Type,
ResourceName: alert.ResourceName,
ResourceID: alert.ResourceID,
Node: alert.Node,
Instance: alert.Instance,
Message: alert.Message,
Value: alert.Value,
Threshold: alert.Threshold,
StartTime: alert.StartTime.Format(time.RFC3339),
Duration: formatWebhookDuration(duration),
Timestamp: time.Now().Format(time.RFC3339),
CustomFields: customFields,
AlertCount: 1,
}
}
// generatePayloadFromTemplate renders the payload using Go templates
func (n *NotificationManager) generatePayloadFromTemplate(templateStr string, data WebhookPayloadData) ([]byte, error) {
// Create template with helper functions
funcMap := template.FuncMap{
"title": strings.Title,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"printf": fmt.Sprintf,
}
tmpl, err := template.New("webhook").Funcs(funcMap).Parse(templateStr)
if err != nil {
return nil, fmt.Errorf("invalid template: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("template execution failed: %w", err)
}
return buf.Bytes(), nil
}
// formatWebhookDuration formats a duration in a human-readable way
func formatWebhookDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
} else if d < time.Hour {
return fmt.Sprintf("%dm", int(d.Minutes()))
} else if d < 24*time.Hour {
return fmt.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60)
} else {
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
return fmt.Sprintf("%dd %dh", days, hours)
}
}
// groupAlerts groups alerts based on configuration
func (n *NotificationManager) groupAlerts(alertList []*alerts.Alert) map[string][]*alerts.Alert {
groups := make(map[string][]*alerts.Alert)

View file

@ -2,11 +2,8 @@ package notifications
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"text/template"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
@ -85,6 +82,8 @@ func (n *NotificationManager) SendEnhancedWebhook(webhook EnhancedWebhookConfig,
}
// prepareWebhookData prepares data for template rendering
// NOTE: This function is now defined in notifications.go to be shared
/*
func (n *NotificationManager) prepareWebhookData(alert *alerts.Alert, customFields map[string]interface{}) WebhookPayloadData {
duration := time.Since(alert.StartTime)
@ -106,8 +105,11 @@ func (n *NotificationManager) prepareWebhookData(alert *alerts.Alert, customFiel
AlertCount: 1,
}
}
*/
// generatePayloadFromTemplate renders the payload using Go templates
// NOTE: This function is now defined in notifications.go to be shared
/*
func (n *NotificationManager) generatePayloadFromTemplate(templateStr string, data WebhookPayloadData) ([]byte, error) {
// Create template with helper functions
funcMap := template.FuncMap{
@ -135,6 +137,7 @@ func (n *NotificationManager) generatePayloadFromTemplate(templateStr string, da
return buf.Bytes(), nil
}
*/
// shouldSendWebhook checks if alert matches webhook filter rules
func (n *NotificationManager) shouldSendWebhook(webhook EnhancedWebhookConfig, alert *alerts.Alert) bool {
@ -300,6 +303,8 @@ func (n *NotificationManager) sendWebhookOnce(webhook EnhancedWebhookConfig, pay
}
// formatWebhookDuration formats a duration in a human-readable way
// NOTE: This function is now defined in notifications.go to be shared
/*
func formatWebhookDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
@ -313,6 +318,7 @@ func formatWebhookDuration(d time.Duration) string {
return fmt.Sprintf("%dd %dh", days, hours)
}
}
*/
// TestEnhancedWebhook tests a webhook with a specific payload
func (n *NotificationManager) TestEnhancedWebhook(webhook EnhancedWebhookConfig) (int, string, error) {