From 8daab83d785f3e4b4ec365c1f61f533938855f9f Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 16 Apr 2026 15:07:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(status):=20=E6=B7=BB=E5=8A=A0=20HTML/Text?= =?UTF-8?q?=20=E8=BE=93=E5=87=BA=E6=A0=BC=E5=BC=8F=E5=92=8C=20localhost=20?= =?UTF-8?q?IP=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 扩展状态端点输出格式支持: - json: 默认 JSON 格式 - text: 纯文本格式,便于终端查看 - html: HTML 表格格式,便于浏览器查看 - prometheus: Prometheus 指标格式 Allow 配置新增 "localhost" 特殊值支持,自动解析为 127.0.0.1/32 和 ::1/128,简化本地调试配置。 Co-Authored-By: Claude Opus 4.6 --- internal/server/status.go | 218 +++++++++++++++++++++++++++++++++++--- 1 file changed, 203 insertions(+), 15 deletions(-) diff --git a/internal/server/status.go b/internal/server/status.go index 4dec6e4..4a4002c 100644 --- a/internal/server/status.go +++ b/internal/server/status.go @@ -136,6 +136,20 @@ func NewStatusHandler(server *Server, cfg *config.StatusConfig) (*StatusHandler, // 解析允许的 IP 列表 for _, cidr := range cfg.Allow { + // 处理 localhost 特殊情况 + if cidr == "localhost" { + // localhost 解析为 127.0.0.1 和 ::1 + _, v4Network, _ := net.ParseCIDR("127.0.0.1/32") + _, v6Network, _ := net.ParseCIDR("::1/128") + if v4Network != nil { + h.allowed = append(h.allowed, *v4Network) + } + if v6Network != nil { + h.allowed = append(h.allowed, *v6Network) + } + continue + } + _, network, err := net.ParseCIDR(cidr) if err != nil { // 尝试作为单个 IP 解析 @@ -174,7 +188,7 @@ func (h *StatusHandler) Path() string { // ServeHTTP 处理状态请求。 // // 验证客户端 IP 权限,收集并返回服务器状态信息。 -// 未授权访问返回 403 Forbidden,授权访问返回 JSON 格式状态。 +// 未授权访问返回 403 Forbidden,授权访问根据配置格式返回状态。 // // 参数: // - ctx: FastHTTP 请求上下文 @@ -189,23 +203,19 @@ func (h *StatusHandler) ServeHTTP(ctx *fasthttp.RequestCtx) { status := h.collectStatus() // 步骤3: 根据格式返回响应 - if h.format == "prometheus" { + switch h.format { + case "prometheus": h.servePrometheus(ctx, status) return - } - - // 默认 JSON 格式 - ctx.SetContentType("application/json; charset=utf-8") - ctx.SetStatusCode(fasthttp.StatusOK) - - data, err := json.MarshalIndent(status, "", " ") - if err != nil { - utils.SendError(ctx, utils.ErrInternalError) + case "html": + h.serveHTML(ctx, status) + return + case "text": + h.serveText(ctx, status) + return + default: + h.serveJSON(ctx, status) return - } - - if _, err := ctx.Write(data); err != nil { - log.Printf("failed to write status response: %v", err) } } @@ -321,6 +331,184 @@ func (h *StatusHandler) servePrometheus(ctx *fasthttp.RequestCtx, status *Status } } +// serveJSON 以 JSON 格式输出状态。 +func (h *StatusHandler) serveJSON(ctx *fasthttp.RequestCtx, status *Status) { + ctx.SetContentType("application/json; charset=utf-8") + ctx.SetStatusCode(fasthttp.StatusOK) + + data, err := json.MarshalIndent(status, "", " ") + if err != nil { + utils.SendError(ctx, utils.ErrInternalError) + return + } + + if _, err := ctx.Write(data); err != nil { + log.Printf("failed to write status response: %v", err) + } +} + +// serveText 以纯文本格式输出状态。 +func (h *StatusHandler) serveText(ctx *fasthttp.RequestCtx, status *Status) { + ctx.SetContentType("text/plain; charset=utf-8") + ctx.SetStatusCode(fasthttp.StatusOK) + + var buf strings.Builder + + buf.WriteString("Lolly Status\n") + buf.WriteString("============\n\n") + fmt.Fprintf(&buf, "Version: %s\n", status.Version) + fmt.Fprintf(&buf, "Uptime: %s\n", status.Uptime.Round(time.Second)) + fmt.Fprintf(&buf, "Connections: %d\n", status.Connections) + fmt.Fprintf(&buf, "Requests: %d\n", status.Requests) + fmt.Fprintf(&buf, "Bytes Sent: %d\n", status.BytesSent) + fmt.Fprintf(&buf, "Bytes Received: %d\n", status.BytesReceived) + + if status.Cache != nil { + buf.WriteString("\nCache:\n") + fmt.Fprintf(&buf, " File Entries: %d / %d\n", status.Cache.FileCache.Entries, status.Cache.FileCache.MaxEntries) + fmt.Fprintf(&buf, " File Size: %d / %d bytes\n", status.Cache.FileCache.Size, status.Cache.FileCache.MaxSize) + fmt.Fprintf(&buf, " Proxy Entries: %d\n", status.Cache.ProxyCache.Entries) + fmt.Fprintf(&buf, " Proxy Pending: %d\n", status.Cache.ProxyCache.Pending) + } + + if status.Pool != nil { + buf.WriteString("\nGoroutine Pool:\n") + fmt.Fprintf(&buf, " Workers: %d (idle: %d)\n", status.Pool.Workers, status.Pool.IdleWorkers) + fmt.Fprintf(&buf, " Queue: %d / %d\n", status.Pool.QueueLen, status.Pool.QueueCap) + } + + if len(status.Upstreams) > 0 { + buf.WriteString("\nUpstreams:\n") + for _, upstream := range status.Upstreams { + fmt.Fprintf(&buf, " %s: %d healthy, %d unhealthy\n", upstream.Name, upstream.HealthyCount, upstream.UnhealthyCount) + fmt.Fprintf(&buf, " Latency: P50=%.2fms, P95=%.2fms, P99=%.2fms\n", upstream.LatencyP50, upstream.LatencyP95, upstream.LatencyP99) + } + } + + if status.SSL != nil { + buf.WriteString("\nSSL:\n") + fmt.Fprintf(&buf, " Handshakes: %d\n", status.SSL.Handshakes) + fmt.Fprintf(&buf, " Session Reused: %d\n", status.SSL.SessionReused) + fmt.Fprintf(&buf, " Reuse Rate: %.2f%%\n", status.SSL.ReuseRate) + } + + if len(status.RateLimits) > 0 { + buf.WriteString("\nRate Limits:\n") + for _, rl := range status.RateLimits { + fmt.Fprintf(&buf, " %s: %d requests, limit=%d, rejected=%d\n", rl.ZoneName, rl.Requests, rl.Limit, rl.Rejected) + } + } + + if _, err := ctx.WriteString(buf.String()); err != nil { + log.Printf("failed to write text response: %v", err) + } +} + +// serveHTML 以 HTML 格式输出状态。 +func (h *StatusHandler) serveHTML(ctx *fasthttp.RequestCtx, status *Status) { + ctx.SetContentType("text/html; charset=utf-8") + ctx.SetStatusCode(fasthttp.StatusOK) + + var buf strings.Builder + + buf.WriteString("\n") + buf.WriteString("\n") + buf.WriteString("\n") + buf.WriteString("\n") + buf.WriteString("Lolly Status\n") + buf.WriteString("\n") + buf.WriteString("\n") + buf.WriteString("\n") + + buf.WriteString("

Lolly Status

\n") + + buf.WriteString("\n") + fmt.Fprintf(&buf, "\n", status.Version) + fmt.Fprintf(&buf, "\n", status.Uptime.Round(time.Second)) + fmt.Fprintf(&buf, "\n", status.Connections) + fmt.Fprintf(&buf, "\n", status.Requests) + fmt.Fprintf(&buf, "\n", status.BytesSent) + fmt.Fprintf(&buf, "\n", status.BytesReceived) + buf.WriteString("
Version%s
Uptime%s
Connections%d
Requests%d
Bytes Sent%d
Bytes Received%d
\n") + + if status.Cache != nil { + buf.WriteString("
\n") + buf.WriteString("

Cache

\n") + buf.WriteString("\n") + buf.WriteString("\n") + fmt.Fprintf(&buf, "\n", + status.Cache.FileCache.Entries, status.Cache.FileCache.MaxEntries, status.Cache.FileCache.Size) + fmt.Fprintf(&buf, "\n", status.Cache.ProxyCache.Entries) + buf.WriteString("
TypeEntriesMaxSize
File%d%d%d bytes
Proxy%d--
\n") + buf.WriteString("
\n") + } + + if status.Pool != nil { + buf.WriteString("
\n") + buf.WriteString("

Goroutine Pool

\n") + buf.WriteString("\n") + buf.WriteString("\n") + fmt.Fprintf(&buf, "\n", + status.Pool.Workers, status.Pool.IdleWorkers, status.Pool.QueueLen, status.Pool.QueueCap) + buf.WriteString("
WorkersIdleQueue
%d%d%d / %d
\n") + buf.WriteString("
\n") + } + + if len(status.Upstreams) > 0 { + buf.WriteString("
\n") + buf.WriteString("

Upstreams

\n") + buf.WriteString("\n") + buf.WriteString("\n") + for _, upstream := range status.Upstreams { + fmt.Fprintf(&buf, "\n", + upstream.Name, upstream.HealthyCount, upstream.UnhealthyCount, + upstream.LatencyP50, upstream.LatencyP95, upstream.LatencyP99) + } + buf.WriteString("
NameHealthyUnhealthyP50P95P99
%s%d%d%.2fms%.2fms%.2fms
\n") + buf.WriteString("
\n") + } + + if status.SSL != nil { + buf.WriteString("
\n") + buf.WriteString("

SSL

\n") + buf.WriteString("\n") + buf.WriteString("\n") + fmt.Fprintf(&buf, "\n", + status.SSL.Handshakes, status.SSL.SessionReused, status.SSL.ReuseRate) + buf.WriteString("
HandshakesSession ReusedReuse Rate
%d%d%.2f%%
\n") + buf.WriteString("
\n") + } + + if len(status.RateLimits) > 0 { + buf.WriteString("
\n") + buf.WriteString("

Rate Limits

\n") + buf.WriteString("\n") + buf.WriteString("\n") + for _, rl := range status.RateLimits { + fmt.Fprintf(&buf, "\n", + rl.ZoneName, rl.Requests, rl.Limit, rl.Rejected) + } + buf.WriteString("
ZoneRequestsLimitRejected
%s%d%d%d
\n") + buf.WriteString("
\n") + } + + buf.WriteString("\n") + buf.WriteString("\n") + + if _, err := ctx.WriteString(buf.String()); err != nil { + log.Printf("failed to write html response: %v", err) + } +} + // checkAccess 检查客户端 IP 是否在允许列表中。 // // 如果未配置允许列表,则允许所有访问。