mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
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:
parent
9b72c26994
commit
a368d3b3c9
5 changed files with 186 additions and 39 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue