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

VAULT-1564 report in-flight requests #13024

Merged
merged 36 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3463e90
VAULT-1564 report in-flight requests
hghaf099 Nov 3, 2021
6420c0c
adding a changelog
hghaf099 Nov 3, 2021
2c3fefe
Changing some variable names and fixing comments
hghaf099 Nov 3, 2021
e2031da
minor style change
hghaf099 Nov 3, 2021
29c5f24
adding unauthenticated support for in-flight-req
hghaf099 Nov 3, 2021
17a4928
adding documentation for the listener.profiling stanza
hghaf099 Nov 3, 2021
18ab7da
adding an atomic counter for the inflight requests
hghaf099 Nov 5, 2021
660ff96
addressing comments
hghaf099 Nov 5, 2021
a06c4a7
Merge branch 'main' into report-in-flight-req
hghaf099 Nov 10, 2021
b2979d5
logging completed requests
hghaf099 Nov 17, 2021
5914acd
fixing a test
hghaf099 Nov 18, 2021
f418b0e
providing log_requests_info as a config option to determine at which …
hghaf099 Nov 18, 2021
c181d15
removing a member and a method from the StatusHeaderResponseWriter st…
hghaf099 Nov 18, 2021
16ed14a
adding api docks
hghaf099 Nov 19, 2021
e9f2312
Merge branch 'main' into report-in-flight-req
hghaf099 Nov 23, 2021
d7682a5
revert changes in NewHTTPResponseWriter
hghaf099 Nov 23, 2021
2ad56c2
Fix logging invalid log_requests_info value
hghaf099 Nov 23, 2021
814128b
Addressing comments
hghaf099 Nov 24, 2021
6ecb241
Fixing a test
hghaf099 Nov 25, 2021
ec22517
use an tomic value for logRequestsInfo, and moving the CreateClientID…
hghaf099 Nov 25, 2021
8a6350e
fixing go.sum
hghaf099 Nov 25, 2021
b9dad8c
minor refactoring
hghaf099 Nov 25, 2021
e3079f0
protecting InFlightRequests from data race
hghaf099 Nov 25, 2021
96e5c71
another try on fixing a data race
hghaf099 Nov 25, 2021
03153e7
another try to fix a data race
hghaf099 Nov 25, 2021
da8a1e0
addressing comments
hghaf099 Dec 6, 2021
18d70ba
Merge branch 'main' into report-in-flight-req
hghaf099 Dec 6, 2021
2a53824
fixing couple of tests
hghaf099 Dec 6, 2021
9893d2e
changing log_requests_info to log_requests_level
hghaf099 Dec 6, 2021
d565b64
minor style change
hghaf099 Dec 6, 2021
97c4cf3
fixing a test
hghaf099 Dec 6, 2021
5649a60
removing the lock in InFlightRequests
hghaf099 Dec 7, 2021
a0bec8d
use single-argument form for interface assertion
hghaf099 Dec 7, 2021
087fb00
adding doc for the new configuration paramter
hghaf099 Dec 7, 2021
5ed569b
adding the new doc to the nav data file
hghaf099 Dec 7, 2021
8c8e5d8
minor fix
hghaf099 Dec 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/13024.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
**Report in-flight requests**:Adding a trace capability to show in-flight requests, and a new gauge metric to show the total number of in-flight requests
```
69 changes: 67 additions & 2 deletions command/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/hashicorp/go-secure-stdlib/gatedwriter"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/version"
"github.com/mholt/archiver"
Expand Down Expand Up @@ -106,6 +107,7 @@ type DebugCommand struct {
metricsCollection []map[string]interface{}
replicationStatusCollection []map[string]interface{}
serverStatusCollection []map[string]interface{}
inFlightReqStatusCollection []map[string]interface{}

// cachedClient holds the client retrieved during preflight
cachedClient *api.Client
Expand Down Expand Up @@ -480,7 +482,7 @@ func (c *DebugCommand) preflight(rawArgs []string) (string, error) {
}

func (c *DebugCommand) defaultTargets() []string {
return []string{"config", "host", "metrics", "pprof", "replication-status", "server-status", "log"}
return []string{"config", "host", "requests", "metrics", "pprof", "replication-status", "server-status", "log"}
}

func (c *DebugCommand) captureStaticTargets() error {
Expand All @@ -492,6 +494,7 @@ func (c *DebugCommand) captureStaticTargets() error {
if err != nil {
c.captureError("config", err)
c.logger.Error("config: error capturing config state", "error", err)
return nil
}

if resp != nil && resp.Data != nil {
Expand Down Expand Up @@ -580,6 +583,16 @@ func (c *DebugCommand) capturePollingTargets() error {
})
}

// Collect in-flight request status if target is specified
if strutil.StrListContains(c.flagTargets, "requests") {
g.Add(func() error {
c.collectInFlightRequestStatus(ctx)
return nil
}, func(error) {
cancelFunc()
})
}

if strutil.StrListContains(c.flagTargets, "log") {
g.Add(func() error {
c.writeLogs(ctx)
Expand Down Expand Up @@ -611,7 +624,9 @@ func (c *DebugCommand) capturePollingTargets() error {
if err := c.persistCollection(c.hostInfoCollection, "host_info.json"); err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %v", "host_info.json", err))
}

if err := c.persistCollection(c.inFlightReqStatusCollection, "requests.json"); err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %v", "requests.json", err))
}
return nil
}

Expand All @@ -635,13 +650,15 @@ func (c *DebugCommand) collectHostInfo(ctx context.Context) {
resp, err := c.cachedClient.RawRequestWithContext(ctx, r)
if err != nil {
c.captureError("host", err)
return
}
if resp != nil {
defer resp.Body.Close()

secret, err := api.ParseSecret(resp.Body)
if err != nil {
c.captureError("host", err)
return
}
if secret != nil && secret.Data != nil {
hostEntry := secret.Data
Expand Down Expand Up @@ -829,13 +846,15 @@ func (c *DebugCommand) collectReplicationStatus(ctx context.Context) {
resp, err := c.cachedClient.RawRequestWithContext(ctx, r)
if err != nil {
c.captureError("replication-status", err)
return
}
if resp != nil {
defer resp.Body.Close()

secret, err := api.ParseSecret(resp.Body)
if err != nil {
c.captureError("replication-status", err)
return
}
if secret != nil && secret.Data != nil {
replicationEntry := secret.Data
Expand Down Expand Up @@ -865,10 +884,12 @@ func (c *DebugCommand) collectServerStatus(ctx context.Context) {
healthInfo, err := c.cachedClient.Sys().Health()
if err != nil {
c.captureError("server-status.health", err)
return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it's true that an error on the health endpoint will probably mean one on the seal-status endpoint too, I'd rather we didn't assume that. Can you revert this change please?

}
sealInfo, err := c.cachedClient.Sys().SealStatus()
if err != nil {
c.captureError("server-status.seal", err)
return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too.

}

statusEntry := map[string]interface{}{
Expand All @@ -880,6 +901,50 @@ func (c *DebugCommand) collectServerStatus(ctx context.Context) {
}
}

func (c *DebugCommand) collectInFlightRequestStatus(ctx context.Context) {

idxCount := 0
intervalTicker := time.Tick(c.flagInterval)

for {
if idxCount > 0 {
select {
case <-ctx.Done():
return
case <-intervalTicker:
}
}

c.logger.Info("capturing in-flight request status", "count", idxCount)
idxCount++

req := c.cachedClient.NewRequest("GET", "/v1/sys/in-flight-req")
resp, err := c.cachedClient.RawRequestWithContext(ctx, req)
if err != nil {
c.captureError("inFlightReq-status", err)
ncabatoff marked this conversation as resolved.
Show resolved Hide resolved
return
}

var data map[string]interface{}
if resp != nil {
defer resp.Body.Close()
err = jsonutil.DecodeJSONFromReader(resp.Body, &data)
if err != nil {
c.captureError("inFlightReq-status", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the first arg to captureError is the target, which we're now calling requests right?

return
}

if data != nil && len(data) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this conditional do we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, there should always be at least one entry which is the /v1/sys/in-flight-req related info

statusEntry := map[string]interface{}{
"timestamp": time.Now().UTC(),
"in_flight_requests": data,
}
c.inFlightReqStatusCollection = append(c.inFlightReqStatusCollection, statusEntry)
}
}
}
}

// persistCollection writes the collected data for a particular target onto the
// specified file. If the collection is empty, it returns immediately.
func (c *DebugCommand) persistCollection(collection []map[string]interface{}, outFile string) error {
Expand Down
5 changes: 5 additions & 0 deletions command/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ func TestDebugCommand_CaptureTargets(t *testing.T) {
[]string{"server-status"},
[]string{"server_status.json"},
},
{
"in-flight-req",
[]string{"requests"},
[]string{"requests.json"},
},
{
"all-minus-pprof",
[]string{"config", "host", "metrics", "replication-status", "server-status"},
Expand Down
13 changes: 13 additions & 0 deletions command/server/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"net"
"testing"

)

type testListenerConnFn func(net.Listener) (net.Conn, error)
Expand Down Expand Up @@ -66,3 +67,15 @@ func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn,
t.Fatalf("bad: %v", buf.String())
}
}


func TestProfilingUnauthenticatedInFlightAccess(t *testing.T) {

config, err := LoadConfigFile("./test-fixtures/profiling_unauth_in_flight_access.hcl")
if err != nil {
t.Fatalf("Error encountered when loading config %+v", err)
}
if !config.Listeners[0].Profiling.UnauthenticatedInFlightAccess {
t.Fatalf("failed to read UnauthenticatedInFlightAccess")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
storage "inmem" {}
listener "tcp" {
address = "127.0.0.1:8200"
tls_disable = true
profiling {
unauthenticated_in_flight_request_access = true
}
}
disable_mlock = true
27 changes: 27 additions & 0 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/hashicorp/go-uuid"
ncabatoff marked this conversation as resolved.
Show resolved Hide resolved
"io"
"io/fs"
"io/ioutil"
Expand Down Expand Up @@ -196,6 +197,11 @@ func Handler(props *vault.HandlerProperties) http.Handler {
mux.Handle("/v1/sys/pprof/", handleLogicalNoForward(core))
}

if props.ListenerConfig != nil && props.ListenerConfig.Profiling.UnauthenticatedInFlightAccess {
mux.Handle("/v1/sys/in-flight-req", handleUnAuthenticatedInFlightRequest(core))
} else {
mux.Handle("/v1/sys/in-flight-req", handleLogicalNoForward(core))
}
additionalRoutes(mux, core)
}

Expand Down Expand Up @@ -389,6 +395,27 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr
hostname, _ := os.Hostname()

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// The uuid for the request is going to be generated when a logical
// request is generated. But, here we generate one to be able to track
// in-flight requests
inFlightReqID, err := uuid.GenerateUUID()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to generate an identifier for the in-flight request"))
}
core.StoreInFlightReqData(
inFlightReqID,
&vault.InFlightReqData {
StartTime: time.Now(),
ClientRemoteAddr: r.RemoteAddr,
ReqPath: r.URL.Path,
})
if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What error are we handling here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks for catching this!

respondError(w, http.StatusBadRequest, err)
}
// deleting the in-flight request entry
defer core.DeleteInFlightReqData(inFlightReqID)

// This block needs to be here so that upon sending SIGHUP, custom response
// headers are also reloaded into the handlers.
if props.ListenerConfig != nil {
Expand Down
98 changes: 98 additions & 0 deletions http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/textproto"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -294,6 +295,103 @@ func TestHandler_CacheControlNoStore(t *testing.T) {
}
}

func TestHandler_InFlightRequest(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)

req, err := http.NewRequest("GET", addr+"/v1/sys/in-flight-req", nil)
if err != nil {
t.Fatalf("err: %s", err)
}
req.Header.Set(consts.AuthHeaderName, token)

client := cleanhttp.DefaultClient()
resp, err := client.Do(req)
if err != nil {
t.Fatalf("err: %s", err)
}

if resp == nil {
t.Fatalf("nil response")
}

var actual map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if actual == nil {
ncabatoff marked this conversation as resolved.
Show resolved Hide resolved
t.Fatalf("")
}
}

func TestHandler_InFlightRequestWithLoad(t *testing.T) {
hghaf099 marked this conversation as resolved.
Show resolved Hide resolved
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)

stop := make(chan string)

go func() {
i := 0
for {
select {
case <-stop:
return
default:
break
}
// WRITE
secResp := testHttpPut(t, token, addr+"/v1/secret/foo"+strconv.Itoa(i), map[string]interface{}{
"data": "bar",
})
testResponseStatus(t, secResp, 204)
i++
}
}()

timeout := time.After(10 * time.Second)

for {
select {
case <-timeout:
stop <- "done"
t.Fatalf("could not capture any in-flight-req")
return
default:
}
req, err := http.NewRequest("GET", addr+"/v1/sys/in-flight-req", nil)
if err != nil {
t.Fatalf("err: %s", err)
}
req.Header.Set(consts.AuthHeaderName, token)

client := cleanhttp.DefaultClient()
resp, err := client.Do(req)
if err != nil {
t.Fatalf("err: %s", err)
}

if resp == nil {
t.Fatalf("nil response")
}

var inFlightReqData map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &inFlightReqData)

if inFlightReqData == nil {
t.Fatalf("")
}

if inFlightReqData != nil || len(inFlightReqData) > 0 {
stop <- "done"
return
}
}
}

// TestHandler_MissingToken tests the response / error code if a request comes
// in with a missing client token. See
// https://github.com/hashicorp/vault/issues/8377
Expand Down
2 changes: 1 addition & 1 deletion http/logical.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.

requestId, err := uuid.GenerateUUID()
if err != nil {
return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to generate identifier for the request: %w", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to generate identifier for the request: %w", err)
}

req := &logical.Request{
Expand Down
Loading