influxdb/pprof/http_server.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)
}