mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 18:44:53 +00:00
Fix multi-tenant persistence and backend stability
- Initialize Alert and Notification managers with tenant-specific data directories - Add panic recovery to WebSocket safeSend for stability - Record host metrics to history for sparkline support
This commit is contained in:
parent
bea3bbe5f6
commit
aeca5e39fa
4 changed files with 70 additions and 8 deletions
|
|
@ -560,9 +560,16 @@ type dockerRestartRecord struct {
|
|||
lastChecked time.Time
|
||||
}
|
||||
|
||||
// NewManager creates a new alert manager
|
||||
// NewManager creates a new alert manager using the global data directory.
|
||||
// For multi-tenant deployments, use NewManagerWithDataDir instead.
|
||||
func NewManager() *Manager {
|
||||
alertsDir := filepath.Join(utils.GetDataDir(), "alerts")
|
||||
return NewManagerWithDataDir(utils.GetDataDir())
|
||||
}
|
||||
|
||||
// NewManagerWithDataDir creates a new alert manager with a custom data directory.
|
||||
// This enables tenant-scoped alert persistence in multi-tenant deployments.
|
||||
func NewManagerWithDataDir(dataDir string) *Manager {
|
||||
alertsDir := filepath.Join(dataDir, "alerts")
|
||||
alertOrphaned := true
|
||||
m := &Manager{
|
||||
activeAlerts: make(map[string]*Alert),
|
||||
|
|
|
|||
|
|
@ -2662,6 +2662,41 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
|
|||
m.alertManager.CheckHost(host)
|
||||
}
|
||||
|
||||
// Record Host metrics for sparkline charts
|
||||
now := time.Now()
|
||||
hostMetricKey := fmt.Sprintf("host:%s", host.ID)
|
||||
|
||||
// Record host CPU usage
|
||||
m.metricsHistory.AddGuestMetric(hostMetricKey, "cpu", host.CPUUsage, now)
|
||||
|
||||
// Record host Memory usage
|
||||
m.metricsHistory.AddGuestMetric(hostMetricKey, "memory", host.Memory.Usage, now)
|
||||
|
||||
// Record host Disk usage (use first disk or calculate aggregate)
|
||||
var hostDiskPercent float64
|
||||
if len(host.Disks) > 0 {
|
||||
hostDiskPercent = host.Disks[0].Usage
|
||||
}
|
||||
m.metricsHistory.AddGuestMetric(hostMetricKey, "disk", hostDiskPercent, now)
|
||||
|
||||
// Record host Network I/O (sum across all interfaces)
|
||||
var totalRXBytes, totalTXBytes uint64
|
||||
for _, nic := range host.NetworkInterfaces {
|
||||
totalRXBytes += nic.RXBytes
|
||||
totalTXBytes += nic.TXBytes
|
||||
}
|
||||
m.metricsHistory.AddGuestMetric(hostMetricKey, "netin", float64(totalRXBytes), now)
|
||||
m.metricsHistory.AddGuestMetric(hostMetricKey, "netout", float64(totalTXBytes), now)
|
||||
|
||||
// Also write to persistent SQLite store
|
||||
if m.metricsStore != nil {
|
||||
m.metricsStore.Write("host", host.ID, "cpu", host.CPUUsage, now)
|
||||
m.metricsStore.Write("host", host.ID, "memory", host.Memory.Usage, now)
|
||||
m.metricsStore.Write("host", host.ID, "disk", hostDiskPercent, now)
|
||||
m.metricsStore.Write("host", host.ID, "netin", float64(totalRXBytes), now)
|
||||
m.metricsStore.Write("host", host.ID, "netout", float64(totalTXBytes), now)
|
||||
}
|
||||
|
||||
return host, nil
|
||||
}
|
||||
|
||||
|
|
@ -3600,9 +3635,9 @@ func New(cfg *config.Config) (*Monitor, error) {
|
|||
rateTracker: NewRateTracker(),
|
||||
metricsHistory: NewMetricsHistory(1000, 24*time.Hour), // Keep up to 1000 points or 24 hours
|
||||
metricsStore: metricsStore, // Persistent SQLite storage
|
||||
alertManager: alerts.NewManager(),
|
||||
alertManager: alerts.NewManagerWithDataDir(cfg.DataPath),
|
||||
incidentStore: incidentStore,
|
||||
notificationMgr: notifications.NewNotificationManager(cfg.PublicURL),
|
||||
notificationMgr: notifications.NewNotificationManagerWithDataDir(cfg.PublicURL, cfg.DataPath),
|
||||
configPersist: config.NewConfigPersistence(cfg.DataPath),
|
||||
discoveryService: nil, // Will be initialized in Start()
|
||||
authFailures: make(map[string]int),
|
||||
|
|
|
|||
|
|
@ -343,8 +343,16 @@ type AppriseConfig struct {
|
|||
SkipTLSVerify bool `json:"skipTlsVerify,omitempty"`
|
||||
}
|
||||
|
||||
// NewNotificationManager creates a new notification manager
|
||||
// NewNotificationManager creates a new notification manager using the global data directory.
|
||||
// For multi-tenant deployments, use NewNotificationManagerWithDataDir instead.
|
||||
func NewNotificationManager(publicURL string) *NotificationManager {
|
||||
return NewNotificationManagerWithDataDir(publicURL, "")
|
||||
}
|
||||
|
||||
// NewNotificationManagerWithDataDir creates a new notification manager with a custom data directory.
|
||||
// This enables tenant-scoped notification queue persistence in multi-tenant deployments.
|
||||
// If dataDir is empty, it uses the global data directory.
|
||||
func NewNotificationManagerWithDataDir(publicURL string, dataDir string) *NotificationManager {
|
||||
cleanURL := strings.TrimRight(strings.TrimSpace(publicURL), "/")
|
||||
if cleanURL != "" {
|
||||
log.Info().Str("publicURL", cleanURL).Msg("NotificationManager initialized with public URL")
|
||||
|
|
@ -352,8 +360,8 @@ func NewNotificationManager(publicURL string) *NotificationManager {
|
|||
log.Info().Msg("NotificationManager initialized without public URL - webhook links may not work")
|
||||
}
|
||||
|
||||
// Initialize persistent queue
|
||||
queue, err := NewNotificationQueue("")
|
||||
// Initialize persistent queue with tenant-specific data directory
|
||||
queue, err := NewNotificationQueue(dataDir)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to initialize notification queue, notifications will be in-memory only")
|
||||
queue = nil
|
||||
|
|
|
|||
|
|
@ -185,10 +185,22 @@ type Client struct {
|
|||
|
||||
// safeSend attempts to send data to the client's send channel.
|
||||
// Returns false if the client is closed or the channel buffer is full.
|
||||
func (c *Client) safeSend(data []byte) bool {
|
||||
// Uses defer/recover to handle the race between close(c.send) and send.
|
||||
func (c *Client) safeSend(data []byte) (sent bool) {
|
||||
// Early check to avoid most attempts on closed clients.
|
||||
if c.closed.Load() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Recover from panic if the channel was closed between the check above
|
||||
// and the send below. This is a defensive pattern to prevent server crashes.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Channel was closed concurrently; mark as not sent.
|
||||
sent = false
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case c.send <- data:
|
||||
return true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue