172 lines
4.9 KiB
Go
172 lines
4.9 KiB
Go
package pprof
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
httppprof "net/http/pprof"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/influxdata/influxdb/v2/kit/platform/errors"
|
|
ihttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
|
)
|
|
|
|
type Handler struct {
|
|
chi.Router
|
|
}
|
|
|
|
func NewHTTPHandler(profilingEnabled bool) *Handler {
|
|
r := chi.NewRouter()
|
|
r.Route("/pprof", func(r chi.Router) {
|
|
if !profilingEnabled {
|
|
r.NotFound(profilingDisabledHandler)
|
|
return
|
|
}
|
|
r.Get("/cmdline", httppprof.Cmdline)
|
|
r.Get("/profile", httppprof.Profile)
|
|
r.Get("/symbol", httppprof.Symbol)
|
|
r.Get("/trace", httppprof.Trace)
|
|
r.Get("/all", archiveProfilesHandler)
|
|
r.Mount("/", http.HandlerFunc(httppprof.Index))
|
|
})
|
|
|
|
return &Handler{r}
|
|
}
|
|
|
|
func profilingDisabledHandler(w http.ResponseWriter, r *http.Request) {
|
|
ihttp.WriteErrorResponse(r.Context(), w, errors.EForbidden, "profiling disabled")
|
|
}
|
|
|
|
func archiveProfilesHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// We parse the form here so that we can use the http.Request.Form map.
|
|
//
|
|
// Otherwise we'd have to use r.FormValue() which makes it impossible to
|
|
// distinguish between a form value that exists and has no value and one that
|
|
// does not exist at all.
|
|
if err := r.ParseForm(); err != nil {
|
|
ihttp.WriteErrorResponse(ctx, w, errors.EInternal, err.Error())
|
|
return
|
|
}
|
|
|
|
// In the following two blocks, we check if the request should include cpu
|
|
// profiles and a trace log.
|
|
//
|
|
// Since the submitted form can contain multiple version of a variable like:
|
|
//
|
|
// http://localhost:8086?cpu=1s&cpu=30s&trace=3s&cpu=5s
|
|
//
|
|
// the question arises: which value should we use? We choose to use the LAST
|
|
// value supplied.
|
|
//
|
|
// This is an edge case but if for some reason, for example, a url is
|
|
// programmatically built and multiple values are supplied, this will do what
|
|
// is expected.
|
|
//
|
|
var traceDuration, cpuDuration time.Duration
|
|
|
|
// last() returns either the last item from a slice of strings or an empty
|
|
// string if the supplied slice is empty or nil.
|
|
last := func(s []string) string {
|
|
if len(s) == 0 {
|
|
return ""
|
|
}
|
|
return s[len(s)-1]
|
|
}
|
|
|
|
// If trace exists as a form value, add it to the profiles slice with the
|
|
// decoded duration.
|
|
//
|
|
// Requests for a trace should look like:
|
|
//
|
|
// ?trace=10s
|
|
//
|
|
if vals, exists := r.Form["trace"]; exists {
|
|
// parse the duration encoded in the last "trace" value supplied.
|
|
val := last(vals)
|
|
duration, err := time.ParseDuration(val)
|
|
|
|
// If we can't parse the duration or if the user supplies a negative
|
|
// number, return an appropriate error status and message.
|
|
//
|
|
// In this case it is a StatusBadRequest (400) since the problem is in the
|
|
// supplied form data.
|
|
if duration < 0 {
|
|
ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, "negative trace durations not allowed")
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, fmt.Sprintf("could not parse supplied duration for trace %q", val))
|
|
return
|
|
}
|
|
|
|
// Trace files can get big. Lets clamp the maximum trace duration to 45s.
|
|
if duration > 45*time.Second {
|
|
ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, "cannot trace for longer than 45s")
|
|
return
|
|
}
|
|
|
|
traceDuration = duration
|
|
}
|
|
|
|
// Capturing CPU profiles is a little trickier. The preferred way to send the
|
|
// cpu profile duration is via the supplied "cpu" variable's value.
|
|
//
|
|
// The duration should be encoded as a Go duration that can be parsed by
|
|
// time.ParseDuration().
|
|
//
|
|
// In the past users were encouraged to assign any value to cpu and provide
|
|
// the duration in a separate "seconds" value.
|
|
//
|
|
// The code below handles both -- first it attempts to use the old method
|
|
// which would look like:
|
|
//
|
|
// ?cpu=foobar&seconds=10
|
|
//
|
|
// Then it attempts to ascertain the duration provided with:
|
|
//
|
|
// ?cpu=10s
|
|
//
|
|
// This preserves backwards compatibility with any tools that have been
|
|
// written to gather profiles.
|
|
//
|
|
if vals, exists := r.Form["cpu"]; exists {
|
|
duration := time.Second * 30
|
|
val := last(vals)
|
|
|
|
// getDuration is a small function literal that encapsulates the logic
|
|
// for getting the duration from either the "seconds" form value or from
|
|
// the value assigned to "cpu".
|
|
getDuration := func() (time.Duration, error) {
|
|
if seconds, exists := r.Form["seconds"]; exists {
|
|
s, err := strconv.ParseInt(last(seconds), 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return time.Second * time.Duration(s), nil
|
|
}
|
|
// see if the value of cpu is a duration like: cpu=10s
|
|
return time.ParseDuration(val)
|
|
}
|
|
|
|
duration, err := getDuration()
|
|
if err != nil {
|
|
ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, fmt.Sprintf("could not parse supplied duration for cpu profile %q", val))
|
|
return
|
|
}
|
|
|
|
cpuDuration = duration
|
|
}
|
|
|
|
tarstream, err := collectAllProfiles(ctx, traceDuration, cpuDuration)
|
|
if err != nil {
|
|
ihttp.WriteErrorResponse(ctx, w, errors.EInternal, err.Error())
|
|
return
|
|
}
|
|
_, _ = io.Copy(w, tarstream)
|
|
}
|