Files
sultan/pkg/checkmk/checkmk.go
T

205 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (siehe ascii) – auch für andere
// Checkmk-Ausgaben, z. B. die des Accountants.
func ASCII(s string) string { return ascii(s) }
// 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()
}