Skip to content

Commit 1e0ca7c

Browse files
authored
Merge pull request #216 from heroku/sherryyao-branch
Set up a pprof server with optional mutex profiling in cmdutil Description We want to configure the pprof listener that is capable of running mutex profiles on server set-up in cmdutil/debug. This PR created a new NewPprofServer method to the cmdutil/debug package, allowing for the setup of a pprof server with configurable profiling types and optional mutex profiling. Changes Add PProfServerConfig struct: holds configuration for the pprof server. Add PProfServer struct: wraps a pprof server Implement NewPProfServer function: creates and configures the pprof server. Implement Run and Stop methods: the NewPProfServer function includes Run and Stop methods for managing the pprof server lifecycle. Write unit tests for NewPProfServer
2 parents c2a84cc + 60faef3 commit 1e0ca7c

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

cmdutil/debug/debug.go

+85
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
package debug
1616

1717
import (
18+
"context"
1819
"fmt"
20+
"net/http"
21+
"net/http/pprof"
22+
"runtime"
23+
"time"
1924

2025
"github.com/google/gops/agent"
2126
"github.com/sirupsen/logrus"
@@ -71,3 +76,83 @@ func (s *Server) Stop(_ error) {
7176

7277
close(s.done)
7378
}
79+
80+
// PProfServer wraps a pprof server.
81+
type PProfServer struct {
82+
logger logrus.FieldLogger
83+
addr string
84+
done chan struct{}
85+
pprofServer *http.Server
86+
}
87+
88+
// ProfileConfig holds the configuration for the pprof server.
89+
type PProfServerConfig struct {
90+
Addr string
91+
MutexProfileFraction int
92+
}
93+
94+
// defaultMutexProfileFraction is the default value for MutexProfileFraction
95+
const defaultMutexProfileFraction = 2
96+
97+
// NewPProfServer sets up a pprof server with configurable profiling types and returns a PProfServer instance.
98+
func NewPProfServer(config PProfServerConfig, l logrus.FieldLogger) *PProfServer {
99+
if config.Addr == "" {
100+
config.Addr = "127.0.0.1:9998" // Default port
101+
}
102+
103+
// Use a local variable for the mutex profile fraction
104+
mpf := defaultMutexProfileFraction
105+
if config.MutexProfileFraction != 0 {
106+
mpf = config.MutexProfileFraction
107+
}
108+
runtime.SetMutexProfileFraction(mpf)
109+
110+
httpServer := &http.Server{
111+
Addr: config.Addr,
112+
Handler: http.HandlerFunc(pprof.Index),
113+
ReadHeaderTimeout: 5 * time.Second,
114+
}
115+
116+
return &PProfServer{
117+
logger: l,
118+
addr: config.Addr,
119+
done: make(chan struct{}),
120+
pprofServer: httpServer,
121+
}
122+
}
123+
124+
// Run starts the pprof server.
125+
//
126+
// It implements oklog group's runFn.
127+
func (s *PProfServer) Run() error {
128+
if s.pprofServer == nil {
129+
return fmt.Errorf("pprofServer is nil")
130+
}
131+
132+
s.logger.WithFields(logrus.Fields{
133+
"at": "binding",
134+
"service": "pprof",
135+
"addr": s.addr,
136+
}).Info()
137+
138+
if err := s.pprofServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
139+
return err
140+
}
141+
142+
<-s.done
143+
return nil
144+
}
145+
146+
// Stop shuts down the pprof server.
147+
//
148+
// It implements oklog group's interruptFn.
149+
func (s *PProfServer) Stop(_ error) {
150+
if s.pprofServer != nil {
151+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
152+
defer cancel()
153+
if err := s.pprofServer.Shutdown(ctx); err != nil {
154+
s.logger.WithError(err).Error("Error shutting down pprof server")
155+
}
156+
}
157+
close(s.done)
158+
}

cmdutil/debug/debug_test.go

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package debug
2+
3+
import (
4+
"net/http"
5+
"runtime"
6+
"testing"
7+
"time"
8+
9+
"github.com/sirupsen/logrus"
10+
)
11+
12+
func TestNewPProfServer(t *testing.T) {
13+
logger := logrus.New()
14+
15+
tests := []struct {
16+
name string
17+
config PProfServerConfig
18+
expectedAddr string
19+
expectedMutexFraction int
20+
}{
21+
{
22+
name: "DefaultAddr",
23+
config: PProfServerConfig{},
24+
expectedAddr: "127.0.0.1:9998",
25+
expectedMutexFraction: defaultMutexProfileFraction,
26+
},
27+
{
28+
name: "CustomAddr",
29+
config: PProfServerConfig{Addr: "127.0.0.1:9090"},
30+
expectedAddr: "127.0.0.1:9090",
31+
expectedMutexFraction: defaultMutexProfileFraction,
32+
},
33+
{
34+
name: "CustomMutexProfileFraction",
35+
config: PProfServerConfig{MutexProfileFraction: 5},
36+
expectedAddr: "127.0.0.1:9998",
37+
expectedMutexFraction: 5,
38+
},
39+
}
40+
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
server := NewPProfServer(tt.config, logger)
44+
45+
// Check server address
46+
if server.addr != tt.expectedAddr {
47+
t.Errorf("NewPProfServer() addr = %v, want %v", server.addr, tt.expectedAddr)
48+
}
49+
50+
// Start the server
51+
go func() {
52+
if err := server.Run(); err != nil {
53+
t.Errorf("NewPProfServer() run error = %v", err)
54+
}
55+
}()
56+
57+
// Give the server a moment to start
58+
time.Sleep(100 * time.Millisecond)
59+
60+
// Check mutex profile fraction
61+
if got := runtime.SetMutexProfileFraction(0); got != tt.expectedMutexFraction {
62+
t.Errorf("runtime.SetMutexProfileFraction() = %v, want %v", got, tt.expectedMutexFraction)
63+
}
64+
runtime.SetMutexProfileFraction(tt.expectedMutexFraction) // Reset to the expected value
65+
66+
// Perform HTTP GET request to the root path
67+
url := "http://" + server.addr + "/debug/pprof/"
68+
client := &http.Client{}
69+
70+
t.Run("GET "+url, func(t *testing.T) {
71+
req, err := http.NewRequest("GET", url, nil)
72+
if err != nil {
73+
t.Errorf("http.NewRequest(%s) error = %v", url, err)
74+
}
75+
76+
resp, err := client.Do(req)
77+
if err != nil {
78+
t.Errorf("http.Client.Do() error = %v", err)
79+
}
80+
81+
if resp.StatusCode != http.StatusOK {
82+
t.Errorf("http.Client.Do() status = %v, want %v", resp.StatusCode, http.StatusOK)
83+
}
84+
85+
resp.Body.Close()
86+
})
87+
88+
// Stop the server
89+
server.Stop(nil)
90+
91+
// Ensure the server is stopped
92+
select {
93+
case <-server.done:
94+
// success
95+
case <-time.After(1 * time.Second):
96+
t.Fatal("server did not stop in time")
97+
}
98+
})
99+
}
100+
}

0 commit comments

Comments
 (0)