Skip to content

Commit d1049e5

Browse files
feat: add pprof debug endpoint for runtime profiling (#127)
* feat: add pprof debug endpoint for runtime profiling Adds optional pprof HTTP endpoint for runtime memory, CPU, and goroutine profiling. Useful for debugging memory issues and performance analysis. Configuration: ```yaml pprof: enabled: true listen_addr: 127.0.0.1 listen_port: 6060 ``` Endpoints exposed: - /debug/pprof/ - Index with links to all profiles - /debug/pprof/heap - Heap memory profile - /debug/pprof/profile - CPU profile (30s default) - /debug/pprof/goroutine - Goroutine stack dumps - /debug/pprof/trace - Execution trace Usage: go tool pprof http://127.0.0.1:6060/debug/pprof/heap WARNING: Only enable in development/debugging scenarios, not production. * fix: add graceful shutdown for pprof server Use http.Server with Shutdown() instead of raw ListenAndServe to properly shut down the pprof endpoint when the application exits. * test: add PprofConfig unmarshal tests - Test that pprof config is properly unmarshalled from YAML - Test that pprof is disabled by default when not specified
1 parent ec203a7 commit d1049e5

File tree

4 files changed

+90
-0
lines changed

4 files changed

+90
-0
lines changed

cmd/root.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"net"
88
"net/http"
9+
"net/http/pprof"
910
"os"
1011
"os/signal"
1112
"strings"
@@ -134,6 +135,45 @@ func Execute() error {
134135
}()
135136
}
136137

138+
// pprof debug endpoint for runtime profiling (memory, CPU, goroutines)
139+
// WARNING: Only enable in development/debugging scenarios
140+
if config.PprofConfig.Enabled {
141+
pprofMux := http.NewServeMux()
142+
pprofMux.HandleFunc("/debug/pprof/", pprof.Index)
143+
pprofMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
144+
pprofMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
145+
pprofMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
146+
pprofMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
147+
148+
listenOn := net.JoinHostPort(
149+
config.PprofConfig.ListenAddress,
150+
config.PprofConfig.ListenPort,
151+
)
152+
153+
pprofServer := &http.Server{
154+
Addr: listenOn,
155+
Handler: pprofMux,
156+
}
157+
158+
g.Go(func() error {
159+
log.Warnf("pprof debug endpoint enabled at %s/debug/pprof/ - DO NOT USE IN PRODUCTION", listenOn)
160+
if err := pprofServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
161+
return fmt.Errorf("pprof server error: %w", err)
162+
}
163+
return nil
164+
})
165+
166+
g.Go(func() error {
167+
<-ctx.Done()
168+
log.Info("Shutting down pprof server...")
169+
// Use background context since parent ctx is already canceled
170+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
171+
defer cancel()
172+
//nolint:contextcheck // parent ctx is canceled, need fresh context for shutdown
173+
return pprofServer.Shutdown(shutdownCtx)
174+
})
175+
}
176+
137177
dataSet := dataset.New()
138178

139179
g.Go(func() error {

config/crowdsec-spoa-bouncer.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ prometheus:
2424
enabled: false
2525
listen_addr: 127.0.0.1
2626
listen_port: 60601
27+
28+
## pprof debug endpoint for runtime profiling
29+
## WARNING: Only enable for debugging, exposes internal runtime data
30+
## Endpoints: /debug/pprof/heap, /debug/pprof/profile, /debug/pprof/goroutine, etc.
31+
#pprof:
32+
# enabled: false
33+
# listen_addr: 127.0.0.1
34+
# listen_port: 6060

pkg/cfg/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ type PrometheusConfig struct {
1818
ListenPort string `yaml:"listen_port"`
1919
}
2020

21+
// PprofConfig configures the pprof debug endpoint for runtime profiling.
22+
// When enabled, exposes Go's pprof endpoints for memory, CPU, and goroutine profiling.
23+
// WARNING: Only enable in development/debugging scenarios, not in production.
24+
type PprofConfig struct {
25+
Enabled bool `yaml:"enabled"`
26+
ListenAddress string `yaml:"listen_addr"`
27+
ListenPort string `yaml:"listen_port"`
28+
}
29+
2130
type BouncerConfig struct {
2231
Logging cslogging.LoggingConfig `yaml:",inline"`
2332
Hosts []*host.Host `yaml:"hosts"`
@@ -26,6 +35,7 @@ type BouncerConfig struct {
2635
ListenTCP string `yaml:"listen_tcp"`
2736
ListenUnix string `yaml:"listen_unix"`
2837
PrometheusConfig PrometheusConfig `yaml:"prometheus"`
38+
PprofConfig PprofConfig `yaml:"pprof"`
2939
}
3040

3141
// MergedConfig() returns the byte content of the patched configuration file (with .yaml.local).

pkg/cfg/config_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,35 @@ logging:
4444
require.Error(t, err)
4545
assert.Contains(t, err.Error(), "at least one listener")
4646
}
47+
48+
func TestPprofConfigUnmarshal(t *testing.T) {
49+
const configYAML = `
50+
log_mode: stdout
51+
listen_tcp: 0.0.0.0:9000
52+
pprof:
53+
enabled: true
54+
listen_addr: 127.0.0.1
55+
listen_port: "6060"
56+
`
57+
58+
cfg, err := NewConfig(strings.NewReader(configYAML))
59+
60+
require.NoError(t, err)
61+
assert.True(t, cfg.PprofConfig.Enabled)
62+
assert.Equal(t, "127.0.0.1", cfg.PprofConfig.ListenAddress)
63+
assert.Equal(t, "6060", cfg.PprofConfig.ListenPort)
64+
}
65+
66+
func TestPprofConfigDefaults(t *testing.T) {
67+
const configYAML = `
68+
log_mode: stdout
69+
listen_tcp: 0.0.0.0:9000
70+
`
71+
72+
cfg, err := NewConfig(strings.NewReader(configYAML))
73+
74+
require.NoError(t, err)
75+
assert.False(t, cfg.PprofConfig.Enabled, "pprof should be disabled by default")
76+
assert.Empty(t, cfg.PprofConfig.ListenAddress)
77+
assert.Empty(t, cfg.PprofConfig.ListenPort)
78+
}

0 commit comments

Comments
 (0)