diff --git a/Dockerfile b/Dockerfile index 81a1b2a..6d070b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN cd /tmp && \ FROM golang:1.19.1-bullseye AS builder COPY . /go/src/pprofweb/ WORKDIR /go/src/pprofweb -RUN go build -o server . +RUN go build -o server ./webserver FROM gcr.io/distroless/base-debian11:latest AS run COPY --from=builder /go/src/pprofweb/server /pprofweb diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..b84a5d2 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,117 @@ +package cli + +import ( + "bytes" + "context" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "strings" +) + +type Context struct { + context.Context +} + +var CLI struct { + Profile Profile `cmd:"" aliases:"p,prof" help:"run a profile and upload"` + Upload Upload `cmd:"" aliases:"u,up" help:"upload a file"` +} + +var httpClient = &http.Client{} + +type BaseArgs struct { + Remote *url.URL `name:"remote" help:"which remote pprofweb server to use" env:"PPROFWEB_REMOTE_URL" default:"https://pprof.tuxpa.in"` +} + +type Profile struct { + Mode string `name:"mode" short:"m" enum:"heap,goroutine,threacreate,block,mutex,profile" help:"type of profile to run" default:"heap"` + Time int `name:"time" short:"t" help:"duration of profile in seconds"` + + Url *url.URL `arg:"" help:"base url of pprof server, ala http://localhost:6060/debug/pprof"` + + BaseArgs +} + +func (c *Profile) Run(ctx Context) error { + req := &http.Request{} + req = req.WithContext(ctx) + + req.Method = "GET" + req.URL = c.Url + req.URL = req.URL.JoinPath(c.Mode) + + q := req.URL.Query() + if c.Time > 0 { + } + req.URL.RawQuery = q.Encode() + + fmt.Printf("getting profile from %s...\n", req.URL) + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + res, err := doUpload(ctx, c.BaseArgs, resp.Body) + if err != nil { + return err + } + fmt.Printf("see profile at %s/%s/ \n", c.Remote, res) + return nil +} + +type Upload struct { + File string `arg:"" help:"file to upload" type:"path"` + BaseArgs +} + +func (c *Upload) Run(ctx Context) error { + fl, err := os.Open(c.File) + if err != nil { + return err + } + defer fl.Close() + res, err := doUpload(ctx, c.BaseArgs, fl) + if err != nil { + return err + } + fmt.Printf("see profile at %s/%s/ \n", c.Remote, res) + + return nil +} + +func doUpload(ctx Context, c BaseArgs, dat io.Reader) (string, error) { + fmt.Printf("uploading profile to %s...\n", c.Remote) + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "pprof.pb.gz") + if err != nil { + return "", err + } + _, err = io.Copy(part, dat) + if err != nil { + return "", err + } + err = writer.Close() + if err != nil { + return "", err + } + req, err := http.NewRequestWithContext(ctx, "POST", c.Remote.JoinPath("upload").String(), body) + if err != nil { + return "", err + } + req.Header.Set("content-type", writer.FormDataContentType()) + resp, err := httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + bts, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return strings.ToValidUTF8(string(bts), "�"), nil +} diff --git a/go.mod b/go.mod index d974443..4e7c914 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( gfx.cafe/util/go/generic v0.0.0-20220921131905-10a2bde53f66 + github.com/alecthomas/kong v0.7.1 github.com/go-chi/chi/v5 v5.0.7 github.com/google/pprof v0.0.0-20211108044417-e9b028704de0 github.com/rs/xid v1.4.0 diff --git a/go.sum b/go.sum index e072cad..be1a152 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,10 @@ gfx.cafe/util/go/generic v0.0.0-20220921131905-10a2bde53f66 h1:t6agT2M/Kl5iOnhkO gfx.cafe/util/go/generic v0.0.0-20220921131905-10a2bde53f66/go.mod h1:0mgIMiX8+M1l4VHMaOqBaydZaztbFgblQAaEDOZpUhQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= +github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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= @@ -118,6 +122,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d h1:uGg2frlt3IcT7kbV6LEp5ONv4vmoO2FW4qSO+my/aoM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8883d5b --- /dev/null +++ b/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "git.tuxpa.in/a/pprofweb/cli" + "github.com/alecthomas/kong" +) + +func main() { + ctx := NewCLI() + sctx, cn := signal.NotifyContext(context.Background(), os.Kill, syscall.SIGTERM, syscall.SIGILL) + defer cn() + if err := ctx.Run(cli.Context{Context: sctx}); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func NewCLI() *kong.Context { + ctx := kong.Parse(&cli.CLI) + return ctx +} diff --git a/pprofweb.go b/webserver/pprofweb.go similarity index 100% rename from pprofweb.go rename to webserver/pprofweb.go