-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
1,002 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.idea/ | ||
|
||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
before: | ||
hooks: | ||
- go mod tidy | ||
builds: | ||
- main: ./cmd/ | ||
env: | ||
- CGO_ENABLED=0 | ||
goos: | ||
- linux | ||
- windows | ||
- darwin | ||
|
||
archives: | ||
- format: tar.gz | ||
# this name template makes the OS and Arch compatible with the results of uname. | ||
name_template: >- | ||
{{ .ProjectName }}_ | ||
{{- title .Os }}_ | ||
{{- if eq .Arch "amd64" }}x86_64 | ||
{{- else if eq .Arch "386" }}i386 | ||
{{- else }}{{ .Arch }}{{ end }} | ||
{{- if .Arm }}v{{ .Arm }}{{ end }} | ||
# use zip for windows archives | ||
format_overrides: | ||
- goos: windows | ||
format: zip | ||
checksum: | ||
name_template: 'checksums.txt' | ||
snapshot: | ||
name_template: "{{ incpatch .Version }}-next" | ||
changelog: | ||
sort: asc | ||
filters: | ||
exclude: | ||
- '^docs:' | ||
- '^test:' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
golang 1.20 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# kubectl pprof plugin | ||
|
||
A simple kubectl plugin to collect Go pprof profiles from pods that | ||
expose the ["net/http/pprof"](https://pkg.go.dev/net/http/pprof) endpoints on a local port. | ||
|
||
The plugin will port-forward to the specified pod and write the pprof profile to the filesystem | ||
to be analyze with `go tool pprof`. | ||
|
||
## Installing | ||
|
||
1. Download the binary from the GH release | ||
2. Move the `kubectl-pprof` binary to anywhere in your `$PATH` | ||
3. Run `kubectl pprof -h` to validate the plugin is working | ||
|
||
## Example usage | ||
|
||
### Collecting a CPU profile | ||
|
||
```bash | ||
kubectl pprof <pod> --port 8080 -n <namespace> --profile cpu | ||
``` | ||
|
||
### Collecting a heap profile for 30 seconds | ||
|
||
```bash | ||
kubectl pprof <pod> --port 8080 -n <namespace> --profile heap --seconds 30 --output /tmp/ | ||
``` | ||
|
||
### Pass a profile directly to `go tool pprof` (suppress output with `-q`) | ||
|
||
```bash | ||
kubectl pprof <pod> --port 8080 -n <namespace> --profile cpu -q | xargs go tool pprof -http=:8080 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package pprof | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/url" | ||
"os" | ||
"path" | ||
"runtime/pprof" | ||
"time" | ||
|
||
"github.com/spf13/cobra" | ||
"k8s.io/cli-runtime/pkg/genericclioptions" | ||
"k8s.io/cli-runtime/pkg/genericiooptions" | ||
"k8s.io/client-go/rest" | ||
|
||
"github.com/cbuto/kubectl-pprof/internal/portforward" | ||
"github.com/cbuto/kubectl-pprof/internal/pprofgetter" | ||
) | ||
|
||
var pprofExample = ` | ||
# collect a heap profile a pod | ||
%[1]s pprof <pod name> --profile heap --seconds 10 | ||
# collect a cpu profile and output profile to /tmp/ | ||
%[1]s pprof <pod name> --port 8080 -n <namespace> --profile cpu --output /tmp/ --seconds 30 | ||
# pass a profile directly to go tool pprof (suppress output with -q) | ||
%[1]s pprof <pod> --port 8080 -n <namespace> --profile cpu -q | xargs go tool pprof -http=:8080 | ||
` | ||
|
||
const ( | ||
DefaultProfileSeconds = 10 | ||
DefaultPProfPort = 8080 | ||
) | ||
|
||
type pprofCmdOptions struct { | ||
genericiooptions.IOStreams | ||
restConfig *rest.Config | ||
configFlags *genericclioptions.ConfigFlags | ||
profile string | ||
seconds int | ||
port int | ||
outDir string | ||
namespace string | ||
pod string | ||
quiet bool | ||
} | ||
|
||
func newPProfOptions(streams genericiooptions.IOStreams) *pprofCmdOptions { | ||
return &pprofCmdOptions{ | ||
configFlags: genericclioptions.NewConfigFlags(true), | ||
IOStreams: streams, | ||
} | ||
} | ||
|
||
func NewPProfCmd(streams genericiooptions.IOStreams) *cobra.Command { | ||
opts := newPProfOptions(streams) | ||
|
||
cmd := &cobra.Command{ | ||
Use: "pprof [pod] [flags]", | ||
Short: "Collects the specified pprof profile from a pod", | ||
Example: fmt.Sprintf(pprofExample, "kubectl"), | ||
SilenceUsage: true, | ||
RunE: func(c *cobra.Command, args []string) error { | ||
if err := opts.Complete(c, args); err != nil { | ||
return err | ||
} | ||
if err := opts.Validate(); err != nil { | ||
return err | ||
} | ||
if err := opts.Run(); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
}, | ||
} | ||
|
||
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "suppresses output and only prints the output file") | ||
cmd.Flags().StringVar(&opts.profile, | ||
"profile", | ||
"cpu", "type of profile to collect (heap, cpu, block, goroutine, mutex, or threadcreate)") | ||
cmd.Flags().StringVar(&opts.outDir, "output", "./", "path to a directory to write the profile") | ||
cmd.Flags().IntVar(&opts.seconds, "seconds", DefaultProfileSeconds, "amount of seconds to collect the profile") | ||
cmd.Flags().IntVar(&opts.port, "port", DefaultPProfPort, "pprof port") | ||
|
||
opts.configFlags.AddFlags(cmd.Flags()) | ||
|
||
return cmd | ||
} | ||
|
||
func (o *pprofCmdOptions) Complete(cmd *cobra.Command, args []string) error { | ||
var err error | ||
o.namespace, _, err = o.configFlags.ToRawKubeConfigLoader().Namespace() | ||
|
||
if err != nil { | ||
return fmt.Errorf("failed to get namespace from config: %w", err) | ||
} | ||
|
||
if len(args) != 1 { | ||
return fmt.Errorf("invalid number of arguments, use --help to see example usage") | ||
} | ||
|
||
o.pod = args[0] | ||
o.restConfig, err = o.configFlags.ToRESTConfig() | ||
|
||
if err != nil { | ||
return fmt.Errorf("failed to get REST config: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (o *pprofCmdOptions) Validate() error { | ||
if o.profile == "" { | ||
return fmt.Errorf("must select type of profile to collect") | ||
} | ||
|
||
if o.profile != "cpu" { | ||
profile := pprof.Lookup(o.profile) | ||
if profile == nil { | ||
return fmt.Errorf("unknown profile: %s", o.profile) | ||
} | ||
} | ||
|
||
fileInfo, err := os.Stat(o.outDir) | ||
|
||
if err != nil { | ||
return fmt.Errorf("failed to check if --output is a dir: %w", err) | ||
} | ||
|
||
if !fileInfo.IsDir() { | ||
return fmt.Errorf("--output flag must be set to a directory: %s", o.outDir) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (o *pprofCmdOptions) Run() error { | ||
var err error | ||
|
||
ctx := context.TODO() | ||
|
||
out, errOut := o.IOStreams.Out, o.IOStreams.ErrOut | ||
if o.quiet { | ||
out, errOut = nil, nil | ||
} | ||
|
||
portforwarder, err := portforward.NewPortForward( | ||
portforward.WithRESTConfig(o.restConfig), | ||
portforward.WithOutput(out, errOut)) | ||
if err != nil { | ||
return fmt.Errorf("failed to get port-forwarder: %w", err) | ||
} | ||
|
||
localPort, stopCh, err := portforwarder.PortForward(ctx, o.namespace, o.pod, o.port) | ||
|
||
if err != nil { | ||
return fmt.Errorf("port-forwarding failed: %w", err) | ||
} | ||
|
||
defer close(stopCh) | ||
|
||
if !o.quiet { | ||
_, err = o.IOStreams.Out.Write([]byte(fmt.Sprintf("collecting profile from %s (for %d secs)...\n", o.pod, o.seconds))) | ||
if err != nil { | ||
return fmt.Errorf("failed to write to output: %w", err) | ||
} | ||
} | ||
|
||
outFilePath := path.Join(o.outDir, fmt.Sprintf("%s_%s_%d.out", o.pod, o.profile, time.Now().Unix())) | ||
pprofHostURL := &url.URL{ | ||
Scheme: "http", | ||
Host: fmt.Sprintf("localhost:%d", localPort), | ||
} | ||
|
||
getter, err := pprofgetter.NewPProfGetter(pprofgetter.WithHostURL(pprofHostURL)) | ||
if err != nil { | ||
return fmt.Errorf("failed to build pprof getter: %w", err) | ||
} | ||
|
||
err = getter.Get(ctx, o.profile, o.seconds, outFilePath) | ||
if err != nil { | ||
return fmt.Errorf("failed to collect profile: %w", err) | ||
} | ||
|
||
_, err = o.IOStreams.Out.Write([]byte(fmt.Sprintf("%s\n", outFilePath))) | ||
if err != nil { | ||
return fmt.Errorf("failed to output profile filename: %w", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package main | ||
|
||
import ( | ||
"os" | ||
|
||
"github.com/cbuto/kubectl-pprof/cmd/pprof" | ||
|
||
"github.com/spf13/pflag" | ||
"k8s.io/cli-runtime/pkg/genericiooptions" | ||
) | ||
|
||
func main() { | ||
flags := pflag.NewFlagSet("kubectl-pprof", pflag.ExitOnError) | ||
pflag.CommandLine = flags | ||
|
||
root := pprof.NewPProfCmd(genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) | ||
if err := root.Execute(); err != nil { | ||
os.Exit(1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
module github.com/cbuto/kubectl-pprof | ||
|
||
go 1.20 | ||
|
||
require ( | ||
github.com/spf13/cobra v1.7.0 | ||
github.com/spf13/pflag v1.0.5 | ||
github.com/stretchr/testify v1.8.2 | ||
k8s.io/cli-runtime v0.0.0-20230718062906-448da40f6f16 | ||
k8s.io/client-go v0.0.0-20230718055620-74c18d3a4044 | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect | ||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect | ||
github.com/go-errors/errors v1.4.2 // indirect | ||
github.com/go-logr/logr v1.2.4 // indirect | ||
github.com/go-openapi/jsonpointer v0.19.6 // indirect | ||
github.com/go-openapi/jsonreference v0.20.2 // indirect | ||
github.com/go-openapi/swag v0.22.3 // indirect | ||
github.com/gogo/protobuf v1.3.2 // indirect | ||
github.com/golang/protobuf v1.5.3 // indirect | ||
github.com/google/btree v1.0.1 // indirect | ||
github.com/google/gnostic-models v0.6.8 // indirect | ||
github.com/google/go-cmp v0.5.9 // indirect | ||
github.com/google/gofuzz v1.2.0 // indirect | ||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect | ||
github.com/google/uuid v1.3.0 // indirect | ||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect | ||
github.com/imdario/mergo v0.3.6 // indirect | ||
github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
github.com/josharian/intern v1.0.0 // indirect | ||
github.com/json-iterator/go v1.1.12 // indirect | ||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect | ||
github.com/mailru/easyjson v0.7.7 // indirect | ||
github.com/moby/spdystream v0.2.0 // indirect | ||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||
github.com/modern-go/reflect2 v1.0.2 // indirect | ||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect | ||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect | ||
github.com/pkg/errors v0.9.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
github.com/xlab/treeprint v1.2.0 // indirect | ||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect | ||
golang.org/x/net v0.12.0 // indirect | ||
golang.org/x/oauth2 v0.8.0 // indirect | ||
golang.org/x/sync v0.2.0 // indirect | ||
golang.org/x/sys v0.10.0 // indirect | ||
golang.org/x/term v0.10.0 // indirect | ||
golang.org/x/text v0.11.0 // indirect | ||
golang.org/x/time v0.3.0 // indirect | ||
google.golang.org/appengine v1.6.7 // indirect | ||
google.golang.org/protobuf v1.30.0 // indirect | ||
gopkg.in/inf.v0 v0.9.1 // indirect | ||
gopkg.in/yaml.v2 v2.4.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
k8s.io/api v0.0.0-20230718054858-1b0ec3bb3296 // indirect | ||
k8s.io/apimachinery v0.0.0-20230718054246-5cb236977966 // indirect | ||
k8s.io/klog/v2 v2.100.1 // indirect | ||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect | ||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect | ||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect | ||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect | ||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect | ||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect | ||
sigs.k8s.io/yaml v1.3.0 // indirect | ||
) |
Oops, something went wrong.