Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 22 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"`

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion internal/logging/global_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ func ResolveLogDirectory(cfg *config.Config) string {
return logDir
}
if !isDirWritable(logDir) {
authDir := strings.TrimSpace(cfg.AuthDir)
authDir, err := util.ResolveAuthDir(cfg.AuthDir)
if err != nil {
log.Warnf("Failed to resolve auth-dir %q for log directory: %v", cfg.AuthDir, err)
}
if authDir != "" {
logDir = filepath.Join(authDir, "logs")
}
Expand Down
6 changes: 6 additions & 0 deletions internal/watcher/diff/config_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
163 changes: 163 additions & 0 deletions sdk/cliproxy/pprof_server.go
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 13 additions & 0 deletions sdk/cliproxy/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down