luispater commited on
Commit
f2dfed7
·
unverified ·
1 Parent(s): bcdc361

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 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 DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
 
 
 
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 {