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, "| 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)
+ buf.WriteString("
\n")
+
+ if status.Cache != nil {
+ buf.WriteString("\n")
+ buf.WriteString("
Cache
\n")
+ buf.WriteString("
\n")
+ buf.WriteString("| Type | Entries | Max | Size |
\n")
+ fmt.Fprintf(&buf, "| File | %d | %d | %d bytes |
\n",
+ status.Cache.FileCache.Entries, status.Cache.FileCache.MaxEntries, status.Cache.FileCache.Size)
+ fmt.Fprintf(&buf, "| Proxy | %d | - | - |
\n", status.Cache.ProxyCache.Entries)
+ buf.WriteString("
\n")
+ buf.WriteString("
\n")
+ }
+
+ if status.Pool != nil {
+ buf.WriteString("\n")
+ buf.WriteString("
Goroutine Pool
\n")
+ buf.WriteString("
\n")
+ buf.WriteString("| Workers | Idle | Queue |
\n")
+ fmt.Fprintf(&buf, "| %d | %d | %d / %d |
\n",
+ status.Pool.Workers, status.Pool.IdleWorkers, status.Pool.QueueLen, status.Pool.QueueCap)
+ buf.WriteString("
\n")
+ buf.WriteString("
\n")
+ }
+
+ if len(status.Upstreams) > 0 {
+ buf.WriteString("\n")
+ buf.WriteString("
Upstreams
\n")
+ buf.WriteString("
\n")
+ buf.WriteString("| Name | Healthy | Unhealthy | P50 | P95 | P99 |
\n")
+ for _, upstream := range status.Upstreams {
+ fmt.Fprintf(&buf, "| %s | %d | %d | %.2fms | %.2fms | %.2fms |
\n",
+ upstream.Name, upstream.HealthyCount, upstream.UnhealthyCount,
+ upstream.LatencyP50, upstream.LatencyP95, upstream.LatencyP99)
+ }
+ buf.WriteString("
\n")
+ buf.WriteString("
\n")
+ }
+
+ if status.SSL != nil {
+ buf.WriteString("\n")
+ buf.WriteString("
SSL
\n")
+ buf.WriteString("
\n")
+ buf.WriteString("| Handshakes | Session Reused | Reuse Rate |
\n")
+ fmt.Fprintf(&buf, "| %d | %d | %.2f%% |
\n",
+ status.SSL.Handshakes, status.SSL.SessionReused, status.SSL.ReuseRate)
+ buf.WriteString("
\n")
+ buf.WriteString("
\n")
+ }
+
+ if len(status.RateLimits) > 0 {
+ buf.WriteString("\n")
+ buf.WriteString("
Rate Limits
\n")
+ buf.WriteString("
\n")
+ buf.WriteString("| Zone | Requests | Limit | Rejected |
\n")
+ for _, rl := range status.RateLimits {
+ fmt.Fprintf(&buf, "| %s | %d | %d | %d |
\n",
+ rl.ZoneName, rl.Requests, rl.Limit, rl.Rejected)
+ }
+ buf.WriteString("
\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 是否在允许列表中。
//
// 如果未配置允许列表,则允许所有访问。