- Rename project from system-monitor to Tyto (barn owl themed) - Update Go module name and all import paths - Update Docker container names (tyto-backend, tyto-frontend) - Update localStorage keys (tyto-settings, tyto-hosts) - Create barn owl SVG favicon and PWA icons (192, 512) - Update header with owl logo icon - Update manifest.json and app.html with Tyto branding Named after Tyto alba, the barn owl — nature's silent, watchful guardian 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
212 lines
5.4 KiB
Go
212 lines
5.4 KiB
Go
package alerts
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"tyto/internal/models"
|
|
)
|
|
|
|
// Manager handles alert threshold checking and tracking
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
config models.AlertConfig
|
|
activeAlerts map[string]*models.Alert
|
|
alertHistory []models.Alert
|
|
thresholdHits map[string]time.Time // Track when threshold was first exceeded
|
|
maxHistory int
|
|
}
|
|
|
|
// NewManager creates a new alert manager with default config
|
|
func NewManager() *Manager {
|
|
return &Manager{
|
|
config: models.DefaultAlertConfig(),
|
|
activeAlerts: make(map[string]*models.Alert),
|
|
alertHistory: make([]models.Alert, 0),
|
|
thresholdHits: make(map[string]time.Time),
|
|
maxHistory: 100,
|
|
}
|
|
}
|
|
|
|
// GetConfig returns current alert configuration
|
|
func (m *Manager) GetConfig() models.AlertConfig {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.config
|
|
}
|
|
|
|
// SetConfig updates alert configuration
|
|
func (m *Manager) SetConfig(config models.AlertConfig) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.config = config
|
|
}
|
|
|
|
// GetActiveAlerts returns all currently active alerts
|
|
func (m *Manager) GetActiveAlerts() []models.Alert {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
alerts := make([]models.Alert, 0, len(m.activeAlerts))
|
|
for _, alert := range m.activeAlerts {
|
|
alerts = append(alerts, *alert)
|
|
}
|
|
return alerts
|
|
}
|
|
|
|
// GetAlertHistory returns recent alert history
|
|
func (m *Manager) GetAlertHistory() []models.Alert {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
history := make([]models.Alert, len(m.alertHistory))
|
|
copy(history, m.alertHistory)
|
|
return history
|
|
}
|
|
|
|
// AcknowledgeAlert marks an alert as acknowledged
|
|
func (m *Manager) AcknowledgeAlert(alertID string) bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Search by alert ID field, not map key
|
|
for _, alert := range m.activeAlerts {
|
|
if alert.ID == alertID {
|
|
alert.Acknowledged = true
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CheckMetrics evaluates metrics against thresholds and triggers/resolves alerts
|
|
func (m *Manager) CheckMetrics(metrics models.AllMetrics) []models.Alert {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
var newAlerts []models.Alert
|
|
now := time.Now()
|
|
|
|
for _, threshold := range m.config.Thresholds {
|
|
if !threshold.Enabled {
|
|
continue
|
|
}
|
|
|
|
var value float64
|
|
var label string
|
|
|
|
switch threshold.Type {
|
|
case models.AlertTypeCPU:
|
|
value = metrics.CPU.TotalUsage
|
|
label = "CPU Usage"
|
|
case models.AlertTypeMemory:
|
|
if metrics.Memory.Total > 0 {
|
|
value = float64(metrics.Memory.Used) / float64(metrics.Memory.Total) * 100
|
|
}
|
|
label = "Memory Usage"
|
|
case models.AlertTypeTemperature:
|
|
// Find max temperature
|
|
for _, sensor := range metrics.Temperature.Sensors {
|
|
if sensor.Temperature > value {
|
|
value = sensor.Temperature
|
|
label = fmt.Sprintf("Temperature (%s)", sensor.Name)
|
|
}
|
|
}
|
|
if label == "" {
|
|
label = "Temperature"
|
|
}
|
|
case models.AlertTypeDisk:
|
|
// Find max disk usage
|
|
for _, mount := range metrics.Disk.Mounts {
|
|
if mount.UsedPercent > value {
|
|
value = mount.UsedPercent
|
|
label = fmt.Sprintf("Disk (%s)", mount.MountPoint)
|
|
}
|
|
}
|
|
if label == "" {
|
|
label = "Disk Usage"
|
|
}
|
|
case models.AlertTypeGPU:
|
|
if metrics.GPU.Available {
|
|
value = float64(metrics.GPU.Utilization)
|
|
label = "GPU Usage"
|
|
}
|
|
}
|
|
|
|
// Check thresholds
|
|
alertKey := string(threshold.Type)
|
|
var severity models.AlertSeverity
|
|
var thresholdValue float64
|
|
|
|
if value >= threshold.CriticalValue {
|
|
severity = models.AlertSeverityCritical
|
|
thresholdValue = threshold.CriticalValue
|
|
} else if value >= threshold.WarningValue {
|
|
severity = models.AlertSeverityWarning
|
|
thresholdValue = threshold.WarningValue
|
|
} else {
|
|
// Value is below thresholds - resolve any active alert
|
|
delete(m.thresholdHits, alertKey)
|
|
if alert, exists := m.activeAlerts[alertKey]; exists {
|
|
resolvedAt := now
|
|
alert.ResolvedAt = &resolvedAt
|
|
m.addToHistory(*alert)
|
|
delete(m.activeAlerts, alertKey)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Check duration requirement
|
|
firstHit, hasHit := m.thresholdHits[alertKey]
|
|
if !hasHit {
|
|
m.thresholdHits[alertKey] = now
|
|
if threshold.DurationSeconds > 0 {
|
|
continue // Wait for duration
|
|
}
|
|
firstHit = now
|
|
}
|
|
|
|
elapsed := now.Sub(firstHit)
|
|
if elapsed.Seconds() < float64(threshold.DurationSeconds) {
|
|
continue // Duration not met yet
|
|
}
|
|
|
|
// Check if we need to create/update alert
|
|
existingAlert, alertExists := m.activeAlerts[alertKey]
|
|
|
|
if !alertExists {
|
|
// Create new alert
|
|
alert := &models.Alert{
|
|
ID: fmt.Sprintf("%s-%d", threshold.Type, now.Unix()),
|
|
Type: threshold.Type,
|
|
Severity: severity,
|
|
Message: fmt.Sprintf("%s is at %.1f%% (threshold: %.1f%%)", label, value, thresholdValue),
|
|
Value: value,
|
|
Threshold: thresholdValue,
|
|
TriggeredAt: now,
|
|
}
|
|
m.activeAlerts[alertKey] = alert
|
|
newAlerts = append(newAlerts, *alert)
|
|
} else if existingAlert.Severity != severity {
|
|
// Severity changed - update alert
|
|
existingAlert.Severity = severity
|
|
existingAlert.Value = value
|
|
existingAlert.Threshold = thresholdValue
|
|
existingAlert.Message = fmt.Sprintf("%s is at %.1f%% (threshold: %.1f%%)", label, value, thresholdValue)
|
|
} else {
|
|
// Just update value
|
|
existingAlert.Value = value
|
|
}
|
|
}
|
|
|
|
return newAlerts
|
|
}
|
|
|
|
func (m *Manager) addToHistory(alert models.Alert) {
|
|
m.alertHistory = append([]models.Alert{alert}, m.alertHistory...)
|
|
if len(m.alertHistory) > m.maxHistory {
|
|
m.alertHistory = m.alertHistory[:m.maxHistory]
|
|
}
|
|
}
|