Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sys: add pprof endpoint #7473

Merged
merged 9 commits into from
Sep 19, 2019
4 changes: 4 additions & 0 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func Handler(props *vault.HandlerProperties) http.Handler {

// Create the muxer to handle the actual endpoints
mux := http.NewServeMux()

// Handle pprof paths
mux.Handle("/v1/sys/pprof/", handleLogicalNoForward(core))

mux.Handle("/v1/sys/init", handleSysInit(core))
mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core))
mux.Handle("/v1/sys/seal", handleSysSeal(core))
Expand Down
6 changes: 5 additions & 1 deletion http/logical.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ func buildLogicalRequest(core *vault.Core, w http.ResponseWriter, r *http.Reques
data = parseQuery(queryVals)
}

if path == "sys/storage/raft/snapshot" {
switch {
case strings.HasPrefix(path, "sys/pprof/"):
passHTTPReq = true
responseWriter = w
case path == "sys/storage/raft/snapshot":
responseWriter = w
}

Expand Down
173 changes: 173 additions & 0 deletions vault/external_tests/pprof/pprof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package pprof

import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"strings"
"testing"

"github.com/hashicorp/go-cleanhttp"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"golang.org/x/net/http2"
)

func TestSysPprof(t *testing.T) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()

core := cluster.Cores[0].Core
vault.TestWaitActive(t, core)
client := cluster.Cores[0].Client

transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = cluster.Cores[0].TLSConfig.Clone()
if err := http2.ConfigureTransport(transport); err != nil {
t.Fatal(err)
}
httpClient := &http.Client{
Transport: transport,
}

cases := []struct {
name string
path string
seconds string
}{
{
"index",
"/v1/sys/pprof/",
"",
},
{
"cmdline",
"/v1/sys/pprof/cmdline",
"",
},
{
"goroutine",
"/v1/sys/pprof/goroutine",
"",
},
{
"heap",
"/v1/sys/pprof/heap",
"",
},
{
"profile",
"/v1/sys/pprof/profile",
"1",
},
{
"symbol",
"/v1/sys/pprof/symbol",
"",
},
{
"trace",
"/v1/sys/pprof/trace",
"1",
},
}

pprofRequest := func(path string, seconds string) {
req := client.NewRequest("GET", path)
if seconds != "" {
req.Params.Set("seconds", seconds)
}
httpReq, err := req.ToHTTP()
if err != nil {
t.Fatal(err)
}
resp, err := httpClient.Do(httpReq)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()

httpRespBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}

httpResp := make(map[string]interface{})

// Skip this error check since some endpoints return binary blobs, we
// only care about the ok check right after as an existence check.
_ = json.Unmarshal(httpRespBody, &httpResp)

// Make sure that we don't get a error response
if _, ok := httpResp["errors"]; ok {
t.Fatalf("unexpected error response: %v", httpResp["errors"])
}

if len(httpRespBody) == 0 {
t.Fatal("no pprof index returned")
}
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
pprofRequest(tc.path, tc.seconds)
})
}
}

func TestSysPprof_MaxRequestDuration(t *testing.T) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client

transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = cluster.Cores[0].TLSConfig.Clone()
if err := http2.ConfigureTransport(transport); err != nil {
t.Fatal(err)
}
httpClient := &http.Client{
Transport: transport,
}

sec := strconv.Itoa(int(vault.DefaultMaxRequestDuration.Seconds()) + 1)

req := client.NewRequest("GET", "/v1/sys/pprof/profile")
req.Params.Set("seconds", sec)
httpReq, err := req.ToHTTP()
if err != nil {
t.Fatal(err)
}
resp, err := httpClient.Do(httpReq)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()

httpRespBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}

httpResp := make(map[string]interface{})

// If we error here, it means that profiling likely happened, which is not
// what we're checking for in this case.
if err := json.Unmarshal(httpRespBody, &httpResp); err != nil {
t.Fatalf("expected valid error response, got: %v", err)
}

errs, ok := httpResp["errors"].([]interface{})
if !ok {
t.Fatalf("expected error response, got: %v", httpResp)
}
if len(errs) == 0 || !strings.Contains(errs[0].(string), "exceeds max request duration") {
t.Fatalf("unexptected error returned: %v", errs)
}
}
1 change: 1 addition & 0 deletions vault/logical_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
b.Backend.Paths = append(b.Backend.Paths, b.toolsPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.capabilitiesPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.internalPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.pprofPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.remountPath())
b.Backend.Paths = append(b.Backend.Paths, b.metricsPath())

Expand Down
Loading