feat(dashboard): HAProxy Listener-Stats + i18n-Relativzeit
- haproxy_stats.go: FRONTEND-Rows aus show-stat CSV auslesen und als frontends[] zurückgeben (Name, Sessions, MaxSess, Bytes, ReqTot/Rate) - Dashboard: Listener-Sektion über Backend-Liste; query jetzt unified haproxyStats mit backends/frontends alias für Abwärtskompatibilität - common.relTime.*-Keys (en+de) eingeführt; relativeTime/relativeFromIso in Dashboard + relTime in WireGuard Servers/Clients auf t-Parameter umgestellt statt hardcodierter Strings - CertExpiry in Cluster-Seite nutzt jetzt certDaysRemaining/certExpiredDaysAgo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,18 +44,31 @@ type backendStat struct {
|
||||
Health string `json:"health,omitempty"` // check_status (e.g. L7OK)
|
||||
}
|
||||
|
||||
// frontendStat holds per-listener traffic counters from HAProxy's
|
||||
// show-stat CSV. Gives the operator a global view of how much HTTP(S)
|
||||
// traffic is flowing through the gateway.
|
||||
type frontendStat struct {
|
||||
Name string `json:"name"` // pxname (e.g. "https_in", "http_in")
|
||||
Sessions int64 `json:"sessions"` // current sessions (scur)
|
||||
MaxSess int64 `json:"max_sess"` // maximum concurrent sessions since start (smax)
|
||||
BIn int64 `json:"bytes_in"`
|
||||
BOut int64 `json:"bytes_out"`
|
||||
ReqTot int64 `json:"req_tot"` // total requests since start
|
||||
ReqRate int64 `json:"req_rate"` // requests/s last second
|
||||
}
|
||||
|
||||
func (h *HAProxyStatsHandler) Stats(c *gin.Context) {
|
||||
conn, err := net.DialTimeout("unix", haproxyAdminSock, 2*time.Second)
|
||||
if err != nil {
|
||||
// Socket nicht erreichbar (haproxy down oder no perm) →
|
||||
// leere Liste statt 500 damit das Dashboard nicht rot wird.
|
||||
response.OK(c, gin.H{"backends": []backendStat{}, "error": err.Error()})
|
||||
response.OK(c, gin.H{"backends": []backendStat{}, "frontends": []frontendStat{}, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
if _, err := conn.Write([]byte("show stat\n")); err != nil {
|
||||
response.OK(c, gin.H{"backends": []backendStat{}, "error": err.Error()})
|
||||
response.OK(c, gin.H{"backends": []backendStat{}, "frontends": []frontendStat{}, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,7 +76,8 @@ func (h *HAProxyStatsHandler) Stats(c *gin.Context) {
|
||||
// "# pxname,svname,..." — die nutzen wir um Spalten-Indizes
|
||||
// zu finden, weil das Format zwischen Versionen wechseln kann.
|
||||
colIdx := map[string]int{}
|
||||
out := []backendStat{}
|
||||
backends := []backendStat{}
|
||||
frontends := []frontendStat{}
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
||||
@@ -81,18 +95,30 @@ func (h *HAProxyStatsHandler) Stats(c *gin.Context) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Skip frontend rows + the "BACKEND" summary row — we want
|
||||
// the per-server view ("L4OK", "L7OK", etc.).
|
||||
svname := safeAt(fields, colIdx["svname"])
|
||||
pxname := safeAt(fields, colIdx["pxname"])
|
||||
if svname == "" || svname == "FRONTEND" || svname == "BACKEND" {
|
||||
|
||||
// Skip our internal stats listener and the BACKEND summary row.
|
||||
if pxname == "internal_stats" || svname == "BACKEND" || svname == "" {
|
||||
continue
|
||||
}
|
||||
// Skip our internal api_backend stats listener and frontends.
|
||||
if pxname == "internal_stats" {
|
||||
|
||||
if svname == "FRONTEND" {
|
||||
// Collect frontend (listener) counters.
|
||||
frontends = append(frontends, frontendStat{
|
||||
Name: pxname,
|
||||
Sessions: parseInt64(safeAt(fields, colIdx["scur"])),
|
||||
MaxSess: parseInt64(safeAt(fields, colIdx["smax"])),
|
||||
BIn: parseInt64(safeAt(fields, colIdx["bin"])),
|
||||
BOut: parseInt64(safeAt(fields, colIdx["bout"])),
|
||||
ReqTot: parseInt64(safeAt(fields, colIdx["req_tot"])),
|
||||
ReqRate: parseInt64(safeAt(fields, colIdx["req_rate"])),
|
||||
})
|
||||
continue
|
||||
}
|
||||
st := backendStat{
|
||||
|
||||
// Per-server backend row.
|
||||
backends = append(backends, backendStat{
|
||||
Backend: pxname,
|
||||
Server: svname,
|
||||
Status: safeAt(fields, colIdx["status"]),
|
||||
@@ -103,10 +129,9 @@ func (h *HAProxyStatsHandler) Stats(c *gin.Context) {
|
||||
ReqRate: parseInt64(safeAt(fields, colIdx["req_rate"])),
|
||||
LastChg: parseInt64(safeAt(fields, colIdx["lastchg"])),
|
||||
Health: safeAt(fields, colIdx["check_status"]),
|
||||
}
|
||||
out = append(out, st)
|
||||
})
|
||||
}
|
||||
response.OK(c, gin.H{"backends": out})
|
||||
response.OK(c, gin.H{"backends": backends, "frontends": frontends})
|
||||
}
|
||||
|
||||
func safeAt(fields []string, i int) string {
|
||||
|
||||
Reference in New Issue
Block a user