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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user