From 042ec6a33f424b9e1722209c3b522b1514a9aab1 Mon Sep 17 00:00:00 2001 From: Prashant Varanasi Date: Thu, 2 Jun 2016 23:05:47 -0700 Subject: [PATCH] Improve help + flags, update README --- README.md | 149 +++++++++++++++++++++++++------------------ glide.lock | 4 +- main.go | 33 ++++++---- main_test.go | 10 +-- torchlog/torchlog.go | 2 +- 5 files changed, 114 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 72e772b..38fdf72 100644 --- a/README.md +++ b/README.md @@ -14,70 +14,104 @@ synthesizes them into a flame graph. Uses Go's built in [pprof][] library. ## Basic Usage ``` -$ go-torch --help +$ go-torch -h +Usage: + go-torch [options] [binary] -NAME: - go-torch - go-torch collects stack traces of a Go application and synthesizes them into into a [flame graph](http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html) +pprof Options: + -u, --url= Base URL of your Go program (default: http://localhost:8080) + -s, --suffix= URL path of pprof profile (default: /debug/pprof/profile) + -b, --binaryinput= File path of previously saved binary profile. (binary profile is anything accepted by https://golang.org/cmd/pprof) + --binaryname= File path of the binary that the binaryinput is for, used for pprof inputs + -t, --seconds= Number of seconds to profile for (default: 30) + --pprofArgs= Extra arguments for pprof -USAGE: - go-torch [global options] command [command options] [arguments...] +Output Options: + -f, --file= Output file name (must be .svg) (default: torch.svg) + -p, --print Print the generated svg to stdout instead of writing to file + -r, --raw Print the raw call graph output to stdout instead of creating a flame graph; use with Brendan Gregg's flame graph perl script (see https://github.com/brendangregg/FlameGraph) + --title= Graph title to display in the output file (default: Flame Graph) -COMMANDS: - help, h Shows a list of commands or help for one command +Help Options: + -h, --help Show this help message +``` + +### Write flamegraph using /debug/pprof endpoint + +The default options will hit `http://localhost:8080/debug/pprof/profile` for +a 30 second CPU profile, and write it out to torch.svg -GLOBAL OPTIONS: - --url, -u "http://localhost:8080" base url of your Go program - --suffix, -s "/debug/pprof/profile" url path of pprof profile - --binaryinput, -b file path of raw binary profile; alternative to having go-torch query pprof endpoint (binary profile is anything accepted by https://golang.org/cmd/pprof) - --binaryname file path of the binary that the binaryinput is for, used for pprof inputs - --time, -t "30" time in seconds to profile for - --file, -f "torch.svg" ouput file name (must be .svg) - --print, -p print the generated svg to stdout instead of writing to file - --raw, -r print the raw call graph output to stdout instead of creating a flame graph; use with Brendan Gregg's flame graph perl script (see https://github.com/brendangregg/FlameGraph) - --title graph title to display in the output file - --help, -h show help - --version, -v print the version +``` +$ go-torch +INFO[19:10:58] Run pprof command: go tool pprof -raw -seconds 30 http://localhost:8080/debug/pprof/profile +INFO[19:11:03] Writing svg to torch.svg ``` -### File Example +You can customize the base URL by using `-u` ``` -$ go-torch --time=15 --file "torch.svg" --url http://localhost:8080 -INFO[0000] Profiling ... -INFO[0015] flame graph has been created as torch.svg +$ go-torch -u http://my-service:8080/ +INFO[19:10:58] Run pprof command: go tool pprof -raw -seconds 30 http://my-service:8080/debug/pprof/profile +INFO[19:11:03] Writing svg to torch.svg ``` -### Stdout Example +Or change the number of seconds to profile using `--seconds`: ``` -$ go-torch --time=15 --print --url http://localhost:8080 -INFO[0000] Profiling ... - -... - -INFO[0015] flame graph has been printed to stdout +$ go-torch --seconds 5 +INFO[19:10:58] Run pprof command: go tool pprof -raw -seconds 5 http://localhost:8080/debug/pprof/profile +INFO[19:11:03] Writing svg to torch.svg +``` + + +### Using pprof arguments + +`go-torch` will pass through arguments to `go tool pprof`, which lets you take +existing pprof commands and easily make them work with `go-torch`. + +For example, after creating a CPU profile from a benchmark: ``` +$ go test -bench . -cpuprofile=cpu.prof -### Raw Example +# This creates a cpu.prof file, and the $PKG.test binary. +``` +The same arguments that can be used with `go tool pprof` will also work +with `go-torch`: ``` -$ go-torch --time=15 --raw --url http://localhost:8080 -INFO[0000] Profiling ... -function1;function2 3 -... -INFO[0015] raw call graph output been printed to stdout +$ go tool pprof main.test cpu.prof + +# Same arguments work with go-torch +$ go-torch main.test cpu.pprof +INFO[19:00:29] Run pprof command: go tool pprof -raw -seconds 30 main.test prof.cpu +INFO[19:00:29] Writing svg to torch.svg ``` -### Local pprof Example +Flags that are not handled by `go-torch` are passed through as well: ``` -$ go test -cpuprofile=cpu.pprof -# This creates a cpu.pprof file, and the golang.test binary. -$ go-torch --binaryinput cpu.pprof --binaryname golang.test -INFO[0000] Profiling ... -INFO[0000] flame graph has been created as torch.svg +$ go-torch --alloc_objects main.test mem.prof +INFO[19:00:29] Run pprof command: go tool pprof -raw -seconds 30 --alloc_objects main.test prof.mem +INFO[19:00:29] Writing svg to torch.svg ``` +## Integrating With Your Application + +To add profiling endpoints in your application, follow the official +Go docs [here][]. +If your application is already running a server on the DefaultServeMux, +just add this import to your application. + +[here]: https://golang.org/pkg/net/http/pprof/ + +```go +import _ "net/http/pprof" +``` + +If your application is not using the DefaultServeMux, you can still easily +expose pprof endpoints by manually registering the net/http/pprof handlers or by +using a library like [this one](https://github.com/e-dard/netbug). + ## Installation ``` @@ -92,36 +126,27 @@ $ docker run uber/go-torch -u http://[address-of-host] -p > torch.svg Using `-p` will print the SVG to standard out, which can then be redirected to a file. This avoids mounting volumes to a container. -### Install the Go dependencies: - -``` -$ go get github.com/Masterminds/glide -$ glide install -``` - ### Get the flame graph script: +When using the `go-torch` binary locally, you will need the Flamegraph scripts +in your `PATH`: + ``` +$ cd $GOPATH/src/github.com/uber/go-torch $ git clone https://github.com/brendangregg/FlameGraph.git ``` -## Integrating With Your Application - -Expose a pprof endpoint. Official Go docs are [here][]. If your application is -already running a server on the DefaultServeMux, just add this import to your -application. +## Development and Testing -[here]: https://golang.org/pkg/net/http/pprof/ +### Install the Go dependencies: -```go -import _ "net/http/pprof" +``` +$ go get github.com/Masterminds/glide +$ cd $GOPATH/src/github.com/uber/go-torch +$ glide install ``` -If your application is not using the DefaultServeMux, you can still easily -expose pprof endpoints by manually registering the net/http/pprof handlers or by -using a library like [this one](https://github.com/e-dard/netbug). - -## Run the Tests +### Run the Tests ``` $ go test ./... diff --git a/glide.lock b/glide.lock index 77db9ba..bd7e8b2 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ hash: b242f7d839e91092957e62d5debf0d1217aa491911cc064a022d142640ec346f -updated: 2016-03-28T18:27:44.307463068-07:00 +updated: 2016-06-02T22:55:54.40038441-07:00 imports: - name: github.com/fatih/color version: 533cd7fd8a85905f67a1753afb4deddc85ea174f @@ -12,7 +12,7 @@ imports: version: 56b76bdf51f7708750eac80fa38b952bb9f32639 repo: https://github.com/mattn/go-isatty - name: github.com/stretchr/testify - version: 6fe211e493929a8aac0469b93f28b1d0688a9a3a + version: 8d64eb7173c7753d6419fd4a9caf057398611364 - name: golang.org/x/sys version: 320cb01ddbbf0473674c2585f9b6e245721de355 subpackages: diff --git a/main.go b/main.go index a727bd3..ed82f4b 100644 --- a/main.go +++ b/main.go @@ -37,16 +37,20 @@ import ( // options are the parameters for go-torch. type options struct { - PProfOptions pprof.Options - File string `short:"f" long:"file" default:"torch.svg" description:"Output file name (must be .svg)"` - Print bool `short:"p" long:"print" description:"Print the generated svg to stdout instead of writing to file"` - Raw bool `short:"r" long:"raw" description:"Print the raw call graph output to stdout instead of creating a flame graph; use with Brendan Gregg's flame graph perl script (see https://github.com/brendangregg/FlameGraph)"` - Title string `long:"title" default:"Flame Graph" description:"Graph title to display in the output file"` + PProfOptions pprof.Options `group:"pprof Options"` + OutputOpts outputOptions `group:"Output Options"` +} + +type outputOptions struct { + File string `short:"f" long:"file" default:"torch.svg" description:"Output file name (must be .svg)"` + Print bool `short:"p" long:"print" description:"Print the generated svg to stdout instead of writing to file"` + Raw bool `short:"r" long:"raw" description:"Print the raw call graph output to stdout instead of creating a flame graph; use with Brendan Gregg's flame graph perl script (see https://github.com/brendangregg/FlameGraph)"` + Title string `long:"title" default:"Flame Graph" description:"Graph title to display in the output file"` } // main is the entry point of the application func main() { - if err := runWithArgs(os.Args...); err != nil { + if err := runWithArgs(os.Args[1:]...); err != nil { torchlog.Fatalf("Failed: %v", err) } } @@ -54,7 +58,10 @@ func main() { func runWithArgs(args ...string) error { opts := &options{} - remaining, err := gflags.ParseArgs(opts, args) + parser := gflags.NewParser(opts, gflags.Default|gflags.IgnoreUnknown) + parser.Usage = "[options] [binary] " + + remaining, err := parser.ParseArgs(args) if err != nil { if flagErr, ok := err.(*gflags.Error); ok && flagErr.Type == gflags.ErrHelp { os.Exit(0) @@ -65,15 +72,11 @@ func runWithArgs(args ...string) error { return fmt.Errorf("invalid options: %v", err) } - // If there are remaining arguments, the first argument is the binary name. - if len(remaining) > 0 { - remaining = remaining[1:] - } return runWithOptions(opts, remaining) } -func runWithOptions(opts *options, remaining []string) error { - pprofRawOutput, err := pprof.GetRaw(opts.PProfOptions, remaining) +func runWithOptions(allOpts *options, remaining []string) error { + pprofRawOutput, err := pprof.GetRaw(allOpts.PProfOptions, remaining) if err != nil { return fmt.Errorf("could not get raw output from pprof: %v", err) } @@ -88,6 +91,7 @@ func runWithOptions(opts *options, remaining []string) error { return fmt.Errorf("could not convert stacks to flamegraph input: %v", err) } + opts := allOpts.OutputOpts if opts.Raw { torchlog.Print("Printing raw flamegraph input to stdout") fmt.Printf("%s\n", flameInput) @@ -114,7 +118,8 @@ func runWithOptions(opts *options, remaining []string) error { } func validateOptions(opts *options) error { - if opts.File != "" && !strings.HasSuffix(opts.File, ".svg") { + file := opts.OutputOpts.File + if file != "" && !strings.HasSuffix(file, ".svg") { return fmt.Errorf("output file must end in .svg") } if opts.PProfOptions.TimeSeconds < 1 { diff --git a/main_test.go b/main_test.go index 9d8b4c4..4150eba 100644 --- a/main_test.go +++ b/main_test.go @@ -101,7 +101,7 @@ func TestInvalidOptions(t *testing.T) { func TestRunRaw(t *testing.T) { opts := getDefaultOptions() - opts.Raw = true + opts.OutputOpts.Raw = true if err := runWithOptions(opts, nil); err != nil { t.Fatalf("Run with Raw failed: %v", err) @@ -120,14 +120,14 @@ func getTempFilename(t *testing.T, suffix string) string { func TestRunFile(t *testing.T) { opts := getDefaultOptions() - opts.File = getTempFilename(t, ".svg") + opts.OutputOpts.File = getTempFilename(t, ".svg") withScriptsInPath(t, func() { if err := runWithOptions(opts, nil); err != nil { t.Fatalf("Run with Print failed: %v", err) } - f, err := os.Open(opts.File) + f, err := os.Open(opts.OutputOpts.File) if err != nil { t.Errorf("Failed to open output file: %v", err) } @@ -147,7 +147,7 @@ func TestRunFile(t *testing.T) { func TestRunBadFile(t *testing.T) { opts := getDefaultOptions() - opts.File = "/dev/zero/invalid/file" + opts.OutputOpts.File = "/dev/zero/invalid/file" withScriptsInPath(t, func() { if err := runWithOptions(opts, nil); err == nil { @@ -158,7 +158,7 @@ func TestRunBadFile(t *testing.T) { func TestRunPrint(t *testing.T) { opts := getDefaultOptions() - opts.Print = true + opts.OutputOpts.Print = true withScriptsInPath(t, func() { if err := runWithOptions(opts, nil); err != nil { diff --git a/torchlog/torchlog.go b/torchlog/torchlog.go index 955319f..671bdde 100644 --- a/torchlog/torchlog.go +++ b/torchlog/torchlog.go @@ -41,7 +41,7 @@ func init() { func getPrefix(level string, color *color.Color) string { currentTime := time.Now().Format("15:04:05") toColoredString := color.SprintFunc() - return toColoredString(fmt.Sprintf("%s[%s]", level, currentTime)) + return toColoredString(fmt.Sprintf("%s[%s] ", level, currentTime)) } // Fatalf wraps log.Fatalf and adds the current time and color.