mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 04:10:51 +08:00
feat(pprof): add support for configurable pprof HTTP debug server
- Introduced a new `pprof` server to enable/debug HTTP profiling. - Added configuration options for enabling/disabling and specifying the server address. - Integrated pprof server lifecycle management with `Service`. #1287
This commit is contained in:
@@ -40,6 +40,11 @@ api-keys:
|
||||
# Enable debug logging
|
||||
debug: false
|
||||
|
||||
# Enable pprof HTTP debug server (host:port). Keep it bound to localhost for safety.
|
||||
pprof:
|
||||
enable: false
|
||||
addr: "127.0.0.1:8316"
|
||||
|
||||
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
||||
commercial-mode: false
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
||||
const (
|
||||
DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
||||
DefaultPprofAddr = "127.0.0.1:8316"
|
||||
)
|
||||
|
||||
// Config represents the application's configuration, loaded from a YAML file.
|
||||
type Config struct {
|
||||
@@ -41,6 +44,9 @@ type Config struct {
|
||||
// Debug enables or disables debug-level logging and other debug features.
|
||||
Debug bool `yaml:"debug" json:"debug"`
|
||||
|
||||
// Pprof config controls the optional pprof HTTP debug server.
|
||||
Pprof PprofConfig `yaml:"pprof" json:"pprof"`
|
||||
|
||||
// CommercialMode disables high-overhead HTTP middleware features to minimize per-request memory usage.
|
||||
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
||||
|
||||
@@ -121,6 +127,14 @@ type TLSConfig struct {
|
||||
Key string `yaml:"key" json:"key"`
|
||||
}
|
||||
|
||||
// PprofConfig holds pprof HTTP server settings.
|
||||
type PprofConfig struct {
|
||||
// Enable toggles the pprof HTTP debug server.
|
||||
Enable bool `yaml:"enable" json:"enable"`
|
||||
// Addr is the host:port address for the pprof HTTP server.
|
||||
Addr string `yaml:"addr" json:"addr"`
|
||||
}
|
||||
|
||||
// RemoteManagement holds management API configuration under 'remote-management'.
|
||||
type RemoteManagement struct {
|
||||
// AllowRemote toggles remote (non-localhost) access to management API.
|
||||
@@ -514,6 +528,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
||||
cfg.ErrorLogsMaxFiles = 10
|
||||
cfg.UsageStatisticsEnabled = false
|
||||
cfg.DisableCooling = false
|
||||
cfg.Pprof.Enable = false
|
||||
cfg.Pprof.Addr = DefaultPprofAddr
|
||||
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
||||
@@ -556,6 +572,11 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||
}
|
||||
|
||||
cfg.Pprof.Addr = strings.TrimSpace(cfg.Pprof.Addr)
|
||||
if cfg.Pprof.Addr == "" {
|
||||
cfg.Pprof.Addr = DefaultPprofAddr
|
||||
}
|
||||
|
||||
if cfg.LogsMaxTotalSizeMB < 0 {
|
||||
cfg.LogsMaxTotalSizeMB = 0
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
||||
if oldCfg.Debug != newCfg.Debug {
|
||||
changes = append(changes, fmt.Sprintf("debug: %t -> %t", oldCfg.Debug, newCfg.Debug))
|
||||
}
|
||||
if oldCfg.Pprof.Enable != newCfg.Pprof.Enable {
|
||||
changes = append(changes, fmt.Sprintf("pprof.enable: %t -> %t", oldCfg.Pprof.Enable, newCfg.Pprof.Enable))
|
||||
}
|
||||
if strings.TrimSpace(oldCfg.Pprof.Addr) != strings.TrimSpace(newCfg.Pprof.Addr) {
|
||||
changes = append(changes, fmt.Sprintf("pprof.addr: %s -> %s", strings.TrimSpace(oldCfg.Pprof.Addr), strings.TrimSpace(newCfg.Pprof.Addr)))
|
||||
}
|
||||
if oldCfg.LoggingToFile != newCfg.LoggingToFile {
|
||||
changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", oldCfg.LoggingToFile, newCfg.LoggingToFile))
|
||||
}
|
||||
|
||||
163
sdk/cliproxy/pprof_server.go
Normal file
163
sdk/cliproxy/pprof_server.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package cliproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type pprofServer struct {
|
||||
mu sync.Mutex
|
||||
server *http.Server
|
||||
addr string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func newPprofServer() *pprofServer {
|
||||
return &pprofServer{}
|
||||
}
|
||||
|
||||
func (s *Service) applyPprofConfig(cfg *config.Config) {
|
||||
if s == nil || cfg == nil {
|
||||
return
|
||||
}
|
||||
if s.pprofServer == nil {
|
||||
s.pprofServer = newPprofServer()
|
||||
}
|
||||
s.pprofServer.Apply(cfg)
|
||||
}
|
||||
|
||||
func (s *Service) shutdownPprof(ctx context.Context) error {
|
||||
if s == nil || s.pprofServer == nil {
|
||||
return nil
|
||||
}
|
||||
return s.pprofServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (p *pprofServer) Apply(cfg *config.Config) {
|
||||
if p == nil || cfg == nil {
|
||||
return
|
||||
}
|
||||
addr := strings.TrimSpace(cfg.Pprof.Addr)
|
||||
if addr == "" {
|
||||
addr = config.DefaultPprofAddr
|
||||
}
|
||||
enabled := cfg.Pprof.Enable
|
||||
|
||||
p.mu.Lock()
|
||||
currentServer := p.server
|
||||
currentAddr := p.addr
|
||||
p.addr = addr
|
||||
p.enabled = enabled
|
||||
if !enabled {
|
||||
p.server = nil
|
||||
p.mu.Unlock()
|
||||
if currentServer != nil {
|
||||
p.stopServer(currentServer, currentAddr, "disabled")
|
||||
}
|
||||
return
|
||||
}
|
||||
if currentServer != nil && currentAddr == addr {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
p.server = nil
|
||||
p.mu.Unlock()
|
||||
|
||||
if currentServer != nil {
|
||||
p.stopServer(currentServer, currentAddr, "restarted")
|
||||
}
|
||||
|
||||
p.startServer(addr)
|
||||
}
|
||||
|
||||
func (p *pprofServer) Shutdown(ctx context.Context) error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
p.mu.Lock()
|
||||
currentServer := p.server
|
||||
currentAddr := p.addr
|
||||
p.server = nil
|
||||
p.enabled = false
|
||||
p.mu.Unlock()
|
||||
|
||||
if currentServer == nil {
|
||||
return nil
|
||||
}
|
||||
return p.stopServerWithContext(ctx, currentServer, currentAddr, "shutdown")
|
||||
}
|
||||
|
||||
func (p *pprofServer) startServer(addr string) {
|
||||
mux := newPprofMux()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
if !p.enabled || p.addr != addr || p.server != nil {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
p.server = server
|
||||
p.mu.Unlock()
|
||||
|
||||
log.Infof("pprof server starting on %s", addr)
|
||||
go func() {
|
||||
if errServe := server.ListenAndServe(); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
|
||||
log.Errorf("pprof server failed on %s: %v", addr, errServe)
|
||||
p.mu.Lock()
|
||||
if p.server == server {
|
||||
p.server = nil
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *pprofServer) stopServer(server *http.Server, addr string, reason string) {
|
||||
_ = p.stopServerWithContext(context.Background(), server, addr, reason)
|
||||
}
|
||||
|
||||
func (p *pprofServer) stopServerWithContext(ctx context.Context, server *http.Server, addr string, reason string) error {
|
||||
if server == nil {
|
||||
return nil
|
||||
}
|
||||
stopCtx := ctx
|
||||
if stopCtx == nil {
|
||||
stopCtx = context.Background()
|
||||
}
|
||||
stopCtx, cancel := context.WithTimeout(stopCtx, 5*time.Second)
|
||||
defer cancel()
|
||||
if errStop := server.Shutdown(stopCtx); errStop != nil {
|
||||
log.Errorf("pprof server stop failed on %s: %v", addr, errStop)
|
||||
return errStop
|
||||
}
|
||||
log.Infof("pprof server stopped on %s (%s)", addr, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPprofMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
||||
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
|
||||
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
|
||||
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
||||
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
||||
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
|
||||
return mux
|
||||
}
|
||||
@@ -57,6 +57,9 @@ type Service struct {
|
||||
// server is the HTTP API server instance.
|
||||
server *api.Server
|
||||
|
||||
// pprofServer manages the optional pprof HTTP debug server.
|
||||
pprofServer *pprofServer
|
||||
|
||||
// serverErr channel for server startup/shutdown errors.
|
||||
serverErr chan error
|
||||
|
||||
@@ -501,6 +504,8 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
||||
|
||||
s.applyPprofConfig(s.cfg)
|
||||
|
||||
if s.hooks.OnAfterStart != nil {
|
||||
s.hooks.OnAfterStart(s)
|
||||
}
|
||||
@@ -546,6 +551,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
s.applyRetryConfig(newCfg)
|
||||
s.applyPprofConfig(newCfg)
|
||||
if s.server != nil {
|
||||
s.server.UpdateClients(newCfg)
|
||||
}
|
||||
@@ -639,6 +645,13 @@ func (s *Service) Shutdown(ctx context.Context) error {
|
||||
s.authQueueStop = nil
|
||||
}
|
||||
|
||||
if errShutdownPprof := s.shutdownPprof(ctx); errShutdownPprof != nil {
|
||||
log.Errorf("failed to stop pprof server: %v", errShutdownPprof)
|
||||
if shutdownErr == nil {
|
||||
shutdownErr = errShutdownPprof
|
||||
}
|
||||
}
|
||||
|
||||
// no legacy clients to persist
|
||||
|
||||
if s.server != nil {
|
||||
|
||||
Reference in New Issue
Block a user