Adds handler for returning a profile archive
Currently, when debugging issues with InfluxDB we often ask for the following profiles: curl -o block.txt "http://localhost:8086/debug/pprof/block?debug=1" curl -o goroutine.txt "http://localhost:8086/debug/pprof/goroutine?debug=1" curl -o heap.txt "http://localhost:8086/debug/pprof/heap?debug=1" curl -o cpu.txt "http://localhost:8086/debug/pprof/profile This can be bothersome for users, or even difficult if they're unfamiliar with cURL (or it's not on their system). This commit adds a new endpoint: /debug/pprof/all which will return a single compressed archive of all of the above profiles. The CPU profile is optional, and not returned by default. To include a CPU profile the URL to request should be: /debug/pprof/all?cpu=true. It's also possible to vary the length of the CPU profile by adding a `seconds=x` parameter, where x defaults to 30, if absent. The new command for gathering profiles from users should now be: curl -o profiles.tar.gz "http://localhost:8086/debug/pprof/all" Or, if we need to see a CPU profile: curl -o profiles.tar.gz "http://localhost:8086/debug/pprof/all?cpu=true" It's important to remember that a CPU profile is a blocking operation and by default it will take 30 seconds for the response to be returned to the user. Finally, if the user is unfamiliar with cURL, they will now be able to visit http://localhost:8086/debug/pprof/all in a web browser, and the archive will be downloaded to their machine.pull/7862/head
parent
3b700863ea
commit
8f8ff0ec61
|
@ -26,6 +26,7 @@ The admin UI is removed and unusable in this release. The `[admin]` configuratio
|
|||
- [#8366](https://github.com/influxdata/influxdb/pull/8366): Add TSI support tooling.
|
||||
- [#8350](https://github.com/influxdata/influxdb/pull/8350): Track HTTP client requests for /write and /query with /debug/requests.
|
||||
- [#8384](https://github.com/influxdata/influxdb/pull/8384): Write and compaction stability
|
||||
- [#7862](https://github.com/influxdata/influxdb/pull/7861): Add new profile endpoint for gathering all debug profiles single in archive.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
|
@ -252,16 +251,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Add("X-Influxdb-Version", h.Version)
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/debug/pprof") && h.Config.PprofEnabled {
|
||||
switch r.URL.Path {
|
||||
case "/debug/pprof/cmdline":
|
||||
pprof.Cmdline(w, r)
|
||||
case "/debug/pprof/profile":
|
||||
pprof.Profile(w, r)
|
||||
case "/debug/pprof/symbol":
|
||||
pprof.Symbol(w, r)
|
||||
default:
|
||||
pprof.Index(w, r)
|
||||
}
|
||||
handleProfiles(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/debug/vars") {
|
||||
h.serveExpvar(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/debug/requests") {
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
httppprof "net/http/pprof"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// handleProfiles determines which profile to return to the requester.
|
||||
func handleProfiles(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/debug/pprof/cmdline":
|
||||
httppprof.Cmdline(w, r)
|
||||
case "/debug/pprof/profile":
|
||||
httppprof.Profile(w, r)
|
||||
case "/debug/pprof/symbol":
|
||||
httppprof.Symbol(w, r)
|
||||
case "/debug/pprof/all":
|
||||
archiveProfiles(w, r)
|
||||
default:
|
||||
httppprof.Index(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// prof describes a profile name and a debug value, or in the case of a CPU
|
||||
// profile, the number of seconds to collect the profile for.
|
||||
type prof struct {
|
||||
Name string
|
||||
Debug int64
|
||||
}
|
||||
|
||||
// archiveProfiles collects the following profiles:
|
||||
// - goroutine profile
|
||||
// - heap profile
|
||||
// - blocking profile
|
||||
// - (optionally) CPU profile
|
||||
//
|
||||
// All profiles are added to a tar archive and then compressed, before being
|
||||
// returned to the requester as an archive file. Where profiles support debug
|
||||
// parameters, the profile is collected with debug=1. To optionally include a
|
||||
// CPU profile, the requester should provide a `cpu` query parameter, and can
|
||||
// also provide a `seconds` parameter to specify a non-default profile
|
||||
// collection time. The default CPU profile collection time is 30 seconds.
|
||||
//
|
||||
// Example request including CPU profile:
|
||||
//
|
||||
// http://localhost:8086/debug/pprof/all?cpu=true&seconds=45
|
||||
//
|
||||
// The value after the `cpu` query parameter is not actually important, as long
|
||||
// as there is something there.
|
||||
//
|
||||
func archiveProfiles(w http.ResponseWriter, r *http.Request) {
|
||||
var all = []*prof{
|
||||
{Name: "goroutine", Debug: 1},
|
||||
{Name: "block", Debug: 1},
|
||||
{Name: "heap", Debug: 1},
|
||||
}
|
||||
|
||||
// Capture a CPU profile?
|
||||
if r.FormValue("cpu") != "" {
|
||||
profile := &prof{Name: "cpu"}
|
||||
|
||||
// For a CPU profile we'll use the Debug field to indicate the number of
|
||||
// seconds to capture the profile for.
|
||||
profile.Debug, _ = strconv.ParseInt(r.FormValue("seconds"), 10, 64)
|
||||
if profile.Debug <= 0 {
|
||||
profile.Debug = 30
|
||||
}
|
||||
all = append([]*prof{profile}, all...) // CPU profile first.
|
||||
}
|
||||
|
||||
var (
|
||||
resp bytes.Buffer // Temporary buffer for entire archive.
|
||||
buf bytes.Buffer // Temporary buffer for each profile.
|
||||
)
|
||||
|
||||
gz := gzip.NewWriter(&resp)
|
||||
tw := tar.NewWriter(gz)
|
||||
for _, profile := range all {
|
||||
if profile.Name == "cpu" {
|
||||
if err := pprof.StartCPUProfile(&buf); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sleep(w, time.Duration(profile.Debug)*time.Second)
|
||||
pprof.StopCPUProfile()
|
||||
} else {
|
||||
prof := pprof.Lookup(profile.Name)
|
||||
if prof == nil {
|
||||
http.Error(w, "unable to find profile "+profile.Name, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := prof.WriteTo(&buf, int(profile.Debug)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Write the profile file's header.
|
||||
err := tw.WriteHeader(&tar.Header{
|
||||
Name: profile.Name + ".txt",
|
||||
Mode: 0600,
|
||||
Size: int64(buf.Len()),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Write the profile file's data.
|
||||
if _, err := tw.Write(buf.Bytes()); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Reset the buffer for the next profile.
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
// Close the tar writer.
|
||||
if err := tw.Close(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Close the gzip writer.
|
||||
if err := gz.Close(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Return the gzipped archive.
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=profiles.tar.gz")
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
io.Copy(w, &resp) // Nothing we can really do about an error at this point.
|
||||
}
|
||||
|
||||
// Taken from net/http/pprof/pprof.go
|
||||
func sleep(w http.ResponseWriter, d time.Duration) {
|
||||
var clientGone <-chan bool
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
clientGone = cn.CloseNotify()
|
||||
}
|
||||
select {
|
||||
case <-time.After(d):
|
||||
case <-clientGone:
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue