github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/server/debug/pprofui/server.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package pprofui
    12  
    13  import (
    14  	"bytes"
    15  	"fmt"
    16  	"io"
    17  	"net/http"
    18  	"net/http/pprof"
    19  	"net/url"
    20  	"path"
    21  	runtimepprof "runtime/pprof"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    28  	"github.com/cockroachdb/errors"
    29  	"github.com/google/pprof/driver"
    30  	"github.com/google/pprof/profile"
    31  	"github.com/spf13/pflag"
    32  )
    33  
    34  // A Server serves up the pprof web ui. A request to /<profiletype>
    35  // generates a profile of the desired type and redirects to the UI for
    36  // it at /<profiletype>/<id>. Valid profile types at the time of
    37  // writing include `profile` (cpu), `goroutine`, `threadcreate`,
    38  // `heap`, `block`, and `mutex`.
    39  type Server struct {
    40  	storage      Storage
    41  	profileSem   syncutil.Mutex
    42  	profileTypes map[string]http.HandlerFunc
    43  	hook         func(profile string, labels bool, do func())
    44  }
    45  
    46  // NewServer creates a new Server backed by the supplied Storage and optionally
    47  // a hook which is called when a new profile is created. The closure passed to
    48  // the hook will carry out the work involved in creating the profile and must
    49  // be called by the hook. The intention is that hook will be a method such as
    50  // this:
    51  //
    52  // func hook(profile string, do func()) {
    53  // 	if profile == "profile" {
    54  // 		something.EnableProfilerLabels()
    55  // 		defer something.DisableProfilerLabels()
    56  // 		do()
    57  // 	}
    58  // }
    59  func NewServer(storage Storage, hook func(profile string, labels bool, do func())) *Server {
    60  	if hook == nil {
    61  		hook = func(_ string, _ bool, do func()) { do() }
    62  	}
    63  	s := &Server{
    64  		storage: storage,
    65  		hook:    hook,
    66  	}
    67  
    68  	s.profileTypes = map[string]http.HandlerFunc{
    69  		// The CPU profile endpoint is special in that the handler actually blocks
    70  		// for a predetermined duration (recording the profile in the meantime).
    71  		// It is not included in `runtimepprof.Profiles` below.
    72  		"profile": func(w http.ResponseWriter, r *http.Request) {
    73  			const defaultProfileDurationSeconds = 5
    74  			if r.Form == nil {
    75  				r.Form = url.Values{}
    76  			}
    77  			if r.Form.Get("seconds") == "" {
    78  				r.Form.Set("seconds", strconv.Itoa(defaultProfileDurationSeconds))
    79  			}
    80  			s.profileSem.Lock()
    81  			defer s.profileSem.Unlock()
    82  			pprof.Profile(w, r)
    83  		},
    84  	}
    85  
    86  	// Register the endpoints for heap, block, threadcreate, etc.
    87  	for _, p := range runtimepprof.Profiles() {
    88  		p := p // copy
    89  		s.profileTypes[p.Name()] = func(w http.ResponseWriter, r *http.Request) {
    90  			if err := p.WriteTo(w, 0 /* debug */); err != nil {
    91  				w.WriteHeader(http.StatusInternalServerError)
    92  				_, _ = w.Write([]byte(err.Error()))
    93  			}
    94  		}
    95  	}
    96  
    97  	return s
    98  }
    99  
   100  // parsePath turns /profile/123/flamegraph/banana into (profile, 123, /flamegraph/banana).
   101  func (s *Server) parsePath(reqPath string) (profType string, id string, remainingPath string) {
   102  	parts := strings.Split(path.Clean(reqPath), "/")
   103  	if parts[0] == "" {
   104  		// The path was absolute (the typical case), pretend it was
   105  		// relative (to this handler's root).
   106  		parts = parts[1:]
   107  	}
   108  	switch len(parts) {
   109  	case 0:
   110  		return "", "", "/"
   111  	case 1:
   112  		return parts[0], "", "/"
   113  	default:
   114  		return parts[0], parts[1], "/" + strings.Join(parts[2:], "/")
   115  	}
   116  }
   117  
   118  func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   119  	profileName, id, remainingPath := s.parsePath(r.URL.Path)
   120  
   121  	if profileName == "" {
   122  		// TODO(tschottdorf): serve an overview page.
   123  		var names []string
   124  		for name := range s.profileTypes {
   125  			names = append(names, name)
   126  		}
   127  		sort.Strings(names)
   128  		msg := fmt.Sprintf("Try %s for one of %s", path.Join(r.RequestURI, "<profileName>"), strings.Join(names, ", "))
   129  		http.Error(w, msg, http.StatusNotFound)
   130  		return
   131  	}
   132  
   133  	if id != "" {
   134  		// Catch nonexistent IDs early or pprof will do a worse job at
   135  		// giving an informative error.
   136  		if err := s.storage.Get(id, func(io.Reader) error { return nil }); err != nil {
   137  			msg := fmt.Sprintf("profile for id %s not found: %s", id, err)
   138  			http.Error(w, msg, http.StatusNotFound)
   139  			return
   140  		}
   141  
   142  		if r.URL.Query().Get("download") != "" {
   143  			// TODO(tbg): this has zero discoverability.
   144  			w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s_%s.pb.gz", profileName, id))
   145  			w.Header().Set("Content-Type", "application/octet-stream")
   146  			if err := s.storage.Get(id, func(r io.Reader) error {
   147  				_, err := io.Copy(w, r)
   148  				return err
   149  			}); err != nil {
   150  				http.Error(w, err.Error(), http.StatusInternalServerError)
   151  			}
   152  			return
   153  		}
   154  
   155  		server := func(args *driver.HTTPServerArgs) error {
   156  			handler, ok := args.Handlers[remainingPath]
   157  			if !ok {
   158  				return errors.Errorf("unknown endpoint %s", remainingPath)
   159  			}
   160  			handler.ServeHTTP(w, r)
   161  			return nil
   162  		}
   163  
   164  		storageFetcher := func(_ string, _, _ time.Duration) (*profile.Profile, string, error) {
   165  			var p *profile.Profile
   166  			if err := s.storage.Get(id, func(reader io.Reader) error {
   167  				var err error
   168  				p, err = profile.Parse(reader)
   169  				return err
   170  			}); err != nil {
   171  				return nil, "", err
   172  			}
   173  			return p, "", nil
   174  		}
   175  
   176  		// Invoke the (library version) of `pprof` with a number of stubs.
   177  		// Specifically, we pass a fake FlagSet that plumbs through the
   178  		// given args, a UI that logs any errors pprof may emit, a fetcher
   179  		// that simply reads the profile we downloaded earlier, and a
   180  		// HTTPServer that pprof will pass the web ui handlers to at the
   181  		// end (and we let it handle this client request).
   182  		if err := driver.PProf(&driver.Options{
   183  			Flagset: &pprofFlags{
   184  				FlagSet: pflag.NewFlagSet("pprof", pflag.ExitOnError),
   185  				args: []string{
   186  					"--symbolize", "none",
   187  					"--http", "localhost:0",
   188  					"", // we inject our own target
   189  				},
   190  			},
   191  			UI:         &fakeUI{},
   192  			Fetch:      fetcherFn(storageFetcher),
   193  			HTTPServer: server,
   194  		}); err != nil {
   195  			_, _ = w.Write([]byte(err.Error()))
   196  		}
   197  
   198  		return
   199  	}
   200  
   201  	// Create and save new profile, then redirect client to corresponding ui URL.
   202  
   203  	id = s.storage.ID()
   204  
   205  	fetchHandler, ok := s.profileTypes[profileName]
   206  	if !ok {
   207  		_, _ = w.Write([]byte(fmt.Sprintf("unknown profile type %s", profileName)))
   208  		return
   209  	}
   210  
   211  	if err := s.storage.Store(id, func(w io.Writer) error {
   212  		req, err := http.NewRequest("GET", "/unused", bytes.NewReader(nil))
   213  		if err != nil {
   214  			return err
   215  		}
   216  
   217  		// Pass through any parameters. Most notably, allow ?seconds=10 for
   218  		// CPU profiles.
   219  		_ = r.ParseForm()
   220  		req.Form = r.Form
   221  
   222  		rw := &responseBridge{target: w}
   223  
   224  		s.hook(profileName, r.Form.Get("labels") != "", func() { fetchHandler(rw, req) })
   225  
   226  		if rw.statusCode != http.StatusOK && rw.statusCode != 0 {
   227  			return errors.Errorf("unexpected status: %d", rw.statusCode)
   228  		}
   229  		return nil
   230  	}); err != nil {
   231  		http.Error(w, err.Error(), http.StatusInternalServerError)
   232  		return
   233  	}
   234  
   235  	// NB: direct straight to the flamegraph. This is because `pprof`
   236  	// shells out to `dot` for the default landing page and thus works
   237  	// only on hosts that have graphviz installed. You can still navigate
   238  	// to the dot page from there.
   239  	origURL, err := url.Parse(r.RequestURI)
   240  	if err != nil {
   241  		http.Error(w, err.Error(), http.StatusInternalServerError)
   242  		return
   243  	}
   244  
   245  	// If this is a request issued by `go tool pprof`, just return the profile
   246  	// directly. This is convenient because it avoids having to expose the pprof
   247  	// endpoints separately, and also allows inserting hooks around CPU profiles
   248  	// in the future.
   249  	isGoPProf := strings.Contains(r.Header.Get("User-Agent"), "Go-http-client")
   250  	origURL.Path = path.Join(origURL.Path, id, "flamegraph")
   251  	if !isGoPProf {
   252  		http.Redirect(w, r, origURL.String(), http.StatusTemporaryRedirect)
   253  	} else {
   254  		_ = s.storage.Get(id, func(r io.Reader) error {
   255  			_, err := io.Copy(w, r)
   256  			return err
   257  		})
   258  	}
   259  }
   260  
   261  type fetcherFn func(_ string, _, _ time.Duration) (*profile.Profile, string, error)
   262  
   263  func (f fetcherFn) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
   264  	return f(s, d, t)
   265  }