├── images
└── pprof-server.png
├── .dockerignore
├── .gitignore
├── Dockerfile
├── flamegraph_test.go
├── .buildkite
└── pipeline.yml
├── README.md
├── Makefile
├── registry.go
├── catalog-info.yaml
├── consul.go
├── LICENSE
├── pprof_test.go
├── kubernetes_test.go
├── pprof.go
├── flamegraph.go
├── go.mod
├── html.go
├── kubernetes.go
├── cmd
└── pprof-server
│ └── main.go
├── handler.go
├── flamegraph.pl
└── go.sum
/images/pprof-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/segmentio/pprof-server/HEAD/images/pprof-server.png
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | pprof-server
3 | Dockerfile
4 | docker-compose*
5 | .buildkite
6 | .dockerignore
7 | .git
8 | .github
9 | .gitignore
10 | .run
11 | *.so
12 | *.out
13 | *.md
14 | *.test
15 | *_test.go
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | # Test binary, build with `go test -c`
8 | *.test
9 |
10 | # Output of the go coverage tool, specifically when used with LiteIDE
11 | *.out
12 |
13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
14 | .glide/
15 |
16 | # Emacs
17 | *~
18 |
19 | # Golang
20 | /vendor/
21 | /pprof-server
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM 528451384384.dkr.ecr.us-west-2.amazonaws.com/segment-golang:1.17.6
2 |
3 | RUN apk add --update --no-cache ca-certificates graphviz perl
4 |
5 | WORKDIR $GOPATH/src/github.com/segmentio/pprof-server
6 |
7 | COPY . .
8 | COPY ./flamegraph.pl /usr/local/bin/flamegraph.pl
9 | RUN go build -o /usr/local/bin/pprof-server ./cmd/pprof-server
10 |
11 | EXPOSE 6061
12 |
13 | ENTRYPOINT ["/usr/local/bin/pprof-server"]
14 |
--------------------------------------------------------------------------------
/flamegraph_test.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import "testing"
4 |
5 | func TestSupportsFlamegraph(t *testing.T) {
6 | params := "?url=http%3A%2F%2F10.30.81.218%3A10240%2Fdebug%2Fpprof%2Fgoroutine%3Fdebug%3D1"
7 | if ok := supportsFlamegraph(params); !ok {
8 | t.Errorf("params=%s: want true, got %v", params, ok)
9 | }
10 | params = "?url=http%3A%2F%2F10.30.81.218%3A10240%2Fdebug%2Fpprof%2Fgoroutine%3Fdebug%3D2"
11 | if ok := supportsFlamegraph(params); ok {
12 | t.Errorf("url=%s: want false, got %v", params, ok)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.buildkite/pipeline.yml:
--------------------------------------------------------------------------------
1 | steps:
2 | - label: build
3 | env:
4 | GOPRIVATE: "github.com/segmentio"
5 | SEGMENT_CONTEXTS: "snyk,aws-credentials"
6 | SEGMENT_BUILDKITE_IMAGE: 'buildkite-agent-golang1.17'
7 | agents:
8 | queue: v1
9 | commands:
10 | - make all
11 |
12 | - label: publish
13 | env:
14 | SEGMENT_CONTEXTS: snyk,aws-credentials
15 | SEGMENT_BUILDKITE_IMAGE: buildkite-agent-golang1.17
16 | agents:
17 | queue: v1
18 | commands:
19 | - make publish branch=$BUILDKITE_BRANCH commit=$BUILDKITE_SHORT_COMMIT
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pprof-server [](https://circleci.com/gh/segmentio/pprof-server)
2 |
3 | Web server exposing performance profiles of Go services.
4 |
5 | ## Building
6 |
7 | ```
8 | govendor sync
9 | ```
10 |
11 | ```
12 | go build ./cmd/pprof-server
13 | ```
14 |
15 | ## Running
16 |
17 | ```
18 | ./pprof-server -registry consul://localhost:8500
19 | ```
20 | ```
21 | docker run -it --rm -p 6061:6061 segment/pprof-server -registry consul://172.17.0.1:8500
22 | ```
23 |
24 | 
25 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | sources := $(wildcard *.go) $(wildcard ./cmd/pprof-server/*.go)
2 | branch ?= $(shell git rev-parse --abbrev-ref HEAD)
3 | commit ?= $(shell git rev-parse --short=7 HEAD)
4 |
5 | all: test pprof-server
6 |
7 | test:
8 | go test -race -coverprofile=coverage.out ./...
9 |
10 | clean:
11 | rm -f pprof-server
12 |
13 | pprof-server: $(sources)
14 | go build ./cmd/pprof-server
15 |
16 | docker.version ?= $(subst /,-,$(branch))-$(commit)
17 | docker.image ?= "528451384384.dkr.ecr.us-west-2.amazonaws.com/pprof-server:$(docker.version)"
18 | docker:
19 | docker build -t $(docker.image) -f Dockerfile .
20 |
21 | publish: docker
22 | docker push $(docker.image)
23 |
24 | .PHONY: all test clean docker publish
25 |
--------------------------------------------------------------------------------
/registry.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | )
8 |
9 | type Service struct {
10 | Name string
11 | Hosts []Host
12 | }
13 |
14 | func (s Service) String() string {
15 | return "service"
16 | }
17 |
18 | func (s Service) ListServices(ctx context.Context) ([]string, error) {
19 | return []string{s.Name}, nil
20 | }
21 |
22 | func (s Service) LookupService(ctx context.Context, name string) (Service, error) {
23 | if s.Name == name {
24 | return s, nil
25 | }
26 | return Service{}, fmt.Errorf("%s: service not found", name)
27 | }
28 |
29 | type Host struct {
30 | Addr net.Addr
31 | Tags []string
32 | }
33 |
34 | type Registry interface {
35 | ListServices(ctx context.Context) ([]string, error)
36 | LookupService(ctx context.Context, name string) (Service, error)
37 | String() string
38 | }
39 |
--------------------------------------------------------------------------------
/catalog-info.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: backstage.io/v1alpha1
2 | kind: Component
3 | metadata:
4 | name: pprof-server
5 | description: Web server exposing performance profiles of Go services.
6 | annotations:
7 | github.com/project-slug: segmentio/pprof-server
8 | buildkite.com/project-slug: segment/pprof-server
9 | segment.io/security-tier: "2"
10 | twilio.com/eu-available: "true"
11 | twilio.com/sla-impacting: "false"
12 | twilio.com/in-scope-gdpr: "false"
13 | twilio.com/in-scope-bcr: "false"
14 | twilio.com/in-scope-hipaa: "false"
15 | twilio.com/in-scope-sox: "false"
16 | twilio.com/in-scope-soc2: "false"
17 | twilio.com/in-scope-pci: "false"
18 | twilio.com/in-scope-iso: "false"
19 | twilio.com/data_classification: confidential
20 | tags:
21 | - go
22 | spec:
23 | type: service
24 | lifecycle: experimental
25 | owner: infra-compute-us
26 |
--------------------------------------------------------------------------------
/consul.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "context"
5 | "sort"
6 |
7 | consul "github.com/segmentio/consul-go"
8 | )
9 |
10 | type ConsulRegistry struct{}
11 |
12 | func (r *ConsulRegistry) String() string {
13 | return "consul"
14 | }
15 |
16 | func (r *ConsulRegistry) ListServices(ctx context.Context) ([]string, error) {
17 | services, err := consul.ListServices(ctx)
18 | if err != nil {
19 | return nil, err
20 | }
21 |
22 | list := make([]string, 0, len(services))
23 |
24 | for srv := range services {
25 | list = append(list, srv)
26 | }
27 |
28 | sort.Strings(list)
29 | return list, nil
30 | }
31 |
32 | func (r *ConsulRegistry) LookupService(ctx context.Context, name string) (Service, error) {
33 | svc := Service{}
34 |
35 | endpoints, err := consul.LookupService(ctx, name)
36 | if err != nil {
37 | return svc, err
38 | }
39 |
40 | svc.Hosts = make([]Host, 0, len(endpoints))
41 | for _, endpoint := range endpoints {
42 | svc.Hosts = append(svc.Hosts, Host{
43 | Addr: endpoint.Addr,
44 | Tags: endpoint.Tags,
45 | })
46 | }
47 | return svc, nil
48 | }
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Segment
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pprof_test.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestParsePprofHome(t *testing.T) {
10 | r := strings.NewReader(`
11 |
12 | /debug/pprof/
13 |
14 |
15 | /debug/pprof/
16 |
17 | profiles:
18 |
31 |
32 | full goroutine stack dump
33 |
34 |
35 | `)
36 |
37 | p, err := parsePprofHome(r)
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 |
42 | if !reflect.DeepEqual(p, []profile{
43 | {Name: "block", URL: "block?debug=1"},
44 | {Name: "goroutine", URL: "goroutine?debug=1"},
45 | {Name: "heap", URL: "heap?debug=1"},
46 | {Name: "mutex", URL: "mutex?debug=1"},
47 | {Name: "threadcreate", URL: "threadcreate?debug=1"},
48 | {Name: "full goroutine stack dump", URL: "goroutine?debug=2"},
49 | }) {
50 | t.Error(p)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/kubernetes_test.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | v1 "k8s.io/api/core/v1"
8 | "k8s.io/apimachinery/pkg/runtime"
9 | "k8s.io/client-go/kubernetes/fake"
10 | )
11 |
12 | func makePod() *v1.Pod {
13 | return &v1.Pod{
14 | Spec: v1.PodSpec{
15 | NodeName: "instance0",
16 | Containers: []v1.Container{
17 | {
18 | Name: "container0",
19 | Ports: []v1.ContainerPort{
20 | {
21 | Name: "http",
22 | Protocol: v1.ProtocolTCP,
23 | ContainerPort: int32(3000),
24 | },
25 | },
26 | },
27 | },
28 | },
29 | Status: v1.PodStatus{
30 | PodIP: "1.2.3.4",
31 | HostIP: "1.2.3.5",
32 | },
33 | }
34 | }
35 |
36 | func kubernetesRegistryWith(objects ...runtime.Object) *KubernetesRegistry {
37 | return &KubernetesRegistry{
38 | client: fake.NewSimpleClientset(objects...),
39 | }
40 | }
41 |
42 | func TestKubernetesRegistry(t *testing.T) {
43 | ctx := context.TODO()
44 |
45 | tests := []struct {
46 | scenario string
47 | objects []runtime.Object
48 | }{
49 | {"single valid pod", []runtime.Object{makePod()}},
50 | }
51 |
52 | for _, test := range tests {
53 | t.Run(test.scenario, func(t *testing.T) {
54 | registry := kubernetesRegistryWith(test.objects...)
55 | registry.Init(ctx)
56 | _, err := registry.LookupService(ctx, "")
57 | if err != nil {
58 | t.Error(err)
59 | }
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/pprof.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "io"
5 |
6 | "golang.org/x/net/html"
7 | "golang.org/x/net/html/atom"
8 | )
9 |
10 | /*
11 | This function parses the output of the /debug/pprof endpoint of services, which
12 | looks like this:
13 |
14 |
15 |
16 | /debug/pprof/
17 |
18 |
19 | /debug/pprof/
20 |
21 | profiles:
22 |
35 |
36 | full goroutine stack dump
37 |
38 |
39 | */
40 | func parsePprofHome(r io.Reader) ([]profile, error) {
41 | doc, err := html.Parse(r)
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | var profiles []profile
47 | var search func(*html.Node)
48 | search = func(n *html.Node) {
49 | if n.Type == html.ElementNode && n.DataAtom == atom.A {
50 | var p profile
51 |
52 | for _, a := range n.Attr {
53 | if a.Key == "href" {
54 | p.URL = a.Val
55 | break
56 | }
57 | }
58 |
59 | if n.FirstChild != nil {
60 | p.Name = n.FirstChild.Data
61 | }
62 |
63 | profiles = append(profiles, p)
64 | return
65 | }
66 |
67 | for c := n.FirstChild; c != nil; c = c.NextSibling {
68 | search(c)
69 | }
70 | }
71 | search(doc)
72 | return profiles, nil
73 | }
74 |
--------------------------------------------------------------------------------
/flamegraph.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/url"
7 | "os/exec"
8 | "path"
9 | "strings"
10 |
11 | "github.com/segmentio/events"
12 | "github.com/uber/go-torch/pprof"
13 | "github.com/uber/go-torch/renderer"
14 | )
15 |
16 | func supportsFlamegraph(params string) bool {
17 | if strings.HasPrefix(params, "?") {
18 | params = params[1:]
19 | }
20 | query, err := url.ParseQuery(params)
21 | if err != nil {
22 | events.Log("flamegraph support check: params=%{params}q: %{error}s", params, err)
23 | return false
24 | }
25 | pprofUrl := query.Get("url")
26 | pprofParsed, err := url.Parse(pprofUrl)
27 | if err != nil {
28 | return false
29 | }
30 | switch path.Base(pprofParsed.Path) {
31 | case "profile", "heap", "block", "mutex":
32 | return true
33 | case "goroutine":
34 | return pprofParsed.Query().Get("debug") == "1"
35 | }
36 | return false
37 | }
38 |
39 | func renderFlamegraph(w io.Writer, url, sampleType string) error {
40 | // Get the raw pprof data
41 | c := exec.Command("go", "tool", "pprof", "-raw", url)
42 | raw, err := c.Output()
43 | if err != nil {
44 | return fmt.Errorf("get raw pprof data: %v", err)
45 | }
46 |
47 | profile, err := pprof.ParseRaw(raw)
48 | if err != nil {
49 | return fmt.Errorf("parse raw pprof output: %v", err)
50 | }
51 |
52 | // Select a sample type from the profile (bytes allocated, objects allocated, etc.)
53 | var args []string
54 | if sampleType != "" {
55 | args = append(args, "-"+sampleType)
56 | }
57 | sampleIndex := pprof.SelectSample(args, profile.SampleNames)
58 | flameInput, err := renderer.ToFlameInput(profile, sampleIndex)
59 | if err != nil {
60 | return fmt.Errorf("convert stacks to flamegraph input: %v", err)
61 | }
62 |
63 | // Construct graph title
64 | title := url
65 | if sampleType != "" {
66 | title = fmt.Sprintf("%s (%s)", url, sampleType)
67 | }
68 |
69 | // Try to find reasonable units
70 | unit := "samples"
71 | if strings.Contains(sampleType, "space") {
72 | unit = "bytes"
73 | } else if strings.Contains(sampleType, "objects") {
74 | unit = "objects"
75 | }
76 |
77 | // Render the graph
78 | flameGraph, err := renderer.GenerateFlameGraph(flameInput, "--title", title, "--countname", unit)
79 | if err != nil {
80 | return fmt.Errorf("generate flame graph: %v", err)
81 | }
82 |
83 | // Write the graph to the response
84 | if _, err := w.Write(flameGraph); err != nil {
85 | return fmt.Errorf("write flamegraph SVG: %v", err)
86 | }
87 |
88 | return nil
89 | }
90 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/segmentio/pprof-server
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/fatih/color v1.7.0 // indirect
7 | github.com/imdario/mergo v0.3.11 // indirect
8 | github.com/mattn/go-colorable v0.0.9 // indirect
9 | github.com/mattn/go-isatty v0.0.4 // indirect
10 | github.com/segmentio/conf v0.0.0-20170612230246-5d701c9ec529
11 | github.com/segmentio/consul-go v0.0.0-20170912072050-42ff3637e5db
12 | github.com/segmentio/events v2.0.1+incompatible
13 | github.com/segmentio/fasthash v1.0.0 // indirect
14 | github.com/segmentio/objconv v0.0.0-20170810202704-5dca7cbec799
15 | github.com/segmentio/stats v0.0.0-20170908015358-6da51b6c447b
16 | github.com/uber/go-torch v0.0.0-20170825044957-ddbe52cdc30e
17 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
18 | gopkg.in/validator.v2 v2.0.0-20170814132753-460c83432a98 // indirect
19 | k8s.io/api v0.20.1
20 | k8s.io/apimachinery v0.20.1
21 | k8s.io/client-go v11.0.0+incompatible
22 | )
23 |
24 | require (
25 | github.com/davecgh/go-spew v1.1.1 // indirect
26 | github.com/evanphx/json-patch v4.9.0+incompatible // indirect
27 | github.com/go-logr/logr v0.2.0 // indirect
28 | github.com/gogo/protobuf v1.3.1 // indirect
29 | github.com/golang/protobuf v1.4.3 // indirect
30 | github.com/google/go-cmp v0.5.2 // indirect
31 | github.com/google/gofuzz v1.1.0 // indirect
32 | github.com/googleapis/gnostic v0.4.1 // indirect
33 | github.com/hashicorp/golang-lru v0.5.1 // indirect
34 | github.com/json-iterator/go v1.1.10 // indirect
35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
36 | github.com/modern-go/reflect2 v1.0.1 // indirect
37 | github.com/pkg/errors v0.9.1 // indirect
38 | github.com/spf13/pflag v1.0.5 // indirect
39 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
40 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
41 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect
42 | golang.org/x/text v0.3.4 // indirect
43 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
44 | google.golang.org/appengine v1.6.5 // indirect
45 | google.golang.org/protobuf v1.25.0 // indirect
46 | gopkg.in/inf.v0 v0.9.1 // indirect
47 | gopkg.in/yaml.v2 v2.3.0 // indirect
48 | k8s.io/klog/v2 v2.4.0 // indirect
49 | k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd // indirect
50 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect
51 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2 // indirect
52 | sigs.k8s.io/yaml v1.2.0 // indirect
53 | )
54 |
55 | replace k8s.io/client-go => k8s.io/client-go v0.20.1
56 |
--------------------------------------------------------------------------------
/html.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import "html/template"
4 |
5 | var listServices = htmlTemplate("listServices", `
6 |
7 |
8 | Service List
9 |
32 |
33 |
34 |
35 | EKS services are not listed here.
36 | Use
kubectl-curl
37 | to download profiles for EKS services.
38 |
39 |
42 |
43 |
44 | `)
45 |
46 | var lookupService = htmlTemplate("lookupService", `
47 |
48 |
49 | {{ .Name }}
50 |
51 |
52 |
53 |
54 | << Services
55 |
56 |
57 | {{ range .Profiles }}
58 |
59 | | {{ .Name }} |
60 | {{- if supportsFlamegraph .Params}}
61 | 🌲 |
62 | 🔥 |
63 | {{- else}}
64 | 📜 |
65 | {{- end}}
66 |
67 | {{ end }}
68 |
69 |
70 |
71 | `)
72 |
73 | var listNodes = htmlTemplate("listNodes", `
74 |
75 |
76 | {{ .Name }}
77 |
78 |
79 |
80 |
81 | << Services
82 |
83 |
84 | |
85 | |
86 | | Nodes |
87 |
88 |
89 | {{ range .Nodes }}
90 |
91 | | {{ .Endpoint }} |
92 |
93 | {{ end }}
94 |
95 |
96 |
97 | `)
98 |
99 | func htmlTemplate(name, s string) *template.Template {
100 | funcMap := template.FuncMap{
101 | "supportsFlamegraph": supportsFlamegraph,
102 | }
103 | tmpl := template.New(name).Funcs(funcMap)
104 |
105 | return template.Must(tmpl.Parse(s))
106 | }
107 |
--------------------------------------------------------------------------------
/kubernetes.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "sort"
8 | "strings"
9 | "time"
10 |
11 | "github.com/segmentio/events"
12 | apiv1 "k8s.io/api/core/v1"
13 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14 | "k8s.io/apimachinery/pkg/runtime"
15 | "k8s.io/apimachinery/pkg/watch"
16 | "k8s.io/client-go/kubernetes"
17 | "k8s.io/client-go/tools/cache"
18 | "k8s.io/client-go/util/workqueue"
19 | )
20 |
21 | // KubernetesRegistry is a registry which discovers PODs running
22 | // on a Kubernetes cluster.
23 | //
24 | // TODO: give the ability to configure multiple Kubernetes clusters.
25 | type KubernetesRegistry struct {
26 | Namespace string
27 |
28 | client kubernetes.Interface
29 | store cache.Store
30 | }
31 |
32 | func NewKubernetesRegistry(client *kubernetes.Clientset) *KubernetesRegistry {
33 | return &KubernetesRegistry{
34 | client: client,
35 | }
36 | }
37 |
38 | // Name implements the Registry interface.
39 | func (k *KubernetesRegistry) String() string {
40 | return "kubernetes"
41 | }
42 |
43 | // Init initialize the watcher and store configuration for the registry.
44 | func (k *KubernetesRegistry) Init(ctx context.Context) {
45 | p := k.client.CoreV1().Pods(k.Namespace)
46 |
47 | listWatch := &cache.ListWatch{
48 | ListFunc: func(options metaV1.ListOptions) (runtime.Object, error) {
49 | return p.List(context.TODO(), options)
50 | },
51 | WatchFunc: func(options metaV1.ListOptions) (watch.Interface, error) {
52 | return p.Watch(context.TODO(), options)
53 | },
54 | }
55 |
56 | queue := workqueue.New()
57 |
58 | informer := cache.NewSharedInformer(listWatch, &apiv1.Pod{}, 10*time.Second)
59 | informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
60 | AddFunc: func(obj interface{}) {
61 | k.handleObj(queue, obj)
62 | },
63 | DeleteFunc: func(obj interface{}) {
64 | k.handleObj(queue, obj)
65 | },
66 | UpdateFunc: func(_, obj interface{}) {
67 | k.handleObj(queue, obj)
68 | },
69 | })
70 |
71 | go informer.Run(ctx.Done())
72 |
73 | k.store = informer.GetStore()
74 | }
75 |
76 | func (k *KubernetesRegistry) handleObj(q *workqueue.Type, o interface{}) {
77 | key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(o)
78 | if err != nil {
79 | events.Log("failed to handle object: %{error}s", err)
80 | return
81 | }
82 |
83 | q.Add(key)
84 | }
85 |
86 | func toPod(o interface{}) (*apiv1.Pod, error) {
87 | pod, ok := o.(*apiv1.Pod)
88 | if ok {
89 | return pod, nil
90 | }
91 |
92 | return nil, fmt.Errorf("received unexpected object: %v", o)
93 | }
94 |
95 | func (k *KubernetesRegistry) ListServices(ctx context.Context) ([]string, error) {
96 | podnames, err := k.client.CoreV1().Pods(k.Namespace).List(ctx, metaV1.ListOptions{})
97 | if err != nil {
98 | return nil, err
99 | }
100 |
101 | list := make([]string, 0, len(podnames.Items))
102 | for _, pod := range podnames.Items {
103 | list = append(list, joinNamespacePodName(pod.Namespace, pod.Name))
104 | }
105 |
106 | sort.Strings(list)
107 | return list, nil
108 | }
109 |
110 | // LookupService implements the Registry interface. The returned Service will contain
111 | // one Host entry per POD IP+container exposed port.
112 | func (k *KubernetesRegistry) LookupService(ctx context.Context, name string) (Service, error) {
113 | svc := Service{
114 | Name: "kubernetes",
115 | }
116 |
117 | namespace, podName := splitNamespacePodName(name)
118 | hosts := []Host{}
119 |
120 | for _, obj := range k.store.List() {
121 | pod, err := toPod(obj)
122 | if err != nil {
123 | events.Log("failed to convert data to pod: %{error}s", err)
124 | continue
125 | }
126 |
127 | // filtering pods based on podname, even if they are diff namepsaces for now, since the route for namespaces isnt made yet
128 | if pod.Namespace == namespace && pod.Name == podName {
129 | for _, container := range pod.Spec.Containers {
130 | // adding container name to display
131 | tags := []string{pod.Name + "-" + container.Name}
132 |
133 | for _, port := range container.Ports {
134 | if port.Name == "http" {
135 | hosts = append(hosts, Host{
136 | Addr: &net.TCPAddr{
137 | IP: net.ParseIP(pod.Status.PodIP),
138 | Port: int(port.ContainerPort),
139 | },
140 | Tags: append(tags, port.Name), // port name must be specified in the pod spec as http
141 | })
142 | }
143 | }
144 | }
145 | }
146 | }
147 |
148 | svc.Hosts = hosts
149 | return svc, nil
150 | }
151 |
152 | func joinNamespacePodName(namespace, podName string) string {
153 | return namespace + "/" + podName
154 | }
155 |
156 | func splitNamespacePodName(name string) (namespace, podName string) {
157 | if i := strings.IndexByte(name, '/'); i < 0 {
158 | podName = name
159 | } else {
160 | namespace, podName = name[:i], name[i+1:]
161 | }
162 | return
163 | }
164 |
--------------------------------------------------------------------------------
/cmd/pprof-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "net/url"
7 | "os"
8 | "strings"
9 | "syscall"
10 |
11 | "github.com/segmentio/conf"
12 | consul "github.com/segmentio/consul-go"
13 | "github.com/segmentio/events"
14 | _ "github.com/segmentio/events/ecslogs"
15 | "github.com/segmentio/events/httpevents"
16 | _ "github.com/segmentio/events/text"
17 | pprofserver "github.com/segmentio/pprof-server"
18 | "github.com/segmentio/stats"
19 | "github.com/segmentio/stats/datadog"
20 | "github.com/segmentio/stats/httpstats"
21 | "k8s.io/client-go/kubernetes"
22 | "k8s.io/client-go/rest"
23 | "k8s.io/client-go/tools/clientcmd"
24 | )
25 |
26 | func main() {
27 | type dogstatsdConfig struct {
28 | Address string `conf:"address" help:"Address of the dogstatsd server to send metrics to."`
29 | BufferSize int `conf:"buffer-size" help:"Buffer size of the dogstatsd client." validet:"min=1024"`
30 | }
31 |
32 | type kubernetesConfig struct {
33 | MasterURL string `conf:"master-url" help:"Master of the Kubernetes URL."`
34 | Kubeconfig string `conf:"kubeconfig" help:"Path of the Kubeconfig file."`
35 | }
36 |
37 | config := struct {
38 | Bind string `conf:"bind" help:"Network address to listen on." validate:"nonzero"`
39 | Registry string `conf:"registry" help:"Address of the registry used to discover services."`
40 | Debug bool `conf:"debug" help:"Enable debug mode."`
41 |
42 | Dogstatsd dogstatsdConfig `conf:"dogstatsd" help:"Configuration of the dogstatsd client."`
43 |
44 | Kubernetes kubernetesConfig `conf:"kubernetes" help:"Kubernetes configuration."`
45 | }{
46 | Bind: ":6061",
47 | Dogstatsd: dogstatsdConfig{
48 | BufferSize: 1024,
49 | },
50 | }
51 | conf.Load(&config)
52 |
53 | events.DefaultLogger.EnableDebug = config.Debug
54 | events.DefaultLogger.EnableSource = config.Debug
55 | defer stats.Flush()
56 |
57 | if len(config.Dogstatsd.Address) != 0 {
58 | stats.Register(datadog.NewClientWith(datadog.ClientConfig{
59 | Address: config.Dogstatsd.Address,
60 | BufferSize: config.Dogstatsd.BufferSize,
61 | }))
62 | }
63 |
64 | ctx, cancel := events.WithSignals(context.Background(), syscall.SIGTERM, syscall.SIGINT)
65 | defer cancel()
66 |
67 | var registry pprofserver.Registry
68 | if len(config.Registry) != 0 {
69 | u, err := url.Parse(config.Registry)
70 | if err != nil {
71 | events.Log("bad registry URL: %{url}s", config.Registry)
72 | os.Exit(1)
73 | }
74 | switch u.Scheme {
75 | case "":
76 | events.Log("no registry is configured")
77 | case "consul":
78 | consul.DefaultClient.Address = u.Host
79 | consul.DefaultResolver.Balancer = nil
80 | registry = &pprofserver.ConsulRegistry{}
81 | events.Log("using consul registry at %{address}s", u.Host)
82 | case "service":
83 | name := strings.TrimPrefix(u.Path, "/")
84 | if name == "" {
85 | name = "Service"
86 | }
87 | registry = pprofserver.Service{
88 | Name: name,
89 | Hosts: []pprofserver.Host{{Addr: hostAddr(u.Host)}},
90 | }
91 | events.Log("using single service registry at %{address}s", u.Host)
92 | case "kubernetes":
93 | kubeclient, err := kubeClient(
94 | u.Host == "in-cluster",
95 | config.Kubernetes.MasterURL,
96 | config.Kubernetes.Kubeconfig)
97 | if err != nil {
98 | panic(err)
99 | }
100 |
101 | kubernetesRegistry := pprofserver.NewKubernetesRegistry(kubeclient)
102 | kubernetesRegistry.Init(ctx)
103 |
104 | registry = kubernetesRegistry
105 | events.Log("using kubernetes registry")
106 | default:
107 | events.Log("unsupported registry: %{url}s", config.Registry)
108 | os.Exit(1)
109 | }
110 | }
111 |
112 | var httpTransport = http.DefaultTransport
113 | httpTransport = httpstats.NewTransport(httpTransport)
114 | httpTransport = httpevents.NewTransport(httpTransport)
115 | http.DefaultTransport = httpTransport
116 |
117 | var httpHandler http.Handler = &pprofserver.Handler{Registry: registry}
118 | httpHandler = httpstats.NewHandler(httpHandler)
119 | httpHandler = httpevents.NewHandler(httpHandler)
120 | http.Handle("/", httpHandler)
121 |
122 | server := http.Server{
123 | Addr: config.Bind,
124 | }
125 |
126 | go func() {
127 | <-ctx.Done()
128 | cancel()
129 | server.Shutdown(context.Background())
130 | }()
131 |
132 | events.Log("pprof server is listening for incoming connections on %{address}s", config.Bind)
133 |
134 | switch err := server.ListenAndServe(); {
135 | case err == http.ErrServerClosed:
136 | case events.IsTermination(err):
137 | case events.IsInterruption(err):
138 | default:
139 | events.Log("fatal error: %{error}s", err)
140 | }
141 | }
142 |
143 | type hostAddr string
144 |
145 | func (a hostAddr) Network() string { return "" }
146 | func (a hostAddr) String() string { return string(a) }
147 |
148 | func kubeClient(inCluster bool, master, kubeconfig string) (*kubernetes.Clientset, error) {
149 | var config *rest.Config
150 | var err error
151 |
152 | if inCluster {
153 | events.Log("kubernetes: using pod service account with InClusterConfig.")
154 | config, err = rest.InClusterConfig()
155 | } else {
156 | events.Log("kubernetes: kubeconfig file.")
157 | config, err = clientcmd.BuildConfigFromFlags(master, kubeconfig)
158 | }
159 |
160 | if err != nil {
161 | return nil, err
162 | }
163 |
164 | return kubernetes.NewForConfig(config)
165 | }
166 |
--------------------------------------------------------------------------------
/handler.go:
--------------------------------------------------------------------------------
1 | package pprofserver
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "html/template"
8 | "io"
9 | "net/http"
10 | "net/url"
11 | "os/exec"
12 | "path"
13 | "sort"
14 | "strings"
15 |
16 | "github.com/segmentio/events"
17 | "github.com/segmentio/events/log"
18 | "github.com/segmentio/objconv/json"
19 | )
20 |
21 | type Handler struct {
22 | Prefix string
23 | Registry Registry
24 | Client *http.Client
25 | }
26 |
27 | func (h *Handler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
28 | header := res.Header()
29 | header.Set("Content-Language", "en")
30 | header.Set("Server", "pprof-server")
31 |
32 | switch req.Method {
33 | case http.MethodGet, http.MethodHead:
34 | default:
35 | http.Error(res, "only GET and HEAD are allowed", http.StatusMethodNotAllowed)
36 | return
37 | }
38 |
39 | switch path := req.URL.Path; {
40 | case path == "/", path == "/services":
41 | if h.Registry != nil && h.Registry.String() == "kubernetes" {
42 | h.serveRedirect(res, req, "/pods/")
43 | } else {
44 | h.serveRedirect(res, req, "/services/")
45 | }
46 | case path == "/tree":
47 | h.serveTree(res, req)
48 |
49 | case path == "/flame":
50 | h.serveFlame(res, req)
51 |
52 | case path == "/services/":
53 | h.serveListServices(res, req)
54 |
55 | case strings.HasPrefix(path, "/services/"):
56 | h.serveListTasks(res, req)
57 |
58 | case strings.HasPrefix(path, "/service/"):
59 | h.serveLookupService(res, req)
60 |
61 | case path == "/pods/":
62 | h.serveListPods(res, req)
63 |
64 | case strings.HasPrefix(path, "/pods/"):
65 | // We currently expose all the PODs in one page. To make it more scalable, we plan
66 | // to implement a tree of pages per type of Kubernetes resource (sts, deployment, ...).
67 | h.serveListContainers(res, req)
68 |
69 | case strings.HasPrefix(path, "/pod/"):
70 | h.serveLookupContainer(res, req)
71 |
72 | default:
73 | h.serveNotFound(res, req)
74 | }
75 | }
76 |
77 | func (h *Handler) serveRedirect(res http.ResponseWriter, req *http.Request, url string) {
78 | http.Redirect(res, req, url, http.StatusFound)
79 | }
80 |
81 | func (h *Handler) serveNotFound(res http.ResponseWriter, req *http.Request) {
82 | http.NotFound(res, req)
83 | }
84 |
85 | func (h *Handler) serveListServices(res http.ResponseWriter, req *http.Request) {
86 | var services []service
87 |
88 | if h.Registry != nil {
89 | names, err := h.Registry.ListServices(req.Context())
90 | if err != nil {
91 | events.Log("error listing services: %{error}s", err)
92 | }
93 | services = make([]service, 0, len(names))
94 | for _, name := range names {
95 | services = append(services, service{
96 | Name: name,
97 | Href: "/services/" + name,
98 | })
99 | }
100 | }
101 |
102 | render(res, req, listServices, services)
103 | }
104 |
105 | func (h *Handler) serveListTasks(res http.ResponseWriter, req *http.Request) {
106 | var name = strings.TrimPrefix(path.Clean(req.URL.Path), "/services/")
107 | var srv service
108 |
109 | if h.Registry != nil {
110 | srvRegistry, err := h.Registry.LookupService(req.Context(), name)
111 | if err != nil {
112 | events.Log("error listing tasks: %{error}s", err)
113 | }
114 |
115 | srv.Nodes = make([]node, 0, len(srvRegistry.Hosts))
116 | for _, host := range srvRegistry.Hosts {
117 | srv.Nodes = append(srv.Nodes, node{
118 | Endpoint: fmt.Sprintf("%s %s", host.Addr, strings.Join(host.Tags, " - ")),
119 | Href: "/service/" + host.Addr.String(),
120 | })
121 | }
122 | }
123 |
124 | srv.Name = name
125 | srv.Href = "/services/" + name
126 | render(res, req, listNodes, srv)
127 | }
128 |
129 | func (h *Handler) serveListPods(res http.ResponseWriter, req *http.Request) {
130 | var services []service
131 |
132 | if h.Registry != nil {
133 | names, err := h.Registry.ListServices(req.Context())
134 | if err != nil {
135 | events.Log("error listing services: %{error}s", err)
136 | }
137 | services = make([]service, 0, len(names))
138 | for _, name := range names {
139 | services = append(services, service{
140 | Name: name,
141 | Href: "/pods/" + name,
142 | })
143 | }
144 | }
145 |
146 | render(res, req, listServices, services)
147 | }
148 |
149 | func (h *Handler) serveListContainers(res http.ResponseWriter, req *http.Request) {
150 | var podname = strings.TrimPrefix(path.Clean(req.URL.Path), "/pods/")
151 | var srv service
152 |
153 | if h.Registry != nil {
154 | srvRegistry, err := h.Registry.LookupService(req.Context(), podname)
155 | if err != nil {
156 | events.Log("error listing pods: %{error}s", err)
157 | }
158 |
159 | srv.Nodes = make([]node, 0, len(srvRegistry.Hosts))
160 | for _, host := range srvRegistry.Hosts {
161 | srv.Nodes = append(srv.Nodes, node{
162 | Endpoint: fmt.Sprintf("%s %s", host.Addr, strings.Join(host.Tags, " - ")),
163 | Href: "/pod/" + host.Addr.String(),
164 | })
165 | }
166 | }
167 |
168 | srv.Name = "kubernetes"
169 | srv.Href = "/pods/" + podname
170 | render(res, req, listNodes, srv)
171 |
172 | }
173 |
174 | func (h *Handler) serveLookupService(res http.ResponseWriter, req *http.Request) {
175 | var ctx = req.Context()
176 | var endpoint = strings.TrimPrefix(path.Clean(req.URL.Path), "/service/")
177 | var n node
178 |
179 | if h.Registry != nil {
180 | p, err := h.fetchService(ctx, endpoint)
181 | if err != nil {
182 | events.Log("error fetching service profiles of %{service}s: %{error}s", endpoint, err)
183 | } else {
184 | n.Profiles = append(n.Profiles, p...)
185 | }
186 | }
187 |
188 | sort.Slice(n.Profiles, func(i int, j int) bool {
189 | p1 := n.Profiles[i]
190 | p2 := n.Profiles[j]
191 |
192 | if p1.Name != p2.Name {
193 | return p1.Name < p2.Name
194 | }
195 |
196 | return p1.URL < p2.URL
197 | })
198 | render(res, req, lookupService, n)
199 | }
200 |
201 | func (h *Handler) serveLookupContainer(res http.ResponseWriter, req *http.Request) {
202 | var ctx = req.Context()
203 | var endpoint = strings.TrimPrefix(path.Clean(req.URL.Path), "/pod/")
204 | var n node
205 |
206 | if h.Registry != nil {
207 | p, err := h.fetchService(ctx, endpoint)
208 | if err != nil {
209 | events.Log("error fetching service profiles of %{service}s: %{error}s", endpoint, err)
210 | } else {
211 | n.Profiles = append(n.Profiles, p...)
212 | }
213 | }
214 |
215 | sort.Slice(n.Profiles, func(i int, j int) bool {
216 | p1 := n.Profiles[i]
217 | p2 := n.Profiles[j]
218 |
219 | if p1.Name != p2.Name {
220 | return p1.Name < p2.Name
221 | }
222 |
223 | return p1.URL < p2.URL
224 | })
225 | render(res, req, lookupService, n)
226 | }
227 |
228 | func (h *Handler) serveFlame(res http.ResponseWriter, req *http.Request) {
229 | queryString := req.URL.Query()
230 | serviceURL := queryString.Get("url")
231 | queryString.Del("url")
232 |
233 | if len(serviceURL) == 0 {
234 | res.WriteHeader(http.StatusNotFound)
235 | return
236 | }
237 |
238 | // Find the sample type (objects allocated, objects in use, etc)
239 | sampleType := ""
240 | for arg := range queryString {
241 | if arg != "url" {
242 | sampleType = arg
243 | break
244 | }
245 | }
246 |
247 | if err := renderFlamegraph(res, serviceURL, sampleType); err != nil {
248 | fmt.Fprintln(res, "Unable to generate flame graph for this profile 🤯")
249 | events.Log("error generating flamegraph: %{error}s", err)
250 | }
251 | }
252 |
253 | func (h *Handler) serveTree(res http.ResponseWriter, req *http.Request) {
254 | queryString := req.URL.Query()
255 | serviceURL := queryString.Get("url")
256 | queryString.Del("url")
257 |
258 | if len(serviceURL) == 0 {
259 | res.WriteHeader(http.StatusNotFound)
260 | return
261 | }
262 |
263 | args := []string{
264 | "tool",
265 | "pprof",
266 | "-svg",
267 | "-symbolize",
268 | "remote",
269 | }
270 |
271 | args = append(args, query2pprofArgs(queryString)...)
272 | args = append(args, serviceURL)
273 |
274 | buffer := &bytes.Buffer{}
275 | buffer.Grow(32768)
276 | events.Log("go " + strings.Join(args, " "))
277 |
278 | pprof := exec.CommandContext(req.Context(), "go", args...)
279 | pprof.Stdin = nil
280 | pprof.Stdout = buffer
281 | pprof.Stderr = log.NewWriter("", 0, events.DefaultHandler)
282 |
283 | if pprof.Run() == nil {
284 | buffer.WriteTo(res)
285 | return
286 | }
287 |
288 | // failed to render a graph; fall back to serving the raw profile
289 | h.serveRawProfile(res, req, serviceURL)
290 | }
291 |
292 | func query2pprofArgs(q url.Values) (args []string) {
293 | for flag, values := range q {
294 | if len(values) == 0 {
295 | args = append(args, "-"+flag)
296 | } else {
297 | for _, value := range values {
298 | args = append(args, "-"+flag, value)
299 | }
300 | }
301 | }
302 | return
303 | }
304 |
305 | func (h *Handler) serveRawProfile(w http.ResponseWriter, r *http.Request, url string) {
306 | res, err := h.client().Get(url)
307 | if err != nil {
308 | w.WriteHeader(http.StatusBadGateway)
309 | events.Log("error querying %{url}s: %{error}s", url, err)
310 | return
311 | }
312 | io.Copy(w, res.Body)
313 | res.Body.Close()
314 | }
315 |
316 | func (h *Handler) fetchService(ctx context.Context, endpoint string) (prof []profile, err error) {
317 | var req *http.Request
318 | var res *http.Response
319 | var prefix = h.prefix()
320 |
321 | if !strings.Contains(endpoint, "://") {
322 | endpoint = "http://" + endpoint
323 | }
324 |
325 | if req, err = http.NewRequest(http.MethodGet, endpoint+"/debug/pprof/", nil); err != nil {
326 | return
327 | }
328 |
329 | if res, err = h.client().Do(req); err != nil {
330 | return
331 | }
332 | defer res.Body.Close()
333 |
334 | if prof, err = parsePprofHome(res.Body); err != nil {
335 | return
336 | }
337 |
338 | // For some reason the default profiles aren't returned by the /debug/pprof/
339 | // home page.
340 | //
341 | // Update: In Go 1.11 the profile and trace endpoints are now exposed by the
342 | // index.
343 | hasProfile, hasTrace := false, false
344 |
345 | for i, p := range prof {
346 | fullPath, query := splitPathQuery(p.URL)
347 | name := path.Base(fullPath)
348 | baseURL := endpoint
349 |
350 | if !strings.HasPrefix(fullPath, "/") {
351 | baseURL += prefix
352 | }
353 |
354 | // For heap profiles, inject the options for capturing the allocated objects
355 | // or the allocated space.
356 | if name == "heap" {
357 | // strip debug=1 or it fails to render svg after Go 1.11, it seems to
358 | // render fine in earlier versions.
359 | p.URL, _ = splitPathQuery(p.URL)
360 |
361 | prof[i].Name = p.Name + " (objects in use)"
362 | prof[i].URL = baseURL + p.URL
363 | prof[i].Params = "?inuse_objects&url=" + url.QueryEscape(prof[i].URL)
364 |
365 | prof = append(prof,
366 | profile{
367 | Name: p.Name + " (space in use)",
368 | URL: baseURL + p.URL,
369 | Params: "?inuse_space&url=" + url.QueryEscape(prof[i].URL),
370 | },
371 | profile{
372 | Name: p.Name + " (objects allocated)",
373 | URL: baseURL + p.URL,
374 | Params: "?alloc_objects&url=" + url.QueryEscape(prof[i].URL),
375 | },
376 | profile{
377 | Name: p.Name + " (space allocated)",
378 | URL: baseURL + p.URL,
379 | Params: "?alloc_space&url=" + url.QueryEscape(prof[i].URL),
380 | },
381 | )
382 | continue
383 | }
384 |
385 | if name == "profile" {
386 | hasProfile = true
387 | }
388 |
389 | if name == "trace" {
390 | hasTrace = true
391 | }
392 |
393 | if (name == "profile" || name == "trace") && query == "" {
394 | query = "?seconds=5"
395 | }
396 |
397 | p.URL = fullPath + query
398 | prof[i].URL = baseURL + p.URL
399 | prof[i].Params = "?url=" + url.QueryEscape(prof[i].URL)
400 | }
401 |
402 | if !hasProfile {
403 | profURL := endpoint + prefix + "profile?seconds=5"
404 | prof = append(prof, profile{
405 | Name: "profile",
406 | URL: profURL,
407 | Params: "?url=" + url.QueryEscape(profURL),
408 | })
409 | }
410 |
411 | if !hasTrace {
412 | profURL := endpoint + prefix + "trace?seconds=5"
413 | prof = append(prof, profile{
414 | Name: "trace",
415 | URL: profURL,
416 | Params: "?url=" + url.QueryEscape(profURL),
417 | })
418 | }
419 |
420 | return
421 | }
422 |
423 | func (h *Handler) client() *http.Client {
424 | if h.Client != nil {
425 | return h.Client
426 | }
427 | return http.DefaultClient
428 | }
429 |
430 | func (h *Handler) prefix() string {
431 | if len(h.Prefix) != 0 {
432 | return h.Prefix
433 | }
434 | return "/debug/pprof/"
435 | }
436 |
437 | type service struct {
438 | Name string `json:"name"`
439 | Href string `json:"href"`
440 | Nodes []node `json:"nodes,omitempty"`
441 | }
442 |
443 | type node struct {
444 | Name string `json:"name"`
445 | Endpoint string `json:"endpoint"`
446 | Href string `json:"href"`
447 | Profiles []profile `json:"profiles,omitempty"`
448 | }
449 |
450 | type profile struct {
451 | Name string `json:"name"`
452 | URL string `json:"url"`
453 | Params string `json:"params"`
454 | }
455 |
456 | func render(res http.ResponseWriter, req *http.Request, tpl *template.Template, val interface{}) {
457 | switch accept := strings.TrimSpace(req.Header.Get("Accept")); {
458 | case strings.Contains(accept, "text/html"):
459 | renderHTML(res, tpl, val)
460 | default:
461 | renderJSON(res, val)
462 | }
463 | }
464 |
465 | func renderJSON(res http.ResponseWriter, val interface{}) {
466 | res.Header().Set("Content-Type", "application/json; charset=utf-8")
467 | json.NewPrettyEncoder(res).Encode(val)
468 | }
469 |
470 | func renderHTML(res http.ResponseWriter, tpl *template.Template, val interface{}) {
471 | res.Header().Set("Content-Type", "text/html; charset=utf-8")
472 | tpl.Execute(res, val)
473 | }
474 |
475 | func splitPathQuery(s string) (path string, query string) {
476 | if i := strings.IndexByte(s, '?'); i >= 0 {
477 | path, query = s[:i], s[i:]
478 | } else {
479 | path = s
480 | }
481 | return
482 | }
483 |
--------------------------------------------------------------------------------
/flamegraph.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | #
3 | # flamegraph.pl flame stack grapher.
4 | #
5 | # This takes stack samples and renders a call graph, allowing hot functions
6 | # and codepaths to be quickly identified. Stack samples can be generated using
7 | # tools such as DTrace, perf, SystemTap, and Instruments.
8 | #
9 | # USAGE: ./flamegraph.pl [options] input.txt > graph.svg
10 | #
11 | # grep funcA input.txt | ./flamegraph.pl [options] > graph.svg
12 | #
13 | # Then open the resulting .svg in a web browser, for interactivity: mouse-over
14 | # frames for info, click to zoom, and ctrl-F to search.
15 | #
16 | # Options are listed in the usage message (--help).
17 | #
18 | # The input is stack frames and sample counts formatted as single lines. Each
19 | # frame in the stack is semicolon separated, with a space and count at the end
20 | # of the line. These can be generated for Linux perf script output using
21 | # stackcollapse-perf.pl, for DTrace using stackcollapse.pl, and for other tools
22 | # using the other stackcollapse programs. Example input:
23 | #
24 | # swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 1
25 | #
26 | # An optional extra column of counts can be provided to generate a differential
27 | # flame graph of the counts, colored red for more, and blue for less. This
28 | # can be useful when using flame graphs for non-regression testing.
29 | # See the header comment in the difffolded.pl program for instructions.
30 | #
31 | # The input functions can optionally have annotations at the end of each
32 | # function name, following a precedent by some tools (Linux perf's _[k]):
33 | # _[k] for kernel
34 | # _[i] for inlined
35 | # _[j] for jit
36 | # _[w] for waker
37 | # Some of the stackcollapse programs support adding these annotations, eg,
38 | # stackcollapse-perf.pl --kernel --jit. They are used merely for colors by
39 | # some palettes, eg, flamegraph.pl --color=java.
40 | #
41 | # The output flame graph shows relative presence of functions in stack samples.
42 | # The ordering on the x-axis has no meaning; since the data is samples, time
43 | # order of events is not known. The order used sorts function names
44 | # alphabetically.
45 | #
46 | # While intended to process stack samples, this can also process stack traces.
47 | # For example, tracing stacks for memory allocation, or resource usage. You
48 | # can use --title to set the title to reflect the content, and --countname
49 | # to change "samples" to "bytes" etc.
50 | #
51 | # There are a few different palettes, selectable using --color. By default,
52 | # the colors are selected at random (except for differentials). Functions
53 | # called "-" will be printed gray, which can be used for stack separators (eg,
54 | # between user and kernel stacks).
55 | #
56 | # HISTORY
57 | #
58 | # This was inspired by Neelakanth Nadgir's excellent function_call_graph.rb
59 | # program, which visualized function entry and return trace events. As Neel
60 | # wrote: "The output displayed is inspired by Roch's CallStackAnalyzer which
61 | # was in turn inspired by the work on vftrace by Jan Boerhout". See:
62 | # https://blogs.oracle.com/realneel/entry/visualizing_callstacks_via_dtrace_and
63 | #
64 | # Copyright 2016 Netflix, Inc.
65 | # Copyright 2011 Joyent, Inc. All rights reserved.
66 | # Copyright 2011 Brendan Gregg. All rights reserved.
67 | #
68 | # CDDL HEADER START
69 | #
70 | # The contents of this file are subject to the terms of the
71 | # Common Development and Distribution License (the "License").
72 | # You may not use this file except in compliance with the License.
73 | #
74 | # You can obtain a copy of the license at docs/cddl1.txt or
75 | # http://opensource.org/licenses/CDDL-1.0.
76 | # See the License for the specific language governing permissions
77 | # and limitations under the License.
78 | #
79 | # When distributing Covered Code, include this CDDL HEADER in each
80 | # file and include the License file at docs/cddl1.txt.
81 | # If applicable, add the following below this CDDL HEADER, with the
82 | # fields enclosed by brackets "[]" replaced with your own identifying
83 | # information: Portions Copyright [yyyy] [name of copyright owner]
84 | #
85 | # CDDL HEADER END
86 | #
87 | # 11-Oct-2014 Adrien Mahieux Added zoom.
88 | # 21-Nov-2013 Shawn Sterling Added consistent palette file option
89 | # 17-Mar-2013 Tim Bunce Added options and more tunables.
90 | # 15-Dec-2011 Dave Pacheco Support for frames with whitespace.
91 | # 10-Sep-2011 Brendan Gregg Created this.
92 |
93 | use strict;
94 |
95 | use Getopt::Long;
96 |
97 | use open qw(:std :utf8);
98 |
99 | # tunables
100 | my $encoding;
101 | my $fonttype = "Verdana";
102 | my $imagewidth = 1200; # max width, pixels
103 | my $frameheight = 16; # max height is dynamic
104 | my $fontsize = 12; # base text size
105 | my $fontwidth = 0.59; # avg width relative to fontsize
106 | my $minwidth = 0.1; # min function width, pixels
107 | my $nametype = "Function:"; # what are the names in the data?
108 | my $countname = "samples"; # what are the counts in the data?
109 | my $colors = "hot"; # color theme
110 | my $bgcolors = ""; # background color theme
111 | my $nameattrfile; # file holding function attributes
112 | my $timemax; # (override the) sum of the counts
113 | my $factor = 1; # factor to scale counts by
114 | my $hash = 0; # color by function name
115 | my $palette = 0; # if we use consistent palettes (default off)
116 | my %palette_map; # palette map hash
117 | my $pal_file = "palette.map"; # palette map file name
118 | my $stackreverse = 0; # reverse stack order, switching merge end
119 | my $inverted = 0; # icicle graph
120 | my $flamechart = 0; # produce a flame chart (sort by time, do not merge stacks)
121 | my $negate = 0; # switch differential hues
122 | my $titletext = ""; # centered heading
123 | my $titledefault = "Flame Graph"; # overwritten by --title
124 | my $titleinverted = "Icicle Graph"; # " "
125 | my $searchcolor = "rgb(230,0,230)"; # color for search highlighting
126 | my $notestext = ""; # embedded notes in SVG
127 | my $subtitletext = ""; # second level title (optional)
128 | my $help = 0;
129 |
130 | sub usage {
131 | die < outfile.svg\n
133 | --title TEXT # change title text
134 | --subtitle TEXT # second level title (optional)
135 | --width NUM # width of image (default 1200)
136 | --height NUM # height of each frame (default 16)
137 | --minwidth NUM # omit smaller functions (default 0.1 pixels)
138 | --fonttype FONT # font type (default "Verdana")
139 | --fontsize NUM # font size (default 12)
140 | --countname TEXT # count type label (default "samples")
141 | --nametype TEXT # name type label (default "Function:")
142 | --colors PALETTE # set color palette. choices are: hot (default), mem,
143 | # io, wakeup, chain, java, js, perl, red, green, blue,
144 | # aqua, yellow, purple, orange
145 | --bgcolors COLOR # set background colors. gradient choices are yellow
146 | # (default), blue, green, grey; flat colors use "#rrggbb"
147 | --hash # colors are keyed by function name hash
148 | --cp # use consistent palette (palette.map)
149 | --reverse # generate stack-reversed flame graph
150 | --inverted # icicle graph
151 | --flamechart # produce a flame chart (sort by time, do not merge stacks)
152 | --negate # switch differential hues (blue<->red)
153 | --notes TEXT # add notes comment in SVG (for debugging)
154 | --help # this message
155 |
156 | eg,
157 | $0 --title="Flame Graph: malloc()" trace.txt > graph.svg
158 | USAGE_END
159 | }
160 |
161 | GetOptions(
162 | 'fonttype=s' => \$fonttype,
163 | 'width=i' => \$imagewidth,
164 | 'height=i' => \$frameheight,
165 | 'encoding=s' => \$encoding,
166 | 'fontsize=f' => \$fontsize,
167 | 'fontwidth=f' => \$fontwidth,
168 | 'minwidth=f' => \$minwidth,
169 | 'title=s' => \$titletext,
170 | 'subtitle=s' => \$subtitletext,
171 | 'nametype=s' => \$nametype,
172 | 'countname=s' => \$countname,
173 | 'nameattr=s' => \$nameattrfile,
174 | 'total=s' => \$timemax,
175 | 'factor=f' => \$factor,
176 | 'colors=s' => \$colors,
177 | 'bgcolors=s' => \$bgcolors,
178 | 'hash' => \$hash,
179 | 'cp' => \$palette,
180 | 'reverse' => \$stackreverse,
181 | 'inverted' => \$inverted,
182 | 'flamechart' => \$flamechart,
183 | 'negate' => \$negate,
184 | 'notes=s' => \$notestext,
185 | 'help' => \$help,
186 | ) or usage();
187 | $help && usage();
188 |
189 | # internals
190 | my $ypad1 = $fontsize * 3; # pad top, include title
191 | my $ypad2 = $fontsize * 2 + 10; # pad bottom, include labels
192 | my $ypad3 = $fontsize * 2; # pad top, include subtitle (optional)
193 | my $xpad = 10; # pad lefm and right
194 | my $framepad = 1; # vertical padding for frames
195 | my $depthmax = 0;
196 | my %Events;
197 | my %nameattr;
198 |
199 | if ($flamechart && $titletext eq "") {
200 | $titletext = "Flame Chart";
201 | }
202 |
203 | if ($titletext eq "") {
204 | unless ($inverted) {
205 | $titletext = $titledefault;
206 | } else {
207 | $titletext = $titleinverted;
208 | }
209 | }
210 |
211 | if ($nameattrfile) {
212 | # The name-attribute file format is a function name followed by a tab then
213 | # a sequence of tab separated name=value pairs.
214 | open my $attrfh, $nameattrfile or die "Can't read $nameattrfile: $!\n";
215 | while (<$attrfh>) {
216 | chomp;
217 | my ($funcname, $attrstr) = split /\t/, $_, 2;
218 | die "Invalid format in $nameattrfile" unless defined $attrstr;
219 | $nameattr{$funcname} = { map { split /=/, $_, 2 } split /\t/, $attrstr };
220 | }
221 | }
222 |
223 | if ($notestext =~ /[<>]/) {
224 | die "Notes string can't contain < or >"
225 | }
226 |
227 | # background colors:
228 | # - yellow gradient: default (hot, java, js, perl)
229 | # - green gradient: mem
230 | # - blue gradient: io, wakeup, chain
231 | # - gray gradient: flat colors (red, green, blue, ...)
232 | if ($bgcolors eq "") {
233 | # choose a default
234 | if ($colors eq "mem") {
235 | $bgcolors = "green";
236 | } elsif ($colors =~ /^(io|wakeup|chain)$/) {
237 | $bgcolors = "blue";
238 | } elsif ($colors =~ /^(red|green|blue|aqua|yellow|purple|orange)$/) {
239 | $bgcolors = "grey";
240 | } else {
241 | $bgcolors = "yellow";
242 | }
243 | }
244 | my ($bgcolor1, $bgcolor2);
245 | if ($bgcolors eq "yellow") {
246 | $bgcolor1 = "#eeeeee"; # background color gradient start
247 | $bgcolor2 = "#eeeeb0"; # background color gradient stop
248 | } elsif ($bgcolors eq "blue") {
249 | $bgcolor1 = "#eeeeee"; $bgcolor2 = "#e0e0ff";
250 | } elsif ($bgcolors eq "green") {
251 | $bgcolor1 = "#eef2ee"; $bgcolor2 = "#e0ffe0";
252 | } elsif ($bgcolors eq "grey") {
253 | $bgcolor1 = "#f8f8f8"; $bgcolor2 = "#e8e8e8";
254 | } elsif ($bgcolors =~ /^#......$/) {
255 | $bgcolor1 = $bgcolor2 = $bgcolors;
256 | } else {
257 | die "Unrecognized bgcolor option \"$bgcolors\""
258 | }
259 |
260 | # SVG functions
261 | { package SVG;
262 | sub new {
263 | my $class = shift;
264 | my $self = {};
265 | bless ($self, $class);
266 | return $self;
267 | }
268 |
269 | sub header {
270 | my ($self, $w, $h) = @_;
271 | my $enc_attr = '';
272 | if (defined $encoding) {
273 | $enc_attr = qq{ encoding="$encoding"};
274 | }
275 | $self->{svg} .= <