feat(haproxy): req_tot + req_rate in Stats-API; Backend-Detail + Dashboard nutzen sie

GET /haproxy/stats liefert jetzt req_tot (kumulierte Requests seit
HAProxy-Start) und req_rate (Requests/s im letzten Messfenster).
Backend-Detail zeigt in der Live-Spalte 'N sess · X/s' wenn Traffic
fließt; Dashboard-HAProxy-Card zeigt req/s ebenfalls an.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-20 23:03:54 +02:00
parent 95583e9657
commit 9a05e8bca8
4 changed files with 20 additions and 12 deletions

View File

@@ -1 +1 @@
1.1.47 1.1.48

View File

@@ -32,14 +32,16 @@ const haproxyAdminSock = "/run/haproxy/admin.sock"
// 'show stat' CSV. We only emit the fields the dashboard cares // 'show stat' CSV. We only emit the fields the dashboard cares
// about — full CSV is ~80 columns of which 90% are noise here. // about — full CSV is ~80 columns of which 90% are noise here.
type backendStat struct { type backendStat struct {
Backend string `json:"backend"` // pxname Backend string `json:"backend"` // pxname
Server string `json:"server"` // svname Server string `json:"server"` // svname
Status string `json:"status"` // UP|DOWN|MAINT|... Status string `json:"status"` // UP|DOWN|MAINT|...
Sessions int64 `json:"sessions"` // current sessions (scur) Sessions int64 `json:"sessions"` // current sessions (scur)
BIn int64 `json:"bytes_in"` BIn int64 `json:"bytes_in"`
BOut int64 `json:"bytes_out"` BOut int64 `json:"bytes_out"`
LastChg int64 `json:"last_change_sec"` // seconds since last status change ReqTot int64 `json:"req_tot"` // total requests since start (req_tot)
Health string `json:"health,omitempty"` // check_status (e.g. L7OK) ReqRate int64 `json:"req_rate"` // requests/s last second (req_rate, server-level)
LastChg int64 `json:"last_change_sec"` // seconds since last status change
Health string `json:"health,omitempty"` // check_status (e.g. L7OK)
} }
func (h *HAProxyStatsHandler) Stats(c *gin.Context) { func (h *HAProxyStatsHandler) Stats(c *gin.Context) {
@@ -97,6 +99,8 @@ func (h *HAProxyStatsHandler) Stats(c *gin.Context) {
Sessions: parseInt64(safeAt(fields, colIdx["scur"])), Sessions: parseInt64(safeAt(fields, colIdx["scur"])),
BIn: parseInt64(safeAt(fields, colIdx["bin"])), BIn: parseInt64(safeAt(fields, colIdx["bin"])),
BOut: parseInt64(safeAt(fields, colIdx["bout"])), BOut: parseInt64(safeAt(fields, colIdx["bout"])),
ReqTot: parseInt64(safeAt(fields, colIdx["req_tot"])),
ReqRate: parseInt64(safeAt(fields, colIdx["req_rate"])),
LastChg: parseInt64(safeAt(fields, colIdx["lastchg"])), LastChg: parseInt64(safeAt(fields, colIdx["lastchg"])),
Health: safeAt(fields, colIdx["check_status"]), Health: safeAt(fields, colIdx["check_status"]),
} }

View File

@@ -59,6 +59,7 @@ async function listDomains(): Promise<DomainFull[]> {
interface HAProxyStat { interface HAProxyStat {
backend: string; server: string; status: string backend: string; server: string; status: string
sessions: number; bytes_in: number; bytes_out: number sessions: number; bytes_in: number; bytes_out: number
req_tot: number; req_rate: number
last_change_sec: number; health: string last_change_sec: number; health: string
} }
async function listHAProxyStats(): Promise<HAProxyStat[]> { async function listHAProxyStats(): Promise<HAProxyStat[]> {
@@ -289,9 +290,11 @@ function ServerPanel({ backendID, haproxyStats }: { backendID: number; haproxySt
if (!stat) return <Text type="secondary" style={{ fontSize: 11 }}></Text> if (!stat) return <Text type="secondary" style={{ fontSize: 11 }}></Text>
const color = stat.status === 'UP' ? 'green' : stat.status === 'no check' ? 'default' : 'red' const color = stat.status === 'UP' ? 'green' : stat.status === 'no check' ? 'default' : 'red'
return ( return (
<Tooltip title={`${fmtBytes(stat.bytes_in)}${fmtBytes(stat.bytes_out)} · ${stat.health || stat.status}`}> <Tooltip title={`${fmtBytes(stat.bytes_in)}${fmtBytes(stat.bytes_out)} · ${stat.req_tot} req total · ${stat.health || stat.status}`}>
<Tag color={color} style={{ margin: 0, fontSize: 11 }}>{stat.status}</Tag> <Tag color={color} style={{ margin: 0, fontSize: 11 }}>{stat.status}</Tag>
<Text type="secondary" style={{ fontSize: 11, marginLeft: 4 }}>{stat.sessions} sess</Text> <Text type="secondary" style={{ fontSize: 11, marginLeft: 4 }}>
{stat.sessions} sess{stat.req_rate > 0 ? ` · ${stat.req_rate}/s` : ''}
</Text>
</Tooltip> </Tooltip>
) )
}, },

View File

@@ -101,6 +101,7 @@ interface AuditEntry {
interface HAProxyBackend { interface HAProxyBackend {
backend: string; server: string; status: string backend: string; server: string; status: string
sessions: number; bytes_in: number; bytes_out: number sessions: number; bytes_in: number; bytes_out: number
req_tot: number; req_rate: number
last_change_sec: number; health?: string last_change_sec: number; health?: string
} }
@@ -407,7 +408,7 @@ export default function DashboardPage() {
style={{ margin: 0, fontSize: 10 }} style={{ margin: 0, fontSize: 10 }}
>{b.status}</Tag> >{b.status}</Tag>
<Text type="secondary" style={{ fontSize: 11 }}> <Text type="secondary" style={{ fontSize: 11 }}>
{b.sessions} sess · {formatBytes(b.bytes_in)} {formatBytes(b.bytes_out)} {b.sessions} sess{b.req_rate > 0 ? ` · ${b.req_rate}/s` : ''} · {formatBytes(b.bytes_in)} {formatBytes(b.bytes_out)}
</Text> </Text>
<Text type="secondary" style={{ fontSize: 11, marginLeft: 'auto' }}>{formatUptime(b.last_change_sec)}</Text> <Text type="secondary" style={{ fontSize: 11, marginLeft: 'auto' }}>{formatUptime(b.last_change_sec)}</Text>
</Space> </Space>