├── 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 [![CircleCI](https://circleci.com/gh/segmentio/pprof-server.svg?style=shield)](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 | ![Screenshot](./images/pprof-server.png) 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 | 19 | 20 |
0block 21 | 22 |
33goroutine 23 | 24 |
3heap 25 | 26 |
0mutex 27 | 28 |
11threadcreate 29 | 30 |
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 | 23 | 24 |
0block 25 | 26 |
33goroutine 27 | 28 |
3heap 29 | 30 |
0mutex 31 | 32 |
11threadcreate 33 | 34 |
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 | 60 | {{- if supportsFlamegraph .Params}} 61 | 62 | 63 | {{- else}} 64 | 65 | {{- end}} 66 | 67 | {{ end }} 68 |
{{ .Name }}🌲🔥📜
69 | 70 | 71 | `) 72 | 73 | var listNodes = htmlTemplate("listNodes", ` 74 | 75 | 76 | {{ .Name }} 77 | 78 | 79 | 80 |

81 | << Services 82 |

83 | 84 | 86 | 87 | 88 | 89 | {{ range .Nodes }} 90 | 91 | 92 | 93 | {{ end }} 94 |
85 |
Nodes
{{ .Endpoint }}
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} .= < 277 | 278 | 279 | 280 | 281 | SVG 282 | } 283 | 284 | sub include { 285 | my ($self, $content) = @_; 286 | $self->{svg} .= $content; 287 | } 288 | 289 | sub colorAllocate { 290 | my ($self, $r, $g, $b) = @_; 291 | return "rgb($r,$g,$b)"; 292 | } 293 | 294 | sub group_start { 295 | my ($self, $attr) = @_; 296 | 297 | my @g_attr = map { 298 | exists $attr->{$_} ? sprintf(qq/$_="%s"/, $attr->{$_}) : () 299 | } qw(id class); 300 | push @g_attr, $attr->{g_extra} if $attr->{g_extra}; 301 | if ($attr->{href}) { 302 | my @a_attr; 303 | push @a_attr, sprintf qq/xlink:href="%s"/, $attr->{href} if $attr->{href}; 304 | # default target=_top else links will open within SVG 305 | push @a_attr, sprintf qq/target="%s"/, $attr->{target} || "_top"; 306 | push @a_attr, $attr->{a_extra} if $attr->{a_extra}; 307 | $self->{svg} .= sprintf qq/\n/, join(' ', (@a_attr, @g_attr)); 308 | } else { 309 | $self->{svg} .= sprintf qq/\n/, join(' ', @g_attr); 310 | } 311 | 312 | $self->{svg} .= sprintf qq/%s<\/title>/, $attr->{title} 313 | if $attr->{title}; # should be first element within g container 314 | } 315 | 316 | sub group_end { 317 | my ($self, $attr) = @_; 318 | $self->{svg} .= $attr->{href} ? qq/<\/a>\n/ : qq/<\/g>\n/; 319 | } 320 | 321 | sub filledRectangle { 322 | my ($self, $x1, $y1, $x2, $y2, $fill, $extra) = @_; 323 | $x1 = sprintf "%0.1f", $x1; 324 | $x2 = sprintf "%0.1f", $x2; 325 | my $w = sprintf "%0.1f", $x2 - $x1; 326 | my $h = sprintf "%0.1f", $y2 - $y1; 327 | $extra = defined $extra ? $extra : ""; 328 | $self->{svg} .= qq/\n/; 329 | } 330 | 331 | sub stringTTF { 332 | my ($self, $id, $x, $y, $str, $extra) = @_; 333 | $x = sprintf "%0.2f", $x; 334 | $id = defined $id ? qq/id="$id"/ : ""; 335 | $extra ||= ""; 336 | $self->{svg} .= qq/$str<\/text>\n/; 337 | } 338 | 339 | sub svg { 340 | my $self = shift; 341 | return "$self->{svg}\n"; 342 | } 343 | 1; 344 | } 345 | 346 | sub namehash { 347 | # Generate a vector hash for the name string, weighting early over 348 | # later characters. We want to pick the same colors for function 349 | # names across different flame graphs. 350 | my $name = shift; 351 | my $vector = 0; 352 | my $weight = 1; 353 | my $max = 1; 354 | my $mod = 10; 355 | # if module name present, trunc to 1st char 356 | $name =~ s/.(.*?)`//; 357 | foreach my $c (split //, $name) { 358 | my $i = (ord $c) % $mod; 359 | $vector += ($i / ($mod++ - 1)) * $weight; 360 | $max += 1 * $weight; 361 | $weight *= 0.70; 362 | last if $mod > 12; 363 | } 364 | return (1 - $vector / $max) 365 | } 366 | 367 | sub color { 368 | my ($type, $hash, $name) = @_; 369 | my ($v1, $v2, $v3); 370 | 371 | if ($hash) { 372 | $v1 = namehash($name); 373 | $v2 = $v3 = namehash(scalar reverse $name); 374 | } else { 375 | $v1 = rand(1); 376 | $v2 = rand(1); 377 | $v3 = rand(1); 378 | } 379 | 380 | # theme palettes 381 | if (defined $type and $type eq "hot") { 382 | my $r = 205 + int(50 * $v3); 383 | my $g = 0 + int(230 * $v1); 384 | my $b = 0 + int(55 * $v2); 385 | return "rgb($r,$g,$b)"; 386 | } 387 | if (defined $type and $type eq "mem") { 388 | my $r = 0; 389 | my $g = 190 + int(50 * $v2); 390 | my $b = 0 + int(210 * $v1); 391 | return "rgb($r,$g,$b)"; 392 | } 393 | if (defined $type and $type eq "io") { 394 | my $r = 80 + int(60 * $v1); 395 | my $g = $r; 396 | my $b = 190 + int(55 * $v2); 397 | return "rgb($r,$g,$b)"; 398 | } 399 | 400 | # multi palettes 401 | if (defined $type and $type eq "java") { 402 | # Handle both annotations (_[j], _[i], ...; which are 403 | # accurate), as well as input that lacks any annotations, as 404 | # best as possible. Without annotations, we get a little hacky 405 | # and match on java|org|com, etc. 406 | if ($name =~ m:_\[j\]$:) { # jit annotation 407 | $type = "green"; 408 | } elsif ($name =~ m:_\[i\]$:) { # inline annotation 409 | $type = "aqua"; 410 | } elsif ($name =~ m:^L?(java|javax|jdk|net|org|com|io|sun)/:) { # Java 411 | $type = "green"; 412 | } elsif ($name =~ /:::/) { # Java, typical perf-map-agent method separator 413 | $type = "green"; 414 | } elsif ($name =~ /::/) { # C++ 415 | $type = "yellow"; 416 | } elsif ($name =~ m:_\[k\]$:) { # kernel annotation 417 | $type = "orange"; 418 | } elsif ($name =~ /::/) { # C++ 419 | $type = "yellow"; 420 | } else { # system 421 | $type = "red"; 422 | } 423 | # fall-through to color palettes 424 | } 425 | if (defined $type and $type eq "perl") { 426 | if ($name =~ /::/) { # C++ 427 | $type = "yellow"; 428 | } elsif ($name =~ m:Perl: or $name =~ m:\.pl:) { # Perl 429 | $type = "green"; 430 | } elsif ($name =~ m:_\[k\]$:) { # kernel 431 | $type = "orange"; 432 | } else { # system 433 | $type = "red"; 434 | } 435 | # fall-through to color palettes 436 | } 437 | if (defined $type and $type eq "js") { 438 | # Handle both annotations (_[j], _[i], ...; which are 439 | # accurate), as well as input that lacks any annotations, as 440 | # best as possible. Without annotations, we get a little hacky, 441 | # and match on a "/" with a ".js", etc. 442 | if ($name =~ m:_\[j\]$:) { # jit annotation 443 | if ($name =~ m:/:) { 444 | $type = "green"; # source 445 | } else { 446 | $type = "aqua"; # builtin 447 | } 448 | } elsif ($name =~ /::/) { # C++ 449 | $type = "yellow"; 450 | } elsif ($name =~ m:/.*\.js:) { # JavaScript (match "/" in path) 451 | $type = "green"; 452 | } elsif ($name =~ m/:/) { # JavaScript (match ":" in builtin) 453 | $type = "aqua"; 454 | } elsif ($name =~ m/^ $/) { # Missing symbol 455 | $type = "green"; 456 | } elsif ($name =~ m:_\[k\]:) { # kernel 457 | $type = "orange"; 458 | } else { # system 459 | $type = "red"; 460 | } 461 | # fall-through to color palettes 462 | } 463 | if (defined $type and $type eq "wakeup") { 464 | $type = "aqua"; 465 | # fall-through to color palettes 466 | } 467 | if (defined $type and $type eq "chain") { 468 | if ($name =~ m:_\[w\]:) { # waker 469 | $type = "aqua" 470 | } else { # off-CPU 471 | $type = "blue"; 472 | } 473 | # fall-through to color palettes 474 | } 475 | 476 | # color palettes 477 | if (defined $type and $type eq "red") { 478 | my $r = 200 + int(55 * $v1); 479 | my $x = 50 + int(80 * $v1); 480 | return "rgb($r,$x,$x)"; 481 | } 482 | if (defined $type and $type eq "green") { 483 | my $g = 200 + int(55 * $v1); 484 | my $x = 50 + int(60 * $v1); 485 | return "rgb($x,$g,$x)"; 486 | } 487 | if (defined $type and $type eq "blue") { 488 | my $b = 205 + int(50 * $v1); 489 | my $x = 80 + int(60 * $v1); 490 | return "rgb($x,$x,$b)"; 491 | } 492 | if (defined $type and $type eq "yellow") { 493 | my $x = 175 + int(55 * $v1); 494 | my $b = 50 + int(20 * $v1); 495 | return "rgb($x,$x,$b)"; 496 | } 497 | if (defined $type and $type eq "purple") { 498 | my $x = 190 + int(65 * $v1); 499 | my $g = 80 + int(60 * $v1); 500 | return "rgb($x,$g,$x)"; 501 | } 502 | if (defined $type and $type eq "aqua") { 503 | my $r = 50 + int(60 * $v1); 504 | my $g = 165 + int(55 * $v1); 505 | my $b = 165 + int(55 * $v1); 506 | return "rgb($r,$g,$b)"; 507 | } 508 | if (defined $type and $type eq "orange") { 509 | my $r = 190 + int(65 * $v1); 510 | my $g = 90 + int(65 * $v1); 511 | return "rgb($r,$g,0)"; 512 | } 513 | 514 | return "rgb(0,0,0)"; 515 | } 516 | 517 | sub color_scale { 518 | my ($value, $max) = @_; 519 | my ($r, $g, $b) = (255, 255, 255); 520 | $value = -$value if $negate; 521 | if ($value > 0) { 522 | $g = $b = int(210 * ($max - $value) / $max); 523 | } elsif ($value < 0) { 524 | $r = $g = int(210 * ($max + $value) / $max); 525 | } 526 | return "rgb($r,$g,$b)"; 527 | } 528 | 529 | sub color_map { 530 | my ($colors, $func) = @_; 531 | if (exists $palette_map{$func}) { 532 | return $palette_map{$func}; 533 | } else { 534 | $palette_map{$func} = color($colors, $hash, $func); 535 | return $palette_map{$func}; 536 | } 537 | } 538 | 539 | sub write_palette { 540 | open(FILE, ">$pal_file"); 541 | foreach my $key (sort keys %palette_map) { 542 | print FILE $key."->".$palette_map{$key}."\n"; 543 | } 544 | close(FILE); 545 | } 546 | 547 | sub read_palette { 548 | if (-e $pal_file) { 549 | open(FILE, $pal_file) or die "can't open file $pal_file: $!"; 550 | while ( my $line = ) { 551 | chomp($line); 552 | (my $key, my $value) = split("->",$line); 553 | $palette_map{$key}=$value; 554 | } 555 | close(FILE) 556 | } 557 | } 558 | 559 | my %Node; # Hash of merged frame data 560 | my %Tmp; 561 | 562 | # flow() merges two stacks, storing the merged frames and value data in %Node. 563 | sub flow { 564 | my ($last, $this, $v, $d) = @_; 565 | 566 | my $len_a = @$last - 1; 567 | my $len_b = @$this - 1; 568 | 569 | my $i = 0; 570 | my $len_same; 571 | for (; $i <= $len_a; $i++) { 572 | last if $i > $len_b; 573 | last if $last->[$i] ne $this->[$i]; 574 | } 575 | $len_same = $i; 576 | 577 | for ($i = $len_a; $i >= $len_same; $i--) { 578 | my $k = "$last->[$i];$i"; 579 | # a unique ID is constructed from "func;depth;etime"; 580 | # func-depth isn't unique, it may be repeated later. 581 | $Node{"$k;$v"}->{stime} = delete $Tmp{$k}->{stime}; 582 | if (defined $Tmp{$k}->{delta}) { 583 | $Node{"$k;$v"}->{delta} = delete $Tmp{$k}->{delta}; 584 | } 585 | delete $Tmp{$k}; 586 | } 587 | 588 | for ($i = $len_same; $i <= $len_b; $i++) { 589 | my $k = "$this->[$i];$i"; 590 | $Tmp{$k}->{stime} = $v; 591 | if (defined $d) { 592 | $Tmp{$k}->{delta} += $i == $len_b ? $d : 0; 593 | } 594 | } 595 | 596 | return $this; 597 | } 598 | 599 | # parse input 600 | my @Data; 601 | my @SortedData; 602 | my $last = []; 603 | my $time = 0; 604 | my $delta = undef; 605 | my $ignored = 0; 606 | my $line; 607 | my $maxdelta = 1; 608 | 609 | # reverse if needed 610 | foreach (<>) { 611 | chomp; 612 | $line = $_; 613 | if ($stackreverse) { 614 | # there may be an extra samples column for differentials 615 | # XXX todo: redo these REs as one. It's repeated below. 616 | my($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 617 | my $samples2 = undef; 618 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 619 | $samples2 = $samples; 620 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 621 | unshift @Data, join(";", reverse split(";", $stack)) . " $samples $samples2"; 622 | } else { 623 | unshift @Data, join(";", reverse split(";", $stack)) . " $samples"; 624 | } 625 | } else { 626 | unshift @Data, $line; 627 | } 628 | } 629 | 630 | if ($flamechart) { 631 | # In flame chart mode, just reverse the data so time moves from left to right. 632 | @SortedData = reverse @Data; 633 | } else { 634 | @SortedData = sort @Data; 635 | } 636 | 637 | # process and merge frames 638 | foreach (@SortedData) { 639 | chomp; 640 | # process: folded_stack count 641 | # eg: func_a;func_b;func_c 31 642 | my ($stack, $samples) = (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 643 | unless (defined $samples and defined $stack) { 644 | ++$ignored; 645 | next; 646 | } 647 | 648 | # there may be an extra samples column for differentials: 649 | my $samples2 = undef; 650 | if ($stack =~ /^(.*)\s+?(\d+(?:\.\d*)?)$/) { 651 | $samples2 = $samples; 652 | ($stack, $samples) = $stack =~ (/^(.*)\s+?(\d+(?:\.\d*)?)$/); 653 | } 654 | $delta = undef; 655 | if (defined $samples2) { 656 | $delta = $samples2 - $samples; 657 | $maxdelta = abs($delta) if abs($delta) > $maxdelta; 658 | } 659 | 660 | # for chain graphs, annotate waker frames with "_[w]", for later 661 | # coloring. This is a hack, but has a precedent ("_[k]" from perf). 662 | if ($colors eq "chain") { 663 | my @parts = split ";--;", $stack; 664 | my @newparts = (); 665 | $stack = shift @parts; 666 | $stack .= ";--;"; 667 | foreach my $part (@parts) { 668 | $part =~ s/;/_[w];/g; 669 | $part .= "_[w]"; 670 | push @newparts, $part; 671 | } 672 | $stack .= join ";--;", @parts; 673 | } 674 | 675 | # merge frames and populate %Node: 676 | $last = flow($last, [ '', split ";", $stack ], $time, $delta); 677 | 678 | if (defined $samples2) { 679 | $time += $samples2; 680 | } else { 681 | $time += $samples; 682 | } 683 | } 684 | flow($last, [], $time, $delta); 685 | 686 | warn "Ignored $ignored lines with invalid format\n" if $ignored; 687 | unless ($time) { 688 | warn "ERROR: No stack counts found\n"; 689 | my $im = SVG->new(); 690 | # emit an error message SVG, for tools automating flamegraph use 691 | my $imageheight = $fontsize * 5; 692 | $im->header($imagewidth, $imageheight); 693 | $im->stringTTF(undef, int($imagewidth / 2), $fontsize * 2, 694 | "ERROR: No valid input provided to flamegraph.pl."); 695 | print $im->svg; 696 | exit 2; 697 | } 698 | if ($timemax and $timemax < $time) { 699 | warn "Specified --total $timemax is less than actual total $time, so ignored\n" 700 | if $timemax/$time > 0.02; # only warn is significant (e.g., not rounding etc) 701 | undef $timemax; 702 | } 703 | $timemax ||= $time; 704 | 705 | my $widthpertime = ($imagewidth - 2 * $xpad) / $timemax; 706 | my $minwidth_time = $minwidth / $widthpertime; 707 | 708 | # prune blocks that are too narrow and determine max depth 709 | while (my ($id, $node) = each %Node) { 710 | my ($func, $depth, $etime) = split ";", $id; 711 | my $stime = $node->{stime}; 712 | die "missing start for $id" if not defined $stime; 713 | 714 | if (($etime-$stime) < $minwidth_time) { 715 | delete $Node{$id}; 716 | next; 717 | } 718 | $depthmax = $depth if $depth > $depthmax; 719 | } 720 | 721 | # draw canvas, and embed interactive JavaScript program 722 | my $imageheight = (($depthmax + 1) * $frameheight) + $ypad1 + $ypad2; 723 | $imageheight += $ypad3 if $subtitletext ne ""; 724 | my $titlesize = $fontsize + 5; 725 | my $im = SVG->new(); 726 | my ($black, $vdgrey, $dgrey) = ( 727 | $im->colorAllocate(0, 0, 0), 728 | $im->colorAllocate(160, 160, 160), 729 | $im->colorAllocate(200, 200, 200), 730 | ); 731 | $im->header($imagewidth, $imageheight); 732 | my $inc = < 734 | 735 | 736 | 737 | 738 | 739 | 750 | 1142 | INC 1143 | $im->include($inc); 1144 | $im->filledRectangle(0, 0, $imagewidth, $imageheight, 'url(#background)'); 1145 | $im->stringTTF("title", int($imagewidth / 2), $fontsize * 2, $titletext); 1146 | $im->stringTTF("subtitle", int($imagewidth / 2), $fontsize * 4, $subtitletext) if $subtitletext ne ""; 1147 | $im->stringTTF("details", $xpad, $imageheight - ($ypad2 / 2), " "); 1148 | $im->stringTTF("unzoom", $xpad, $fontsize * 2, "Reset Zoom", 'class="hide"'); 1149 | $im->stringTTF("search", $imagewidth - $xpad - 100, $fontsize * 2, "Search"); 1150 | $im->stringTTF("ignorecase", $imagewidth - $xpad - 16, $fontsize * 2, "ic"); 1151 | $im->stringTTF("matched", $imagewidth - $xpad - 100, $imageheight - ($ypad2 / 2), " "); 1152 | 1153 | if ($palette) { 1154 | read_palette(); 1155 | } 1156 | 1157 | # draw frames 1158 | $im->group_start({id => "frames"}); 1159 | while (my ($id, $node) = each %Node) { 1160 | my ($func, $depth, $etime) = split ";", $id; 1161 | my $stime = $node->{stime}; 1162 | my $delta = $node->{delta}; 1163 | 1164 | $etime = $timemax if $func eq "" and $depth == 0; 1165 | 1166 | my $x1 = $xpad + $stime * $widthpertime; 1167 | my $x2 = $xpad + $etime * $widthpertime; 1168 | my ($y1, $y2); 1169 | unless ($inverted) { 1170 | $y1 = $imageheight - $ypad2 - ($depth + 1) * $frameheight + $framepad; 1171 | $y2 = $imageheight - $ypad2 - $depth * $frameheight; 1172 | } else { 1173 | $y1 = $ypad1 + $depth * $frameheight; 1174 | $y2 = $ypad1 + ($depth + 1) * $frameheight - $framepad; 1175 | } 1176 | 1177 | my $samples = sprintf "%.0f", ($etime - $stime) * $factor; 1178 | (my $samples_txt = $samples) # add commas per perlfaq5 1179 | =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; 1180 | 1181 | my $info; 1182 | if ($func eq "" and $depth == 0) { 1183 | $info = "all ($samples_txt $countname, 100%)"; 1184 | } else { 1185 | my $pct = sprintf "%.2f", ((100 * $samples) / ($timemax * $factor)); 1186 | my $escaped_func = $func; 1187 | # clean up SVG breaking characters: 1188 | $escaped_func =~ s/&/&/g; 1189 | $escaped_func =~ s//>/g; 1191 | $escaped_func =~ s/"/"/g; 1192 | $escaped_func =~ s/_\[[kwij]\]$//; # strip any annotation 1193 | unless (defined $delta) { 1194 | $info = "$escaped_func ($samples_txt $countname, $pct%)"; 1195 | } else { 1196 | my $d = $negate ? -$delta : $delta; 1197 | my $deltapct = sprintf "%.2f", ((100 * $d) / ($timemax * $factor)); 1198 | $deltapct = $d > 0 ? "+$deltapct" : $deltapct; 1199 | $info = "$escaped_func ($samples_txt $countname, $pct%; $deltapct%)"; 1200 | } 1201 | } 1202 | 1203 | my $nameattr = { %{ $nameattr{$func}||{} } }; # shallow clone 1204 | $nameattr->{title} ||= $info; 1205 | $im->group_start($nameattr); 1206 | 1207 | my $color; 1208 | if ($func eq "--") { 1209 | $color = $vdgrey; 1210 | } elsif ($func eq "-") { 1211 | $color = $dgrey; 1212 | } elsif (defined $delta) { 1213 | $color = color_scale($delta, $maxdelta); 1214 | } elsif ($palette) { 1215 | $color = color_map($colors, $func); 1216 | } else { 1217 | $color = color($colors, $hash, $func); 1218 | } 1219 | $im->filledRectangle($x1, $y1, $x2, $y2, $color, 'rx="2" ry="2"'); 1220 | 1221 | my $chars = int( ($x2 - $x1) / ($fontsize * $fontwidth)); 1222 | my $text = ""; 1223 | if ($chars >= 3) { # room for one char plus two dots 1224 | $func =~ s/_\[[kwij]\]$//; # strip any annotation 1225 | $text = substr $func, 0, $chars; 1226 | substr($text, -2, 2) = ".." if $chars < length $func; 1227 | $text =~ s/&/&/g; 1228 | $text =~ s//>/g; 1230 | } 1231 | $im->stringTTF(undef, $x1 + 3, 3 + ($y1 + $y2) / 2, $text); 1232 | 1233 | $im->group_end($nameattr); 1234 | } 1235 | $im->group_end(); 1236 | 1237 | print $im->svg; 1238 | 1239 | if ($palette) { 1240 | write_palette(); 1241 | } 1242 | 1243 | # vim: ts=8 sts=8 sw=8 noexpandtab 1244 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 13 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 14 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 15 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 16 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 17 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 18 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 19 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 20 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 21 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 22 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 23 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 24 | github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 25 | github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= 26 | github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= 27 | github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= 28 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= 29 | github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 30 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 31 | github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 32 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= 33 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 34 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 35 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 36 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 37 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 38 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 39 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 40 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 41 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 42 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 48 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 49 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 50 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 51 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 52 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 54 | github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= 55 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 56 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 57 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 58 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 59 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 60 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 61 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 62 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 63 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 64 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 65 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 66 | github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= 67 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 68 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 69 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 70 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 71 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 72 | github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 73 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 74 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 75 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 76 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 77 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 78 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 80 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 81 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 83 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 84 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 85 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 86 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 87 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 88 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 89 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 90 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 91 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 92 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 93 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 94 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 95 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 96 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 97 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 98 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 99 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 100 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 101 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 102 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 103 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 104 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 105 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 106 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 108 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 109 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 110 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 111 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 112 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 113 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 114 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 115 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 116 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 117 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 118 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 119 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 120 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 121 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 122 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 123 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 124 | github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= 125 | github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= 126 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 127 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 128 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 129 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 130 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 131 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 132 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 133 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 134 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= 135 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 136 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 137 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 138 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 139 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 140 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 141 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 142 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 143 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 144 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 145 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 146 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 147 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 148 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 149 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 150 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 151 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 152 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 153 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 154 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 155 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 156 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 157 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 158 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 159 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 160 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 161 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 162 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 163 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 164 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 165 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 166 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 167 | github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= 168 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 169 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 170 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 171 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 172 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 173 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 174 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 175 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 176 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 177 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 178 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 179 | github.com/segmentio/conf v0.0.0-20170612230246-5d701c9ec529 h1:zoi1x3Q4/F9vmmoULTZTFYvUq4LORmllo8DoZ0Qc/Og= 180 | github.com/segmentio/conf v0.0.0-20170612230246-5d701c9ec529/go.mod h1:y0VyxYAlU2slxCjm7XX7tGKFlN39bwHCZrbOpCcLsr8= 181 | github.com/segmentio/consul-go v0.0.0-20170912072050-42ff3637e5db h1:M9I3iJw5iYtFBtHIwvM6XBz37iMspid97uROcgvM9z8= 182 | github.com/segmentio/consul-go v0.0.0-20170912072050-42ff3637e5db/go.mod h1:GWqKnaNTz1/4Bmvnrw6iGv2eAmLgQf6bSasrCHopKL0= 183 | github.com/segmentio/events v2.0.1+incompatible h1:5Cdtdv9FX2XWmf+CQVWyEuAJ+pEi4Ec8AkT8wCYurfQ= 184 | github.com/segmentio/events v2.0.1+incompatible/go.mod h1:npQUbmKYO33tlRpaQNZjgD2mXv0fb2hbOH0CNVs6g2Y= 185 | github.com/segmentio/fasthash v1.0.0 h1:7D0T9cPBdXpSUIH+wa8E6PuiccPrg5UGnCGSeQSR7cQ= 186 | github.com/segmentio/fasthash v1.0.0/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= 187 | github.com/segmentio/objconv v0.0.0-20170810202704-5dca7cbec799 h1:xJJ7H+BnUgX9mw+wrxcSdhPgCgscB4GXqeb8RwIS0tE= 188 | github.com/segmentio/objconv v0.0.0-20170810202704-5dca7cbec799/go.mod h1:l6IU9rGW/wpM+MUyfJLDCoJ+XXAv6wbpkqej46ESQ6g= 189 | github.com/segmentio/stats v0.0.0-20170908015358-6da51b6c447b h1:bGOLkFoKVeCPgAhLCbiGvAjsx7CbxgjD/orzbq+6fg4= 190 | github.com/segmentio/stats v0.0.0-20170908015358-6da51b6c447b/go.mod h1:ZkGKMkt6GVRIsV5Biy4HotVqonMWEsr+uMtOD2NBDeU= 191 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 192 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 193 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 194 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 195 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 196 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 197 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 198 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 199 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 200 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 201 | github.com/uber/go-torch v0.0.0-20170825044957-ddbe52cdc30e h1:tDAQndJJR2oBQJuxkIhCTf4aERHCDRc1yovxprJOlN4= 202 | github.com/uber/go-torch v0.0.0-20170825044957-ddbe52cdc30e/go.mod h1:uuMPbyv6WJykZcarrIuJiTjfSGC997/jnfHyyeeG2Jo= 203 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 204 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 205 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 206 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 207 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 208 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 209 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 210 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 211 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 212 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 213 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= 214 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 215 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 216 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 217 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 218 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 219 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 220 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 221 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 222 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 223 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 224 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 225 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 226 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 227 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 228 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 229 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 230 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 231 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 232 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 233 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 234 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 235 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 236 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 237 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 238 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 239 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 240 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 241 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 242 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 243 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 244 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 245 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 246 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 247 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 248 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 249 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 250 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 251 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 252 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 253 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 254 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 255 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 256 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 257 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 258 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 259 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 260 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 261 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 262 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 263 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 264 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 265 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 266 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 267 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 268 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 269 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 270 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 271 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 272 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 273 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 274 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 275 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 278 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 279 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 280 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 281 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 282 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 302 | golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= 304 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 305 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 306 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 307 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 308 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 309 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 310 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= 311 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 312 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 313 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 314 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 315 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= 316 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 317 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 318 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 319 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 320 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 321 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 322 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 323 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 324 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 325 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 326 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 327 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 328 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 329 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 330 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 331 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 332 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 333 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 334 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 335 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 336 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 337 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 338 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 339 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 340 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 341 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 342 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 343 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 344 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 345 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 346 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 347 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 348 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 349 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 350 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 351 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 352 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 353 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 354 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 355 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 356 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 357 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 358 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 359 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 360 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 361 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 362 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 363 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 364 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 365 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 366 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 367 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 368 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 369 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 370 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 371 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 372 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 373 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 374 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 375 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 376 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 377 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 378 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 379 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 380 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 381 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 382 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 383 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 384 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 385 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 386 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 387 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 388 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 389 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 390 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 391 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 392 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 393 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 394 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 395 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 396 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 397 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 398 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 399 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 400 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 401 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 402 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 403 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 404 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 405 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 406 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 407 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 408 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 409 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 410 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 411 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 412 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 413 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 414 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 415 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 416 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 417 | gopkg.in/validator.v2 v2.0.0-20170814132753-460c83432a98 h1:QLe0XLNdJd1xb0trLuWBM9ysdjdi6/uXU4Oypbh72m8= 418 | gopkg.in/validator.v2 v2.0.0-20170814132753-460c83432a98/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= 419 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 420 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 421 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 422 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 423 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 424 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 425 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 426 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 427 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 428 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 429 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 430 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 431 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 432 | k8s.io/api v0.20.1 h1:ud1c3W3YNzGd6ABJlbFfKXBKXO+1KdGfcgGGNgFR03E= 433 | k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= 434 | k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ= 435 | k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= 436 | k8s.io/client-go v0.20.1 h1:Qquik0xNFbK9aUG92pxHYsyfea5/RPO9o9bSywNor+M= 437 | k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= 438 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 439 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 440 | k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= 441 | k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 442 | k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= 443 | k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= 444 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= 445 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 446 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 447 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 448 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 449 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= 450 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 451 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 452 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 453 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 454 | --------------------------------------------------------------------------------