initial commit
This commit is contained in:
commit
87b634aecb
27
Dockerfile
Normal file
27
Dockerfile
Normal 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
21
LICENSE
Normal 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
18
README.md
Normal 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
5
cloudbuild.yaml
Normal 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
5
go.mod
Normal 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
8
go.sum
Normal 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
233
pprofweb.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user