From 87b634aecba3d1d47ab447d406ab51940baee11f Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 11 Jan 2020 15:18:50 -0500 Subject: [PATCH] initial commit --- Dockerfile | 27 ++++++ LICENSE | 21 +++++ README.md | 18 ++++ cloudbuild.yaml | 5 ++ go.mod | 5 ++ go.sum | 8 ++ pprofweb.go | 233 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 317 insertions(+) create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cloudbuild.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pprofweb.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3fa0403 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.13.6-buster AS builder +COPY go.mod go.sum pprofweb.go /go/src/pprofweb/ +WORKDIR /go/src/pprofweb +RUN go build --mod=readonly pprofweb.go + + +# Extract graphviz and dependencies +FROM golang:1.13.6-buster AS deb_extractor +RUN cd /tmp && \ + apt-get update && apt-get download \ + graphviz libgvc6 libcgraph6 libltdl7 libxdot4 libcdt5 libpathplan4 libexpat1 zlib1g && \ + mkdir /dpkg && \ + for deb in *.deb; do dpkg --extract $deb /dpkg || exit 10; done + + +FROM gcr.io/distroless/base-debian10:debug AS run +COPY --from=builder /go/src/pprofweb/pprofweb /pprofweb +COPY --from=deb_extractor /dpkg / +# Configure dot plugins +RUN ["dot", "-c"] + + +# Use a non-root user: slightly more secure (defense in depth) +USER nonroot +WORKDIR / +EXPOSE 8080 +ENTRYPOINT ["/pprofweb"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..970ac6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Evan Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1380a01 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# PProf Web UI + +This is a total hack to upload pprof files and serve the UI. This avoids needing to install any tools. + +Try it: https://pprofweb-kgdmaenclq-uc.a.run.app/ + + +## Run Locally + +docker build . --tag=pprofweb +docker run --rm -ti --publish=127.0.0.1:8080:8080 pprofweb + +Open http://localhost:8080/ + + +## Check that the container works + +docker run --rm -ti --entrypoint=dot pprofweb diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..19ec57b --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,5 @@ +steps: +- name: 'gcr.io/cloud-builders/docker' + args: ['build', '--tag=us.gcr.io/$PROJECT_ID/pprofweb', '.'] +images: +- 'us.gcr.io/$PROJECT_ID/pprofweb' diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8f2844e --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/evanj/pprofweb + +go 1.13 + +require github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4016ac1 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc h1:DLpL8pWq0v4JYoRpEhDfsJhhJyGKCcQM2WPW2TJs31c= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pprofweb.go b/pprofweb.go new file mode 100644 index 0000000..9fa1407 --- /dev/null +++ b/pprofweb.go @@ -0,0 +1,233 @@ +package main + +import ( + "flag" + "io" + "log" + "net/http" + "os" + "path" + "strings" + + "github.com/google/pprof/driver" +) + +const portEnvVar = "PORT" +const defaultPort = "8080" +const maxUploadSize = 32 << 20 // 32 MiB + +const pprofFilePath = "/tmp/pprofweb-temp" + +const fileFormID = "file" +const uploadPath = "/upload" +const pprofWebPath = "/pprofweb/" + +type server struct { + // serves pprof handlers after it is loaded + pprofMux *http.ServeMux +} + +func (s *server) startHTTP(args *driver.HTTPServerArgs) error { + s.pprofMux = http.NewServeMux() + for pattern, handler := range args.Handlers { + var joinedPattern string + if pattern == "/" { + joinedPattern = pprofWebPath + } else { + joinedPattern = path.Join(pprofWebPath, pattern) + } + s.pprofMux.Handle(joinedPattern, handler) + } + return nil +} + +func (s *server) servePprof(w http.ResponseWriter, r *http.Request) { + if s.pprofMux == nil { + http.Error(w, "must upload profile first", http.StatusInternalServerError) + return + } + s.pprofMux.ServeHTTP(w, r) +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("rootHandler %s %s", r.Method, r.URL.String()) + if r.Method != http.MethodGet { + http.Error(w, "wrong method", http.StatusMethodNotAllowed) + return + } + if r.URL.Path != "/" { + http.Error(w, "not found", http.StatusNotFound) + return + } + + w.Write([]byte(rootTemplate)) +} + +func (s *server) uploadHandlerErrHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("uploadHandler %s %s", r.Method, r.URL.String()) + if r.Method != http.MethodPost { + http.Error(w, "wrong method", http.StatusMethodNotAllowed) + return + } + err := s.uploadHandler(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (s *server) uploadHandler(w http.ResponseWriter, r *http.Request) error { + if err := r.ParseMultipartForm(maxUploadSize); err != nil { + return err + } + uploadedFile, _, err := r.FormFile(fileFormID) + if err != nil { + return err + } + defer uploadedFile.Close() + + // write the file out to a temporary location + f, err := os.OpenFile(pprofFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer f.Close() + if _, err := io.Copy(f, uploadedFile); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + if err := uploadedFile.Close(); err != nil { + return err + } + + // start the pprof web handler: pass -http and -no_browser so it starts the + // handler but does not try to launch a browser + // our startHTTP will do the appropriate interception + flags := &pprofFlags{ + args: []string{"-http=localhost:0", "-no_browser", pprofFilePath}, + } + options := &driver.Options{ + Flagset: flags, + HTTPServer: s.startHTTP, + } + if err := driver.PProf(options); err != nil { + return err + } + + http.Redirect(w, r, pprofWebPath, http.StatusSeeOther) + return nil +} + +func main() { + s := &server{} + + mux := http.NewServeMux() + mux.HandleFunc("/", rootHandler) + mux.HandleFunc(uploadPath, s.uploadHandlerErrHandler) + mux.HandleFunc(pprofWebPath, s.servePprof) + + port := os.Getenv(portEnvVar) + if port == "" { + port = defaultPort + log.Printf("warning: %s not specified; using default %s", portEnvVar, port) + } + + addr := ":" + port + log.Printf("listen addr %s (http://localhost:%s/)", addr, port) + if err := http.ListenAndServe(addr, mux); err != nil { + panic(err) + } +} + +const rootTemplate = ` + +PProf Web Interface + +

