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 }