Add SIGUSR2 pprof to agent and proxy (#27510)

* Add SIGUSR2 pprof to agent

* changelog

* Update command/agent.go

Co-authored-by: Violet Hynes <violet.hynes@hashicorp.com>

* Update command/agent.go

Co-authored-by: Violet Hynes <violet.hynes@hashicorp.com>

* Add to proxy, update tests

* Fix path

* Changelog

* dry

* choose one error style

---------

Co-authored-by: Violet Hynes <violet.hynes@hashicorp.com>
This commit is contained in:
Jason O'Donnell
2024-06-17 13:10:00 -04:00
committed by GitHub
parent 299cd3d1f7
commit fe1f36a1dc
8 changed files with 76 additions and 31 deletions

6
changelog/27510.txt Normal file
View File

@@ -0,0 +1,6 @@
```release-note:improvement
agent: Add the ability to dump pprof to the filesystem using SIGUSR2
```
```release-note:improvement
proxy: Add the ability to dump pprof to the filesystem using SIGUSR2
```

View File

@@ -13,6 +13,7 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -74,6 +75,7 @@ type AgentCommand struct {
ShutdownCh chan struct{} ShutdownCh chan struct{}
SighupCh chan struct{} SighupCh chan struct{}
SigUSR2Ch chan struct{}
tlsReloadFuncsLock sync.RWMutex tlsReloadFuncsLock sync.RWMutex
tlsReloadFuncs []reloadutil.ReloadFunc tlsReloadFuncs []reloadutil.ReloadFunc
@@ -758,6 +760,16 @@ func (c *AgentCommand) Run(args []string) int {
case c.reloadedCh <- struct{}{}: case c.reloadedCh <- struct{}{}:
default: default:
} }
case <-c.SigUSR2Ch:
pprofPath := filepath.Join(os.TempDir(), "vault-agent-pprof")
cpuProfileDuration := time.Second * 1
err := WritePprofToFile(pprofPath, cpuProfileDuration)
if err != nil {
c.logger.Error(err.Error())
continue
}
c.logger.Info(fmt.Sprintf("Wrote pprof files to: %s", pprofPath))
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
} }

View File

@@ -91,6 +91,7 @@ func testAgentCommand(tb testing.TB, logger hclog.Logger) (*cli.MockUi, *AgentCo
}, },
ShutdownCh: MakeShutdownCh(), ShutdownCh: MakeShutdownCh(),
SighupCh: MakeSighupCh(), SighupCh: MakeSighupCh(),
SigUSR2Ch: MakeSigUSR2Ch(),
logger: logger, logger: logger,
startedCh: make(chan struct{}, 5), startedCh: make(chan struct{}, 5),
reloadedCh: make(chan struct{}, 5), reloadedCh: make(chan struct{}, 5),

View File

@@ -192,6 +192,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co
}, },
ShutdownCh: MakeShutdownCh(), ShutdownCh: MakeShutdownCh(),
SighupCh: MakeSighupCh(), SighupCh: MakeSighupCh(),
SigUSR2Ch: MakeSigUSR2Ch(),
}, nil }, nil
}, },
"agent generate-config": func() (cli.Command, error) { "agent generate-config": func() (cli.Command, error) {
@@ -576,6 +577,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co
}, },
ShutdownCh: MakeShutdownCh(), ShutdownCh: MakeShutdownCh(),
SighupCh: MakeSighupCh(), SighupCh: MakeSighupCh(),
SigUSR2Ch: MakeSigUSR2Ch(),
}, nil }, nil
}, },
"policy": func() (cli.Command, error) { "policy": func() (cli.Command, error) {

View File

@@ -12,6 +12,7 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -71,6 +72,7 @@ type ProxyCommand struct {
ShutdownCh chan struct{} ShutdownCh chan struct{}
SighupCh chan struct{} SighupCh chan struct{}
SigUSR2Ch chan struct{}
tlsReloadFuncsLock sync.RWMutex tlsReloadFuncsLock sync.RWMutex
tlsReloadFuncs []reloadutil.ReloadFunc tlsReloadFuncs []reloadutil.ReloadFunc
@@ -715,6 +717,16 @@ func (c *ProxyCommand) Run(args []string) int {
case c.reloadedCh <- struct{}{}: case c.reloadedCh <- struct{}{}:
default: default:
} }
case <-c.SigUSR2Ch:
pprofPath := filepath.Join(os.TempDir(), "vault-proxy-pprof")
cpuProfileDuration := time.Second * 1
err := WritePprofToFile(pprofPath, cpuProfileDuration)
if err != nil {
c.logger.Error(err.Error())
continue
}
c.logger.Info(fmt.Sprintf("Wrote pprof files to: %s", pprofPath))
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
} }