PProf Web Interface

+

Upload a file to explore it using the Pprof web interface.

+

NOTE: Will not work with multiple users

+

This is currently a hack: it runs in Google Cloud Run, which will restart instances whenever it wants. This means your state may get lost at any time, and it won't work if there are multiple people using it at the same time.

+

TODO: Write all the output from pprof out to static files in Google Cloud Storage and serve them from there.

+ +
+

Upload file:

+
+ + +` + +// Mostly copied from https://github.com/google/pprof/blob/master/internal/driver/flags.go +type pprofFlags struct { + args []string + s flag.FlagSet + usage []string +} + +// Bool implements the plugin.FlagSet interface. +func (p *pprofFlags) Bool(o string, d bool, c string) *bool { + return p.s.Bool(o, d, c) +} + +// Int implements the plugin.FlagSet interface. +func (p *pprofFlags) Int(o string, d int, c string) *int { + return p.s.Int(o, d, c) +} + +// Float64 implements the plugin.FlagSet interface. +func (p *pprofFlags) Float64(o string, d float64, c string) *float64 { + return p.s.Float64(o, d, c) +} + +// String implements the plugin.FlagSet interface. +func (p *pprofFlags) String(o, d, c string) *string { + return p.s.String(o, d, c) +} + +// BoolVar implements the plugin.FlagSet interface. +func (p *pprofFlags) BoolVar(b *bool, o string, d bool, c string) { + p.s.BoolVar(b, o, d, c) +} + +// IntVar implements the plugin.FlagSet interface. +func (p *pprofFlags) IntVar(i *int, o string, d int, c string) { + p.s.IntVar(i, o, d, c) +} + +// Float64Var implements the plugin.FlagSet interface. +// the value of the flag. +func (p *pprofFlags) Float64Var(f *float64, o string, d float64, c string) { + p.s.Float64Var(f, o, d, c) +} + +// StringVar implements the plugin.FlagSet interface. +func (p *pprofFlags) StringVar(s *string, o, d, c string) { + p.s.StringVar(s, o, d, c) +} + +// StringList implements the plugin.FlagSet interface. +func (p *pprofFlags) StringList(o, d, c string) *[]*string { + return &[]*string{p.s.String(o, d, c)} +} + +// AddExtraUsage implements the plugin.FlagSet interface. +func (p *pprofFlags) AddExtraUsage(eu string) { + p.usage = append(p.usage, eu) +} + +// ExtraUsage implements the plugin.FlagSet interface. +func (p *pprofFlags) ExtraUsage() string { + return strings.Join(p.usage, "\n") +} + +// Parse implements the plugin.FlagSet interface. +func (p *pprofFlags) Parse(usage func()) []string { + p.s.Usage = usage + p.s.Parse(p.args) + args := p.s.Args() + if len(args) == 0 { + usage() + } + return args +}