Skip to content

Commit 9a9cd5d

Browse files
Copilotbootjp
andcommitted
Add pprof debug endpoint
Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> Agent-Logs-Url: https://github.com/bootjp/elastickv/sessions/741b7dcf-a2c1-4615-a753-272df78e9ec5
1 parent 136e7ef commit 9a9cd5d

File tree

8 files changed

+244
-11
lines changed

8 files changed

+244
-11
lines changed

cmd/server/demo.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ var (
3838
dynamoAddress = flag.String("dynamoAddress", ":8000", "DynamoDB-compatible API address")
3939
metricsAddress = flag.String("metricsAddress", "127.0.0.1:9090", "Prometheus metrics address")
4040
metricsToken = flag.String("metricsToken", "", "Bearer token for Prometheus metrics; required for non-loopback metricsAddress")
41+
pprofAddress = flag.String("pprofAddress", "localhost:6060", "TCP host+port for pprof debug endpoints; empty to disable")
42+
pprofToken = flag.String("pprofToken", "", "Bearer token for pprof; required for non-loopback pprofAddress")
4143
raftID = flag.String("raftId", "", "Raft ID")
4244
raftDataDir = flag.String("raftDataDir", "/var/lib/elastickv", "Raft data directory")
4345
raftBootstrap = flag.Bool("raftBootstrap", false, "Bootstrap cluster")
@@ -67,6 +69,8 @@ type config struct {
6769
dynamoAddress string
6870
metricsAddress string
6971
metricsToken string
72+
pprofAddress string
73+
pprofToken string
7074
raftID string
7175
raftDataDir string
7276
raftBootstrap bool
@@ -86,6 +90,8 @@ func main() {
8690
dynamoAddress: *dynamoAddress,
8791
metricsAddress: *metricsAddress,
8892
metricsToken: *metricsToken,
93+
pprofAddress: *pprofAddress,
94+
pprofToken: *pprofToken,
8995
raftID: *raftID,
9096
raftDataDir: *raftDataDir,
9197
raftBootstrap: *raftBootstrap,
@@ -106,6 +112,7 @@ func main() {
106112
dynamoAddress: "127.0.0.1:63801",
107113
metricsAddress: "0.0.0.0:9091",
108114
metricsToken: demoMetricsToken,
115+
pprofAddress: "127.0.0.1:6061",
109116
raftID: "n1",
110117
raftDataDir: "", // In-memory
111118
raftBootstrap: true,
@@ -116,6 +123,7 @@ func main() {
116123
dynamoAddress: "127.0.0.1:63802",
117124
metricsAddress: "0.0.0.0:9092",
118125
metricsToken: demoMetricsToken,
126+
pprofAddress: "127.0.0.1:6062",
119127
raftID: "n2",
120128
raftDataDir: "",
121129
raftBootstrap: false,
@@ -126,6 +134,7 @@ func main() {
126134
dynamoAddress: "127.0.0.1:63803",
127135
metricsAddress: "0.0.0.0:9093",
128136
metricsToken: demoMetricsToken,
137+
pprofAddress: "127.0.0.1:6063",
129138
raftID: "n3",
130139
raftDataDir: "",
131140
raftBootstrap: false,
@@ -452,6 +461,15 @@ func run(ctx context.Context, eg *errgroup.Group, cfg config) error {
452461
_ = metricsL.Close()
453462
})
454463
}
464+
pprofL, ps, err := setupPprofHTTPServer(ctx, lc, cfg.pprofAddress, cfg.pprofToken)
465+
if err != nil {
466+
return err
467+
}
468+
if pprofL != nil {
469+
cleanup.Add(func() {
470+
_ = pprofL.Close()
471+
})
472+
}
455473

456474
eg.Go(catalogWatcherTask(ctx, distCatalog, distEngine))
457475
eg.Go(func() error { return compactor.Run(ctx) })
@@ -463,6 +481,8 @@ func run(ctx context.Context, eg *errgroup.Group, cfg config) error {
463481
eg.Go(dynamoServeTask(ds, cfg.dynamoAddress))
464482
eg.Go(monitoring.MetricsShutdownTask(ctx, ms, cfg.metricsAddress))
465483
eg.Go(monitoring.MetricsServeTask(ms, metricsL, cfg.metricsAddress))
484+
eg.Go(monitoring.PprofShutdownTask(ctx, ps, cfg.pprofAddress))
485+
eg.Go(monitoring.PprofServeTask(ps, pprofL, cfg.pprofAddress))
466486

467487
cleanup.Release()
468488
return nil
@@ -476,7 +496,7 @@ func setupMetricsHTTPServer(ctx context.Context, lc net.ListenConfig, metricsAdd
476496
if _, _, err := net.SplitHostPort(metricsAddress); err != nil {
477497
return nil, nil, errors.Wrapf(err, "invalid metricsAddress %q", metricsAddress)
478498
}
479-
if monitoring.MetricsAddressRequiresToken(metricsAddress) && strings.TrimSpace(metricsToken) == "" {
499+
if monitoring.AddressRequiresToken(metricsAddress) && strings.TrimSpace(metricsToken) == "" {
480500
return nil, nil, errors.New("metricsToken is required when metricsAddress is not loopback")
481501
}
482502
metricsL, err := lc.Listen(ctx, "tcp", metricsAddress)
@@ -487,6 +507,25 @@ func setupMetricsHTTPServer(ctx context.Context, lc net.ListenConfig, metricsAdd
487507
return metricsL, ms, nil
488508
}
489509

510+
func setupPprofHTTPServer(ctx context.Context, lc net.ListenConfig, pprofAddress string, pprofToken string) (net.Listener, *http.Server, error) {
511+
pprofAddress = strings.TrimSpace(pprofAddress)
512+
if pprofAddress == "" {
513+
return nil, nil, nil
514+
}
515+
if _, _, err := net.SplitHostPort(pprofAddress); err != nil {
516+
return nil, nil, errors.Wrapf(err, "invalid pprofAddress %q", pprofAddress)
517+
}
518+
if monitoring.AddressRequiresToken(pprofAddress) && strings.TrimSpace(pprofToken) == "" {
519+
return nil, nil, errors.New("pprofToken is required when pprofAddress is not loopback")
520+
}
521+
pprofL, err := lc.Listen(ctx, "tcp", pprofAddress)
522+
if err != nil {
523+
return nil, nil, errors.WithStack(err)
524+
}
525+
ps := monitoring.NewPprofServer(pprofToken)
526+
return pprofL, ps, nil
527+
}
528+
490529
func bootstrapClusterIfNeeded(r *raft.Raft, cfg config) error {
491530
if !cfg.raftBootstrap {
492531
return nil

cmd/server/demo_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,18 @@ func TestSetupMetricsHTTPServerRejectsInvalidAddressBeforeTokenCheck(t *testing.
2727
require.Nil(t, listener)
2828
require.Nil(t, server)
2929
}
30+
31+
func TestSetupPprofHTTPServerAllowsBlankAddress(t *testing.T) {
32+
listener, server, err := setupPprofHTTPServer(context.Background(), net.ListenConfig{}, "", "")
33+
require.NoError(t, err)
34+
require.Nil(t, listener)
35+
require.Nil(t, server)
36+
}
37+
38+
func TestSetupPprofHTTPServerRejectsInvalidAddressBeforeTokenCheck(t *testing.T) {
39+
listener, server, err := setupPprofHTTPServer(context.Background(), net.ListenConfig{}, "localhost", "")
40+
require.ErrorContains(t, err, `invalid pprofAddress "localhost"`)
41+
require.Nil(t, listener)
42+
require.Nil(t, server)
43+
}
44+

main.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ var (
3939
dynamoAddr = flag.String("dynamoAddress", "localhost:8000", "TCP host+port for DynamoDB-compatible API")
4040
metricsAddr = flag.String("metricsAddress", "localhost:9090", "TCP host+port for Prometheus metrics")
4141
metricsToken = flag.String("metricsToken", "", "Bearer token for Prometheus metrics; required for non-loopback metricsAddress")
42+
pprofAddr = flag.String("pprofAddress", "localhost:6060", "TCP host+port for pprof debug endpoints; empty to disable")
43+
pprofToken = flag.String("pprofToken", "", "Bearer token for pprof; required for non-loopback pprofAddress")
4244
raftId = flag.String("raftId", "", "Node id used by Raft")
4345
raftDir = flag.String("raftDataDir", "data/", "Raft data dir")
4446
raftBootstrap = flag.Bool("raftBootstrap", false, "Whether to bootstrap the Raft cluster")
@@ -133,6 +135,8 @@ func run() error {
133135
dynamoAddress: *dynamoAddr,
134136
metricsAddress: *metricsAddr,
135137
metricsToken: *metricsToken,
138+
pprofAddress: *pprofAddr,
139+
pprofToken: *pprofToken,
136140
metricsRegistry: metricsRegistry,
137141
}
138142
if err := runner.start(); err != nil {
@@ -415,6 +419,27 @@ func startDynamoDBServer(ctx context.Context, lc *net.ListenConfig, eg *errgroup
415419
return nil
416420
}
417421

422+
func startPprofServer(ctx context.Context, lc *net.ListenConfig, eg *errgroup.Group, pprofAddr string, pprofToken string) error {
423+
pprofAddr = strings.TrimSpace(pprofAddr)
424+
if pprofAddr == "" {
425+
return nil
426+
}
427+
if _, _, err := net.SplitHostPort(pprofAddr); err != nil {
428+
return errors.Wrapf(err, "invalid pprofAddress %q; expected host:port", pprofAddr)
429+
}
430+
if monitoring.AddressRequiresToken(pprofAddr) && strings.TrimSpace(pprofToken) == "" {
431+
return errors.New("pprofToken is required when pprofAddress is not loopback")
432+
}
433+
pprofL, err := lc.Listen(ctx, "tcp", pprofAddr)
434+
if err != nil {
435+
return errors.Wrapf(err, "failed to listen on %s", pprofAddr)
436+
}
437+
pprofServer := monitoring.NewPprofServer(pprofToken)
438+
eg.Go(monitoring.PprofShutdownTask(ctx, pprofServer, pprofAddr))
439+
eg.Go(monitoring.PprofServeTask(pprofServer, pprofL, pprofAddr))
440+
return nil
441+
}
442+
418443
func startMetricsServer(ctx context.Context, lc *net.ListenConfig, eg *errgroup.Group, metricsAddr string, metricsToken string, handler http.Handler) error {
419444
metricsAddr = strings.TrimSpace(metricsAddr)
420445
if metricsAddr == "" || handler == nil {
@@ -423,7 +448,7 @@ func startMetricsServer(ctx context.Context, lc *net.ListenConfig, eg *errgroup.
423448
if _, _, err := net.SplitHostPort(metricsAddr); err != nil {
424449
return errors.Wrapf(err, "invalid metricsAddress %q; expected host:port", metricsAddr)
425450
}
426-
if monitoring.MetricsAddressRequiresToken(metricsAddr) && strings.TrimSpace(metricsToken) == "" {
451+
if monitoring.AddressRequiresToken(metricsAddr) && strings.TrimSpace(metricsToken) == "" {
427452
return errors.New("metricsToken is required when metricsAddress is not loopback")
428453
}
429454
metricsL, err := lc.Listen(ctx, "tcp", metricsAddr)
@@ -516,6 +541,8 @@ type runtimeServerRunner struct {
516541
dynamoAddress string
517542
metricsAddress string
518543
metricsToken string
544+
pprofAddress string
545+
pprofToken string
519546
metricsRegistry *monitoring.Registry
520547
}
521548

@@ -532,5 +559,8 @@ func (r runtimeServerRunner) start() error {
532559
if err := startMetricsServer(r.ctx, r.lc, r.eg, r.metricsAddress, r.metricsToken, r.metricsRegistry.Handler()); err != nil {
533560
return waitErrgroupAfterStartupFailure(r.cancel, r.eg, err)
534561
}
562+
if err := startPprofServer(r.ctx, r.lc, r.eg, r.pprofAddress, r.pprofToken); err != nil {
563+
return waitErrgroupAfterStartupFailure(r.cancel, r.eg, err)
564+
}
535565
return nil
536566
}

main_pprof_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"net"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
"golang.org/x/sync/errgroup"
10+
)
11+
12+
func TestStartPprofServerRejectsInvalidAddressBeforeTokenCheck(t *testing.T) {
13+
eg, ctx := errgroup.WithContext(context.Background())
14+
err := startPprofServer(ctx, &net.ListenConfig{}, eg, "localhost", "")
15+
require.ErrorContains(t, err, `invalid pprofAddress "localhost"`)
16+
}
17+
18+
func TestStartPprofServerAllowsEmptyAddress(t *testing.T) {
19+
eg, ctx := errgroup.WithContext(context.Background())
20+
err := startPprofServer(ctx, &net.ListenConfig{}, eg, "", "")
21+
require.NoError(t, err)
22+
}

monitoring/http.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ const (
1818
metricsShutdownTimeout = 5 * time.Second
1919
)
2020

21-
// MetricsAddressRequiresToken reports whether the metrics endpoint is exposed beyond loopback.
22-
func MetricsAddressRequiresToken(addr string) bool {
21+
// AddressRequiresToken reports whether the address is exposed beyond loopback
22+
// and therefore requires bearer-token protection.
23+
func AddressRequiresToken(addr string) bool {
2324
host, _, err := net.SplitHostPort(strings.TrimSpace(addr))
2425
if err != nil {
2526
return true

monitoring/http_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12-
func TestMetricsAddressRequiresToken(t *testing.T) {
13-
require.False(t, MetricsAddressRequiresToken("localhost:9090"))
14-
require.False(t, MetricsAddressRequiresToken("127.0.0.1:9090"))
15-
require.False(t, MetricsAddressRequiresToken("[::1]:9090"))
16-
require.True(t, MetricsAddressRequiresToken(":9090"))
17-
require.True(t, MetricsAddressRequiresToken("0.0.0.0:9090"))
18-
require.True(t, MetricsAddressRequiresToken("10.0.0.1:9090"))
12+
func TestAddressRequiresToken(t *testing.T) {
13+
require.False(t, AddressRequiresToken("localhost:9090"))
14+
require.False(t, AddressRequiresToken("127.0.0.1:9090"))
15+
require.False(t, AddressRequiresToken("[::1]:9090"))
16+
require.True(t, AddressRequiresToken(":9090"))
17+
require.True(t, AddressRequiresToken("0.0.0.0:9090"))
18+
require.True(t, AddressRequiresToken("10.0.0.1:9090"))
1919
}
2020

2121
func TestProtectHandlerRequiresBearerToken(t *testing.T) {

monitoring/pprof.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package monitoring
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"net"
7+
"net/http"
8+
"net/http/pprof"
9+
10+
"github.com/cockroachdb/errors"
11+
)
12+
13+
// NewPprofHandler returns an http.Handler that serves the Go runtime profiling
14+
// endpoints under /debug/pprof/.
15+
func NewPprofHandler() http.Handler {
16+
mux := http.NewServeMux()
17+
mux.HandleFunc("/debug/pprof/", pprof.Index)
18+
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
19+
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
20+
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
21+
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
22+
return mux
23+
}
24+
25+
// NewPprofServer creates an HTTP server for the pprof debug endpoints,
26+
// optionally protected by bearer-token authentication.
27+
func NewPprofServer(bearerToken string) *http.Server {
28+
return &http.Server{
29+
Handler: ProtectHandler(NewPprofHandler(), bearerToken),
30+
ReadHeaderTimeout: metricsReadHeaderTimeout,
31+
}
32+
}
33+
34+
// PprofShutdownTask returns an errgroup task that stops the pprof server on
35+
// context cancellation.
36+
func PprofShutdownTask(ctx context.Context, server *http.Server, address string) func() error {
37+
return func() error {
38+
if server == nil {
39+
return nil
40+
}
41+
<-ctx.Done()
42+
slog.Info("Shutting down pprof server", "address", address, "reason", ctx.Err())
43+
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), metricsShutdownTimeout)
44+
defer cancel()
45+
err := server.Shutdown(shutdownCtx)
46+
if err == nil || errors.Is(err, http.ErrServerClosed) || errors.Is(err, net.ErrClosed) {
47+
return nil
48+
}
49+
return errors.WithStack(err)
50+
}
51+
}
52+
53+
// PprofServeTask returns an errgroup task that serves the pprof endpoint until
54+
// shutdown.
55+
func PprofServeTask(server *http.Server, listener net.Listener, address string) func() error {
56+
return func() error {
57+
if server == nil || listener == nil {
58+
return nil
59+
}
60+
slog.Info("Starting pprof server", "address", address)
61+
err := server.Serve(listener)
62+
if err == nil || errors.Is(err, http.ErrServerClosed) || errors.Is(err, net.ErrClosed) {
63+
return nil
64+
}
65+
return errors.WithStack(err)
66+
}
67+
}

monitoring/pprof_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package monitoring
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestNewPprofHandlerServesIndexRoute(t *testing.T) {
13+
h := NewPprofHandler()
14+
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/debug/pprof/", nil)
15+
rec := httptest.NewRecorder()
16+
h.ServeHTTP(rec, req)
17+
require.Equal(t, http.StatusOK, rec.Code)
18+
}
19+
20+
func TestNewPprofHandlerServesProfileRoute(t *testing.T) {
21+
h := NewPprofHandler()
22+
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/debug/pprof/profile?seconds=1", nil)
23+
rec := httptest.NewRecorder()
24+
25+
done := make(chan struct{})
26+
go func() {
27+
defer close(done)
28+
h.ServeHTTP(rec, req)
29+
}()
30+
<-done
31+
require.Equal(t, http.StatusOK, rec.Code)
32+
}
33+
34+
func TestNewPprofServerRequiresBearerToken(t *testing.T) {
35+
server := NewPprofServer("pprof-token")
36+
require.NotNil(t, server)
37+
require.NotNil(t, server.Handler)
38+
39+
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/debug/pprof/", nil)
40+
rec := httptest.NewRecorder()
41+
server.Handler.ServeHTTP(rec, req)
42+
require.Equal(t, http.StatusUnauthorized, rec.Code)
43+
44+
req = httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/debug/pprof/", nil)
45+
req.Header.Set("Authorization", "Bearer pprof-token")
46+
rec = httptest.NewRecorder()
47+
server.Handler.ServeHTTP(rec, req)
48+
require.Equal(t, http.StatusOK, rec.Code)
49+
}
50+
51+
func TestNewPprofServerNoTokenAllowsAllRequests(t *testing.T) {
52+
server := NewPprofServer("")
53+
require.NotNil(t, server)
54+
55+
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/debug/pprof/", nil)
56+
rec := httptest.NewRecorder()
57+
server.Handler.ServeHTTP(rec, req)
58+
require.Equal(t, http.StatusOK, rec.Code)
59+
}

0 commit comments

Comments
 (0)