View File

@@ -44,6 +44,7 @@ func testProxyCommand(tb testing.TB, logger hclog.Logger) (*cli.MockUi, *ProxyCo
}, },
ShutdownCh: MakeShutdownCh(), ShutdownCh: MakeShutdownCh(),
SighupCh: MakeSighupCh(), SighupCh: MakeSighupCh(),
SigUSR2Ch: MakeSigUSR2Ch(),
logger: logger, logger: logger,
startedCh: make(chan struct{}, 5), startedCh: make(chan struct{}, 5),
reloadedCh: make(chan struct{}, 5), reloadedCh: make(chan struct{}, 5),

View File

@@ -1782,41 +1782,13 @@ func (c *ServerCommand) Run(args []string) int {
// into a state where it cannot process requests so we can get pprof outputs // into a state where it cannot process requests so we can get pprof outputs
// via SIGUSR2. // via SIGUSR2.
pprofPath := filepath.Join(os.TempDir(), "vault-pprof") pprofPath := filepath.Join(os.TempDir(), "vault-pprof")
err := os.MkdirAll(pprofPath, os.ModePerm) cpuProfileDuration := time.Second * 1
err := WritePprofToFile(pprofPath, cpuProfileDuration)
if err != nil { if err != nil {
c.logger.Error("Could not create temporary directory for pprof", "error", err) c.logger.Error(err.Error())
continue continue
} }
dumps := []string{"goroutine", "heap", "allocs", "threadcreate", "profile"}
for _, dump := range dumps {
pFile, err := os.Create(filepath.Join(pprofPath, dump))
if err != nil {
c.logger.Error("error creating pprof file", "name", dump, "error", err)
break
}
if dump != "profile" {
err = pprof.Lookup(dump).WriteTo(pFile, 0)
if err != nil {
c.logger.Error("error generating pprof data", "name", dump, "error", err)
pFile.Close()
break
}
} else {
// CPU profiles need to run for a duration so we're going to run it
// just for one second to avoid blocking here.
if err := pprof.StartCPUProfile(pFile); err != nil {
c.logger.Error("could not start CPU profile: ", err)
pFile.Close()
break
}
time.Sleep(time.Second * 1)
pprof.StopCPUProfile()
}
pFile.Close()
}
c.logger.Info(fmt.Sprintf("Wrote pprof files to: %s", pprofPath)) c.logger.Info(fmt.Sprintf("Wrote pprof files to: %s", pprofPath))
} }
} }

View File

@@ -8,6 +8,8 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"path/filepath"
"runtime/pprof"
"testing" "testing"
"time" "time"
@@ -201,3 +203,40 @@ func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er
StatusCode: 200, StatusCode: 200,
}, nil }, nil
} }
// WritePprofToFile will create a temporary directory at the specified path
// and generate pprof files at that location. CPU requires polling over a
// duration. For most situations 1 second is enough.
func WritePprofToFile(path string, cpuProfileDuration time.Duration) error {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return fmt.Errorf("could not create temporary directory for pprof: %v", err)
}
dumps := []string{"goroutine", "heap", "allocs", "threadcreate", "profile"}
for _, dump := range dumps {
pFile, err := os.Create(filepath.Join(path, dump))
if err != nil {
return fmt.Errorf("error creating pprof file %s: %v", dump, err)
}
if dump != "profile" {
err = pprof.Lookup(dump).WriteTo(pFile, 0)
if err != nil {
pFile.Close()
return fmt.Errorf("error generating pprof data for %s: %v", dump, err)
}
} else {
// CPU profiles need to run for a duration so we're going to run it
// just for one second to avoid blocking here.
if err := pprof.StartCPUProfile(pFile); err != nil {
pFile.Close()
return fmt.Errorf("could not start CPU profile: %v", err)
}
time.Sleep(cpuProfileDuration)
pprof.StopCPUProfile()
}
pFile.Close()
}
return nil
}