initial commit

This commit is contained in:
Evan Jones 2020-01-11 15:18:50 -05:00
commit 87b634aecb
7 changed files with 317 additions and 0 deletions

27
Dockerfile Normal file
View File

@ -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"]

21
LICENSE Normal file
View File

@ -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.

18
README.md Normal file
View File

@ -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

5
cloudbuild.yaml Normal file
View File

@ -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'

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/evanj/pprofweb
go 1.13
require github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc

8
go.sum Normal file
View File

@ -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=

233
pprofweb.go Normal file
View File

@ -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 = `<!doctype html>
<html>
<head><title>PProf Web Interface</title></head>
<body>
<h1>PProf Web Interface</h1>
<p>Upload a file to explore it using the Pprof web interface.</p>
<h2>NOTE: Will not work with multiple users</h2>
<p>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.</p>
<p>TODO: Write all the output from pprof out to static files in Google Cloud Storage and serve them from there.</p>
<form method="post" action="` + uploadPath + `" enctype="multipart/form-data">
<p>Upload file: <input type="file" name="` + fileFormID + `"> <input type="submit" value="Upload"></p>
</form>
</body>
</html>
`
// 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
}