feat(scheduler): NTP-Sync-Alert — warnt wenn chrony keine Zeitquelle hat — v1.1.109
- runNTPSyncCheck() läuft alle 10 Minuten: ruft chronyc tracking auf, prüft ob Stratum 0 / ≥16 oder Reference ID 00000000 — feuert Warning mit Fix-Hints (1h dedupe) - Initial-Check absichtlich NICHT beim Boot, da chrony nach dem Start einige Sekunden zur ersten Synchronisation braucht - ntp.unsync-Trigger in beiden i18n-Dateien dokumentiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ import (
|
||||
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
||||
)
|
||||
|
||||
var version = "1.1.108"
|
||||
var version = "1.1.109"
|
||||
|
||||
func main() {
|
||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
||||
)
|
||||
|
||||
var version = "1.1.108"
|
||||
var version = "1.1.109"
|
||||
|
||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -40,7 +41,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||
)
|
||||
|
||||
var version = "1.1.108"
|
||||
var version = "1.1.109"
|
||||
|
||||
const (
|
||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||
@@ -124,6 +125,14 @@ const (
|
||||
conntrackCheckInterval = 2 * time.Minute
|
||||
conntrackWarnPct = 80.0
|
||||
conntrackCriticalPct = 90.0
|
||||
|
||||
// ntpSyncCheckInterval — alle 10 Minuten chronyc tracking aufrufen.
|
||||
// Keine Sync bedeutet: Uhr driftet → TLS-Cert-Prüfung schlägt fehl
|
||||
// wenn die Abweichung > Toleranz des Gegenstücks (i.d.R. ±1 min),
|
||||
// JWT-Ablauf inkonsistent, Cluster-Split-Brain möglich. Dedupe 1h
|
||||
// damit ein kurzer Upstream-Ausfall (Reboot, DHCP-Pause) keinen
|
||||
// Alert-Regen produziert.
|
||||
ntpSyncCheckInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -221,6 +230,12 @@ func main() {
|
||||
defer conntrackTick.Stop()
|
||||
runConntrackCheck(ctx, alertSvc, alertDedupe)
|
||||
|
||||
ntpSyncTick := time.NewTicker(ntpSyncCheckInterval)
|
||||
defer ntpSyncTick.Stop()
|
||||
// Kein Initial-Check bei Boot: chrony braucht nach dem Start
|
||||
// einige Sekunden bis zur ersten Synchronisation — ein
|
||||
// sofortiger Check würde immer feuern.
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-renewTick.C:
|
||||
@@ -250,6 +265,8 @@ func main() {
|
||||
runMemoryCheck(ctx, alertSvc, alertDedupe)
|
||||
case <-conntrackTick.C:
|
||||
runConntrackCheck(ctx, alertSvc, alertDedupe)
|
||||
case <-ntpSyncTick.C:
|
||||
runNTPSyncCheck(ctx, alertSvc, alertDedupe)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,6 +491,78 @@ func runConntrackCheck(ctx context.Context, a *alerts.Service, d *dedupe) {
|
||||
}
|
||||
}
|
||||
|
||||
// runNTPSyncCheck ruft chronyc tracking auf und feuert einen Alert wenn
|
||||
// chrony keine synchronisierte Zeitquelle hat (Stratum 0 oder ≥ 16).
|
||||
// Zeitdrift > ~1 Minute führt zu TLS-Handshake-Fehlern, JWT-Ablauf-
|
||||
// Inkonsistenzen und möglichen Cluster-Problemen. Dedupe 1h.
|
||||
func runNTPSyncCheck(ctx context.Context, a *alerts.Service, d *dedupe) {
|
||||
if a == nil || d == nil {
|
||||
return
|
||||
}
|
||||
out, err := exec.Command("chronyc", "tracking").Output()
|
||||
if err != nil {
|
||||
// chrony nicht installiert oder nicht gestartet — kein Alert,
|
||||
// weil wir nicht wissen ob chrony hier überhaupt erwartet wird.
|
||||
return
|
||||
}
|
||||
synced, stratum, ref := parseChronyTrackingForAlert(string(out))
|
||||
if synced {
|
||||
return
|
||||
}
|
||||
const key = "ntp.unsync"
|
||||
if !d.shouldFire(key) {
|
||||
return
|
||||
}
|
||||
refStr := ref
|
||||
if refStr == "" {
|
||||
refStr = "(keine Referenz)"
|
||||
}
|
||||
title := fmt.Sprintf("NTP nicht synchronisiert (Stratum %d)", stratum)
|
||||
desc := fmt.Sprintf(
|
||||
"chrony hat keine synchronisierte Zeitquelle.\n"+
|
||||
"Referenz: %s Stratum: %d\n\n"+
|
||||
"Mögliche Ursachen:\n"+
|
||||
" • Upstream-NTP-Server nicht erreichbar (UDP/123 blockiert?)\n"+
|
||||
" • Pool-DNS-Einträge lösen nicht auf\n"+
|
||||
" • chrony läuft, braucht aber noch Zeit nach Boot (warten)\n\n"+
|
||||
"Prüfen: chronyc sources -v — chronyc tracking",
|
||||
refStr, stratum)
|
||||
if _, err := a.Fire(ctx, "ntp.unsync", alerts.SeverityWarning, title, desc); err != nil {
|
||||
slog.Warn("scheduler: ntp-sync-check alert fire failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parseChronyTrackingForAlert ist eine schlanke Variante des NTP-Handler-
|
||||
// Parsers: liefert nur synced/stratum/reference ohne die vollen Felder.
|
||||
func parseChronyTrackingForAlert(out string) (synced bool, stratum int, reference string) {
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
key, val, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
val = strings.TrimSpace(val)
|
||||
switch key {
|
||||
case "Reference ID":
|
||||
if i := strings.Index(val, "("); i >= 0 {
|
||||
reference = strings.Trim(val[i:], "()")
|
||||
}
|
||||
if val != "00000000 ()" {
|
||||
synced = true
|
||||
}
|
||||
case "Stratum":
|
||||
fmt.Sscanf(val, "%d", &stratum)
|
||||
if stratum > 0 && stratum < 16 {
|
||||
synced = true
|
||||
} else if stratum == 0 || stratum >= 16 {
|
||||
synced = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var egBackendRE = regexp.MustCompile(`^eg_backend_(\d+)$`)
|
||||
|
||||
// runBackendDownCheck liest HAProxy-Stats via Admin-Socket und feuert
|
||||
|
||||
Reference in New Issue
Block a user