feat(fwd-proxy): Squid cache stats card — GET /forward-proxy/stats + squidclient mgr:counters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-25 16:47:31 +02:00
parent f29d74bad7
commit 711c4c7eb1
8 changed files with 176 additions and 7 deletions

View File

@@ -4,7 +4,9 @@ import (
"context"
"errors"
"log/slog"
"os/exec"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -35,7 +37,10 @@ func (h *ForwardProxyHandler) reload(ctx context.Context, op string) {
}
func (h *ForwardProxyHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/forward-proxy/acls")
base := rg.Group("/forward-proxy")
base.GET("/stats", h.Stats)
g := base.Group("/acls")
g.GET("", h.List)
g.POST("", h.Create)
g.GET("/:id", h.Get)
@@ -135,6 +140,68 @@ func (h *ForwardProxyHandler) Delete(c *gin.Context) {
h.reload(c.Request.Context(), "delete")
}
// Stats liefert Squid-Cache-Statistiken via `squidclient mgr:counters`.
// Die Ausgabe enthält HTTP-Header gefolgt von key = value Zeilen.
func (h *ForwardProxyHandler) Stats(c *gin.Context) {
out, err := exec.Command("squidclient", "-h", "127.0.0.1", "-p", "3128", "mgr:counters").Output()
if err != nil {
response.OK(c, gin.H{
"error": "squidclient nicht verfügbar: " + err.Error(),
})
return
}
response.OK(c, gin.H{"stats": parseSquidCounters(string(out))})
}
type squidStats struct {
ClientRequests int64 `json:"client_requests"`
CacheHits int64 `json:"cache_hits"`
CacheHitPct float64 `json:"cache_hit_pct"`
ClientErrors int64 `json:"client_errors"`
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
ServerRequests int64 `json:"server_requests"`
ServerErrors int64 `json:"server_errors"`
}
func parseSquidCounters(out string) squidStats {
// squidclient prefixes an HTTP response header block — skip it.
body := out
if idx := strings.Index(out, "\r\n\r\n"); idx >= 0 {
body = out[idx+4:]
} else if idx := strings.Index(out, "\n\n"); idx >= 0 {
body = out[idx+2:]
}
s := squidStats{}
for _, line := range strings.Split(body, "\n") {
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
n, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
switch strings.TrimSpace(k) {
case "client_http.requests":
s.ClientRequests = n
case "client_http.hits":
s.CacheHits = n
case "client_http.errors":
s.ClientErrors = n
case "client_http.kbytes_in":
s.BytesIn = n * 1024
case "client_http.kbytes_out":
s.BytesOut = n * 1024
case "server.all.requests":
s.ServerRequests = n
case "server.all.errors":
s.ServerErrors = n
}
}
if s.ClientRequests > 0 {
s.CacheHitPct = float64(s.CacheHits) / float64(s.ClientRequests) * 100
}
return s
}
// validateACL prüft Name (squid-konform), action, acl_type. Squid
// nimmt viele Typen — wir whitelisten die, die in einem Forward-
// Proxy-Setup üblich sind, damit Tippfehler nicht beim reload