├── .githooks └── pre-commit ├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── channel-page.go ├── channel-page_test.go ├── channels-page.go ├── channels-page_test.go ├── channelz.go ├── client.go ├── doc ├── channel.png ├── server.png ├── socket.png ├── subchannel.png └── top-channels.png ├── go.mod ├── go.sum ├── handler.go ├── handler_test.go ├── internal ├── demo │ ├── client │ │ └── client.go │ └── server │ │ ├── main │ │ └── main.go │ │ └── server.go ├── generated │ └── service │ │ ├── demo.pb.go │ │ └── demo_grpc.pb.go └── proto │ └── demo.proto ├── mock-channelz-client_test.go ├── routes.go ├── routes_test.go ├── server-page.go ├── server-page_test.go ├── servers-page.go ├── socket-page.go ├── socket-page_test.go ├── sub-channel-page.go ├── subchannel-page_test.go ├── templates.go ├── top-channels-page.go └── top-channels-page_test.go /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | set -e 10 | 11 | make lint || (echo "lint failed"; exit 1) 12 | 13 | if git rev-parse --verify HEAD >/dev/null 2>&1 14 | then 15 | against=HEAD 16 | else 17 | # Initial commit: diff against an empty tree object 18 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 19 | fi 20 | 21 | 22 | # If there are whitespace errors, print the offending file names and fail. 23 | exec git diff-index --check --cached $against -- 24 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, Test and Lint 6 | strategy: 7 | matrix: 8 | go-version: [1.18.x] 9 | platform: [ubuntu-latest] 10 | runs-on: ${{ matrix.platform }} 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | id: go 17 | - name: Check out code 18 | uses: actions/checkout@v2 19 | - name: Build 20 | run: go build ./... 21 | - name: Test 22 | run: go test -cover -race ./... 23 | - name: Lint 24 | run: make lint 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp/ 2 | bin/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ran Tavory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_DIR := ./bin 2 | GOLANGCI_LINT_VERSION := 1.21.0 3 | GOLANGCI_LINT := $(BIN_DIR)/golangci-lint 4 | PROTOC_VERSION := 3.20.1 5 | RELEASE_OS := 6 | PROTOC_DIR := .tmp/protoc-$(PROTOC_VERSION) 7 | PROTOC_BIN := $(PROTOC_DIR)/bin/protoc 8 | 9 | ifeq ($(OS),Windows_NT) 10 | echo "Windows not supported yet, sorry...." 11 | exit 1 12 | else 13 | UNAME_S := $(shell uname -s) 14 | ifeq ($(UNAME_S),Linux) 15 | RELEASE_OS = linux 16 | endif 17 | ifeq ($(UNAME_S),Darwin) 18 | RELEASE_OS = osx 19 | endif 20 | endif 21 | 22 | 23 | all: test lint 24 | 25 | tidy: 26 | go mod tidy -v 27 | 28 | build: protoc 29 | go build ./... 30 | 31 | test: build 32 | go test -cover -race ./... 33 | 34 | test-coverage: 35 | go test ./... -race -coverprofile=coverage.txt && go tool cover -html=coverage.txt 36 | 37 | ci-test: build 38 | go test -race $$(go list ./...) -v -coverprofile coverage.txt -covermode=atomic 39 | 40 | setup: setup-git-hooks 41 | 42 | setup-git-hooks: 43 | git config core.hooksPath .githooks 44 | 45 | lint: lint-install 46 | # -D typecheck until golangci-lint gets it together to propery work with go1.13 47 | $(GOLANGCI_LINT) run --fast --enable-all -D gochecknoglobals -D dupl -D typecheck -D wsl 48 | 49 | lint-install: 50 | # Check if golanglint-ci exists and is of the correct version, if not install 51 | $(GOLANGCI_LINT) --version | grep $(GOLANGCI_LINT_VERSION) || \ 52 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(BIN_DIR) v$(GOLANGCI_LINT_VERSION) 53 | 54 | guard-%: 55 | @ if [ "${${*}}" = "" ]; then \ 56 | echo "Environment variable $* not set"; \ 57 | exit 1; \ 58 | fi 59 | 60 | 61 | $(PROTOC_BIN): 62 | @echo "Installing unzip (if required)" 63 | @which unzip || apt-get update || sudo apt-get update 64 | @which unzip || apt-get install unzip || sudo apt-get install unzip 65 | @echo Installing protoc 66 | rm -rf $(PROTOC_DIR) 67 | mkdir -p $(PROTOC_DIR) 68 | cd $(PROTOC_DIR) &&\ 69 | curl -OL https://github.com/google/protobuf/releases/download/v$(PROTOC_VERSION)/protoc-$(PROTOC_VERSION)-$(RELEASE_OS)-x86_64.zip &&\ 70 | unzip protoc-$(PROTOC_VERSION)-$(RELEASE_OS)-x86_64.zip 71 | chmod +x $(PROTOC_BIN) 72 | @echo "Installing protoc-gen-go (if required)" 73 | @which protoc-gen-go > /dev/null || go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 74 | @echo "Installing protoc-gen-go-grpc (if required)" 75 | @which protoc-gen-go-grpc > /dev/null || go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 76 | 77 | run-demo-server: 78 | go run internal/demo/server/main/main.go 79 | 80 | protoc: $(PROTOC_BIN) 81 | mkdir -p internal/generated/service 82 | $(PROTOC_BIN) --proto_path=internal/proto \ 83 | --go_out=internal/generated/service --go_opt=paths=source_relative \ 84 | --go-grpc_out=internal/generated/service --go-grpc_opt=paths=source_relative \ 85 | internal/proto/*.proto 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-grpc-channelz 2 | 3 | An in-process Channelz UI for gRPC in Golang 4 | 5 | ## What is Channelz? 6 | 7 | Channelz is a gRPC spec for introspection into gRPC channels. 8 | Channels in gRPC represent connections and sockets. Channelz provides introspection into the current active grpc connections, including incoming and outgoing connections. 9 | The full spec can be found [here](https://github.com/grpc/proposal/blob/master/A14-channelz.md) 10 | 11 | ## What is `go-grpc-channelz`? 12 | 13 | `go-grpc-channelz` provides a web UI to view the current start of all gRPC channels. For each channel you'd be able to look into the remote peer, sub-channels, load balancing stategies, number of calls, socket activity and events and so on. 14 | 15 | You install go-grpc-channelz into your service and expose it's web page and that's it. All in all, about 2-5 lines of code. 16 | 17 | ## Screenshots 18 | 19 | ![Top Channels](doc/top-channels.png) 20 | 21 | ![Channel](doc/channel.png) 22 | 23 | ![Subchannel](doc/subchannel.png) 24 | 25 | ![Socket](doc/socket.png) 26 | 27 | ![Server](doc/server.png) 28 | 29 | ## Usage 30 | 31 | Channelz is implemented as a gRPC service. This service is turned off by by default, so you have to turn it on as so: 32 | 33 | ```go 34 | import ( 35 | channelzservice "google.golang.org/grpc/channelz/service" 36 | ) 37 | 38 | // Register the channelz gRPC service to grpcServer so that we can query it for this service. 39 | channelzservice.RegisterChannelzServiceToServer(grpcServer) 40 | ``` 41 | 42 | In this example `grpcServer` is a grpc server that you create externally. In many cases this server already exists (you only need one) but if not then here's how to create it: 43 | 44 | ```go 45 | import "google.golang.org/grpc" 46 | 47 | grpcServer := grpc.NewServer() 48 | ``` 49 | 50 | Now you should register the channelz web handler: 51 | 52 | ```go 53 | import channelz "github.com/rantav/go-grpc-channelz" 54 | 55 | // Register the channelz handler and mount it to /foo. 56 | // Resources will be available at /foo/channelz 57 | http.Handle("/", channelz.CreateHandler("/foo", grpcBindAddress)) 58 | ``` 59 | 60 | Where `grpcBindAddress` is the address to which `grpcServer` is bound. This could be for example `":8080"` or `"localhost:8080"` etc. This address is required because the channelz web service accesses the channelz grpc service in order to query it. 61 | 62 | Lastly, launch that web listener in order to serve the web UI: 63 | 64 | ```go 65 | // Listen and serve HTTP for the default serve mux 66 | adminListener, err := net.Listen("tcp", ":8081") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | go http.Serve(adminListener, nil) 71 | ``` 72 | 73 | Now the service will be available at `http://localhost:8081/foo/channelz` 74 | 75 | A complete example: 76 | 77 | ```go 78 | import ( 79 | "google.golang.org/grpc" 80 | channelzservice "google.golang.org/grpc/channelz/service" 81 | channelz "github.com/rantav/go-grpc-channelz" 82 | ) 83 | 84 | grpcServer := grpc.NewServer() 85 | 86 | // Register the channelz handler 87 | http.Handle("/", channelz.CreateHandler("/foo", grpcBindAddress)) 88 | 89 | // Register the channelz gRPC service to grpcServer so that we can query it for this service. 90 | channelzservice.RegisterChannelzServiceToServer(grpcServer) 91 | 92 | // Listen and serve HTTP for the default serve mux 93 | adminListener, err := net.Listen("tcp", ":8081") 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | go http.Serve(adminListener, nil) 98 | ``` 99 | -------------------------------------------------------------------------------- /channel-page.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 9 | log "google.golang.org/grpc/grpclog" 10 | ) 11 | 12 | // WriteChannelPage writes an HTML document to w containing per-channel RPC stats, including a header and a footer. 13 | func (h *grpcChannelzHandler) WriteChannelPage(w io.Writer, channel int64) { 14 | writeHeader(w, fmt.Sprintf("ChannelZ channel %d", channel)) 15 | h.writeChannel(w, channel) 16 | writeFooter(w) 17 | } 18 | 19 | func (h *grpcChannelzHandler) writeChannel(w io.Writer, channel int64) { 20 | if err := channelTemplate.Execute(w, h.getChannel(channel)); err != nil { 21 | log.Errorf("channelz: executing template: %v", err) 22 | } 23 | } 24 | 25 | func (h *grpcChannelzHandler) getChannel(channelID int64) *channelzgrpc.GetChannelResponse { 26 | client, err := h.connect() 27 | if err != nil { 28 | log.Errorf("Error creating channelz client %+v", err) 29 | return nil 30 | } 31 | ctx := context.Background() 32 | channel, err := client.GetChannel(ctx, &channelzgrpc.GetChannelRequest{ChannelId: channelID}) 33 | if err != nil { 34 | log.Errorf("Error querying GetChannel %+v", err) 35 | return nil 36 | } 37 | return channel 38 | } 39 | 40 | const channelTemplateHTML = ` 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 111 | 112 |
ChannelId{{.Channel.Ref.ChannelId}} 45 |
Channel Name{{.Channel.Ref.Name}}
State{{.Channel.Data.State}}
Target{{.Channel.Data.Target}}
Subchannels 61 | {{range .Channel.SubchannelRef}} 62 | {{.SubchannelId}} {{.Name}}
63 | {{end}} 64 |
Child Channels 69 | {{range .Channel.ChannelRef}} 70 | {{.ChannelId}} {{.Name}}
71 | {{end}} 72 |
Sockets 77 | {{range .Channel.SocketRef}} 78 | {{.SocketId}} {{.Name}}
79 | {{end}} 80 |
CreationTimestamp{{.Channel.Data.Trace.CreationTimestamp | timestamp}}
CallsStarted{{.Channel.Data.CallsStarted}}
CallsSucceeded{{.Channel.Data.CallsSucceeded}}
CallsFailed{{.Channel.Data.CallsFailed}}
LastCallStartedTimestamp{{.Channel.Data.LastCallStartedTimestamp | timestamp}}
Events 105 |
106 | 			{{- range .Channel.Data.Trace.Events}}
107 | {{.Severity}} [{{.Timestamp | timestamp}}]: {{.Description}}
108 | 			{{- end -}}
109 | 			
110 |
113 | ` 114 | -------------------------------------------------------------------------------- /channel-page_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWriteChannelPage(t *testing.T) { 11 | assert := assert.New(t) 12 | handler := grpcChannelzHandler{client: &mockChannelzClient{}} 13 | var b strings.Builder 14 | handler.WriteChannelPage(&b, 2) 15 | assert.Contains(b.String(), "channel 2") 16 | assert.Contains(b.String(), "CT_INFO [1970-01-01T00:00:06Z]: setup") 17 | } 18 | -------------------------------------------------------------------------------- /channels-page.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 8 | log "google.golang.org/grpc/grpclog" 9 | ) 10 | 11 | // WriteChannelsPage writes an HTML document to w containing per-channel RPC stats, including a header and a footer. 12 | func (h *grpcChannelzHandler) WriteChannelsPage(w io.Writer, start int64) { 13 | writeHeader(w, "Channels") 14 | h.writeChannels(w, start) 15 | writeFooter(w) 16 | } 17 | 18 | // writeTopChannels writes HTML to w containing per-channel RPC stats. 19 | // 20 | // It includes neither a header nor footer, so you can embed this data in other pages. 21 | func (h *grpcChannelzHandler) writeChannels(w io.Writer, start int64) { 22 | if err := channelsTemplate.Execute(w, h.getTopChannels(start)); err != nil { 23 | log.Errorf("channelz: executing template: %v", err) 24 | } 25 | } 26 | 27 | func (h *grpcChannelzHandler) getTopChannels(start int64) *channelzgrpc.GetTopChannelsResponse { 28 | client, err := h.connect() 29 | if err != nil { 30 | log.Errorf("Error creating channelz client %+v", err) 31 | return nil 32 | } 33 | ctx := context.Background() 34 | channels, err := client.GetTopChannels(ctx, &channelzgrpc.GetTopChannelsRequest{ 35 | StartChannelId: start, 36 | }) 37 | if err != nil { 38 | log.Errorf("Error querying GetTopChannels %+v", err) 39 | return nil 40 | } 41 | return channels 42 | } 43 | 44 | const channelsTemplateHTML = ` 45 | {{define "channel-header"}} 46 | 47 | Channel 48 | State 49 | Target 50 | Subchannels 51 | Child Channels 52 | Sockets 53 | CreationTimestamp 54 | CallsStarted 55 | CallsSucceeded 56 | CallsFailed 57 | LastCallStartedTimestamp 58 | 59 | {{end}} 60 | 61 | {{define "channel-body"}} 62 | 63 | {{.Ref.ChannelId}} {{.Ref.Name}} 64 | {{.Data.State}} 65 | {{.Data.Target}} 66 | 67 | {{range .SubchannelRef}} 68 | {{.SubchannelId}} {{.Name}}
69 | {{end}} 70 | 71 | 72 | {{range .ChannelRef}} 73 | {{.ChannelId}} {{.Name}}
74 | {{end}} 75 | 76 | 77 | {{range .SocketRef}} 78 | {{.SocketId}} {{.Name}}
79 | {{end}} 80 | 81 | {{.Data.Trace.CreationTimestamp | timestamp}} 82 | {{.Data.CallsStarted}} 83 | {{.Data.CallsSucceeded}} 84 | {{.Data.CallsFailed}} 85 | {{.Data.LastCallStartedTimestamp | timestamp}} 86 | 87 | {{end}} 88 |

