mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-19 04:40:52 +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
|
# Enable debug logging
|
||||||
debug: false
|
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.
|
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
||||||
commercial-mode: false
|
commercial-mode: false
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"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.
|
// Config represents the application's configuration, loaded from a YAML file.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -41,6 +44,9 @@ type Config struct {
|
|||||||
// Debug enables or disables debug-level logging and other debug features.
|
// Debug enables or disables debug-level logging and other debug features.
|
||||||
Debug bool `yaml:"debug" json:"debug"`
|
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 disables high-overhead HTTP middleware features to minimize per-request memory usage.
|
||||||
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
||||||
|
|
||||||
@@ -121,6 +127,14 @@ type TLSConfig struct {
|
|||||||
Key string `yaml:"key" json:"key"`
|
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'.
|
// RemoteManagement holds management API configuration under 'remote-management'.
|
||||||
type RemoteManagement struct {
|
type RemoteManagement struct {
|
||||||
// AllowRemote toggles remote (non-localhost) access to management API.
|
// 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.ErrorLogsMaxFiles = 10
|
||||||
cfg.UsageStatisticsEnabled = false
|
cfg.UsageStatisticsEnabled = false
|
||||||
cfg.DisableCooling = 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.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
||||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||||
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
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.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Pprof.Addr = strings.TrimSpace(cfg.Pprof.Addr)
|
||||||
|
if cfg.Pprof.Addr == "" {
|
||||||
|
cfg.Pprof.Addr = DefaultPprofAddr
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.LogsMaxTotalSizeMB < 0 {
|
if cfg.LogsMaxTotalSizeMB < 0 {
|
||||||
cfg.LogsMaxTotalSizeMB = 0
|
cfg.LogsMaxTotalSizeMB = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|||||||
if oldCfg.Debug != newCfg.Debug {
|
if oldCfg.Debug != newCfg.Debug {
|
||||||
changes = append(changes, fmt.Sprintf("debug: %t -> %t", 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 {
|
if oldCfg.LoggingToFile != newCfg.LoggingToFile {
|
||||||
changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", 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 is the HTTP API server instance.
|
||||||
server *api.Server
|
server *api.Server
|
||||||
|
|
||||||
|
// pprofServer manages the optional pprof HTTP debug server.
|
||||||
|
pprofServer *pprofServer
|
||||||
|
|
||||||
// serverErr channel for server startup/shutdown errors.
|
// serverErr channel for server startup/shutdown errors.
|
||||||
serverErr chan error
|
serverErr chan error
|
||||||
|
|
||||||
@@ -501,6 +504,8 @@ func (s *Service) Run(ctx context.Context) error {
|
|||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
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 {
|
if s.hooks.OnAfterStart != nil {
|
||||||
s.hooks.OnAfterStart(s)
|
s.hooks.OnAfterStart(s)
|
||||||
}
|
}
|
||||||
@@ -546,6 +551,7 @@ func (s *Service) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.applyRetryConfig(newCfg)
|
s.applyRetryConfig(newCfg)
|
||||||
|
s.applyPprofConfig(newCfg)
|
||||||
if s.server != nil {
|
if s.server != nil {
|
||||||
s.server.UpdateClients(newCfg)
|
s.server.UpdateClients(newCfg)
|
||||||
}
|
}
|
||||||
@@ -639,6 +645,13 @@ func (s *Service) Shutdown(ctx context.Context) error {
|
|||||||
s.authQueueStop = nil
|
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
|
// no legacy clients to persist
|
||||||
|
|
||||||
if s.server != nil {
|
if s.server != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user