├── .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 | 
20 |
21 | 
22 |
23 | 
24 |
25 | 
26 |
27 | 
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 | ChannelId |
44 | {{.Channel.Ref.ChannelId}}
45 | |
46 |
47 | Channel Name |
48 | {{.Channel.Ref.Name}} |
49 |
50 |
51 | State |
52 | {{.Channel.Data.State}} |
53 |
54 |
55 | Target |
56 | {{.Channel.Data.Target}} |
57 |
58 |
59 | Subchannels |
60 |
61 | {{range .Channel.SubchannelRef}}
62 | {{.SubchannelId}} {{.Name}}
63 | {{end}}
64 | |
65 |
66 |
67 | Child Channels |
68 |
69 | {{range .Channel.ChannelRef}}
70 | {{.ChannelId}} {{.Name}}
71 | {{end}}
72 | |
73 |
74 |
75 | Sockets |
76 |
77 | {{range .Channel.SocketRef}}
78 | {{.SocketId}} {{.Name}}
79 | {{end}}
80 | |
81 |
82 |
83 | CreationTimestamp |
84 | {{.Channel.Data.Trace.CreationTimestamp | timestamp}} |
85 |
86 |
87 | CallsStarted |
88 | {{.Channel.Data.CallsStarted}} |
89 |
90 |
91 | CallsSucceeded |
92 | {{.Channel.Data.CallsSucceeded}} |
93 |
94 |
95 | CallsFailed |
96 | {{.Channel.Data.CallsFailed}} |
97 |
98 |
99 | LastCallStartedTimestamp |
100 | {{.Channel.Data.LastCallStartedTimestamp | timestamp}} |
101 |
102 |
103 | Events |
104 |
105 |
106 | {{- range .Channel.Data.Trace.Events}}
107 | {{.Severity}} [{{.Timestamp | timestamp}}]: {{.Description}}
108 | {{- end -}}
109 |
110 | |
111 |
112 |
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 |
89 |
90 |
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 |
103 | Next >
104 | |
105 |
106 | {{end}}
107 |
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 | ServerId |
46 | {{.Server.Ref.ServerId}} |
47 |
48 |
49 | Server Name |
50 | {{.Server.Ref.Name}} |
51 |
52 |
53 | CreationTimestamp |
54 | {{with .Server.Data.Trace}} {{.CreationTimestamp | timestamp}} {{end}} |
55 |
56 |
57 | CallsStarted |
58 | {{.Server.Data.CallsStarted}} |
59 |
60 |
61 | CallsSucceeded |
62 | {{.Server.Data.CallsSucceeded}} |
63 |
64 |
65 | CallsFailed |
66 | {{.Server.Data.CallsFailed}} |
67 |
68 |
69 | LastCallStartedTimestamp |
70 | {{.Server.Data.LastCallStartedTimestamp | timestamp}} |
71 |
72 |
73 | Sockets |
74 |
75 | {{range .Server.ListenSocket}}
76 | {{.SocketId}} {{.Name}}
77 | {{end}}
78 | |
79 |
80 | {{with .Server.Data.Trace}}
81 |
82 | Events |
83 |
84 |
85 | {{- range .Events}}
86 | {{.Severity}} [{{.Timestamp | timestamp}}]: {{.Description}}
87 | {{- end -}}
88 |
89 | |
90 |
91 | {{end}}
92 |
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 |
80 |
81 |
84 |
85 | {{template "server-header"}}
86 | {{range .Server}}
87 | {{template "server-body" .}}
88 | {{end}}
89 |
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 | SocketId |
46 |
47 | {{.Socket.Ref.SocketId}}
48 | |
49 |
50 |
51 | Socket Name |
52 |
53 | {{.Socket.Ref.Name}}
54 | |
55 |
56 |
57 | Socket Local -> Remote |
58 |
59 | {{.Socket.Local}} -> {{.Socket.Remote}} {{with .Socket.RemoteName}}({{.}}){{end}}
60 | |
61 |
62 |
63 | StreamsStarted |
64 | {{.Socket.Data.StreamsStarted}} |
65 |
66 |
67 | StreamsSucceeded |
68 | {{.Socket.Data.StreamsSucceeded}} |
69 |
70 |
71 | StreamsFailed |
72 | {{.Socket.Data.StreamsFailed}} |
73 |
74 |
75 | MessagesSent |
76 | {{.Socket.Data.MessagesSent}} |
77 |
78 |
79 | MessagesReceived |
80 | {{.Socket.Data.MessagesReceived}} |
81 |
82 |
83 | KeepAlivesSent |
84 | {{.Socket.Data.KeepAlivesSent}} |
85 |
86 |
87 | LastLocalStreamCreated |
88 | {{.Socket.Data.LastLocalStreamCreatedTimestamp | timestamp}} |
89 |
90 |
91 | LastRemoteStreamCreated |
92 | {{.Socket.Data.LastRemoteStreamCreatedTimestamp | timestamp}} |
93 |
94 |
95 | LastMessageSent |
96 | {{.Socket.Data.LastMessageSentTimestamp | timestamp}} |
97 |
98 |
99 | LastMessageReceived |
100 | {{.Socket.Data.LastMessageReceivedTimestamp | timestamp}} |
101 |
102 |
103 | LocalFlowControlWindow |
104 | {{.Socket.Data.LocalFlowControlWindow.Value}} |
105 |
106 |
107 | RemoteFlowControlWindow |
108 | {{.Socket.Data.RemoteFlowControlWindow.Value}} |
109 |
110 |
111 | Options |
112 |
113 | {{range .Socket.Data.Option}}
114 | {{.Name}}: {{.Value}} {{with .Additional}}({{.}}){{end}}
115 | {{end}}
116 | |
117 |
118 |
119 | Security |
120 | {{.Socket.Security}} |
121 |
122 |
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 | Subchannel |
49 |
50 |
51 | {{.Subchannel.Ref.SubchannelId}} {{.Subchannel.Ref.Name}}
52 |
53 | |
54 |
55 |
56 | State |
57 | {{.Subchannel.Data.State}} |
58 |
59 |
60 | Target |
61 | {{.Subchannel.Data.Target}} |
62 |
63 |
64 | CreationTimestamp |
65 | {{.Subchannel.Data.Trace.CreationTimestamp | timestamp}} |
66 |
67 |
68 | CallsStarted |
69 | {{.Subchannel.Data.CallsStarted}} |
70 |
71 |
72 | CallsSucceeded |
73 | {{.Subchannel.Data.CallsSucceeded}} |
74 |
75 |
76 | CallsFailed |
77 | {{.Subchannel.Data.CallsFailed}} |
78 |
79 |
80 | LastCallStartedTimestamp |
81 | {{.Subchannel.Data.LastCallStartedTimestamp | timestamp}} |
82 |
83 |
84 | Child Channels |
85 |
86 | {{range .Subchannel.ChannelRef}}
87 | {{.ChannelId}} {{.Name}}
88 | {{end}}
89 | |
90 |
91 |
92 | Child Subchannels |
93 |
94 | {{range .Subchannel.SubchannelRef}}
95 | {{.SubchannelId}} {{.Name}}
96 | {{end}}
97 | |
98 |
99 |
100 | Socket |
101 |
102 | {{range .Subchannel.SocketRef}}
103 | {{.SocketId}} {{.Name}}
104 | {{end}}
105 | |
106 |
107 |
108 | Events |
109 |
110 |
111 | {{- range .Subchannel.Data.Trace.Events}}
112 | {{.Severity}} [{{.Timestamp | timestamp}}]: {{.Description}}
113 | {{- end -}}
114 |
115 | |
116 |
117 |
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 |
--------------------------------------------------------------------------------