Clients

89 | 90 | 91 | 92 | 93 | 94 | {{template "channel-header"}} 95 | {{$last := .Channel}} 96 | {{range .Channel}} 97 | {{template "channel-body" .}} 98 | {{$last = .}} 99 | {{end}} 100 | {{if not .End}} 101 | 102 | 105 | 106 | {{end}} 107 |
Top Channels: {{.Channel | len}}
103 | Next > 104 |
108 |
109 |
110 | ` 111 | -------------------------------------------------------------------------------- /channels-page_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWriteChannelsPage(t *testing.T) { 11 | assert := assert.New(t) 12 | handler := grpcChannelzHandler{client: &mockChannelzClient{}} 13 | var b strings.Builder 14 | handler.WriteChannelsPage(&b, 2) 15 | assert.Contains(b.String(), `8 eight`) 16 | } 17 | -------------------------------------------------------------------------------- /channelz.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | // Package channelz provides a web UI for the channelz information 4 | // defined at https://github.com/grpc/proposal/blob/master/A14-channelz.md 5 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | "google.golang.org/grpc" 9 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 10 | ) 11 | 12 | func (h *grpcChannelzHandler) connect() (channelzgrpc.ChannelzClient, error) { 13 | if h.client != nil { 14 | // Already connected 15 | return h.client, nil 16 | } 17 | 18 | host := getHostFromBindAddress(h.bindAddress) 19 | h.mu.Lock() 20 | defer h.mu.Unlock() 21 | client, err := newChannelzClient(host, h.dialOpts...) 22 | if err != nil { 23 | return nil, err 24 | } 25 | h.client = client 26 | return h.client, nil 27 | } 28 | 29 | func newChannelzClient(dialString string, opts ...grpc.DialOption) (channelzgrpc.ChannelzClient, error) { 30 | conn, err := grpc.Dial(dialString, opts...) 31 | if err != nil { 32 | return nil, errors.Wrapf(err, "error dialing to %s", dialString) 33 | } 34 | client := channelzgrpc.NewChannelzClient(conn) 35 | return client, nil 36 | } 37 | 38 | func getHostFromBindAddress(bindAddress string) string { 39 | if strings.HasPrefix(bindAddress, ":") { 40 | return fmt.Sprintf("localhost%s", bindAddress) 41 | } 42 | return bindAddress 43 | } 44 | -------------------------------------------------------------------------------- /doc/channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/go-grpc-channelz/5af296d55e08b00918f735ceea96ffc5d1ddf740/doc/channel.png -------------------------------------------------------------------------------- /doc/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/go-grpc-channelz/5af296d55e08b00918f735ceea96ffc5d1ddf740/doc/server.png -------------------------------------------------------------------------------- /doc/socket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/go-grpc-channelz/5af296d55e08b00918f735ceea96ffc5d1ddf740/doc/socket.png -------------------------------------------------------------------------------- /doc/subchannel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/go-grpc-channelz/5af296d55e08b00918f735ceea96ffc5d1ddf740/doc/subchannel.png -------------------------------------------------------------------------------- /doc/top-channels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/go-grpc-channelz/5af296d55e08b00918f735ceea96ffc5d1ddf740/doc/top-channels.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rantav/go-grpc-channelz 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.7 7 | github.com/pkg/errors v0.9.1 8 | github.com/stretchr/testify v1.7.1 9 | golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 10 | google.golang.org/grpc v1.46.2 11 | google.golang.org/protobuf v1.28.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/golang/protobuf v1.5.2 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect 19 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect 20 | golang.org/x/text v0.3.3 // indirect 21 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /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 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 8 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 9 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 10 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 11 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 12 | github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 13 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 18 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 19 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 20 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 21 | github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= 22 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 23 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 24 | github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= 25 | github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 26 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 27 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 31 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 32 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 33 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 34 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 35 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 36 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 37 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 38 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 39 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 40 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 41 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 42 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 43 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 44 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 45 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 46 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 47 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 49 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 52 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 53 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 57 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 60 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 62 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 64 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 65 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 66 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 67 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 68 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 69 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 70 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 71 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 72 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 73 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 74 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 75 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 76 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 77 | golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= 78 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 79 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 80 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 81 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 85 | golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= 86 | golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 88 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= 93 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 95 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 96 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 97 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 98 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 99 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 100 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 101 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 102 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 103 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 104 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 105 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 106 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 107 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 108 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 109 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 110 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 111 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 112 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 113 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 114 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 115 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 116 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 117 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 118 | google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= 119 | google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 120 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 121 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 122 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 123 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 124 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 125 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 126 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 127 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 128 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 129 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 130 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 131 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 132 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 133 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 138 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 139 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 140 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 141 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 142 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | "sync" 7 | 8 | "google.golang.org/grpc" 9 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 10 | "google.golang.org/grpc/credentials/insecure" 11 | ) 12 | 13 | // CreateHandler creates an http handler with the routes of channelz mounted to the provided prefix. 14 | // pathPrefix is the prefix to which /channelz will be prepended 15 | // grpcBindAddress is the TCP bind address for the gRPC service you'd like to monitor. 16 | // grpcBindAddress is required since the channelz interface connects to this gRPC service. 17 | // Typically you'd use the return value of CreateHandler as an argument to http.Handle 18 | // For example: 19 | // 20 | // http.Handle("/", channelz.CreateHandler("/foo", grpcBindAddress)) 21 | // 22 | // grpc.Dial is called using grpc.WithTransportCredentials(insecure.NewCredentials()). 23 | // If you need custom DialOptions like credentials, TLS or interceptors, please 24 | // refer to CreateHandlerWithDialOpts(). 25 | func CreateHandler(pathPrefix, grpcBindAddress string) http.Handler { 26 | return CreateHandlerWithDialOpts( 27 | pathPrefix, 28 | grpcBindAddress, 29 | grpc.WithTransportCredentials(insecure.NewCredentials()), 30 | ) 31 | } 32 | 33 | // CreateHandlerWithDialOpts is the same as CreateHandler but with custom []grpc.DialOption 34 | // You need to provide all grpc.DialOption to be used for the internal call to grpc.Dial(). 35 | // This typically includes some form of grpc.WithTransportCredentials(). 36 | // Here's an example on how to use a bufconn instead of a real TCP listener: 37 | // lis := bufconn.Listen(1024 * 1024) 38 | // grpcserver.Serve(lis) 39 | // http.Handle("/", channelzWeb.CreateHandlerWithDialOpts("/", "", 40 | // 41 | // []grpc.DialOption{ 42 | // grpc.WithTransportCredentials(insecure.NewCredentials()), 43 | // grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { 44 | // return lis.DialContext(ctx) 45 | // }), 46 | // }..., 47 | // 48 | // )) 49 | func CreateHandlerWithDialOpts(pathPrefix, grpcBindAddress string, dialOpts ...grpc.DialOption) http.Handler { 50 | prefix := path.Join(pathPrefix, "channelz") + "/" 51 | handler := &grpcChannelzHandler{ 52 | bindAddress: grpcBindAddress, 53 | dialOpts: dialOpts, 54 | } 55 | return createRouter(prefix, handler) 56 | } 57 | 58 | type grpcChannelzHandler struct { 59 | // the target server's bind address 60 | bindAddress string 61 | 62 | // The client connection (lazily initialized) 63 | client channelzgrpc.ChannelzClient 64 | 65 | // []grpc.DialOption to use for grpc.Dial 66 | dialOpts []grpc.DialOption 67 | 68 | mu sync.Mutex 69 | } 70 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "testing" 8 | "time" 9 | 10 | "net/http" 11 | "net/http/httptest" 12 | 13 | "github.com/rantav/go-grpc-channelz/internal/demo/client" 14 | "github.com/rantav/go-grpc-channelz/internal/demo/server" 15 | "golang.org/x/sync/errgroup" 16 | "google.golang.org/grpc" 17 | channelzSrv "google.golang.org/grpc/channelz/service" 18 | "google.golang.org/grpc/credentials/insecure" 19 | "google.golang.org/grpc/test/bufconn" 20 | "google.golang.org/protobuf/types/known/emptypb" 21 | 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | // Just a simple smoke test 27 | func TestCreateHandler(t *testing.T) { 28 | h := CreateHandler("/prefix", ":8080") 29 | assert.NotNil(t, h) 30 | } 31 | 32 | // TestCreateHandlerWithDialOpts performs an end-to-end test with custom dialOpts. 33 | // It is using an in memory grpc demo server which also acts as a channelz provider. 34 | // A goroutine will perform demo calls to the server and the tests search the 35 | // output html for hints that everything is working. 36 | func TestCreateHandlerWithDialOpts(t *testing.T) { 37 | d, _ := t.Deadline() 38 | ctx, cancel := context.WithDeadline(context.Background(), d) 39 | defer cancel() 40 | errs, ctx := errgroup.WithContext(ctx) 41 | 42 | // in memory listener for demo grpc server 43 | listener := bufconn.Listen(1024 * 1024) 44 | 45 | // demo grpc server including channelz 46 | demoServer, err := server.New() 47 | require.Nil(t, err) 48 | channelzSrv.RegisterChannelzServiceToServer(demoServer) 49 | 50 | // serve demo grpc server 51 | errs.Go(func() error { 52 | return demoServer.Serve(listener) 53 | }) 54 | 55 | // cleanup grpc server once ctx is done 56 | errs.Go(func() error { 57 | <-ctx.Done() 58 | demoServer.Stop() 59 | return nil 60 | }) 61 | 62 | // demo client loop 63 | errs.Go(func() error { 64 | // give the grpc server some time to spin up 65 | time.Sleep(time.Millisecond * 100) 66 | 67 | // construct demo client 68 | demoClient, err := client.NewWithDialOpts("", 69 | []grpc.DialOption{ 70 | grpc.WithTransportCredentials(insecure.NewCredentials()), 71 | grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { 72 | return listener.DialContext(ctx) 73 | }), 74 | }...) 75 | require.Nil(t, err) 76 | 77 | // create demoClient.Hello calls each second until ctx is done 78 | for { 79 | select { 80 | case <-ctx.Done(): 81 | return nil 82 | case <-time.After(time.Second): 83 | _, err := demoClient.Hello(ctx, &emptypb.Empty{}) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | } 89 | }) 90 | 91 | // give the demo client a chance to do a request before performing tests 92 | time.Sleep(time.Second) 93 | 94 | // create http.Handler which uses dialOpts to dial to in memory listener 95 | handler := CreateHandlerWithDialOpts("/prefix", ":8080", 96 | []grpc.DialOption{ 97 | grpc.WithTransportCredentials(insecure.NewCredentials()), 98 | grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { 99 | return listener.DialContext(ctx) 100 | }), 101 | }...) 102 | 103 | // start a testing http server with handler 104 | s := httptest.NewServer(handler) 105 | defer s.Close() 106 | 107 | // actual test: get main page 108 | resp, err := http.Get(s.URL + "/prefix/channelz/") 109 | require.Nil(t, err) 110 | assert.Equal(t, http.StatusOK, resp.StatusCode) 111 | 112 | bodyBytes, err := io.ReadAll(resp.Body) 113 | require.Nil(t, err) 114 | body := string(bodyBytes) 115 | 116 | assert.Contains(t, string(body), "Servers: 1") 117 | assert.Contains(t, string(body), "/prefix/channelz/server/") 118 | assert.Contains(t, string(body), "state:READY") 119 | 120 | // teardown and cleanup 121 | cancel() 122 | err = errs.Wait() 123 | require.Nil(t, err) 124 | } 125 | -------------------------------------------------------------------------------- /internal/demo/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "google.golang.org/grpc" 6 | "google.golang.org/grpc/credentials/insecure" 7 | 8 | demoservice "github.com/rantav/go-grpc-channelz/internal/generated/service" 9 | ) 10 | 11 | // New creates a new gRPC client 12 | func New(connectionString string) (demoservice.DemoServiceClient, error) { 13 | return NewWithDialOpts(connectionString, 14 | grpc.WithTransportCredentials(insecure.NewCredentials())) 15 | } 16 | 17 | // NewWithDialOpts creates a new gRPC client with custom []grpc.DialOption 18 | func NewWithDialOpts(connectionString string, dialOpts ...grpc.DialOption) (demoservice.DemoServiceClient, error) { 19 | conn, err := grpc.Dial(connectionString, dialOpts...) 20 | if err != nil { 21 | return nil, errors.Wrapf(err, "error dialing to %s", connectionString) 22 | } 23 | 24 | client := demoservice.NewDemoServiceClient(conn) 25 | return client, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/demo/server/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "golang.org/x/sync/errgroup" 11 | channelzservice "google.golang.org/grpc/channelz/service" 12 | log "google.golang.org/grpc/grpclog" 13 | "google.golang.org/protobuf/types/known/emptypb" 14 | 15 | channelz "github.com/rantav/go-grpc-channelz" 16 | "github.com/rantav/go-grpc-channelz/internal/demo/client" 17 | "github.com/rantav/go-grpc-channelz/internal/demo/server" 18 | ) 19 | 20 | func main() { 21 | const ( 22 | grpcBindAddress = ":8080" 23 | adminBindAddress = ":8081" 24 | ) 25 | 26 | // nolint:gosec 27 | grpcListener, err := net.Listen("tcp", grpcBindAddress) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | // nolint:gosec 33 | adminListener, err := net.Listen("tcp", adminBindAddress) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | grpcServer, err := server.New() 39 | if err != nil { 40 | log.Fatalf("Failed to create grpc server %+v", err) 41 | } 42 | 43 | // Register the channelz handler 44 | http.Handle("/", channelz.CreateHandler("/foo", grpcBindAddress)) 45 | 46 | // Register the channelz gRPC service to grpcServer so that we can query it for this service. 47 | channelzservice.RegisterChannelzServiceToServer(grpcServer) 48 | 49 | g := new(errgroup.Group) 50 | g.Go(func() error { return http.Serve(adminListener, nil) }) 51 | g.Go(func() error { return grpcServer.Serve(grpcListener) }) 52 | 53 | fmt.Printf("demo server is up is up; gRPC bind address: %s, http admin address: %s \n", 54 | grpcBindAddress, adminBindAddress) 55 | 56 | go runClient(fmt.Sprintf("localhost%s", grpcBindAddress)) 57 | 58 | // should never return 59 | err = g.Wait() 60 | log.Fatalf("Error running server: %+v", err) 61 | } 62 | 63 | // runs a client gRPC call in a loop with some sleeps. 64 | func runClient(dialString string) { 65 | client, err := client.New(dialString) 66 | if err != nil { 67 | log.Fatalf("Cannot create gRPC client to %s. %v", dialString, err) 68 | } 69 | for { 70 | time.Sleep(10 * time.Second) 71 | _, err = client.Hello(context.Background(), &emptypb.Empty{}) 72 | if err != nil { 73 | log.Errorf("Error saying hello. %+v", err) 74 | return 75 | } 76 | fmt.Println("Hello was successful") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/demo/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | "google.golang.org/protobuf/types/known/emptypb" 8 | 9 | demoservice "github.com/rantav/go-grpc-channelz/internal/generated/service" 10 | ) 11 | 12 | type server struct { 13 | demoservice.UnimplementedDemoServiceServer 14 | } 15 | 16 | // New creates a new grpc server 17 | func New() (*grpc.Server, error) { 18 | grpcServer := grpc.NewServer() 19 | server := &server{} 20 | demoservice.RegisterDemoServiceServer(grpcServer, server) 21 | return grpcServer, nil 22 | } 23 | 24 | func (s *server) Hello(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { 25 | return &emptypb.Empty{}, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/generated/service/demo.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.20.1 5 | // source: demo.proto 6 | 7 | package demo 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | emptypb "google.golang.org/protobuf/types/known/emptypb" 13 | reflect "reflect" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | var File_demo_proto protoreflect.FileDescriptor 24 | 25 | var file_demo_proto_rawDesc = []byte{ 26 | 0x0a, 0x0a, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x64, 0x65, 27 | 0x6d, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 28 | 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 29 | 0x46, 0x0a, 0x0b, 0x44, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 30 | 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 31 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 32 | 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 33 | 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 34 | 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x76, 0x2f, 0x67, 0x6f, 0x2d, 35 | 0x67, 0x72, 0x70, 0x63, 0x2d, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x7a, 0x3b, 0x64, 0x65, 36 | 0x6d, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 37 | } 38 | 39 | var file_demo_proto_goTypes = []interface{}{ 40 | (*emptypb.Empty)(nil), // 0: google.protobuf.Empty 41 | } 42 | var file_demo_proto_depIdxs = []int32{ 43 | 0, // 0: demo.DemoService.Hello:input_type -> google.protobuf.Empty 44 | 0, // 1: demo.DemoService.Hello:output_type -> google.protobuf.Empty 45 | 1, // [1:2] is the sub-list for method output_type 46 | 0, // [0:1] is the sub-list for method input_type 47 | 0, // [0:0] is the sub-list for extension type_name 48 | 0, // [0:0] is the sub-list for extension extendee 49 | 0, // [0:0] is the sub-list for field type_name 50 | } 51 | 52 | func init() { file_demo_proto_init() } 53 | func file_demo_proto_init() { 54 | if File_demo_proto != nil { 55 | return 56 | } 57 | type x struct{} 58 | out := protoimpl.TypeBuilder{ 59 | File: protoimpl.DescBuilder{ 60 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 61 | RawDescriptor: file_demo_proto_rawDesc, 62 | NumEnums: 0, 63 | NumMessages: 0, 64 | NumExtensions: 0, 65 | NumServices: 1, 66 | }, 67 | GoTypes: file_demo_proto_goTypes, 68 | DependencyIndexes: file_demo_proto_depIdxs, 69 | }.Build() 70 | File_demo_proto = out.File 71 | file_demo_proto_rawDesc = nil 72 | file_demo_proto_goTypes = nil 73 | file_demo_proto_depIdxs = nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/generated/service/demo_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package demo 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | emptypb "google.golang.org/protobuf/types/known/emptypb" 11 | ) 12 | 13 | // This is a compile-time assertion to ensure that this generated file 14 | // is compatible with the grpc package it is being compiled against. 15 | // Requires gRPC-Go v1.32.0 or later. 16 | const _ = grpc.SupportPackageIsVersion7 17 | 18 | // DemoServiceClient is the client API for DemoService service. 19 | // 20 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 21 | type DemoServiceClient interface { 22 | Hello(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) 23 | } 24 | 25 | type demoServiceClient struct { 26 | cc grpc.ClientConnInterface 27 | } 28 | 29 | func NewDemoServiceClient(cc grpc.ClientConnInterface) DemoServiceClient { 30 | return &demoServiceClient{cc} 31 | } 32 | 33 | func (c *demoServiceClient) Hello(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { 34 | out := new(emptypb.Empty) 35 | err := c.cc.Invoke(ctx, "/demo.DemoService/Hello", in, out, opts...) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return out, nil 40 | } 41 | 42 | // DemoServiceServer is the server API for DemoService service. 43 | // All implementations must embed UnimplementedDemoServiceServer 44 | // for forward compatibility 45 | type DemoServiceServer interface { 46 | Hello(context.Context, *emptypb.Empty) (*emptypb.Empty, error) 47 | mustEmbedUnimplementedDemoServiceServer() 48 | } 49 | 50 | // UnimplementedDemoServiceServer must be embedded to have forward compatible implementations. 51 | type UnimplementedDemoServiceServer struct { 52 | } 53 | 54 | func (UnimplementedDemoServiceServer) Hello(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { 55 | return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented") 56 | } 57 | func (UnimplementedDemoServiceServer) mustEmbedUnimplementedDemoServiceServer() {} 58 | 59 | // UnsafeDemoServiceServer may be embedded to opt out of forward compatibility for this service. 60 | // Use of this interface is not recommended, as added methods to DemoServiceServer will 61 | // result in compilation errors. 62 | type UnsafeDemoServiceServer interface { 63 | mustEmbedUnimplementedDemoServiceServer() 64 | } 65 | 66 | func RegisterDemoServiceServer(s grpc.ServiceRegistrar, srv DemoServiceServer) { 67 | s.RegisterService(&DemoService_ServiceDesc, srv) 68 | } 69 | 70 | func _DemoService_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 71 | in := new(emptypb.Empty) 72 | if err := dec(in); err != nil { 73 | return nil, err 74 | } 75 | if interceptor == nil { 76 | return srv.(DemoServiceServer).Hello(ctx, in) 77 | } 78 | info := &grpc.UnaryServerInfo{ 79 | Server: srv, 80 | FullMethod: "/demo.DemoService/Hello", 81 | } 82 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 83 | return srv.(DemoServiceServer).Hello(ctx, req.(*emptypb.Empty)) 84 | } 85 | return interceptor(ctx, in, info, handler) 86 | } 87 | 88 | // DemoService_ServiceDesc is the grpc.ServiceDesc for DemoService service. 89 | // It's only intended for direct use with grpc.RegisterService, 90 | // and not to be introspected or modified (even as a copy) 91 | var DemoService_ServiceDesc = grpc.ServiceDesc{ 92 | ServiceName: "demo.DemoService", 93 | HandlerType: (*DemoServiceServer)(nil), 94 | Methods: []grpc.MethodDesc{ 95 | { 96 | MethodName: "Hello", 97 | Handler: _DemoService_Hello_Handler, 98 | }, 99 | }, 100 | Streams: []grpc.StreamDesc{}, 101 | Metadata: "demo.proto", 102 | } 103 | -------------------------------------------------------------------------------- /internal/proto/demo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | import "google/protobuf/empty.proto"; 3 | 4 | package demo; 5 | 6 | option go_package = "github.com/rantav/go-grpc-channelz;demo"; 7 | 8 | service DemoService { 9 | rpc Hello(google.protobuf.Empty) returns (google.protobuf.Empty); 10 | } 11 | -------------------------------------------------------------------------------- /mock-channelz-client_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | channelzclient "google.golang.org/grpc/channelz/grpc_channelz_v1" 8 | "google.golang.org/protobuf/types/known/timestamppb" 9 | "google.golang.org/protobuf/types/known/wrapperspb" 10 | ) 11 | 12 | type mockChannelzClient struct{} 13 | 14 | func (m *mockChannelzClient) GetTopChannels( 15 | ctx context.Context, 16 | in *channelzclient.GetTopChannelsRequest, 17 | opts ...grpc.CallOption) (*channelzclient.GetTopChannelsResponse, error) { 18 | return &channelzclient.GetTopChannelsResponse{ 19 | Channel: []*channelzclient.Channel{ 20 | createMockChannel(), 21 | }, 22 | }, nil 23 | } 24 | 25 | func (m *mockChannelzClient) GetServers( 26 | ctx context.Context, 27 | in *channelzclient.GetServersRequest, 28 | opts ...grpc.CallOption) (*channelzclient.GetServersResponse, error) { 29 | return &channelzclient.GetServersResponse{ 30 | Server: []*channelzclient.Server{ 31 | createMockServer(), 32 | }, 33 | }, nil 34 | } 35 | 36 | func (m *mockChannelzClient) GetServer( 37 | ctx context.Context, 38 | in *channelzclient.GetServerRequest, 39 | opts ...grpc.CallOption) (*channelzclient.GetServerResponse, error) { 40 | return &channelzclient.GetServerResponse{ 41 | Server: createMockServer(), 42 | }, nil 43 | } 44 | 45 | func (m *mockChannelzClient) GetServerSockets( 46 | ctx context.Context, 47 | in *channelzclient.GetServerSocketsRequest, 48 | opts ...grpc.CallOption) (*channelzclient.GetServerSocketsResponse, error) { 49 | return nil, nil 50 | } 51 | 52 | func (m *mockChannelzClient) GetChannel( 53 | ctx context.Context, 54 | in *channelzclient.GetChannelRequest, 55 | opts ...grpc.CallOption) (*channelzclient.GetChannelResponse, error) { 56 | return &channelzclient.GetChannelResponse{ 57 | Channel: createMockChannel(), 58 | }, nil 59 | } 60 | 61 | func (m *mockChannelzClient) GetSubchannel( 62 | ctx context.Context, in *channelzclient.GetSubchannelRequest, 63 | opts ...grpc.CallOption) (*channelzclient.GetSubchannelResponse, error) { 64 | return &channelzclient.GetSubchannelResponse{ 65 | Subchannel: createMockSubchannel(), 66 | }, nil 67 | } 68 | 69 | func (m *mockChannelzClient) GetSocket( 70 | ctx context.Context, 71 | in *channelzclient.GetSocketRequest, 72 | opts ...grpc.CallOption) (*channelzclient.GetSocketResponse, error) { 73 | return &channelzclient.GetSocketResponse{ 74 | Socket: createMockSocket(), 75 | }, nil 76 | } 77 | 78 | func createMockSubchannel() *channelzclient.Subchannel { 79 | return &channelzclient.Subchannel{ 80 | Ref: &channelzclient.SubchannelRef{ 81 | SubchannelId: 4, 82 | Name: "four", 83 | }, 84 | Data: createMockChannelData(), 85 | ChannelRef: []*channelzclient.ChannelRef{}, 86 | SubchannelRef: []*channelzclient.SubchannelRef{}, 87 | SocketRef: []*channelzclient.SocketRef{{ 88 | SocketId: 9, 89 | Name: "nine", 90 | }}, 91 | } 92 | } 93 | 94 | func createMockChannelTrace() *channelzclient.ChannelTrace { 95 | return &channelzclient.ChannelTrace{ 96 | NumEventsLogged: 5, 97 | CreationTimestamp: ×tamppb.Timestamp{ 98 | Seconds: 6, 99 | Nanos: 7, 100 | }, 101 | Events: []*channelzclient.ChannelTraceEvent{{ 102 | Description: "setup", 103 | Severity: channelzclient.ChannelTraceEvent_CT_INFO, 104 | Timestamp: ×tamppb.Timestamp{ 105 | Seconds: 6, 106 | Nanos: 7, 107 | }, 108 | }}, 109 | } 110 | } 111 | func createMockChannelData() *channelzclient.ChannelData { 112 | return &channelzclient.ChannelData{ 113 | State: &channelzclient.ChannelConnectivityState{ 114 | State: channelzclient.ChannelConnectivityState_CONNECTING, 115 | }, 116 | Target: "the world", 117 | Trace: createMockChannelTrace(), 118 | CallsStarted: 1, 119 | CallsSucceeded: 2, 120 | CallsFailed: 0, 121 | LastCallStartedTimestamp: ×tamppb.Timestamp{ 122 | Seconds: 6, 123 | Nanos: 7, 124 | }, 125 | } 126 | } 127 | func createMockChannel() *channelzclient.Channel { 128 | return &channelzclient.Channel{ 129 | Ref: &channelzclient.ChannelRef{ 130 | ChannelId: 5, 131 | Name: "five", 132 | }, 133 | Data: createMockChannelData(), 134 | ChannelRef: []*channelzclient.ChannelRef{{ 135 | ChannelId: 7, 136 | Name: "seven", 137 | }}, 138 | SubchannelRef: []*channelzclient.SubchannelRef{{ 139 | SubchannelId: 8, 140 | Name: "eight", 141 | }}, 142 | } 143 | } 144 | 145 | func createMockServer() *channelzclient.Server { 146 | return &channelzclient.Server{ 147 | Ref: &channelzclient.ServerRef{ 148 | ServerId: 1, 149 | Name: "one", 150 | }, 151 | Data: &channelzclient.ServerData{ 152 | Trace: createMockChannelTrace(), 153 | CallsStarted: 1, 154 | CallsSucceeded: 1, 155 | CallsFailed: 0, 156 | LastCallStartedTimestamp: ×tamppb.Timestamp{ 157 | Seconds: 6, 158 | Nanos: 7, 159 | }, 160 | }, 161 | ListenSocket: []*channelzclient.SocketRef{{ 162 | SocketId: 6, 163 | Name: "six", 164 | }}, 165 | } 166 | } 167 | 168 | func createMockSocket() *channelzclient.Socket { 169 | return &channelzclient.Socket{ 170 | Ref: &channelzclient.SocketRef{ 171 | SocketId: 1, 172 | Name: "one", 173 | }, 174 | Data: &channelzclient.SocketData{ 175 | StreamsStarted: 5, 176 | StreamsSucceeded: 6, 177 | StreamsFailed: 2, 178 | MessagesSent: 3, 179 | MessagesReceived: 7, 180 | KeepAlivesSent: 9, 181 | LastLocalStreamCreatedTimestamp: ×tamppb.Timestamp{Seconds: 6, Nanos: 7}, 182 | LastRemoteStreamCreatedTimestamp: ×tamppb.Timestamp{Seconds: 6, Nanos: 7}, 183 | LastMessageSentTimestamp: ×tamppb.Timestamp{Seconds: 6, Nanos: 7}, 184 | LastMessageReceivedTimestamp: ×tamppb.Timestamp{Seconds: 6, Nanos: 7}, 185 | LocalFlowControlWindow: &wrapperspb.Int64Value{Value: 6}, 186 | RemoteFlowControlWindow: &wrapperspb.Int64Value{Value: 99}, 187 | Option: []*channelzclient.SocketOption{{Name: "hello", Value: "world"}}, 188 | }, 189 | Local: &channelzclient.Address{}, 190 | Remote: &channelzclient.Address{}, 191 | Security: &channelzclient.Security{}, 192 | RemoteName: "wowa", 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "path" 7 | "strconv" 8 | 9 | "github.com/go-chi/chi/v5" 10 | log "google.golang.org/grpc/grpclog" 11 | ) 12 | 13 | type channelzHandler interface { 14 | WriteTopChannelsPage(io.Writer) 15 | WriteChannelsPage(io.Writer, int64) 16 | WriteChannelPage(io.Writer, int64) 17 | WriteSubchannelPage(io.Writer, int64) 18 | WriteServerPage(io.Writer, int64) 19 | WriteSocketPage(io.Writer, int64) 20 | } 21 | 22 | var pathPrefix string 23 | 24 | func createRouter(prefix string, handler channelzHandler) *chi.Mux { 25 | pathPrefix = prefix 26 | router := chi.NewRouter() 27 | router.Route(prefix, func(r chi.Router) { 28 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 29 | handler.WriteTopChannelsPage(w) 30 | }) 31 | r.Get("/channel/{channel}", func(w http.ResponseWriter, r *http.Request) { 32 | channelStr := chi.URLParam(r, "channel") 33 | channel, err := strconv.ParseInt(channelStr, 10, 0) 34 | if err != nil { 35 | log.Errorf("channelz: Unable to parse int for channel ID. %s", channelStr) 36 | return 37 | } 38 | handler.WriteChannelPage(w, channel) 39 | }) 40 | r.Get("/channels", func(w http.ResponseWriter, r *http.Request) { 41 | startStr := r.URL.Query().Get("start") 42 | start, err := strconv.ParseInt(startStr, 10, 0) 43 | if err != nil { 44 | log.Errorf("channelz: Unable to parse int for start channel ID. %s", startStr) 45 | return 46 | } 47 | handler.WriteChannelsPage(w, start) 48 | }) 49 | r.Get("/subchannel/{channel}", func(w http.ResponseWriter, r *http.Request) { 50 | channelStr := chi.URLParam(r, "channel") 51 | channel, err := strconv.ParseInt(channelStr, 10, 0) 52 | if err != nil { 53 | log.Errorf("channelz: Unable to parse int for sub-channel ID. %s", channelStr) 54 | return 55 | } 56 | handler.WriteSubchannelPage(w, channel) 57 | }) 58 | r.Get("/server/{server}", func(w http.ResponseWriter, r *http.Request) { 59 | serverStr := chi.URLParam(r, "server") 60 | server, err := strconv.ParseInt(serverStr, 10, 0) 61 | if err != nil { 62 | log.Errorf("channelz: Unable to parse int for server ID. %s", serverStr) 63 | return 64 | } 65 | handler.WriteServerPage(w, server) 66 | }) 67 | r.Get("/socket/{socket}", func(w http.ResponseWriter, r *http.Request) { 68 | socketStr := chi.URLParam(r, "socket") 69 | socket, err := strconv.ParseInt(socketStr, 10, 0) 70 | if err != nil { 71 | log.Errorf("channelz: Unable to parse int for socket ID. %s", socketStr) 72 | return 73 | } 74 | handler.WriteSocketPage(w, socket) 75 | }) 76 | }) 77 | return router 78 | } 79 | 80 | func createHyperlink(parts ...interface{}) string { 81 | asStrings := []string{"/" + pathPrefix} 82 | for _, p := range parts { 83 | switch t := p.(type) { 84 | case string: 85 | asStrings = append(asStrings, t) 86 | case int: 87 | s := strconv.Itoa(t) 88 | asStrings = append(asStrings, s) 89 | case int64: 90 | s := strconv.FormatInt(t, 10) 91 | asStrings = append(asStrings, s) 92 | } 93 | } 94 | return path.Join(asStrings...) 95 | } 96 | -------------------------------------------------------------------------------- /routes_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type mockHandler struct { 16 | } 17 | 18 | func (m *mockHandler) WriteTopChannelsPage(w io.Writer) { 19 | // nolint:errcheck 20 | w.Write([]byte("top")) 21 | } 22 | func (m *mockHandler) WriteChannelsPage(w io.Writer, c int64) { 23 | // nolint:errcheck 24 | w.Write([]byte(fmt.Sprintf("channels %d", c))) 25 | } 26 | func (m *mockHandler) WriteChannelPage(w io.Writer, c int64) { 27 | // nolint:errcheck 28 | w.Write([]byte(fmt.Sprintf("channel %d", c))) 29 | } 30 | func (m *mockHandler) WriteSubchannelPage(w io.Writer, c int64) { 31 | // nolint:errcheck 32 | w.Write([]byte(fmt.Sprintf("subchannel %d", c))) 33 | } 34 | func (m *mockHandler) WriteServerPage(w io.Writer, c int64) { 35 | // nolint:errcheck 36 | w.Write([]byte(fmt.Sprintf("server %d", c))) 37 | } 38 | func (m *mockHandler) WriteSocketPage(w io.Writer, c int64) { 39 | // nolint:errcheck 40 | w.Write([]byte(fmt.Sprintf("socket %d", c))) 41 | } 42 | 43 | func TestCreateRouter(t *testing.T) { 44 | assert := assert.New(t) 45 | require := require.New(t) 46 | 47 | r := createRouter("/channelz", &mockHandler{}) 48 | assert.NotNil(r) 49 | ts := httptest.NewServer(r) 50 | defer ts.Close() 51 | 52 | expects := map[string]string{ 53 | "/channelz": "top", 54 | "/channelz/channel/4": "channel 4", 55 | "/channelz/channels?start=4": "channels 4", 56 | "/channelz/subchannel/5": "subchannel 5", 57 | "/channelz/server/3": "server 3", 58 | "/channelz/socket/3": "socket 3", 59 | 60 | // Non matched or errornous paths 61 | "/channelz/channel/x": "", 62 | "/channelz/subchannel/x": "", 63 | "/channelz/server/x": "", 64 | "/channelx": "404 page not found\n", 65 | } 66 | for route, expected := range expects { 67 | res, err := http.Get(ts.URL + route) 68 | require.NoError(err) 69 | 70 | response, err := ioutil.ReadAll(res.Body) 71 | require.NoError(err) 72 | res.Body.Close() 73 | 74 | assert.Equal([]byte(expected), response, "For path %s the expected result was %q, but instead we got %q", 75 | route, expected, response) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /server-page.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 9 | log "google.golang.org/grpc/grpclog" 10 | ) 11 | 12 | func (h *grpcChannelzHandler) WriteServerPage(w io.Writer, server int64) { 13 | writeHeader(w, fmt.Sprintf("ChannelZ server %d", server)) 14 | h.writeServer(w, server) 15 | writeFooter(w) 16 | } 17 | 18 | // writeServer writes HTML to w containing RPC single server stats. 19 | // 20 | // It includes neither a header nor footer, so you can embed this data in other pages. 21 | func (h *grpcChannelzHandler) writeServer(w io.Writer, server int64) { 22 | if err := serverTemplate.Execute(w, h.getServer(server)); err != nil { 23 | log.Errorf("channelz: executing template: %v", err) 24 | } 25 | } 26 | 27 | func (h *grpcChannelzHandler) getServer(serverID int64) *channelzgrpc.GetServerResponse { 28 | client, err := h.connect() 29 | if err != nil { 30 | log.Errorf("Error creating channelz client %+v", err) 31 | return nil 32 | } 33 | ctx := context.Background() 34 | server, err := client.GetServer(ctx, &channelzgrpc.GetServerRequest{ServerId: serverID}) 35 | if err != nil { 36 | log.Errorf("Error querying GetServer %+v", err) 37 | return nil 38 | } 39 | return server 40 | } 41 | 42 | const serverTemplateHTML = ` 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | {{with .Server.Data.Trace}} 81 | 82 | 83 | 90 | 91 | {{end}} 92 |
ServerId{{.Server.Ref.ServerId}}
Server Name{{.Server.Ref.Name}}
CreationTimestamp{{with .Server.Data.Trace}} {{.CreationTimestamp | timestamp}} {{end}}
CallsStarted{{.Server.Data.CallsStarted}}
CallsSucceeded{{.Server.Data.CallsSucceeded}}
CallsFailed{{.Server.Data.CallsFailed}}
LastCallStartedTimestamp{{.Server.Data.LastCallStartedTimestamp | timestamp}}
Sockets 75 | {{range .Server.ListenSocket}} 76 | {{.SocketId}} {{.Name}}
77 | {{end}} 78 |
Events 84 |
85 | 				{{- range .Events}}
86 | {{.Severity}} [{{.Timestamp | timestamp}}]: {{.Description}}
87 | 				{{- end -}}
88 | 				
89 |
93 | ` 94 | -------------------------------------------------------------------------------- /server-page_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWriteServerPage(t *testing.T) { 11 | assert := assert.New(t) 12 | handler := grpcChannelzHandler{client: &mockChannelzClient{}} 13 | var b strings.Builder 14 | handler.WriteServerPage(&b, 6) 15 | assert.Contains(b.String(), `ChannelZ server 6`) 16 | assert.Contains(b.String(), `CT_INFO [1970-01-01T00:00:06Z]: setup`) 17 | } 18 | -------------------------------------------------------------------------------- /servers-page.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 8 | log "google.golang.org/grpc/grpclog" 9 | ) 10 | 11 | // writeServers writes HTML to w containing RPC servers stats. 12 | // 13 | // It includes neither a header nor footer, so you can embed this data in other pages. 14 | func (h *grpcChannelzHandler) writeServers(w io.Writer) { 15 | if err := serversTemplate.Execute(w, h.getServers()); err != nil { 16 | log.Errorf("channelz: executing template: %v", err) 17 | } 18 | } 19 | 20 | func (h *grpcChannelzHandler) getServers() *channelzgrpc.GetServersResponse { 21 | client, err := h.connect() 22 | if err != nil { 23 | log.Errorf("Error creating channelz client %+v", err) 24 | return nil 25 | } 26 | ctx := context.Background() 27 | servers, err := client.GetServers(ctx, &channelzgrpc.GetServersRequest{}) 28 | if err != nil { 29 | log.Errorf("Error querying GetServers %+v", err) 30 | return nil 31 | } 32 | return servers 33 | } 34 | 35 | const serversTemplateHTML = ` 36 | {{define "server-header"}} 37 | 38 | Server 39 | CreationTimestamp 40 | CallsStarted 41 | CallsSucceeded 42 | CallsFailed 43 | LastCallStartedTimestamp 44 | Sockets 45 | 46 | {{end}} 47 | 48 | {{define "server-body"}} 49 | 50 | {{.Ref.ServerId}} {{.Ref.Name}} 51 | {{with .Data.Trace}} {{.CreationTimestamp | timestamp}} {{end}} 52 | {{.Data.CallsStarted}} 53 | {{.Data.CallsSucceeded}} 54 | {{.Data.CallsFailed}} 55 | {{.Data.LastCallStartedTimestamp | timestamp}} 56 | 57 | {{range .ListenSocket}} 58 | {{.SocketId}} {{.Name}}
59 | {{end}} 60 | 61 | 62 | {{with .Data.Trace}} 63 | 64 | Events 65 | 66 | 67 |   68 | 69 |
70 | 				{{- range .Events}}
71 | {{.Severity}} [{{.Timestamp | timestamp}}]: {{.Description}}
72 | 				{{- end -}}
73 | 				
74 | 75 | 76 | {{end}} 77 | {{end}} 78 | 79 |

