feat(pprof): add support for configurable pprof HTTP debug server
Browse files- 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
- config.example.yaml +5 -0
- internal/config/config.go +22 -1
- internal/watcher/diff/config_diff.go +6 -0
- sdk/cliproxy/pprof_server.go +163 -0
- sdk/cliproxy/service.go +13 -0
config.example.yaml
CHANGED
|
@@ -40,6 +40,11 @@ api-keys:
|
|
| 40 |
# Enable debug logging
|
| 41 |
debug: false
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
| 44 |
commercial-mode: false
|
| 45 |
|
|
|
|
| 40 |
# Enable debug logging
|
| 41 |
debug: false
|
| 42 |
|
| 43 |
+
# Enable pprof HTTP debug server (host:port). Keep it bound to localhost for safety.
|
| 44 |
+
pprof:
|
| 45 |
+
enable: false
|
| 46 |
+
addr: "127.0.0.1:8316"
|
| 47 |
+
|
| 48 |
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
| 49 |
commercial-mode: false
|
| 50 |
|
internal/config/config.go
CHANGED
|
@@ -18,7 +18,10 @@ import (
|
|
| 18 |
"gopkg.in/yaml.v3"
|
| 19 |
)
|
| 20 |
|
| 21 |
-
const
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
// Config represents the application's configuration, loaded from a YAML file.
|
| 24 |
type Config struct {
|
|
@@ -41,6 +44,9 @@ type Config struct {
|
|
| 41 |
// Debug enables or disables debug-level logging and other debug features.
|
| 42 |
Debug bool `yaml:"debug" json:"debug"`
|
| 43 |
|
|
|
|
|
|
|
|
|
|
| 44 |
// CommercialMode disables high-overhead HTTP middleware features to minimize per-request memory usage.
|
| 45 |
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
| 46 |
|
|
@@ -121,6 +127,14 @@ type TLSConfig struct {
|
|
| 121 |
Key string `yaml:"key" json:"key"`
|
| 122 |
}
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
// RemoteManagement holds management API configuration under 'remote-management'.
|
| 125 |
type RemoteManagement struct {
|
| 126 |
// AllowRemote toggles remote (non-localhost) access to management API.
|
|
@@ -514,6 +528,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|
| 514 |
cfg.ErrorLogsMaxFiles = 10
|
| 515 |
cfg.UsageStatisticsEnabled = false
|
| 516 |
cfg.DisableCooling = false
|
|
|
|
|
|
|
| 517 |
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
| 518 |
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
| 519 |
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
|
@@ -556,6 +572,11 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|
| 556 |
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
| 557 |
}
|
| 558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
if cfg.LogsMaxTotalSizeMB < 0 {
|
| 560 |
cfg.LogsMaxTotalSizeMB = 0
|
| 561 |
}
|
|
|
|
| 18 |
"gopkg.in/yaml.v3"
|
| 19 |
)
|
| 20 |
|
| 21 |
+
const (
|
| 22 |
+
DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
| 23 |
+
DefaultPprofAddr = "127.0.0.1:8316"
|
| 24 |
+
)
|
| 25 |
|
| 26 |
// Config represents the application's configuration, loaded from a YAML file.
|
| 27 |
type Config struct {
|
|
|
|
| 44 |
// Debug enables or disables debug-level logging and other debug features.
|
| 45 |
Debug bool `yaml:"debug" json:"debug"`
|
| 46 |
|
| 47 |
+
// Pprof config controls the optional pprof HTTP debug server.
|
| 48 |
+
Pprof PprofConfig `yaml:"pprof" json:"pprof"`
|
| 49 |
+
|
| 50 |
// CommercialMode disables high-overhead HTTP middleware features to minimize per-request memory usage.
|
| 51 |
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
| 52 |
|
|
|
|
| 127 |
Key string `yaml:"key" json:"key"`
|
| 128 |
}
|
| 129 |
|
| 130 |
+
// PprofConfig holds pprof HTTP server settings.
|
| 131 |
+
type PprofConfig struct {
|
| 132 |
+
// Enable toggles the pprof HTTP debug server.
|
| 133 |
+
Enable bool `yaml:"enable" json:"enable"`
|
| 134 |
+
// Addr is the host:port address for the pprof HTTP server.
|
| 135 |
+
Addr string `yaml:"addr" json:"addr"`
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
// RemoteManagement holds management API configuration under 'remote-management'.
|
| 139 |
type RemoteManagement struct {
|
| 140 |
// AllowRemote toggles remote (non-localhost) access to management API.
|
|
|
|
| 528 |
cfg.ErrorLogsMaxFiles = 10
|
| 529 |
cfg.UsageStatisticsEnabled = false
|
| 530 |
cfg.DisableCooling = false
|
| 531 |
+
cfg.Pprof.Enable = false
|
| 532 |
+
cfg.Pprof.Addr = DefaultPprofAddr
|
| 533 |
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
| 534 |
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
| 535 |
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
|
|
|
| 572 |
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
| 573 |
}
|
| 574 |
|
| 575 |
+
cfg.Pprof.Addr = strings.TrimSpace(cfg.Pprof.Addr)
|
| 576 |
+
if cfg.Pprof.Addr == "" {
|
| 577 |
+
cfg.Pprof.Addr = DefaultPprofAddr
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
if cfg.LogsMaxTotalSizeMB < 0 {
|
| 581 |
cfg.LogsMaxTotalSizeMB = 0
|
| 582 |
}
|
internal/watcher/diff/config_diff.go
CHANGED
|
@@ -27,6 +27,12 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|
| 27 |
if oldCfg.Debug != newCfg.Debug {
|
| 28 |
changes = append(changes, fmt.Sprintf("debug: %t -> %t", oldCfg.Debug, newCfg.Debug))
|
| 29 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
if oldCfg.LoggingToFile != newCfg.LoggingToFile {
|
| 31 |
changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", oldCfg.LoggingToFile, newCfg.LoggingToFile))
|
| 32 |
}
|
|
|
|
| 27 |
if oldCfg.Debug != newCfg.Debug {
|
| 28 |
changes = append(changes, fmt.Sprintf("debug: %t -> %t", oldCfg.Debug, newCfg.Debug))
|
| 29 |
}
|
| 30 |
+
if oldCfg.Pprof.Enable != newCfg.Pprof.Enable {
|
| 31 |
+
changes = append(changes, fmt.Sprintf("pprof.enable: %t -> %t", oldCfg.Pprof.Enable, newCfg.Pprof.Enable))
|
| 32 |
+
}
|
| 33 |
+
if strings.TrimSpace(oldCfg.Pprof.Addr) != strings.TrimSpace(newCfg.Pprof.Addr) {
|
| 34 |
+
changes = append(changes, fmt.Sprintf("pprof.addr: %s -> %s", strings.TrimSpace(oldCfg.Pprof.Addr), strings.TrimSpace(newCfg.Pprof.Addr)))
|
| 35 |
+
}
|
| 36 |
if oldCfg.LoggingToFile != newCfg.LoggingToFile {
|
| 37 |
changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", oldCfg.LoggingToFile, newCfg.LoggingToFile))
|
| 38 |
}
|
sdk/cliproxy/pprof_server.go
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cliproxy
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"net/http"
|
| 7 |
+
"net/http/pprof"
|
| 8 |
+
"strings"
|
| 9 |
+
"sync"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
| 13 |
+
log "github.com/sirupsen/logrus"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type pprofServer struct {
|
| 17 |
+
mu sync.Mutex
|
| 18 |
+
server *http.Server
|
| 19 |
+
addr string
|
| 20 |
+
enabled bool
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func newPprofServer() *pprofServer {
|
| 24 |
+
return &pprofServer{}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func (s *Service) applyPprofConfig(cfg *config.Config) {
|
| 28 |
+
if s == nil || cfg == nil {
|
| 29 |
+
return
|
| 30 |
+
}
|
| 31 |
+
if s.pprofServer == nil {
|
| 32 |
+
s.pprofServer = newPprofServer()
|
| 33 |
+
}
|
| 34 |
+
s.pprofServer.Apply(cfg)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func (s *Service) shutdownPprof(ctx context.Context) error {
|
| 38 |
+
if s == nil || s.pprofServer == nil {
|
| 39 |
+
return nil
|
| 40 |
+
}
|
| 41 |
+
return s.pprofServer.Shutdown(ctx)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
func (p *pprofServer) Apply(cfg *config.Config) {
|
| 45 |
+
if p == nil || cfg == nil {
|
| 46 |
+
return
|
| 47 |
+
}
|
| 48 |
+
addr := strings.TrimSpace(cfg.Pprof.Addr)
|
| 49 |
+
if addr == "" {
|
| 50 |
+
addr = config.DefaultPprofAddr
|
| 51 |
+
}
|
| 52 |
+
enabled := cfg.Pprof.Enable
|
| 53 |
+
|
| 54 |
+
p.mu.Lock()
|
| 55 |
+
currentServer := p.server
|
| 56 |
+
currentAddr := p.addr
|
| 57 |
+
p.addr = addr
|
| 58 |
+
p.enabled = enabled
|
| 59 |
+
if !enabled {
|
| 60 |
+
p.server = nil
|
| 61 |
+
p.mu.Unlock()
|
| 62 |
+
if currentServer != nil {
|
| 63 |
+
p.stopServer(currentServer, currentAddr, "disabled")
|
| 64 |
+
}
|
| 65 |
+
return
|
| 66 |
+
}
|
| 67 |
+
if currentServer != nil && currentAddr == addr {
|
| 68 |
+
p.mu.Unlock()
|
| 69 |
+
return
|
| 70 |
+
}
|
| 71 |
+
p.server = nil
|
| 72 |
+
p.mu.Unlock()
|
| 73 |
+
|
| 74 |
+
if currentServer != nil {
|
| 75 |
+
p.stopServer(currentServer, currentAddr, "restarted")
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
p.startServer(addr)
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func (p *pprofServer) Shutdown(ctx context.Context) error {
|
| 82 |
+
if p == nil {
|
| 83 |
+
return nil
|
| 84 |
+
}
|
| 85 |
+
p.mu.Lock()
|
| 86 |
+
currentServer := p.server
|
| 87 |
+
currentAddr := p.addr
|
| 88 |
+
p.server = nil
|
| 89 |
+
p.enabled = false
|
| 90 |
+
p.mu.Unlock()
|
| 91 |
+
|
| 92 |
+
if currentServer == nil {
|
| 93 |
+
return nil
|
| 94 |
+
}
|
| 95 |
+
return p.stopServerWithContext(ctx, currentServer, currentAddr, "shutdown")
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
func (p *pprofServer) startServer(addr string) {
|
| 99 |
+
mux := newPprofMux()
|
| 100 |
+
server := &http.Server{
|
| 101 |
+
Addr: addr,
|
| 102 |
+
Handler: mux,
|
| 103 |
+
ReadHeaderTimeout: 5 * time.Second,
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
p.mu.Lock()
|
| 107 |
+
if !p.enabled || p.addr != addr || p.server != nil {
|
| 108 |
+
p.mu.Unlock()
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
p.server = server
|
| 112 |
+
p.mu.Unlock()
|
| 113 |
+
|
| 114 |
+
log.Infof("pprof server starting on %s", addr)
|
| 115 |
+
go func() {
|
| 116 |
+
if errServe := server.ListenAndServe(); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
|
| 117 |
+
log.Errorf("pprof server failed on %s: %v", addr, errServe)
|
| 118 |
+
p.mu.Lock()
|
| 119 |
+
if p.server == server {
|
| 120 |
+
p.server = nil
|
| 121 |
+
}
|
| 122 |
+
p.mu.Unlock()
|
| 123 |
+
}
|
| 124 |
+
}()
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
func (p *pprofServer) stopServer(server *http.Server, addr string, reason string) {
|
| 128 |
+
_ = p.stopServerWithContext(context.Background(), server, addr, reason)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
func (p *pprofServer) stopServerWithContext(ctx context.Context, server *http.Server, addr string, reason string) error {
|
| 132 |
+
if server == nil {
|
| 133 |
+
return nil
|
| 134 |
+
}
|
| 135 |
+
stopCtx := ctx
|
| 136 |
+
if stopCtx == nil {
|
| 137 |
+
stopCtx = context.Background()
|
| 138 |
+
}
|
| 139 |
+
stopCtx, cancel := context.WithTimeout(stopCtx, 5*time.Second)
|
| 140 |
+
defer cancel()
|
| 141 |
+
if errStop := server.Shutdown(stopCtx); errStop != nil {
|
| 142 |
+
log.Errorf("pprof server stop failed on %s: %v", addr, errStop)
|
| 143 |
+
return errStop
|
| 144 |
+
}
|
| 145 |
+
log.Infof("pprof server stopped on %s (%s)", addr, reason)
|
| 146 |
+
return nil
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
func newPprofMux() *http.ServeMux {
|
| 150 |
+
mux := http.NewServeMux()
|
| 151 |
+
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
| 152 |
+
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
| 153 |
+
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
| 154 |
+
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
| 155 |
+
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
| 156 |
+
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
| 157 |
+
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
|
| 158 |
+
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
|
| 159 |
+
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
| 160 |
+
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
| 161 |
+
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
|
| 162 |
+
return mux
|
| 163 |
+
}
|
sdk/cliproxy/service.go
CHANGED
|
@@ -57,6 +57,9 @@ type Service struct {
|
|
| 57 |
// server is the HTTP API server instance.
|
| 58 |
server *api.Server
|
| 59 |
|
|
|
|
|
|
|
|
|
|
| 60 |
// serverErr channel for server startup/shutdown errors.
|
| 61 |
serverErr chan error
|
| 62 |
|
|
@@ -501,6 +504,8 @@ func (s *Service) Run(ctx context.Context) error {
|
|
| 501 |
time.Sleep(100 * time.Millisecond)
|
| 502 |
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
| 503 |
|
|
|
|
|
|
|
| 504 |
if s.hooks.OnAfterStart != nil {
|
| 505 |
s.hooks.OnAfterStart(s)
|
| 506 |
}
|
|
@@ -546,6 +551,7 @@ func (s *Service) Run(ctx context.Context) error {
|
|
| 546 |
}
|
| 547 |
|
| 548 |
s.applyRetryConfig(newCfg)
|
|
|
|
| 549 |
if s.server != nil {
|
| 550 |
s.server.UpdateClients(newCfg)
|
| 551 |
}
|
|
@@ -639,6 +645,13 @@ func (s *Service) Shutdown(ctx context.Context) error {
|
|
| 639 |
s.authQueueStop = nil
|
| 640 |
}
|
| 641 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
// no legacy clients to persist
|
| 643 |
|
| 644 |
if s.server != nil {
|
|
|
|
| 57 |
// server is the HTTP API server instance.
|
| 58 |
server *api.Server
|
| 59 |
|
| 60 |
+
// pprofServer manages the optional pprof HTTP debug server.
|
| 61 |
+
pprofServer *pprofServer
|
| 62 |
+
|
| 63 |
// serverErr channel for server startup/shutdown errors.
|
| 64 |
serverErr chan error
|
| 65 |
|
|
|
|
| 504 |
time.Sleep(100 * time.Millisecond)
|
| 505 |
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
| 506 |
|
| 507 |
+
s.applyPprofConfig(s.cfg)
|
| 508 |
+
|
| 509 |
if s.hooks.OnAfterStart != nil {
|
| 510 |
s.hooks.OnAfterStart(s)
|
| 511 |
}
|
|
|
|
| 551 |
}
|
| 552 |
|
| 553 |
s.applyRetryConfig(newCfg)
|
| 554 |
+
s.applyPprofConfig(newCfg)
|
| 555 |
if s.server != nil {
|
| 556 |
s.server.UpdateClients(newCfg)
|
| 557 |
}
|
|
|
|
| 645 |
s.authQueueStop = nil
|
| 646 |
}
|
| 647 |
|
| 648 |
+
if errShutdownPprof := s.shutdownPprof(ctx); errShutdownPprof != nil {
|
| 649 |
+
log.Errorf("failed to stop pprof server: %v", errShutdownPprof)
|
| 650 |
+
if shutdownErr == nil {
|
| 651 |
+
shutdownErr = errShutdownPprof
|
| 652 |
+
}
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
// no legacy clients to persist
|
| 656 |
|
| 657 |
if s.server != nil {
|