software steht so weit.

jetzt build + docker build und testen
This commit is contained in:
br committed 2026-10-02 13:25:34 +02:00
1 parent 42f0c0accb
commit ca6cdcca63
90 files changed
+10298 -480

No files matched your search

+200
View File
@@ -0,0 +1,200 @@
// Package checkmk erzeugt die Ausgabe für Checkmk im Local-Check-Format
// (Spec 6.1): `<status> "<service>" <metriken> <text>` mit 0 OK, 1 WARN,
// 2 CRIT. Der Text ist ASCII (Umlaute umschrieben), einzeilig.
package checkmk
import (
"fmt"
"math"
"slices"
"strings"
"time"
)
// Schwellen (Spec 6.2 / Entscheidungen 2026-10-02).
const (
ContractWarnDays = 30
ContractCritDays = 14
OfflineCritAfter = 15 * time.Minute
)
const (
statusOK = 0
statusW = 1
statusCrit = 2
)
// Tenant ist ein Mandant aus Sicht des Monitorings.
type Tenant struct {
ID string
Commercial bool // Community: keine Vertragsfristen, kein Mahnwesen
Status string
ValidUntil time.Time
DunningLevel int
OverdueSince time.Time
}
// Node ist ein Knoten aus Sicht des Monitorings.
type Node struct {
ID string
Status string // online, offline, drained
OfflineSince time.Time
MaintenanceActive bool
MaintenanceUntil time.Time // Nullwert = unbefristet
MaintenanceReason string // intern – Monitoring ist intern
LeaseUntil time.Time // Nullwert = keine Lease
}
// State ist alles, was ausgegeben wird.
type State struct {
Tenants []Tenant
Nodes []Node
Violations24h int
// OfflineCrit: ab wann ein getrennter Knoten CRIT ist (Default 15 min).
OfflineCrit time.Duration
// Location: Zeitzone für angezeigte Zeitpunkte (Default UTC).
Location *time.Location
}
var label = map[int]string{statusOK: "OK", statusW: "WARN", statusCrit: "CRIT"}
const day = 24 * time.Hour
// Render liefert die Ausgabe, sortiert nach Mandant bzw. Knoten.
func Render(s State, now time.Time) string {
var b strings.Builder
line := func(status int, service, metrics, text string) {
fmt.Fprintf(&b, "%d %q %s %s - %s\n", status, service, metrics, label[status], ascii(text))
}
tenants := slices.Clone(s.Tenants)
slices.SortFunc(tenants, func(a, b Tenant) int { return strings.Compare(a.ID, b.ID) })
for _, t := range tenants {
if !t.Commercial {
continue
}
renderContract(line, t, now)
renderPayment(line, t, now)
}
nodes := slices.Clone(s.Nodes)
slices.SortFunc(nodes, func(a, b Node) int { return strings.Compare(a.ID, b.ID) })
crit := s.OfflineCrit
if crit <= 0 {
crit = OfflineCritAfter
}
loc := s.Location
if loc == nil {
loc = time.UTC
}
for _, n := range nodes {
renderNode(line, n, now, crit, loc)
}
if s.Violations24h == 0 {
line(statusOK, "Sultan_Policy_Violations", "violations_24h=0;1", "keine Policy-Verstoesse in 24 h")
} else {
line(statusW, "Sultan_Policy_Violations", fmt.Sprintf("violations_24h=%d;1", s.Violations24h),
fmt.Sprintf("%d Policy-Verstoesse in 24 h (siehe Server-Log)", s.Violations24h))
}
return b.String()
}
func renderContract(line func(int, string, string, string), t Tenant, now time.Time) {
svc := "Sultan_Tenant_" + t.ID
days := int(math.Floor(t.ValidUntil.Sub(now).Hours() / 24))
metrics := func(d int) string {
return fmt.Sprintf("days_left=%d;%d;%d", max(d, 0), ContractWarnDays, ContractCritDays)
}
switch {
case t.Status == "expired" || !now.Before(t.ValidUntil):
since := int(math.Ceil(now.Sub(t.ValidUntil).Hours() / 24))
line(statusCrit, svc, metrics(0), fmt.Sprintf("Abo seit %d Tagen abgelaufen! Status: %s", since, t.Status))
case t.Status == "quarantined":
line(statusCrit, svc, metrics(days), "Mandant in Quarantaene")
case days <= ContractCritDays:
line(statusCrit, svc, metrics(days), fmt.Sprintf("Abo laeuft in %d Tagen ab! Verlaengerung erforderlich.", days))
case days <= ContractWarnDays:
line(statusW, svc, metrics(days), fmt.Sprintf("Abo laeuft in %d Tagen ab! Verlaengerung erforderlich.", days))
default:
line(statusOK, svc, metrics(days), fmt.Sprintf("Abo aktiv bis %s (%d Tage verbleibend)", t.ValidUntil.UTC().Format(time.DateOnly), days))
}
}
func renderPayment(line func(int, string, string, string), t Tenant, now time.Time) {
svc := "Sultan_Payment_" + t.ID
metrics := fmt.Sprintf("dunning_level=%d;1;2", t.DunningLevel)
if t.DunningLevel == 0 {
line(statusOK, svc, metrics, "keine offene Zahlung")
return
}
text := fmt.Sprintf("Mahnstufe %d (Zahlung seit %d Tagen offen)", t.DunningLevel, int(now.Sub(t.OverdueSince)/day))
if t.DunningLevel >= 3 {
text += " - Kunde offline"
}
status := statusW
if t.DunningLevel >= 2 {
status = statusCrit
}
line(status, svc, metrics, text)
}
func renderNode(line func(int, string, string, string), n Node, now time.Time, offlineCrit time.Duration, loc *time.Location) {
svc := "Sultan_Node_" + n.ID
leaseHours := 0
if n.LeaseUntil.After(now) {
leaseHours = int(n.LeaseUntil.Sub(now) / time.Hour)
}
metrics := fmt.Sprintf("lease_hours_left=%d", leaseHours)
switch {
case n.Status == "drained":
line(statusOK, svc, metrics, "drained")
case n.MaintenanceActive:
text := "Wartung unbefristet"
if !n.MaintenanceUntil.IsZero() {
text = "Wartung bis " + n.MaintenanceUntil.In(loc).Format("2006-01-02 15:04 MST")
}
if n.MaintenanceReason != "" {
text += " (" + n.MaintenanceReason + ")"
}
line(statusW, svc, metrics, text)
case n.Status == "offline":
since := now.Sub(n.OfflineSince)
status := statusW
if since >= offlineCrit {
status = statusCrit
}
line(status, svc, metrics, "offline seit "+shortDuration(since))
default:
line(statusOK, svc, metrics, "online")
}
}
// shortDuration: unter einer Minute in Sekunden, sonst in Minuten.
func shortDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%d s", int(d/time.Second))
}
return fmt.Sprintf("%d min", int(d/time.Minute))
}
var umlauts = strings.NewReplacer("ä", "ae", "ö", "oe", "ü", "ue", "Ä", "Ae", "Ö", "Oe", "Ü", "Ue", "ß", "ss")
// ascii macht Text Checkmk-tauglich: einzeilig, Umlaute umschrieben,
// übrige Nicht-ASCII-Zeichen ersetzt.
func ascii(s string) string {
s = umlauts.Replace(s)
var b strings.Builder
for _, r := range s {
switch {
case r == '\n' || r == '\r' || r == '\t':
b.WriteByte(' ')
case r < 32 || r == 127:
case r > 127:
b.WriteByte('?')
default:
b.WriteRune(r)
}
}
return b.String()
}