diff --git a/VERSION b/VERSION index 67452e4..8c43a3a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.106 +1.1.107 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 245a4e5..4dc65a0 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -60,7 +60,7 @@ import ( usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" ) -var version = "1.1.106" +var version = "1.1.107" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index 2223dba..d98a00a 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -11,7 +11,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) -var version = "1.1.106" +var version = "1.1.107" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index 3b73b65..d47d1ad 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -40,7 +40,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.106" +var version = "1.1.107" const ( // renewTickInterval — how often we re-evaluate expiring certs. @@ -108,6 +108,13 @@ const ( // Dedupe 12h pro Backend → kein Alert-Spam. Frischer Alert wenn das // Backend nach 12h immer noch unten ist. backendDownCheckInterval = 2 * time.Minute + + // memCheckInterval — alle 5 Minuten /proc/meminfo lesen. Schwellen + // warning 85%, critical 95%. Dedupe 1h pro Severity damit bei einem + // kurzfristigen Spike nicht jeder Tick feuert. + memCheckInterval = 5 * time.Minute + memWarnPct = 85.0 + memCriticalPct = 95.0 ) func main() { @@ -197,6 +204,10 @@ func main() { // Scheduler-Restart down ist, brauchen wir nicht 2 Minuten zu warten. runBackendDownCheck(ctx, pool, alertSvc, alertDedupe) + memTick := time.NewTicker(memCheckInterval) + defer memTick.Stop() + runMemoryCheck(ctx, alertSvc, alertDedupe) + for { select { case <-renewTick.C: @@ -222,6 +233,8 @@ func main() { runAuditCleanup(ctx, auditRepo, setupStore) case <-backendDownTick.C: runBackendDownCheck(ctx, pool, alertSvc, alertDedupe) + case <-memTick.C: + runMemoryCheck(ctx, alertSvc, alertDedupe) } } } @@ -326,6 +339,68 @@ func runDiskCheck(ctx context.Context, a *alerts.Service, d *dedupe) { // remaining hat UND eine lokale CA existiert, wird automatisch neu // signiert. Restart-Hinweis als Info-Alert — wir starten edgeguard-api // nicht selbst neu, das passiert beim nächsten geplanten Update/Reboot. +// runMemoryCheck liest /proc/meminfo und feuert bei hoher RAM-Belegung. +// Schwellen: warning >= 85%, critical >= 95%. Dedupe 1h pro Severity +// damit kurze Spikes (Backup, apt-Upgrade) keine Alert-Flut erzeugen. +func runMemoryCheck(ctx context.Context, a *alerts.Service, d *dedupe) { + if a == nil || d == nil { + return + } + data, err := os.ReadFile("/proc/meminfo") + if err != nil { + return + } + var memTotal, memAvail int64 + for _, line := range strings.Split(string(data), "\n") { + var key string + var val int64 + if _, err := fmt.Sscanf(line, "%s %d", &key, &val); err != nil { + continue + } + switch key { + case "MemTotal:": + memTotal = val + case "MemAvailable:": + memAvail = val + } + } + if memTotal <= 0 { + return + } + usedPct := float64(memTotal-memAvail) * 100 / float64(memTotal) + usedGB := float64(memTotal-memAvail) / 1024 / 1024 + totalGB := float64(memTotal) / 1024 / 1024 + + var key, title string + var sev alerts.Severity + switch { + case usedPct >= memCriticalPct: + key = "mem.high.critical" + sev = alerts.SeverityError + title = fmt.Sprintf("RAM kritisch hoch: %.0f%%", usedPct) + case usedPct >= memWarnPct: + key = "mem.high.warning" + sev = alerts.SeverityWarning + title = fmt.Sprintf("RAM-Belegung hoch: %.0f%%", usedPct) + default: + return + } + if !d.shouldFire(key) { + return + } + desc := fmt.Sprintf( + "RAM-Auslastung: %.1f%% — %.1f von %.1f GB belegt.\n\n"+ + "Häufige Ursachen:\n"+ + " • Unbound-Cache zu groß (rrset-cache-size in /etc/edgeguard/unbound/unbound.conf)\n"+ + " • Squid cache_mem zu groß (64 MB default)\n"+ + " • PostgreSQL shared_buffers (default ~128 MB)\n"+ + " • Prozesse prüfen: ps aux --sort=-%mem | head -10", + usedPct, usedGB, totalGB) + if _, err := a.Fire(ctx, "mem.high", sev, title, desc); err != nil { + slog.Warn("scheduler: memory-check alert fire failed", "error", err) + } +} + var egBackendRE = regexp.MustCompile(`^eg_backend_(\d+)$`) // runBackendDownCheck liest HAProxy-Stats via Admin-Socket und feuert diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 95dbb6c..1fde6a5 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -837,6 +837,9 @@ "stratum": "Stratum", "offset": "Offset", "offsetHint": "Zeitdifferenz zur Referenzquelle. Werte > 100 ms sind ungewöhnlich — Netzwerkprobleme oder fehlkonfigurierte Quelle prüfen.", + "freqPpm": "Frequenzfehler", + "freqPpmHint": "Frequenzabweichung der lokalen Uhr zur Referenz. Werte > ±100 ppm weisen auf eine driftende Uhr oder einen Hardware-Defekt hin.", + "rmsOffset": "RMS-Offset", "loading": "Lade…" }, "pool": { @@ -1116,7 +1119,7 @@ "title": "Health-Alarme", "intro": "Notification-Channels für kritische Events. Webhook (Slack/Discord/Teams/Generic-HTTP) oder Email (SMTP). Triggers: cert.expiring (<14 d), cert.renew_failed, backup.failed, license.invalid.", "scopeTitle": "Was triggert Alarme?", - "scopeDesc": "cert.expiring — TLS-Zertifikat <14 Tage Restzeit (dedupe 12h). cert.renew_failed — ACME-Renewer hat Fails. backup.failed — Scheduled Backup konnte nicht erstellt werden. license.invalid — License-Server liefert valid=false. backend.down — alle Server eines Backend-Pools sind DOWN (2-Min-Check, dedupe 12h). disk.full — Root-Filesystem ≥80% Warnung, ≥90% Critical (stündlich, dedupe 12h).", + "scopeDesc": "cert.expiring — TLS-Zertifikat <14 Tage Restzeit (dedupe 12h). cert.renew_failed — ACME-Renewer hat Fails. backup.failed — Scheduled Backup konnte nicht erstellt werden. license.invalid — License-Server liefert valid=false. backend.down — alle Server eines Backend-Pools sind DOWN (2-Min-Check, dedupe 12h). disk.full — Root-Filesystem ≥80% Warnung, ≥90% Critical (stündlich, dedupe 12h). mem.high — RAM-Auslastung ≥85% Warnung, ≥95% Critical (5-Min-Check, dedupe 1h).", "tabs": { "channels": "Channels", "events": "History" }, "add": "Channel hinzufügen", "addTitle": "Notification-Channel anlegen", diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index b479c95..d36c692 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -837,6 +837,9 @@ "stratum": "Stratum", "offset": "Offset", "offsetHint": "Time difference to reference source. Values > 100 ms are unusual — check network or misconfigured source.", + "freqPpm": "Freq. error", + "freqPpmHint": "Frequency error of the local clock vs. reference. Values > ±100 ppm indicate a drifting clock or hardware issue.", + "rmsOffset": "RMS offset", "loading": "Loading…" }, "pool": { @@ -1116,7 +1119,7 @@ "title": "Health alerts", "intro": "Notification channels for critical events. Webhook (Slack/Discord/Teams/generic-HTTP) or email (SMTP). Triggers: cert.expiring (<14 d), cert.renew_failed, backup.failed, license.invalid.", "scopeTitle": "What triggers alerts?", - "scopeDesc": "cert.expiring — TLS cert <14 days remaining (12 h dedupe). cert.renew_failed — ACME renewer cycle had failures. backup.failed — scheduled backup couldn't run. license.invalid — License server returns valid=false. backend.down — all servers in a backend pool are DOWN (2 min check, 12 h dedupe). disk.full — root filesystem ≥80% warning, ≥90% critical (hourly check, 12 h dedupe).", + "scopeDesc": "cert.expiring — TLS cert <14 days remaining (12 h dedupe). cert.renew_failed — ACME renewer cycle had failures. backup.failed — scheduled backup couldn't run. license.invalid — License server returns valid=false. backend.down — all servers in a backend pool are DOWN (2 min check, 12 h dedupe). disk.full — root filesystem ≥80% warning, ≥90% critical (hourly check, 12 h dedupe). mem.high — RAM usage ≥85% warning, ≥95% critical (5 min check, 1 h dedupe).", "tabs": { "channels": "Channels", "events": "History" }, "add": "Add channel", "addTitle": "Add notification channel", diff --git a/management-ui/src/pages/NTP/index.tsx b/management-ui/src/pages/NTP/index.tsx index 510677c..31ffe7d 100644 --- a/management-ui/src/pages/NTP/index.tsx +++ b/management-ui/src/pages/NTP/index.tsx @@ -161,6 +161,29 @@ export default function NTPPage() { /> + {ntpStatus.freq_ppm != null && ( +