Servers

80 | 81 | 82 | 83 | 84 | 85 | {{template "server-header"}} 86 | {{range .Server}} 87 | {{template "server-body" .}} 88 | {{end}} 89 |
Servers: {{.Server | len}}
90 | ` 91 | -------------------------------------------------------------------------------- /socket-page.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 9 | log "google.golang.org/grpc/grpclog" 10 | ) 11 | 12 | func (h *grpcChannelzHandler) WriteSocketPage(w io.Writer, socket int64) { 13 | writeHeader(w, fmt.Sprintf("ChannelZ socket %d", socket)) 14 | h.writeSocket(w, socket) 15 | writeFooter(w) 16 | } 17 | 18 | // writeSocket writes HTML to w containing RPC single socket stats. 19 | // 20 | // It includes neither a header nor footer, so you can embed this data in other pages. 21 | func (h *grpcChannelzHandler) writeSocket(w io.Writer, socket int64) { 22 | if err := socketTemplate.Execute(w, h.getSocket(socket)); err != nil { 23 | log.Errorf("channelz: executing template: %v", err) 24 | } 25 | } 26 | 27 | func (h *grpcChannelzHandler) getSocket(socketID int64) *channelzgrpc.GetSocketResponse { 28 | client, err := h.connect() 29 | if err != nil { 30 | log.Errorf("Error creating channelz client %+v", err) 31 | return nil 32 | } 33 | ctx := context.Background() 34 | socket, err := client.GetSocket(ctx, &channelzgrpc.GetSocketRequest{SocketId: socketID}) 35 | if err != nil { 36 | log.Errorf("Error querying GetSocket %+v", err) 37 | return nil 38 | } 39 | return socket 40 | } 41 | 42 | const socketTemplateHTML = ` 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 117 | 118 | 119 | 120 | 121 | 122 |
SocketId 47 | {{.Socket.Ref.SocketId}} 48 |
Socket Name 53 | {{.Socket.Ref.Name}} 54 |
Socket Local -> Remote 59 |
{{.Socket.Local}} -> {{.Socket.Remote}} {{with .Socket.RemoteName}}({{.}}){{end}}
60 |
StreamsStarted{{.Socket.Data.StreamsStarted}}
StreamsSucceeded{{.Socket.Data.StreamsSucceeded}}
StreamsFailed{{.Socket.Data.StreamsFailed}}
MessagesSent{{.Socket.Data.MessagesSent}}
MessagesReceived{{.Socket.Data.MessagesReceived}}
KeepAlivesSent{{.Socket.Data.KeepAlivesSent}}
LastLocalStreamCreated{{.Socket.Data.LastLocalStreamCreatedTimestamp | timestamp}}
LastRemoteStreamCreated{{.Socket.Data.LastRemoteStreamCreatedTimestamp | timestamp}}
LastMessageSent{{.Socket.Data.LastMessageSentTimestamp | timestamp}}
LastMessageReceived{{.Socket.Data.LastMessageReceivedTimestamp | timestamp}}
LocalFlowControlWindow{{.Socket.Data.LocalFlowControlWindow.Value}}
RemoteFlowControlWindow{{.Socket.Data.RemoteFlowControlWindow.Value}}
Options 113 | {{range .Socket.Data.Option}} 114 | {{.Name}}: {{.Value}} {{with .Additional}}({{.}}){{end}}
115 | {{end}} 116 |
Security{{.Socket.Security}}
123 | ` 124 | -------------------------------------------------------------------------------- /socket-page_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWriteSocketPage(t *testing.T) { 11 | assert := assert.New(t) 12 | handler := grpcChannelzHandler{client: &mockChannelzClient{}} 13 | var b strings.Builder 14 | handler.WriteSocketPage(&b, 9) 15 | assert.Contains(b.String(), `ChannelZ socket 9`) 16 | assert.Contains(b.String(), `hello: world`) 17 | } 18 | -------------------------------------------------------------------------------- /sub-channel-page.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1" 9 | log "google.golang.org/grpc/grpclog" 10 | ) 11 | 12 | // WriteSubchannelsPage writes an HTML document to w containing per-channel RPC stats, including a header and a footer. 13 | func (h *grpcChannelzHandler) WriteSubchannelPage(w io.Writer, subchannel int64) { 14 | writeHeader(w, fmt.Sprintf("ChannelZ subchannel %d", subchannel)) 15 | h.writeSubchannel(w, subchannel) 16 | writeFooter(w) 17 | } 18 | 19 | // writeSubchannel writes HTML to w containing sub-channel RPC stats. 20 | // 21 | // It includes neither a header nor footer, so you can embed this data in other pages. 22 | func (h *grpcChannelzHandler) writeSubchannel(w io.Writer, subchannel int64) { 23 | if err := subChannelTemplate.Execute(w, h.getSubchannel(subchannel)); err != nil { 24 | log.Errorf("channelz: executing template: %v", err) 25 | } 26 | } 27 | 28 | func (h *grpcChannelzHandler) getSubchannel(subchannelID int64) *channelzgrpc.GetSubchannelResponse { 29 | client, err := h.connect() 30 | if err != nil { 31 | log.Errorf("Error creating channelz client %+v", err) 32 | return nil 33 | } 34 | ctx := context.Background() 35 | subchannel, err := client.GetSubchannel(ctx, &channelzgrpc.GetSubchannelRequest{ 36 | SubchannelId: subchannelID, 37 | }) 38 | if err != nil { 39 | log.Errorf("Error querying GetSubchannel %+v", err) 40 | return nil 41 | } 42 | return subchannel 43 | } 44 | 45 | const subChannelsTemplateHTML = ` 46 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 90 | 91 | 92 | 93 | 98 | 99 | 100 | 101 | 106 | 107 | 108 | 109 | 116 | 117 |
Subchannel 50 | 51 | {{.Subchannel.Ref.SubchannelId}} {{.Subchannel.Ref.Name}} 52 | 53 |
State{{.Subchannel.Data.State}}
Target{{.Subchannel.Data.Target}}
CreationTimestamp{{.Subchannel.Data.Trace.CreationTimestamp | timestamp}}
CallsStarted{{.Subchannel.Data.CallsStarted}}
CallsSucceeded{{.Subchannel.Data.CallsSucceeded}}
CallsFailed{{.Subchannel.Data.CallsFailed}}
LastCallStartedTimestamp{{.Subchannel.Data.LastCallStartedTimestamp | timestamp}}
Child Channels 86 | {{range .Subchannel.ChannelRef}} 87 | {{.ChannelId}} {{.Name}}
88 | {{end}} 89 |
Child Subchannels 94 | {{range .Subchannel.SubchannelRef}} 95 | {{.SubchannelId}} {{.Name}}
96 | {{end}} 97 |
Socket 102 | {{range .Subchannel.SocketRef}} 103 | {{.SocketId}} {{.Name}}
104 | {{end}} 105 |
Events 110 |
111 | 			{{- range .Subchannel.Data.Trace.Events}}
112 | {{.Severity}} [{{.Timestamp | timestamp}}]: {{.Description}}
113 | 			{{- end -}}
114 | 			
115 |
118 | ` 119 | -------------------------------------------------------------------------------- /subchannel-page_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWriteSubhannelPage(t *testing.T) { 11 | assert := assert.New(t) 12 | handler := grpcChannelzHandler{client: &mockChannelzClient{}} 13 | var b strings.Builder 14 | handler.WriteSubchannelPage(&b, 2) 15 | assert.Contains(b.String(), "subchannel 2") 16 | assert.Contains(b.String(), "CT_INFO [1970-01-01T00:00:06Z]: setup") 17 | } 18 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "io" 5 | "text/template" 6 | "time" 7 | 8 | log "google.golang.org/grpc/grpclog" 9 | "google.golang.org/protobuf/types/known/timestamppb" 10 | ) 11 | 12 | var ( 13 | common *template.Template 14 | headerTemplate = parseTemplate("header", headerTemplateHTML) 15 | channelsTemplate = parseTemplate("channels", channelsTemplateHTML) 16 | subChannelTemplate = parseTemplate("subchannel", subChannelsTemplateHTML) 17 | channelTemplate = parseTemplate("channel", channelTemplateHTML) 18 | serversTemplate = parseTemplate("servers", serversTemplateHTML) 19 | serverTemplate = parseTemplate("server", serverTemplateHTML) 20 | socketTemplate = parseTemplate("socket", socketTemplateHTML) 21 | footerTemplate = parseTemplate("footer", footerTemplateHTML) 22 | ) 23 | 24 | func parseTemplate(name, html string) *template.Template { 25 | if common == nil { 26 | common = template.Must(template.New(name).Funcs(getFuncs()).Parse(html)) 27 | return common 28 | } 29 | common = template.Must(common.New(name).Funcs(getFuncs()).Parse(html)) 30 | return common 31 | } 32 | 33 | func getFuncs() template.FuncMap { 34 | return template.FuncMap{ 35 | "timestamp": formatTimestamp, 36 | "link": createHyperlink, 37 | } 38 | } 39 | 40 | func formatTimestamp(ts *timestamppb.Timestamp) string { 41 | return ts.AsTime().Format(time.RFC3339) 42 | } 43 | 44 | func writeHeader(w io.Writer, title string) { 45 | if err := headerTemplate.Execute(w, headerData{Title: title}); err != nil { 46 | log.Errorf("channelz: executing template: %v", err) 47 | } 48 | } 49 | 50 | func writeFooter(w io.Writer) { 51 | if err := footerTemplate.Execute(w, nil); err != nil { 52 | log.Errorf("channelz: executing template: %v", err) 53 | } 54 | } 55 | 56 | // headerData contains data for the header template. 57 | type headerData struct { 58 | Title string 59 | } 60 | 61 | var ( 62 | headerTemplateHTML = ` 63 | 64 | 65 | 66 | {{.Title}} 67 | 68 | 69 | 92 | 93 | 94 |

{{.Title}}

95 | ` 96 | 97 | footerTemplateHTML = ` 98 | 101 | 102 | 103 | ` 104 | ) 105 | -------------------------------------------------------------------------------- /top-channels-page.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // WriteTopChannelsPage writes an HTML document to w containing per-channel RPC stats, including a header and a footer. 8 | func (h *grpcChannelzHandler) WriteTopChannelsPage(w io.Writer) { 9 | writeHeader(w, "ChannelZ Stats") 10 | h.writeChannels(w, 0) 11 | h.writeServers(w) 12 | writeFooter(w) 13 | } 14 | -------------------------------------------------------------------------------- /top-channels-page_test.go: -------------------------------------------------------------------------------- 1 | package channelz 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWriteTopChannelsPage(t *testing.T) { 11 | assert := assert.New(t) 12 | handler := grpcChannelzHandler{client: &mockChannelzClient{}} 13 | var b strings.Builder 14 | handler.WriteTopChannelsPage(&b) 15 | assert.Contains(b.String(), `8 eight`) 16 | assert.Contains(b.String(), `1 one`) 17 | } 18 | --------------------------------------------------------------------------------