├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── bin └── lint ├── go.mod ├── go.sum ├── main.go ├── pkg ├── cache │ ├── cache.go │ └── prom.go ├── graph │ ├── client.go │ ├── goda.go │ ├── graph.go │ └── prom.go ├── prom │ └── prom.go ├── render │ └── render.go └── web │ └── web.go └── public ├── badge.svg ├── css └── gographs.css ├── img ├── gographs.full.png └── gographs.png ├── index.html └── js └── gographs.js /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file specifies which linters golangci-lint should run. 2 | # 3 | # For descriptions of all available linters, run: 4 | # ./.golangci-lint-1.37.0 linters 5 | # or browse to: 6 | # https://github.com/golangci/golangci-lint#supported-linters 7 | 8 | run: 9 | deadline: 5m 10 | issues: 11 | exclude-use-default: false 12 | linters: 13 | enable: 14 | - asciicheck 15 | - bodyclose 16 | - deadcode 17 | - depguard 18 | - dogsled 19 | - exportloopref 20 | - goconst 21 | - goheader 22 | - goimports 23 | - golint 24 | - gomodguard 25 | - goprintffuncname 26 | - gosimple 27 | - govet 28 | - ineffassign 29 | - interfacer 30 | - maligned 31 | - misspell 32 | - nakedret 33 | - nolintlint 34 | - predeclared 35 | - rowserrcheck 36 | - scopelint 37 | - sqlclosecheck 38 | - structcheck 39 | - tparallel 40 | - typecheck 41 | - unconvert 42 | - unparam 43 | - unused 44 | - varcheck 45 | - whitespace 46 | # TODO: enable more linters! 47 | # - cyclop 48 | # - dupl 49 | # - durationcheck 50 | # - errorlint 51 | # - exhaustive 52 | # - exhaustivestruct 53 | # - forbidigo 54 | # - funlen 55 | # - gci 56 | # - gochecknoglobals 57 | # - gochecknoinits 58 | # - gocognit 59 | # - gocritic 60 | # - gocyclo 61 | # - godot 62 | # - godox 63 | # - goerr113 64 | # - gofmt 65 | # - gofumpt 66 | # - gomnd 67 | # - gosec 68 | # - ifshort 69 | # - lll 70 | # - makezero 71 | # - nestif 72 | # - nlreturn 73 | # - noctx 74 | # - prealloc 75 | # - revive 76 | # - stylecheck 77 | # - testpackage 78 | # - thelper 79 | # - wrapcheck 80 | # - wsl 81 | disable: 82 | - errcheck 83 | - staticcheck 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Andrew Seigner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gographs 2 | 3 | [](https://gographs.io/repo/github.com/siggy/gographs?cluster=true) 4 | [](https://goreportcard.com/report/github.com/siggy/gographs) 5 | 6 | [gographs](https://gographs.io) renders dependency graphs for Go packages. 7 | 8 | [](https://gographs.io) 9 | 10 | ## Badge Markdown 11 | 12 | ```md 13 | [](https://gographs.io/repo/GO_REPO?[cluster=true|false]) 14 | ``` 15 | 16 | Example 17 | ```md 18 | [](https://gographs.io/repo/github.com/siggy/gographs?cluster=true) 19 | ``` 20 | 21 | ## HTTP Endpoints 22 | 23 | | Endpoint | Desc | 24 | | --- | --- | 25 | | [/](https://gographs.io) | Defaults to rendering this Go repo. | 26 | | [/repo/GO_REPO?cluster=false\|true](https://gographs.io/repo/github.com/siggy/gographs?cluster=true) | Permalink to a repo. Use `POST` to refresh. | 27 | | [/graph/GO_REPO.svg?cluster=false\|true](https://gographs.io/graph/github.com/siggy/gographs.svg?cluster=true) | SVG direct link. Use `POST` to refresh. | 28 | | [/graph/GO_REPO.dot?cluster=false\|true](https://gographs.io/graph/github.com/siggy/gographs.dot?cluster=true) | GraphViz DOT direct link. Use `POST` to refresh. | 29 | | [/svg?url=SVG_URL](https://gographs.io/svg?url=https://upload.wikimedia.org/wikipedia/commons/0/05/Go_Logo_Blue.svg) | Permalink to view an arbitrary SVG URL. | 30 | 31 | ## Local dev 32 | 33 | ### First-time setup 34 | 35 | ```bash 36 | go install github.com/loov/goda@v0.4.3 37 | brew install dot # or equivalent 38 | brew install redis # or equivalent 39 | redis-server /usr/local/etc/redis.conf 40 | ``` 41 | 42 | ### Boot server 43 | 44 | ```bash 45 | go run main.go --log-level debug 46 | ``` 47 | 48 | Browse to http://localhost:8888 49 | 50 | ## Lint check 51 | 52 | ```bash 53 | bin/lint 54 | ``` 55 | 56 | ## Credits 57 | 58 | This tool is built using many open source packages, but two in particular 59 | deserve special mention, as this site is essentially a mashup of them: 60 | 61 | - [goda](https://github.com/loov/goda) 62 | - [SVGPan](https://github.com/bumbu/svg-pan-zoom) 63 | 64 | [`pkg/repo`](./pkg/repo) is based on [Go Report Card](https://github.com/gojp/goreportcard) 65 | 66 | [](https://gographs.io/repo/github.com/siggy/gographs?cluster=true) courtesy of [shields.io](https://shields.io/) 67 | -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # copied from https://github.com/BuoyantIO/linkerd-buoyant/blob/e1212a9297672033840547ac197519e3e959d130/bin/lint 4 | 5 | set -eu 6 | 7 | lintversion=1.37.0 8 | 9 | cd "$(pwd -P)" 10 | 11 | bindir=$( cd "${0%/*}" && pwd ) 12 | rootdir=$( cd "$bindir"/.. && pwd ) 13 | targetbin=$rootdir/target/bin 14 | 15 | cd "$rootdir" 16 | 17 | exe= 18 | if [ "$(uname -s)" = Darwin ]; then 19 | # Darwin's uname doesn't support the -o flag so we short circuit here. 20 | :; 21 | elif [ "$(uname -o)" = Msys ]; then 22 | exe=.exe 23 | fi 24 | 25 | lintbin=$targetbin/.golangci-lint-$lintversion$exe 26 | 27 | if [ ! -f "$lintbin" ]; then 28 | mkdir -p "$targetbin" 29 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/v$lintversion/install.sh | sh -s -- -b . v$lintversion 30 | mv ./golangci-lint$exe "$lintbin" 31 | fi 32 | 33 | "$lintbin" run "$@" 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/siggy/gographs 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.4 7 | github.com/gorilla/mux v1.8.0 8 | github.com/prometheus/client_golang v1.12.0 9 | github.com/sirupsen/logrus v1.8.1 10 | golang.org/x/tools v0.1.8 11 | ) 12 | 13 | require ( 14 | github.com/beorn7/perks v1.0.1 // indirect 15 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 16 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 17 | github.com/golang/protobuf v1.5.2 // indirect 18 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 19 | github.com/prometheus/client_model v0.2.0 // indirect 20 | github.com/prometheus/common v0.32.1 // indirect 21 | github.com/prometheus/procfs v0.7.3 // indirect 22 | golang.org/x/sys v0.1.0 // indirect 23 | google.golang.org/protobuf v1.27.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /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 v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 37 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 38 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 39 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 40 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 41 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 42 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 43 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 44 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 45 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 46 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 47 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 48 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 49 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 50 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 51 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 52 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 53 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 54 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 56 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 58 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 59 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 60 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 61 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 62 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 63 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 64 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 65 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 66 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 67 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 68 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 69 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 70 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 71 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 72 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 73 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 74 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 75 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 76 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 77 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 78 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 79 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 81 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 83 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 84 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 85 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 86 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 87 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 88 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 89 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 90 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 91 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 92 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 93 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 94 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 95 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 96 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 97 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 98 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 99 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 100 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 101 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 102 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 103 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 104 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 105 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 106 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 107 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 108 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 109 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 110 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 111 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 112 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 113 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 115 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 116 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 117 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 118 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 119 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 120 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 121 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 122 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 123 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 124 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 125 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 126 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 127 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 128 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 129 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 130 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 131 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 132 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 133 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 134 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 135 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 136 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 137 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 138 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 139 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 140 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 141 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 142 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 143 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 144 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 145 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 146 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 147 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 148 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 149 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 150 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 151 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 152 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 153 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 154 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 155 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 156 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 157 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 158 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 162 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 163 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 164 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 165 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 166 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 167 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 168 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 169 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 170 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 171 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 172 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 173 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 174 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 175 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 176 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 177 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 178 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 179 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 180 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 181 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 182 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 183 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 184 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 185 | github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= 186 | github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 187 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 188 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 189 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 190 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 191 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 192 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 193 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 194 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 195 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 196 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 197 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 198 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 199 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 200 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 201 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 202 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 203 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 204 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 205 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 206 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 207 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 208 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 209 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 210 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 211 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 212 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 213 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 214 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 215 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 216 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 217 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 218 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 219 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 220 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 221 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 222 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 223 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 224 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 225 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 226 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 227 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 228 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 229 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 230 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 231 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 232 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 233 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 234 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 235 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 236 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 237 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 238 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 239 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 240 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 241 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 242 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 243 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 244 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 245 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 246 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 247 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 248 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 249 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 250 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 251 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 252 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 253 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 254 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 255 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 256 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 257 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 258 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 259 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 260 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 261 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 262 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 263 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 264 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 265 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 266 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 267 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 268 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 269 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 270 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 271 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 272 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 273 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 274 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 275 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 276 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 277 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 278 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 279 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 280 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 281 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 282 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 283 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 284 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 285 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 286 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 287 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 288 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 289 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 290 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 291 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 292 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 293 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 294 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 295 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 296 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= 297 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 298 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 299 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 300 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 301 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 302 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 303 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 304 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 305 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 306 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 307 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 308 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 309 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 310 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 311 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 312 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 313 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 314 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 315 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 316 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 317 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 318 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 319 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 320 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 321 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 329 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 357 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 358 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 359 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 360 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 361 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 362 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 363 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 364 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 365 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 366 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 367 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 368 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 369 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 370 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 371 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 372 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 373 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 374 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 375 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 376 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 377 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 378 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 379 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 380 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 381 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 382 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 383 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 384 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 385 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 386 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 387 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 388 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 389 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 390 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 391 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 392 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 393 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 394 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 395 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 396 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 397 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 398 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 399 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 400 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 401 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 402 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 403 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 404 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 405 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 406 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 407 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 408 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 409 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 410 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 411 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 412 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 413 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 414 | golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= 415 | golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 416 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 417 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 418 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 419 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 420 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 421 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 422 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 423 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 424 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 425 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 426 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 427 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 428 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 429 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 430 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 431 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 432 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 433 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 434 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 435 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 436 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 437 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 438 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 439 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 440 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 441 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 442 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 443 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 444 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 445 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 446 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 447 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 448 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 449 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 450 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 451 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 452 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 453 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 454 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 455 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 456 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 457 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 458 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 459 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 460 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 461 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 462 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 463 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 464 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 465 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 466 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 467 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 468 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 469 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 470 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 471 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 472 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 473 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 474 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 475 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 476 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 477 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 478 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 479 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 480 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 481 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 482 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 483 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 484 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 485 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 486 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 487 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 488 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 489 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 490 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 491 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 492 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 493 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 494 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 495 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 496 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 497 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 498 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 499 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 500 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 501 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 502 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 503 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 504 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 505 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 506 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 507 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 508 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 509 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 510 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 511 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 512 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 513 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 514 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 515 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 516 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 517 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 518 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 519 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 520 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 521 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 522 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 523 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | 10 | _ "net/http/pprof" 11 | 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | "github.com/siggy/gographs/pkg/cache" 14 | "github.com/siggy/gographs/pkg/graph" 15 | "github.com/siggy/gographs/pkg/web" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const ( 20 | targetAll = "all" 21 | targetWeb = "web" 22 | targetGraph = "graph" 23 | ) 24 | 25 | func main() { 26 | target := flag.String("target", targetAll, fmt.Sprintf("program target, must be one of: %s, %s, %s", targetAll, targetWeb, targetGraph)) 27 | webAddr := flag.String("addr", "localhost:8888", "web address to listen on") 28 | graphAddr := flag.String("graph-addr", graph.DefaultGraphAddr, "graph address to listen on") 29 | logLevel := flag.String("log-level", log.DebugLevel.String(), "log level, must be one of: panic, fatal, error, warn, info, debug, trace") 30 | metricsAddr := flag.String("metrics-addr", "localhost:8080", "address to listen on for metrics requests") 31 | redisAddr := flag.String("redis-addr", "localhost:6379", "address to connect to redis") 32 | flag.Parse() 33 | 34 | level, err := log.ParseLevel(*logLevel) 35 | if err != nil { 36 | log.Fatalf("invalid log-level: %s", *logLevel) 37 | } 38 | log.SetLevel(level) 39 | 40 | http.Handle("/metrics", promhttp.Handler()) 41 | go func() { 42 | log.Infof("metrics server listening on %s", *metricsAddr) 43 | err = http.ListenAndServe(*metricsAddr, nil) 44 | if err != nil { 45 | log.Fatalf("failed to listen on metrics address [%s]: %s", *metricsAddr, err) 46 | } 47 | }() 48 | 49 | if *target == targetAll || *target == targetGraph { 50 | go func() { 51 | err := graph.Start(*graphAddr) 52 | if err != nil { 53 | log.Fatalf("failed to start graph server [%s]: %s", *webAddr, err) 54 | } 55 | }() 56 | } 57 | 58 | if *target == targetAll || *target == targetWeb { 59 | go func() { 60 | c, err := cache.New(*redisAddr) 61 | if err != nil { 62 | log.Fatalf("failed to initialize cache: %s", err) 63 | } 64 | 65 | graph := graph.NewClient(*graphAddr) 66 | err = web.Start(c, *webAddr, graph) 67 | if err != nil { 68 | log.Fatalf("failed to start web server [%s]: %s", *webAddr, err) 69 | } 70 | }() 71 | } 72 | 73 | signalChan := make(chan os.Signal, 1) 74 | signal.Notify(signalChan, os.Interrupt) 75 | <-signalChan 76 | } 77 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-redis/redis/v8" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Cache holds Redis client and log state. 11 | type Cache struct { 12 | client *redis.Client 13 | log *log.Entry 14 | } 15 | 16 | const ( 17 | // dot[repo+cluster] 18 | // github.com/siggy/gographs+false 19 | // => 20 | // [dot file] 21 | dotHash = "dot" 22 | 23 | // svg[repo+cluster] 24 | // github.com/siggy/gographs+false 25 | // => 26 | // [svg file] 27 | svgHash = "svg" 28 | 29 | // repo-scores[repo] 30 | // github.com/siggy/gographs 31 | // => 32 | // [numeric popularity score] 33 | repoScores = "reposcores" 34 | ) 35 | 36 | // New initializes a new cache client. 37 | func New(addr string) (*Cache, error) { 38 | client := redis.NewClient(&redis.Options{ 39 | Addr: addr, 40 | }) 41 | 42 | _, err := client.Ping(client.Context()).Result() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | log := log.WithFields( 48 | log.Fields{ 49 | "cache": addr, 50 | }, 51 | ) 52 | 53 | registerGauges(client) 54 | 55 | log.Infof("Cache initialized") 56 | 57 | return &Cache{ 58 | client: client, 59 | log: log, 60 | }, nil 61 | } 62 | 63 | // Clear deletes all cache entries relevant to a GoLang repo. 64 | func (c *Cache) Clear(repo string) error { 65 | var rerr error 66 | _, err := c.hdel(dotHash, repoKey(repo, false)) 67 | if err != nil && rerr == nil { 68 | rerr = err 69 | } 70 | _, err = c.hdel(dotHash, repoKey(repo, true)) 71 | if err != nil && rerr == nil { 72 | rerr = err 73 | } 74 | 75 | _, err = c.hdel(svgHash, repoKey(repo, false)) 76 | if err != nil && rerr == nil { 77 | rerr = err 78 | } 79 | _, err = c.hdel(svgHash, repoKey(repo, true)) 80 | if err != nil && rerr == nil { 81 | rerr = err 82 | } 83 | 84 | return rerr 85 | } 86 | 87 | // SetSVG sets an SVG for a repo. 88 | func (c *Cache) SetSVG(repo string, cluster bool, svg string) { 89 | if err := c.hset(svgHash, repoKey(repo, cluster), svg); err != nil { 90 | c.log.Errorf("SetSVG failed: %s", err) 91 | } 92 | } 93 | 94 | // GetSVG gets an SVG for a repo. 95 | func (c *Cache) GetSVG(repo string, cluster bool) (string, error) { 96 | return c.hget(svgHash, repoKey(repo, cluster)) 97 | } 98 | 99 | // SetDOT sets a DOT for a repo. 100 | func (c *Cache) SetDOT(repo string, cluster bool, dot string) { 101 | if err := c.hset(dotHash, repoKey(repo, cluster), dot); err != nil { 102 | c.log.Errorf("SetDOT failed: %s", err) 103 | } 104 | } 105 | 106 | // GetDOT gets a DOT for a repo. 107 | func (c *Cache) GetDOT(repo string, cluster bool) (string, error) { 108 | return c.hget(dotHash, repoKey(repo, cluster)) 109 | } 110 | 111 | // RepoScoreIncr increments the popularity score for a repo. 112 | func (c *Cache) RepoScoreIncr(repo string) { 113 | c.client.ZIncrBy(c.client.Context(), repoScores, 1, repo) 114 | } 115 | 116 | // RepoScores returns the top-10 most popular repos 117 | func (c *Cache) RepoScores() ([]string, error) { 118 | cmd := c.client.ZRevRangeByScore(c.client.Context(), repoScores, &redis.ZRangeBy{ 119 | Min: "0", 120 | Max: "+inf", 121 | Offset: 0, 122 | Count: 1000, 123 | }) 124 | if cmd.Err() != nil { 125 | return nil, cmd.Err() 126 | } 127 | 128 | return cmd.Val(), nil 129 | } 130 | 131 | func (c *Cache) hget(key, field string) (string, error) { 132 | c.log.Tracef("hget[%s,%s]", key, field) 133 | return c.client.HGet(c.client.Context(), key, field).Result() 134 | } 135 | 136 | func (c *Cache) hset(key, field string, value interface{}) error { 137 | c.log.Tracef("hset[%s,%s]", key, field) 138 | return c.client.HSet(c.client.Context(), key, field, value).Err() 139 | } 140 | 141 | func (c *Cache) hdel(key, field string) (int64, error) { 142 | c.log.Debugf("hdel[%s,%s]", key, field) 143 | return c.client.HDel(c.client.Context(), key, field).Result() 144 | } 145 | 146 | func repoKey(repo string, cluster bool) string { 147 | return fmt.Sprintf("%s+%t", repo, cluster) 148 | } 149 | -------------------------------------------------------------------------------- /pkg/cache/prom.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "github.com/prometheus/client_golang/prometheus/promauto" 7 | ) 8 | 9 | func registerGauges(client *redis.Client) { 10 | registerHashGauge(client, dotHash) 11 | registerHashGauge(client, svgHash) 12 | registerSetGauge(client, repoScores) 13 | } 14 | 15 | func registerHashGauge(client *redis.Client, key string) { 16 | registerGauge( 17 | func() float64 { 18 | size, _ := client.HLen(client.Context(), key).Result() 19 | return float64(size) 20 | }, 21 | key, 22 | ) 23 | } 24 | 25 | func registerSetGauge(client *redis.Client, key string) { 26 | registerGauge( 27 | func() float64 { 28 | size, _ := client.ZCount(client.Context(), key, "-inf", "+inf").Result() 29 | return float64(size) 30 | }, 31 | key, 32 | ) 33 | } 34 | 35 | func registerGauge(function func() float64, key string) { 36 | promauto.NewGaugeFunc( 37 | prometheus.GaugeOpts{ 38 | Namespace: "gographs", 39 | Subsystem: "cache", 40 | Name: "size", 41 | Help: "Size of the cache", 42 | ConstLabels: prometheus.Labels{"key": key}, 43 | }, 44 | function, 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/graph/client.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "strconv" 11 | 12 | "github.com/prometheus/client_golang/prometheus" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // Post defines the input POST body to the `/graph` endpoint. 17 | // curl --data '{"repo":"github.com/siggy/gographs","cluster":true}' -X POST [graph-addr]/graph 18 | type Post struct { 19 | Repo string `json:"repo"` 20 | Cluster bool `json:"cluster"` 21 | } 22 | 23 | // Client provides a client to the graph server. 24 | type Client struct { 25 | url string 26 | log *log.Entry 27 | } 28 | 29 | // DefaultGraphAddr defines the graph server's address when running locally. If 30 | // the caller uses something other than the default it is assumed to be TLS'd. 31 | const DefaultGraphAddr = "localhost:8889" 32 | 33 | // NewClient creates a client to the graph server. 34 | func NewClient(addr string) *Client { 35 | url := fmt.Sprintf("http://%s/graph", addr) 36 | if addr != DefaultGraphAddr { 37 | url = fmt.Sprintf("https://%s/graph", addr) 38 | } 39 | 40 | log := log.WithFields( 41 | log.Fields{ 42 | "graphclient": url, 43 | }, 44 | ) 45 | 46 | log.Infof("Graph client initialized") 47 | 48 | return &Client{url, log} 49 | } 50 | 51 | // Get takes a repo and cluster flag and returns a DOT representation of the 52 | // repo. 53 | func (c *Client) Get(repo string, cluster bool) (string, error) { 54 | body, err := json.Marshal( 55 | Post{ 56 | Repo: repo, 57 | Cluster: cluster, 58 | }, 59 | ) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | c.log.Debugf("POST Request: %s", string(body)) 65 | 66 | labels := prometheus.Labels{"repo": repo, "cluster": strconv.FormatBool(cluster)} 67 | httpRequests.With(labels).Inc() 68 | httpErrors, err := httpErrors.CurryWith(labels) 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | timer := prometheus.NewTimer(httpDuration.With(labels)) 74 | defer timer.ObserveDuration() 75 | 76 | resp, err := http.Post(c.url, "text/plain; charset=utf-8", bytes.NewBuffer(body)) 77 | if err != nil { 78 | httpErrors.WithLabelValues(err.Error()).Inc() 79 | return "", err 80 | } 81 | defer resp.Body.Close() 82 | 83 | respBody, err := ioutil.ReadAll(resp.Body) 84 | if err != nil { 85 | httpErrors.WithLabelValues(err.Error()).Inc() 86 | return "", err 87 | } 88 | 89 | debugStr := fmt.Sprintf("POST response[%d] (%d bytes): %s ", resp.StatusCode, len(respBody), string(respBody)) 90 | c.log.Debug(debugStr) 91 | 92 | if resp.StatusCode != http.StatusOK { 93 | err := errors.New(debugStr) 94 | httpErrors.WithLabelValues(err.Error()).Inc() 95 | return "", err 96 | } 97 | 98 | return string(respBody), nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/graph/goda.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | // 4 | // Based on https://github.com/gojp/goreportcard, specifically: 5 | // https://github.com/gojp/goreportcard/blob/6ecdf3c5c38cf0855cec02ab2a02ecb78b6e456f/download/download.go 6 | // 7 | 8 | // This file takes GoLang repos as input and outputs DOT files: 9 | // 10 | // 1. repo => dir 11 | // git clone --depth 1 https://github.com/siggy/gographs /repos/https://github.com/siggy/gographs" 12 | // 2. dir => dot 13 | // goda graph -short -cluster github.com/siggy/gographs... 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "io/ioutil" 19 | "os" 20 | "os/exec" 21 | "strings" 22 | 23 | log "github.com/sirupsen/logrus" 24 | "golang.org/x/tools/go/vcs" 25 | ) 26 | 27 | func repoToDot(repo string, cluster bool) (string, error) { 28 | codeDir, err := toDir(repo) 29 | if err != nil { 30 | log.Errorf("failed to get dir: %s", err) 31 | return "", err 32 | } 33 | 34 | return dirToDot(codeDir, cluster) 35 | } 36 | 37 | func toDir(repo string) (string, error) { 38 | codeDir, err := ioutil.TempDir("", "") 39 | if err != nil { 40 | log.Errorf("TempDir err: %s", err) 41 | return "", err 42 | } 43 | log.Debugf("writing to tempDir: %s", codeDir) 44 | err = os.MkdirAll(codeDir, os.ModePerm) 45 | if err != nil { 46 | log.Errorf("MkdirAll err: %s", err) 47 | return "", err 48 | } 49 | 50 | vcs.ShowCmd = true 51 | root, err := vcs.RepoRootForImportPath(trimScheme(repo), true) 52 | if err != nil { 53 | log.Errorf("RepoRootForImportPath err: %s", err) 54 | return "", err 55 | } 56 | 57 | root.VCS.CreateCmd = "clone --depth 1 --no-tags {repo} {dir}" 58 | err = root.VCS.Create(codeDir, root.Repo) 59 | if err != nil { 60 | log.Errorf("cmd.Create err: %s", err) 61 | return "", err 62 | } 63 | 64 | return codeDir, nil 65 | } 66 | 67 | func dirToDot(dir string, cluster bool) (string, error) { 68 | args := []string{"graph", "-short"} 69 | if cluster { 70 | args = append(args, "-cluster") 71 | } 72 | args = append(args, "./...") 73 | 74 | cmd := exec.Command("goda", args...) 75 | cmd.Dir = dir 76 | 77 | var stdout, stderr bytes.Buffer 78 | cmd.Stdout = &stdout 79 | cmd.Stderr = &stderr 80 | 81 | log.Debugf("running goda: %s", cmd) 82 | err := cmd.Run() 83 | if err != nil { 84 | log.Errorf("goda cmd failed [%s]: %s", err, stderr.String()) 85 | return "", err 86 | } 87 | 88 | serr := stderr.String() 89 | if strings.Contains(serr, "matched no packages") { 90 | err := fmt.Errorf("goda cmd returned stderr: %s", serr) 91 | log.Error(err) 92 | return "", err 93 | } 94 | 95 | return stdout.String(), nil 96 | } 97 | 98 | func trimScheme(repo string) string { 99 | schemeSep := "://" 100 | schemeSepIdx := strings.Index(repo, schemeSep) 101 | if schemeSepIdx > -1 { 102 | return repo[schemeSepIdx+len(schemeSep):] 103 | } 104 | 105 | return repo 106 | } 107 | -------------------------------------------------------------------------------- /pkg/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/siggy/gographs/pkg/prom" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const graphServer = "graph" 14 | 15 | // Start initializes the graph server and starts listening. 16 | func Start(addr string) error { 17 | router := mux.NewRouter() 18 | router.Use(prom.Middleware(graphServer)) 19 | 20 | log := log.WithFields( 21 | log.Fields{ 22 | graphServer: addr, 23 | }, 24 | ) 25 | 26 | // apis 27 | graphHandler := mkGraphHandler(log) 28 | router.HandleFunc("/graph", graphHandler).Methods(http.MethodPost) 29 | 30 | log.Infof("%s server listening on %s", graphServer, addr) 31 | 32 | return http.ListenAndServe(addr, router) 33 | } 34 | 35 | func mkGraphHandler(log *log.Entry) http.HandlerFunc { 36 | // curl --data '{"repo":"github.com/siggy/gographs","cluster":true}' -X POST /graph 37 | return func(rw http.ResponseWriter, r *http.Request) { 38 | decoder := json.NewDecoder(r.Body) 39 | var p Post 40 | err := decoder.Decode(&p) 41 | if err != nil { 42 | message := fmt.Sprintf("Failed to decode POST body %s", p.Repo) 43 | writeError(rw, r, http.StatusInternalServerError, message, err) 44 | return 45 | } 46 | 47 | log.Debugf("Processing %s", p.Repo) 48 | 49 | dot, err := repoToDot(p.Repo, p.Cluster) 50 | if err != nil { 51 | message := fmt.Sprintf("Failed to render dot: %s", p.Repo) 52 | writeError(rw, r, http.StatusInternalServerError, message, err) 53 | return 54 | } 55 | 56 | rw.Header().Set("Content-Type", "text/plain; charset=utf-8") 57 | rw.WriteHeader(http.StatusOK) 58 | rw.Write([]byte(dot)) 59 | } 60 | } 61 | 62 | // writeError handles all errors returned by the web server. It writes an error 63 | // header, an optional error message, counts the error in metrics, and logs it. 64 | // TODO: factor out with web.go 65 | func writeError(rw http.ResponseWriter, r *http.Request, status int, message string, err error) { 66 | rw.WriteHeader(status) 67 | if message != "" { 68 | rw.Write([]byte(message)) 69 | } 70 | 71 | route := mux.CurrentRoute(r) 72 | path, _ := route.GetPathTemplate() 73 | 74 | log.Errorf("Failed request for [%s]: [%d] Message: [%s] Error: [%s]", path, status, message, err) 75 | prom.CountError(graphServer, r, status, message, err) 76 | } 77 | -------------------------------------------------------------------------------- /pkg/graph/prom.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | // TODO: move to prom package? 9 | 10 | var ( 11 | httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{ 12 | Namespace: "gographs", 13 | Subsystem: "graphclient", 14 | Name: "requests_total", 15 | Help: "Count of HTTP requests.", 16 | }, []string{"repo", "cluster"}) 17 | 18 | httpErrors = promauto.NewCounterVec(prometheus.CounterOpts{ 19 | Namespace: "gographs", 20 | Subsystem: "graphclient", 21 | Name: "errors_total", 22 | Help: "Count of HTTP errors.", 23 | }, []string{"repo", "cluster", "error"}) 24 | 25 | httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 26 | Namespace: "gographs", 27 | Subsystem: "graphclient", 28 | Name: "duration_seconds", 29 | Help: "Duration of HTTP requests.", 30 | Buckets: prometheus.ExponentialBuckets(0.001, 1.3, 50), 31 | }, []string{"repo", "cluster"}) 32 | ) 33 | -------------------------------------------------------------------------------- /pkg/prom/prom.go: -------------------------------------------------------------------------------- 1 | package prom 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promauto" 9 | ) 10 | 11 | var ( 12 | httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{ 13 | Namespace: "gographs", 14 | Subsystem: "server", 15 | Name: "requests_total", 16 | Help: "Count of HTTP requests.", 17 | }, []string{"server", "method", "path"}) 18 | 19 | httpErrors = promauto.NewCounterVec(prometheus.CounterOpts{ 20 | Namespace: "gographs", 21 | Subsystem: "server", 22 | Name: "errors_total", 23 | Help: "Count of HTTP errors.", 24 | }, []string{"server", "method", "path", "status", "message", "error"}) 25 | 26 | httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 27 | Namespace: "gographs", 28 | Subsystem: "server", 29 | Name: "duration_seconds", 30 | Help: "Duration of HTTP requests.", 31 | Buckets: prometheus.ExponentialBuckets(0.001, 1.3, 50), 32 | }, []string{"server", "method", "path"}) 33 | ) 34 | 35 | // Middleware returns a function that implements mux.MiddlewareFunc. 36 | func Middleware(server string) mux.MiddlewareFunc { 37 | return func(next http.Handler) http.Handler { 38 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | route := mux.CurrentRoute(r) 40 | path, _ := route.GetPathTemplate() 41 | httpRequests.WithLabelValues(server, r.Method, path).Inc() 42 | timer := prometheus.NewTimer(httpDuration.WithLabelValues(server, r.Method, path)) 43 | next.ServeHTTP(w, r) 44 | timer.ObserveDuration() 45 | }) 46 | } 47 | } 48 | 49 | // CountError increments http error counters. 50 | func CountError(server string, r *http.Request, status int, message string, err error) { 51 | route := mux.CurrentRoute(r) 52 | path, _ := route.GetPathTemplate() 53 | errStr := "" 54 | if err != nil { 55 | errStr = err.Error() 56 | } 57 | httpErrors.WithLabelValues(server, r.Method, path, http.StatusText(status), message, errStr).Inc() 58 | } 59 | -------------------------------------------------------------------------------- /pkg/render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | // This package takes GoLang repos as input and outputs SVG and DOT files: 4 | // 5 | // 1. repo => dot 6 | // curl --data '{"repo":"github.com/siggy/gographs","cluster":true}' -X POST [graph-addr]/graph 7 | // 2. dot => svg 8 | // echo "..." | \ 9 | // dot -Tsvg \ 10 | // -Gfontname=Roboto,Arial,sans-serif \ 11 | // -Nfontname=Roboto,Arial,sans-serif \ 12 | // -Efontname=Roboto,Arial,sans-serif \ 13 | // -o graph2.svg 14 | // 15 | // Nested control-flow accommodates caching: 16 | // 17 | // ToSVG(repo) { 18 | // ToDOT(repo) {} => DOT 19 | // dotToSVG(DOT) {} => SVG 20 | // } => SVG 21 | 22 | import ( 23 | "bytes" 24 | "os/exec" 25 | "strings" 26 | 27 | "github.com/siggy/gographs/pkg/cache" 28 | "github.com/siggy/gographs/pkg/graph" 29 | log "github.com/sirupsen/logrus" 30 | ) 31 | 32 | // ToSVG takes a GoLang repo as input and returns an SVG dependency graph 33 | func ToSVG(graph *graph.Client, cache *cache.Cache, repo string, cluster bool) (string, error) { 34 | svg, err := cache.GetSVG(repo, cluster) 35 | if err == nil { 36 | return svg, nil 37 | } 38 | 39 | dot, err := ToDOT(graph, cache, repo, cluster) 40 | if err != nil { 41 | log.Errorf("error generating dot: %s", err) 42 | return "", err 43 | } 44 | 45 | svg, err = dotToSVG(dot) 46 | if err != nil { 47 | log.Errorf("error converting dot to svg: %s", err) 48 | return "", err 49 | } 50 | 51 | go cache.SetSVG(repo, cluster, svg) 52 | 53 | return svg, nil 54 | } 55 | 56 | // ToDOT takes a GoLang repo as input and returns a DOT dependency graph 57 | func ToDOT(graph *graph.Client, cache *cache.Cache, repo string, cluster bool) (string, error) { 58 | dot, err := cache.GetDOT(repo, cluster) 59 | if err == nil { 60 | return dot, nil 61 | } 62 | 63 | dot, err = graph.Get(repo, cluster) 64 | if err != nil { 65 | return "", err 66 | } 67 | 68 | go cache.SetDOT(repo, cluster, dot) 69 | 70 | return dot, nil 71 | } 72 | 73 | func dotToSVG(dot string) (string, error) { 74 | command := exec.Command( 75 | "dot", 76 | "-Tsvg", 77 | "-Gfontname=Roboto,Arial,sans-serif", 78 | "-Nfontname=Roboto,Arial,sans-serif", 79 | "-Efontname=Roboto,Arial,sans-serif", 80 | ) 81 | command.Stdin = strings.NewReader(dot) 82 | var stderr bytes.Buffer 83 | command.Stderr = &stderr 84 | 85 | log.Debugf("running dot: %s", command) 86 | svg, err := command.Output() 87 | if err != nil { 88 | log.Errorf("dot cmd failed: %s", err) 89 | return "", err 90 | } 91 | 92 | return string(svg), nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/siggy/gographs/pkg/cache" 11 | "github.com/siggy/gographs/pkg/graph" 12 | "github.com/siggy/gographs/pkg/prom" 13 | "github.com/siggy/gographs/pkg/render" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | const webServer = "web" 18 | 19 | // Start initializes the web server and starts listening. 20 | func Start(c *cache.Cache, addr string, graph *graph.Client) error { 21 | router := mux.NewRouter() 22 | router.Use(prom.Middleware(webServer)) 23 | 24 | getRouter := router.Methods(http.MethodGet).Subrouter() 25 | postRouter := router.Methods(http.MethodPost).Subrouter() 26 | 27 | log := log.WithFields( 28 | log.Fields{ 29 | webServer: addr, 30 | }, 31 | ) 32 | 33 | // web views 34 | getRouter.PathPrefix("/repo").HandlerFunc(repoHandler) 35 | getRouter.HandleFunc("/svg", repoHandler) 36 | getRouter.HandleFunc("/", repoHandler) 37 | 38 | // apis 39 | graphHandler := mkGraphHandler(graph, c, log) 40 | getRouter.PathPrefix("/graph").HandlerFunc(graphHandler) 41 | postRouter.PathPrefix("/graph").HandlerFunc(graphHandler) 42 | getRouter.HandleFunc("/top-repos", mkTopReposHandler(c)) 43 | 44 | // assets 45 | getRouter.PathPrefix("/").Handler(http.FileServer(http.Dir("./public/"))) 46 | 47 | log.Infof("%s server listening on %s", webServer, addr) 48 | 49 | return http.ListenAndServe(addr, router) 50 | } 51 | 52 | func repoHandler(w http.ResponseWriter, r *http.Request) { 53 | http.ServeFile(w, r, "public/index.html") 54 | } 55 | 56 | func mkGraphHandler(graph *graph.Client, cache *cache.Cache, log *log.Entry) http.HandlerFunc { 57 | // GET /graph/github.com/siggy/gographs.svg 58 | // POST /graph/github.com/siggy/gographs.svg (for refresh) 59 | return func(rw http.ResponseWriter, r *http.Request) { 60 | vars := r.URL.Query() 61 | cluster := vars.Get("cluster") == "true" 62 | 63 | refresh := r.Method == http.MethodPost 64 | 65 | tpl, err := mux.CurrentRoute(r).GetPathTemplate() 66 | if err != nil { 67 | writeError(rw, r, http.StatusInternalServerError, err.Error(), err) 68 | return 69 | } 70 | 71 | suffix := "" 72 | contentType := "" 73 | if strings.HasSuffix(r.URL.Path, ".svg") { 74 | suffix = ".svg" 75 | contentType = "image/svg+xml; charset=utf-8" 76 | } else if strings.HasSuffix(r.URL.Path, ".dot") { 77 | suffix = ".dot" 78 | contentType = "text/plain; charset=utf-8" 79 | } else { 80 | writeError(rw, r, http.StatusBadRequest, "svg or dot suffix required", nil) 81 | return 82 | } 83 | 84 | goRepo := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, tpl+"/"), suffix) 85 | 86 | if refresh { 87 | log.Debugf("Clearing cache for %s", goRepo) 88 | err = cache.Clear(goRepo) 89 | if err != nil { 90 | log.Errorf("Failed to clear cache for repo %s: %s", goRepo, err) 91 | } 92 | } 93 | 94 | log.Debugf("Processing %s", goRepo) 95 | 96 | out := "" 97 | if suffix == ".svg" { 98 | out, err = render.ToSVG(graph, cache, goRepo, cluster) 99 | } else if suffix == ".dot" { 100 | out, err = render.ToDOT(graph, cache, goRepo, cluster) 101 | } 102 | if err != nil { 103 | message := fmt.Sprintf("Failed to render %s to %s", goRepo, suffix) 104 | writeError(rw, r, http.StatusInternalServerError, message, err) 105 | return 106 | } 107 | 108 | go cache.RepoScoreIncr(goRepo) 109 | 110 | rw.Header().Set("Content-Type", contentType) 111 | rw.WriteHeader(http.StatusOK) 112 | rw.Write([]byte(out)) 113 | } 114 | } 115 | 116 | // TODO: poll for this every interval, hold result in local mem 117 | func mkTopReposHandler(cache *cache.Cache) http.HandlerFunc { 118 | // /top-repos 119 | return func(rw http.ResponseWriter, r *http.Request) { 120 | if r.Method != http.MethodGet { 121 | writeError(rw, r, http.StatusMethodNotAllowed, "", nil) 122 | return 123 | } 124 | 125 | scores, err := cache.RepoScores() 126 | if err != nil { 127 | writeError(rw, r, http.StatusInternalServerError, "", err) 128 | return 129 | } 130 | 131 | j, err := json.Marshal(scores) 132 | if err != nil { 133 | writeError(rw, r, http.StatusInternalServerError, "", err) 134 | return 135 | } 136 | 137 | rw.Header().Set("Content-Type", "application/json; charset=utf-8") 138 | rw.WriteHeader(http.StatusOK) 139 | rw.Write(j) 140 | } 141 | } 142 | 143 | // writeError handles all errors returned by the web server. It writes an error 144 | // header, an optional error message, counts the error in metrics, and logs it. 145 | func writeError(rw http.ResponseWriter, r *http.Request, status int, message string, err error) { 146 | rw.WriteHeader(status) 147 | if message != "" { 148 | rw.Write([]byte(message)) 149 | } 150 | 151 | route := mux.CurrentRoute(r) 152 | path, _ := route.GetPathTemplate() 153 | 154 | log.Errorf("Failed request for [%s]: [%d] Message: [%s] Error: [%s]", path, status, message, err) 155 | prom.CountError(webServer, r, status, message, err) 156 | } 157 | -------------------------------------------------------------------------------- /public/badge.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/css/gographs.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: fixed; 3 | overflow: hidden; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | body { 9 | font-family: Roboto, Arial, sans-serif; 10 | color: #555; 11 | margin: 0; 12 | z-index: -2; 13 | overscroll-behavior: none; 14 | } 15 | 16 | .floating-panel { 17 | position: absolute; 18 | margin: 12px; 19 | left: 0; 20 | border: 2px solid rgb(210, 225, 240); 21 | border-radius: 8px; 22 | box-shadow: 2px 2px 1px #555; 23 | } 24 | 25 | /* 26 | * full screen svg 27 | */ 28 | 29 | .main { 30 | z-index: -1; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | width: 100%; 35 | height: 100%; 36 | } 37 | 38 | /* 39 | * thumbnail browser 40 | */ 41 | 42 | .thumb { 43 | bottom: 0; 44 | height: 25%; 45 | width: auto; 46 | max-width: 25%; 47 | } 48 | 49 | #scope-container { 50 | border: 2px solid #000; 51 | box-shadow: unset; 52 | } 53 | 54 | /* 55 | * control panel 56 | */ 57 | 58 | #control-panel { 59 | top: 0; 60 | height: 50px; 61 | width: 500px; 62 | padding: 6px; 63 | background: rgba(225, 235, 245, 0.9); 64 | transition: transform 250ms ease-in-out; 65 | } 66 | 67 | /* 68 | * dangerous refresh button 69 | */ 70 | 71 | #refresh { 72 | display: none; 73 | position: absolute; 74 | right: 25px; 75 | bottom: 80px; 76 | } 77 | #refresh.visible { 78 | display: inline; 79 | } 80 | 81 | /* 82 | * primary input text 83 | */ 84 | 85 | .control-panel-module { 86 | width: 100%; 87 | color: #555; 88 | padding: 6px 12px; 89 | font-size: 14px; 90 | border: 1px solid #ccc; 91 | border-radius: 4px; 92 | box-sizing: border-box; 93 | } 94 | 95 | #input-error { 96 | color: white; 97 | position: absolute; 98 | top: 72px; 99 | border-color: #bf4040; 100 | background-color: rgb(191, 64, 64, 0.9); 101 | opacity: 0; 102 | width: inherit; 103 | transition: opacity 250ms linear; 104 | } 105 | 106 | #input-error.visible { 107 | opacity: 1; 108 | transition: opacity 250ms linear; 109 | } 110 | 111 | /* 112 | * control panel open/close 113 | * based on https://codepen.io/plavookac/pen/qomrMw?editors=1100 114 | */ 115 | 116 | #control-toggle { 117 | box-sizing: border-box; 118 | display: none; 119 | } 120 | 121 | #control-toggle:checked ~ #control-panel { 122 | /* 123 | * width: 500px + 2 * (padding: 6px) + 2 * (border: 2px) + 1 * (margin: 12px) + box-shadow: 2px 124 | */ 125 | transform: translateX(-530px); 126 | } 127 | 128 | #control-toggle:checked ~ #control-toggle-icon { 129 | transform: translateX(-530px); 130 | } 131 | 132 | #control-toggle-icon { 133 | box-sizing: border-box; 134 | cursor: pointer; 135 | position: absolute; 136 | top: 26px; 137 | left: 530px; 138 | height: 22px; 139 | width: 22px; 140 | transition: transform 250ms ease-in-out; 141 | } 142 | 143 | .toggler { 144 | position: relative; 145 | float: left; 146 | box-sizing: border-box; 147 | transition: all 0.3s; 148 | height: 3px; 149 | width: 100%; 150 | background-color: #555; 151 | } 152 | 153 | .horizontal { 154 | margin-top: 3px; 155 | opacity: 0; 156 | } 157 | .diagonal.part-1 { 158 | transform: rotate(135deg); 159 | margin-top: 8px; 160 | } 161 | .diagonal.part-2 { 162 | transform: rotate(-135deg); 163 | margin-top: -9px; 164 | } 165 | #control-toggle:checked ~ #control-toggle-icon > .horizontal { 166 | opacity: 1; 167 | } 168 | #control-toggle:checked ~ #control-toggle-icon > .diagonal.part-1 { 169 | transform: unset; 170 | margin-top: 0; 171 | } 172 | #control-toggle:checked ~ #control-toggle-icon > .diagonal.part-2 { 173 | transform: unset; 174 | margin-top: 3px; 175 | } 176 | 177 | #check-cluster-label { 178 | display: none; 179 | position: absolute; 180 | left: 3px; 181 | bottom: 0; 182 | } 183 | #check-cluster-label.visible { 184 | display: inline; 185 | } 186 | 187 | #check-cluster { 188 | vertical-align: bottom; 189 | } 190 | 191 | .external-links { 192 | position: absolute; 193 | right: 110px; 194 | bottom: 0; 195 | } 196 | .external-link { 197 | display: none; 198 | margin-right: 10px; 199 | } 200 | .external-link.visible { 201 | display: inline; 202 | } 203 | 204 | #badge { 205 | display: none; 206 | position: absolute; 207 | right: 2px; 208 | top: 41px; 209 | } 210 | #badge.visible { 211 | display: inline; 212 | } 213 | 214 | #badge-markdown { 215 | z-index: 1; 216 | display: none; 217 | position: absolute; 218 | width: 400px; 219 | right: 2px; 220 | top: 70px; 221 | } 222 | #badge-markdown.visible { 223 | display: inline; 224 | } 225 | 226 | .autocomplete-suggestion { 227 | color: #555; 228 | } 229 | .autocomplete-suggestion b { 230 | color: #000; 231 | } 232 | 233 | /* 234 | * spinner 235 | * 236 | * based on: 237 | * https://dev.to/wangonya/displaying-a-css-spinner-on-ajax-calls-with-fetch-api-4ndo 238 | */ 239 | 240 | #spinner { 241 | z-index: 2; 242 | position: fixed; 243 | top: 0; 244 | left: 0; 245 | right: 0; 246 | bottom: 0; 247 | display: none; 248 | justify-content: center; 249 | align-items: center; 250 | background-color: rgba(85,85,85,0.5); 251 | } 252 | 253 | #spinner::after { 254 | content: ""; 255 | width: 80px; 256 | height: 80px; 257 | border: 8px solid #f3f3f3; 258 | border-top: 8px solid rgb(210,225,240); 259 | border-radius: 100%; 260 | will-change: transform; 261 | animation: spin 1s infinite linear 262 | } 263 | 264 | @keyframes spin { 265 | from { 266 | transform: rotate(0deg); 267 | } 268 | to { 269 | transform: rotate(360deg); 270 | } 271 | } 272 | 273 | #spinner .message { 274 | position: fixed; 275 | justify-content: center; 276 | align-items: center; 277 | font-weight: bold; 278 | background-color: rgb(243,243,243);; 279 | padding: 4px; 280 | border-radius: 4px; 281 | transform: translate(0, 70px); 282 | } 283 | 284 | /* 285 | * misc 286 | */ 287 | 288 | a { 289 | text-decoration: none; 290 | } 291 | -------------------------------------------------------------------------------- /public/img/gographs.full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siggy/gographs/7f9a81de5f7dc47f407950fa5bdf91a89dde5c0f/public/img/gographs.full.png -------------------------------------------------------------------------------- /public/img/gographs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siggy/gographs/7f9a81de5f7dc47f407950fa5bdf91a89dde5c0f/public/img/gographs.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |