Repository: grpc-ecosystem/grpcdebug
Branch: main
Commit: 9609af11b247
Files: 20
Total size: 112.6 KB
Directory structure:
gitextract_vgznyx4c/
├── .github/
│ └── workflows/
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── cmd/
│ ├── channelz.go
│ ├── config/
│ │ └── config.go
│ ├── health.go
│ ├── root.go
│ ├── transport/
│ │ └── grpc.go
│ ├── verbose/
│ │ └── verbose.go
│ ├── xds.go
│ └── xds_test.go
├── go.mod
├── go.sum
├── internal/
│ └── testing/
│ ├── ca.pem
│ ├── grpcdebug_config.yaml
│ └── testserver/
│ ├── csds_config_dump.json
│ ├── csds_config_dump_multi_scope.json
│ └── main.go
└── main.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
release:
types: [published]
jobs:
release:
name: Release grpcdebug
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, darwin, windows]
goarch: [386, amd64, arm64]
exclude:
- goos: darwin
goarch: 386
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
- name: Prepare build directory
run: |
mkdir -p build/
cp README.md build/
cp LICENSE build/
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go build -trimpath -o $GITHUB_WORKSPACE/build
- name: Create package
id: package
run: |
PACKAGE_NAME=grpcdebug.${GITHUB_REF#refs/tags/}.${{ matrix.goos }}.${{ matrix.goarch }}.tar.gz
tar -czvf $PACKAGE_NAME -C build .
echo ::set-output name=name::${PACKAGE_NAME}
- name: Upload asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./${{ steps.package.outputs.name }}
asset_name: ${{ steps.package.outputs.name }}
asset_content_type: application/gzip
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# grpcdebug
[](https://goreportcard.com/report/github.com/grpc-ecosystem/grpcdebug)
grpcdebug is a command line interface focusing on simplifying the debugging
process of gRPC applications. grpcdebug fetches the internal states of the gRPC
library from the application via gRPC protocol and provide a human-friendly UX
to browse them. Currently, it supports Channelz/Health Checking/CSDS (aka. admin
services). In other words, it can fetch statistics about how many RPCs has being
sent or failed on a given gRPC channel, it can inspect address resolution
results, it can dump the in-effective xDS configuration that directs the routing
of RPCs.
If you are looking for a tool to send gRPC requests and interact with a gRPC
server, please checkout https://github.com/fullstorydev/grpcurl.
```
grpcdebug is an gRPC service admin CLI
Usage:
grpcdebug <target address> [flags] <command>
Available Commands:
channelz Display gRPC states in a human readable way.
health Check health status of the target service (default "").
help Help about any command
xds Fetch xDS related information.
Flags:
--credential_file string Sets the path of the credential file; used in [tls] mode
-h, --help help for grpcdebug
--security string Defines the type of credentials to use [tls, google-default, insecure] (default "insecure")
--server_name_override string Overrides the peer server name if non empty; used in [tls] mode
-t, --timestamp Print timestamp as RFC3339 instead of human readable strings
-v, --verbose Print verbose information for debugging
Use "grpcdebug <target address> [command] --help" for more information about a command.
```
## Table of Contents
- [grpcdebug](#grpcdebug)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Use Compiled Binaries](#use-compiled-binaries)
- [Compile From Source](#compile-from-source)
- [Quick Start](#quick-start)
- [Connect & Security](#connect--security)
- [Insecure Connection](#insecure-connection)
- [TLS Connection - Flags](#tls-connection---flags)
- [Server Connection Config](#server-connection-config)
- [Health](#health)
- [Channelz](#channelz)
- [Usage 1: Raw Channelz Output](#usage-1-raw-channelz-output)
- [Usage 2: List Client Channels](#usage-2-list-client-channels)
- [Usage 3: List Servers](#usage-3-list-servers)
- [Usage 4: Inspect a Channel](#usage-4-inspect-a-channel)
- [Usage 5: Inspect a Subchannel](#usage-5-inspect-a-subchannel)
- [Usage 6: Inspect a Socket](#usage-6-inspect-a-socket)
- [Usage 7: Inspect a Server](#usage-7-inspect-a-server)
- [Usage 8: Pagination](#usage-8-pagination)
- [Debug xDS](#debug-xds)
- [Usage 1: xDS Resources Overview](#usage-1-xds-resources-overview)
- [Usage 2: Dump xDS Configs](#usage-2-dump-xds-configs)
- [Usage 3: Filter xDS Configs](#usage-3-filter-xds-configs)
- [Admin Services](#admin-services)
- [gRPC Java:](#grpc-java)
- [gRPC Go:](#grpc-go)
- [gRPC C++:](#grpc-c)
## Installation
### Use Compiled Binaries
The download links of the binaries can be found at
https://github.com/grpc-ecosystem/grpcdebug/releases. You can find the
precompiled artifacts for `macOS`/`Linux`/`Windows`.
### Compile From Source
Minimum Golang Version 1.22. Official Golang install guide:
https://golang.org/doc/install.
You can install the `grpcdebug` tool using command:
```shell
go install -v github.com/grpc-ecosystem/grpcdebug@latest
```
You can check your Golang version with:
```shell
go version
```
Don't forget to add Golang binaries to your `PATH`:
```shell
export PATH=$PATH:$(go env GOPATH)/bin
```
## Quick Start
If certain commands are confusing, please try to use `-h` to get more context.
Suggestions and ideas are welcome, please post them to
https://github.com/grpc-ecosystem/grpcdebug/issues!
If you haven't got your gRPC application instrumented, feel free to try out the
mocking `testserver` which implemented admin services.
```shell
cd internal/testing/testserver
go run main.go
# Serving Business Logic on :10001
# Serving Insecure Admin Services on :50051
# Serving Secure Admin Services on :50052
# ...
```
### Connect & Security
#### Insecure Connection
To connect to a gRPC endpoint without any credentials, we don't use any special
flags. If the local network can connect to the given gRPC endpoint, it should
just work. For example, if I have a gRPC application exposing admin services at
`localhost:50051`:
```shell
grpcdebug localhost:50051 channelz channels
```
#### TLS Connection - Flags
One way to establish a TLS connection with grpcdebug is by specifying the
credentials via command line flags. For example:
```shell
grpcdebug localhost:50052 --security=tls --credential_file=./internal/testing/ca.pem --server_name_override="*.test.youtube.com" channelz channels
```
#### Server Connection Config
Alternatively, like OpenSSH clients, you can specify the security settings in a
`grpcdebug_config.yaml` file. grpcdebug CLI will find matching connection config and
then use it to connect.
```yaml
servers:
"pattern string":
real_address: string
security: insecure/tls
credential_file: string
server_name_override: string
```
Here is an example config file
[grpcdebug_config.yaml](internal/testing/grpcdebug_config.yaml).
Each server config can have the following settings:
* Pattern: the string right after `Server ` which dictates if this rule should
apply;
* RealAddress: if present, override the given target address, which allows
giving nicknames/aliases to frequently used addresses;
* Security: allows `insecure` or `tls`, expecting more in the future;
* CredentialFile: path to the credential file;
* ServerNameOverride: override the hostname, which is useful for local reproductions to
comply with the certificates' common name requirement.
grpcdebug searches the config file in the following order:
1. Check if the environment variable `GRPCDEBUG_CONFIG` is set, if so, load from the
given path;
2. Try to load the `grpcdebug_config.yaml` file in the current working directory;
3. Try to load the `grpcdebug_config.yaml` file in the user config directory (Linux:
`$HOME/.config`, macOS: `$HOME/Library/Application Support`, Windows:
`%AppData%`, see
[`os.UserConfigDir()`](https://golang.org/pkg/os/#UserConfigDir)).
For example, we can connect to our mock test server's secure admin port via:
```shell
GRPCDEBUG_CONFIG=internal/testing/grpcdebug_config.yaml grpcdebug localhost:50052 channelz channels
# Or
GRPCDEBUG_CONFIG=internal/testing/grpcdebug_config.yaml grpcdebug prod channelz channels
```
### Health
grpcdebug can be used to fetch the health checking status of a peer gRPC
application (see
[health.proto](https://github.com/grpc/grpc/blob/master/src/proto/grpc/health/v1/health.proto)).
gRPC's health checking works at the service-level, meaning services registered on
the same gRPC server may have different health statuses. The health status of
service `""` is used to represent the overall health status of the gRPC
application.
To simply fetch the overall health status:
```shell
grpcdebug localhost:50051 health
# <Overall>: SERVING
# or
# <Overall>: NOT_SERVING
```
Or fetch individual service's health status:
```shell
grpcdebug localhost:50051 health helloworld.Greeter
# <Overall>: SERVING
# helloworld.Greeter: SERVING
```
### Channelz
[Channelz](https://github.com/grpc/proposal/blob/master/A14-channelz.md) is a
channel tracing library that allows applications to remotely query gRPC internal
debug information. Also, Channelz has a web interface (see
[gdebug](https://github.com/grpc/grpc-experiments/tree/master/gdebug)).
grpcdebug is able to fetch information and present it in a more readable way.
Generally, you wil start with either the `servers` or `channels` command and
then work down to the details.
#### Usage 1: Raw Channelz Output
For all Channelz commands, you can add `--json` to get the raw Channelz output.
```shell
grpcdebug localhost:50051 channelz servers --json
#[
# {
# "ref": {
# "server_id": 2,
# "name": "ServerImpl{logId=2, transportServer=NettyServer{logId=1, addresses=[0.0.0.0/0.0.0.0:50051]}}"
# },
# "data": {
# "calls_started": 3,
# "calls_succeeded": 2,
# "last_call_started_timestamp": {
# "seconds": 1680220688,
# "nanos": 444000000
# }
# },
# "listen_socket": [
# {
# "socket_id": 3,
# "name": "ListenSocket{logId=3, channel=[id: 0x05f9f16c, L:/0:0:0:0:0:0:0:0%0:50051]}"
# }
# ]
# }
#]
```
#### Usage 2: List Client Channels
```shell
grpcdebug localhost:50051 channelz channels
# Channel ID Target State Calls(Started/Succeeded/Failed) Created Time
# 7 localhost:10001 READY 5136/4631/505 8 minutes ago
```
#### Usage 3: List Servers
```shell
grpcdebug localhost:50051 channelz servers
# Server ID Listen Addresses Calls(Started/Succeeded/Failed) Last Call Started
# 1 [:::10001] 2852/2530/322 now
# 2 [:::50051] 29/28/0 now
# 3 [:::50052] 4/4/0 26 seconds ago
```
#### Usage 4: Inspect a Channel
You can identify a channel via the Channel ID.
```shell
grpcdebug localhost:50051 channelz channel 7
# Channel ID: 7
# Target: localhost:10001
# State: READY
# Calls Started: 3976
# Calls Succeeded: 3520
# Calls Failed: 456
# Created Time: 6 minutes ago
# ---
# Subchannel ID Target State Calls(Started/Succeeded/Failed) CreatedTime
# 8 localhost:10001 READY 3976/3520/456 6 minutes ago
# ---
# Severity Time Child Ref Description
# CT_INFO 6 minutes ago Channel Created
# CT_INFO 6 minutes ago Resolver state updated: {Addresses:[{Addr:localhost:10001 ServerName: Attributes:<nil> Type:0 Metadata:<nil>}] ServiceConfig:<nil> Attributes:<nil>} (resolver returned new addresses)
# CT_INFO 6 minutes ago Channel switches to new LB policy "pick_first"
# CT_INFO 6 minutes ago subchannel(subchannel_id:8 ) Subchannel(id:8) created
# CT_INFO 6 minutes ago Channel Connectivity change to CONNECTING
# CT_INFO 6 minutes ago Channel Connectivity change to READY
```
#### Usage 5: Inspect a Subchannel
```shell
grpcdebug localhost:50051 channelz subchannel 8
# Subchannel ID: 8
# Target: localhost:10001
# State: READY
# Calls Started: 4490
# Calls Succeeded: 3966
# Calls Failed: 524
# Created Time: 7 minutes ago
# ---
# Socket ID Local->Remote Streams(Started/Succeeded/Failed) Messages(Sent/Received)
# 9 ::1:47436->::1:10001 4490/4490/0 4490/3966
```
#### Usage 6: Inspect a Socket
```shell
grpcdebug localhost:50051 channelz socket 9
# Socket ID: 9
# Address: ::1:47436->::1:10001
# Streams Started: 4807
# Streams Succeeded: 4807
# Streams Failed: 0
# Messages Sent: 4807
# Messages Received: 4243
# Keep Alives Sent: 0
# Last Local Stream Created: now
# Last Remote Stream Created: a long while ago
# Last Message Sent Created: now
# Last Message Received Created: now
# Local Flow Control Window: 65535
# Remote Flow Control Window: 65535
# ---
# Socket Options Name Value
# SO_LINGER [type.googleapis.com/grpc.channelz.v1.SocketOptionLinger]:{duration:{}}
# SO_RCVTIMEO [type.googleapis.com/grpc.channelz.v1.SocketOptionTimeout]:{duration:{}}
# SO_SNDTIMEO [type.googleapis.com/grpc.channelz.v1.SocketOptionTimeout]:{duration:{}}
# TCP_INFO [type.googleapis.com/grpc.channelz.v1.SocketOptionTcpInfo]:{tcpi_state:1 tcpi_options:7 tcpi_rto:204000 tcpi_ato:40000 tcpi_snd_mss:32768 tcpi_rcv_mss:1093 tcpi_last_data_sent:16 tcpi_last_data_recv:16 tcpi_last_ack_recv:16 tcpi_pmtu:65536 tcpi_rcv_ssthresh:65476 tcpi_rtt:192 tcpi_rttvar:153 tcpi_snd_ssthresh:2147483647 tcpi_snd_cwnd:10 tcpi_advmss:65464 tcpi_reordering:3}
# ---
# Security Model: TLS
# Standard Name: TLS_AES_128_GCM_SHA256
```
#### Usage 7: Inspect a Server
```shell
grpcdebug localhost:50051 channelz server 1
# Server Id: 1
# Listen Addresses: [:::10001]
# Calls Started: 5250
# Calls Succeeded: 4647
# Calls Failed: 603
# Last Call Started: now
# ---
# Socket ID Local->Remote Streams(Started/Succeeded/Failed) Messages(Sent/Received)
# 10 ::1:10001->::1:47436 5250/5250/0 4647/5250
```
#### Usage 8: Pagination
In production, there may be thousands of clients/servers/sockets. It would be very noisy to print all of them at once, so Channelz supports pagination through `start_id` and `max_results`
```shell
grpcdebug localhost:50051 channelz servers --start_id=0 --max_results=1
# Server ID Listen Addresses Calls(Started/Succeeded/Failed) Last Call Started
# 1 [:::10001] 2852/2530/322 now
grpcdebug localhost:50051 channelz servers --start_id=2 --max_results=2
# Server ID Listen Addresses Calls(Started/Succeeded/Failed) Last Call Started
# 2 [:::50051] 29/28/0 now
# 3 [:::50052] 4/4/0 26 seconds ago
```
It works similarly for printing channels via `channelz channels` and printing server sockets via `channelz server`.
### Debug xDS
[xDS](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration)
is a data plane configuration API commonly used in service mesh projects. It's
created by Envoy, used by Istio, Traffic Director, and gRPC.
#### Usage 1: xDS Resources Overview
The xDS resources status might be `REQUESTED`/`DOES_NOT_EXIST`/`ACKED`/`NACKED` (see
[config_dump.proto](https://github.com/envoyproxy/envoy/blob/b0ce15c96cebd89cf391869e49017325cd7faaa8/api/envoy/admin/v3/config_dump.proto#L22)).
This view is intended for a quick scan if a configuration is propagated from the
service mesh control plane.
```shell
grpcdebug localhost:50051 xds status
# Name Status Version Type LastUpdated
# xds-test-server:1337 ACKED 1617141154495058478 type.googleapis.com/envoy.config.listener.v3.Listener 2 days ago
# URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337 ACKED 1617141154495058478 type.googleapis.com/envoy.config.route.v3.RouteConfiguration 2 days ago
# cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229 ACKED 1617141154495058478 type.googleapis.com/envoy.config.cluster.v3.Cluster 2 days ago
# cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229 ACKED 1 type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment 2 days ago
```
#### Usage 2: Dump xDS Configs
```shell
grpcdebug localhost:50051 xds config
# {
# "config": [
# {
# "node": {
# "id": "projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b431-c1d43e6ac0ab",
# "cluster": "cluster",
# "metadata": {
# "INSTANCE_IP": "192.168.120.31",
# "TRAFFICDIRECTOR_GCP_PROJECT_NUMBER": "1040920224690",
# "TRAFFICDIRECTOR_NETWORK_NAME": "default"
# },
# ...
```
For an example config dump, see
[csds_config_dump.json](internal/testing/testserver/csds_config_dump.json).
#### Usage 3: Filter xDS Configs
The dumped xDS config can be quite verbose, if I only interested in certain xDS
type, grpcdebug can only print the selected section.
```shell
grpcdebug localhost:50051 xds config --type=eds
# {
# "dynamicEndpointConfigs": [
# {
# "versionInfo": "1",
# "endpointConfig": {
# "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
# "clusterName": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
# "endpoints": [
# {
# "locality": {
# "subZone": "jf:us-central1-a_7062512536751318190_neg"
# },
# "lbEndpoints": [
# {
# "endpoint": {
# "address": {
# "socketAddress": {
# "address": "192.168.120.26",
# "portValue": 8080
# }
# }
# },
# "healthStatus": "HEALTHY"
# }
# ],
# "loadBalancingWeight": 100
# }
# ]
# },
# "lastUpdated": "2021-03-31T01:20:33.936Z",
# "clientStatus": "ACKED"
# }
# ]
# }
```
## Admin Services
### gRPC Java:
```diff
--- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java
+++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java
@@ -18,6 +18,7 @@ package io.grpc.examples.helloworld;
import io.grpc.Server;
import io.grpc.ServerBuilder;
+import io.grpc.services.AdminInterface;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@@ -36,6 +37,7 @@ public class HelloWorldServer {
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new GreeterImpl())
+ .addServices(AdminInterface.getStandardServices())
.build()
.start();
logger.info("Server started, listening on " + port);
```
### gRPC Go:
```diff
--- a/examples/helloworld/greeter_server/main.go
+++ b/examples/helloworld/greeter_server/main.go
@@ -27,6 +27,7 @@ import (
"net"
"google.golang.org/grpc"
+ "google.golang.org/grpc/admin"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
@@ -51,6 +52,11 @@ func main() {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
+ cleanup, err := admin.Register(s)
+ if err != nil {
+ log.Fatalf("failed to register admin: %v", err)
+ }
+ defer cleanup()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
```
### gRPC C++:
```diff
--- a/examples/cpp/helloworld/greeter_server.cc
+++ b/examples/cpp/helloworld/greeter_server.cc
@@ -20,6 +20,7 @@
#include <memory>
#include <string>
+#include <grpcpp/ext/admin_services.h>
#include <grpcpp/ext/proto_server_reflection_plugin.h>
#include <grpcpp/grpcpp.h>
#include <grpcpp/health_check_service_interface.h>
@@ -60,6 +61,7 @@ void RunServer() {
// Register "service" as the instance through which we'll communicate with
// clients. In this case it corresponds to an *synchronous* service.
builder.RegisterService(&service);
+ grpc::AddAdminServices(&builder);
// Finally assemble the server.
std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
```
================================================
FILE: cmd/channelz.go
================================================
package cmd
import (
"encoding/json"
"fmt"
"net"
"strconv"
"time"
"github.com/dustin/go-humanize"
"github.com/grpc-ecosystem/grpcdebug/cmd/transport"
"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"
"github.com/spf13/cobra"
zpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
var (
jsonOutputFlag bool
startIDFlag int64
maxResultsFlag int64
)
func prettyTime(ts *timestamppb.Timestamp) string {
if ts == nil || (ts.Seconds == 0 && ts.Nanos == 0) {
return ""
}
if timestampFlag {
return ts.AsTime().Format(time.RFC3339Nano)
}
return humanize.Time(ts.AsTime())
}
func prettyAddress(addr *zpb.Address) string {
if ipPort := addr.GetTcpipAddress(); ipPort != nil {
address := net.TCPAddr{IP: net.IP(ipPort.IpAddress), Port: int(ipPort.Port)}
return address.String()
}
panic(fmt.Sprintf("Address type not supported for %s", addr))
}
func printChannelTraceEvents(events []*zpb.ChannelTraceEvent) {
fmt.Fprintln(w, "Severity\tTime\tChild Ref\tDescription\t")
for _, event := range events {
var childRef string
switch event.ChildRef.(type) {
case *zpb.ChannelTraceEvent_SubchannelRef:
childRef = fmt.Sprintf("subchannel(%v)", event.GetSubchannelRef())
case *zpb.ChannelTraceEvent_ChannelRef:
childRef = fmt.Sprintf("channel(%v)", event.GetChannelRef())
}
fmt.Fprintf(
w, "%v\t%v\t%v\t%v\t\n",
event.Severity,
prettyTime(event.Timestamp),
childRef,
event.Description,
)
}
w.Flush()
}
func printSockets(sockets []*zpb.Socket) {
fmt.Fprintln(w, "Socket ID\tLocal->Remote\tStreams(Started/Succeeded/Failed)\tMessages(Sent/Received)\t")
for _, socket := range sockets {
if socket.GetRef() == nil || socket.GetData() == nil {
verbose.Debugf("failed to print socket: %s", socket)
continue
}
fmt.Fprintf(
w, "%v\t%v\t%v/%v/%v\t%v/%v\t\n",
socket.Ref.SocketId,
fmt.Sprintf("%v->%v", prettyAddress(socket.Local), prettyAddress(socket.Remote)),
socket.Data.StreamsStarted,
socket.Data.StreamsSucceeded,
socket.Data.StreamsFailed,
socket.Data.MessagesSent,
socket.Data.MessagesReceived,
)
}
w.Flush()
}
func printObjectAsJSON(data any) error {
json, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
fmt.Println(string(json))
return nil
}
func printCreationTimestamp(data *zpb.ChannelData) string {
return prettyTime(data.GetTrace().GetCreationTimestamp())
}
func channelzChannelsCommandRunWithError(cmd *cobra.Command, args []string) error {
var channels = transport.Channels(startIDFlag, maxResultsFlag)
// Print as JSON
if jsonOutputFlag {
return printObjectAsJSON(channels)
}
// Print as table
fmt.Fprintln(w, "Channel ID\tTarget\tState\tCalls(Started/Succeeded/Failed)\tCreated Time\t")
for _, channel := range channels {
if channel.GetRef() == nil || channel.GetData() == nil {
verbose.Debugf("failed to print channel: %s", channel)
continue
}
fmt.Fprintf(
w, "%v\t%v\t%v\t%v/%v/%v\t%v\t\n",
channel.Ref.ChannelId,
channel.Data.Target,
channel.Data.GetState().GetState(),
channel.Data.CallsStarted,
channel.Data.CallsSucceeded,
channel.Data.CallsFailed,
printCreationTimestamp(channel.Data),
)
}
w.Flush()
return nil
}
var channelzChannelsCmd = &cobra.Command{
Use: "channels",
Short: "List client channels for the target application.",
Args: cobra.NoArgs,
RunE: channelzChannelsCommandRunWithError,
}
func channelzChannelCommandRunWithError(cmd *cobra.Command, args []string) error {
id, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("Failed to parse ID=%v: %v", args[0], err)
}
selected := transport.Channel(id)
// Print as JSON
if jsonOutputFlag {
return printObjectAsJSON(selected)
}
// Print as table
// Print Channel information
fmt.Fprintf(w, "Channel ID:\t%v\t\n", selected.GetRef().GetChannelId())
fmt.Fprintf(w, "Target:\t%v\t\n", selected.GetData().GetTarget())
fmt.Fprintf(w, "State:\t%v\t\n", selected.GetData().GetState().GetState())
fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted())
fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded())
fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed())
fmt.Fprintf(w, "Created Time:\t%v\t\n", printCreationTimestamp(selected.GetData()))
w.Flush()
// Print Subchannel list
if len(selected.GetSubchannelRef()) > 0 {
fmt.Println("---")
fmt.Fprintln(w, "Subchannel ID\tTarget\tState\tCalls(Started/Succeeded/Failed)\tCreatedTime\t")
for _, subchannelRef := range selected.GetSubchannelRef() {
var subchannel = transport.Subchannel(subchannelRef.GetSubchannelId())
if subchannel.GetRef() == nil || subchannel.GetData() == nil {
verbose.Debugf("failed to print subchannel: %s", subchannel)
continue
}
fmt.Fprintf(
w, "%v\t%.50s\t%v\t%v/%v/%v\t%v\t\n",
subchannel.Ref.SubchannelId,
subchannel.Data.Target,
subchannel.Data.State.State,
subchannel.Data.CallsStarted,
subchannel.Data.CallsSucceeded,
subchannel.Data.CallsFailed,
printCreationTimestamp(subchannel.Data),
)
}
w.Flush()
}
// Print channel trace events
if len(selected.GetData().GetTrace().GetEvents()) != 0 {
fmt.Println("---")
printChannelTraceEvents(selected.Data.Trace.Events)
}
return nil
}
var channelzChannelCmd = &cobra.Command{
Use: "channel <channel id or URL>",
Short: "Display channel states in a human readable way.",
Args: cobra.ExactArgs(1),
RunE: channelzChannelCommandRunWithError,
}
func channelzSubchannelCommandRunWithError(cmd *cobra.Command, args []string) error {
id, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("Failed to parse ID=%v: %v", args[0], err)
}
selected := transport.Subchannel(id)
// Print as JSON
if jsonOutputFlag {
return printObjectAsJSON(selected)
}
// Print as table
// Print Subchannel information
fmt.Fprintf(w, "Subchannel ID:\t%v\t\n", selected.GetRef().GetSubchannelId())
fmt.Fprintf(w, "Target:\t%v\t\n", selected.GetData().GetTarget())
fmt.Fprintf(w, "State:\t%v\t\n", selected.GetData().GetState().GetState())
fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted())
fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded())
fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed())
fmt.Fprintf(w, "Created Time:\t%v\t\n", printCreationTimestamp(selected.GetData()))
w.Flush()
if len(selected.SocketRef) > 0 {
// Print socket list
fmt.Println("---")
var sockets []*zpb.Socket
for _, socketRef := range selected.GetSocketRef() {
sockets = append(sockets, transport.Socket(socketRef.GetSocketId()))
}
printSockets(sockets)
}
return nil
}
var channelzSubchannelCmd = &cobra.Command{
Use: "subchannel <id>",
Short: "Display subchannel states in a human readable way.",
Args: cobra.ExactArgs(1),
RunE: channelzSubchannelCommandRunWithError,
}
func channelzSocketCommandRunWithError(cmd *cobra.Command, args []string) error {
socketID, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("Invalid socket ID %v", socketID)
}
selected := transport.Socket(socketID)
// Print as JSON
if jsonOutputFlag {
return printObjectAsJSON(selected)
}
// Print as table
// Print Socket information
fmt.Fprintf(w, "Socket ID:\t%v\t\n", selected.GetRef().GetSocketId())
fmt.Fprintf(w, "Address:\t%v\t\n", fmt.Sprintf("%v->%v", prettyAddress(selected.GetLocal()), prettyAddress(selected.GetRemote())))
fmt.Fprintf(w, "Streams Started:\t%v\t\n", selected.GetData().GetStreamsStarted())
fmt.Fprintf(w, "Streams Succeeded:\t%v\t\n", selected.GetData().GetStreamsSucceeded())
fmt.Fprintf(w, "Streams Failed:\t%v\t\n", selected.GetData().GetStreamsFailed())
fmt.Fprintf(w, "Messages Sent:\t%v\t\n", selected.GetData().GetMessagesSent())
fmt.Fprintf(w, "Messages Received:\t%v\t\n", selected.GetData().GetMessagesReceived())
fmt.Fprintf(w, "Keep Alives Sent:\t%v\t\n", selected.GetData().GetKeepAlivesSent())
fmt.Fprintf(w, "Last Local Stream Created:\t%v\t\n", prettyTime(selected.GetData().GetLastLocalStreamCreatedTimestamp()))
fmt.Fprintf(w, "Last Remote Stream Created:\t%v\t\n", prettyTime(selected.GetData().GetLastRemoteStreamCreatedTimestamp()))
fmt.Fprintf(w, "Last Message Sent Created:\t%v\t\n", prettyTime(selected.GetData().GetLastMessageSentTimestamp()))
fmt.Fprintf(w, "Last Message Received Created:\t%v\t\n", prettyTime(selected.GetData().GetLastMessageReceivedTimestamp()))
fmt.Fprintf(w, "Local Flow Control Window:\t%v\t\n", selected.GetData().GetLocalFlowControlWindow().GetValue())
fmt.Fprintf(w, "Remote Flow Control Window:\t%v\t\n", selected.GetData().GetRemoteFlowControlWindow().GetValue())
w.Flush()
if len(selected.GetData().GetOption()) > 0 {
fmt.Println("---")
fmt.Fprintln(w, "Socket Options Name\tValue\t")
for _, option := range selected.GetData().GetOption() {
if option.GetValue() != "" {
// Prefer human readable value than the Any proto
fmt.Fprintf(w, "%v\t%v\t\n", option.GetName(), option.GetValue())
} else {
fmt.Fprintf(w, "%v\t%v\t\n", option.GetName(), option.GetAdditional())
}
}
w.Flush()
}
// Print security information
if security := selected.GetSecurity(); security != nil {
fmt.Println("---")
switch x := security.Model.(type) {
case *zpb.Security_Tls_:
fmt.Fprintf(w, "Security Model:\t%v\t\n", "TLS")
switch y := security.GetTls().GetCipherSuite().(type) {
case *zpb.Security_Tls_StandardName:
fmt.Fprintf(w, "Standard Name:\t%v\t\n", security.GetTls().GetStandardName())
case *zpb.Security_Tls_OtherName:
fmt.Fprintf(w, "Other Name:\t%v\t\n", security.GetTls().GetOtherName())
default:
return fmt.Errorf("Unexpected Cipher suite name type %T", y)
}
// fmt.Fprintf(w, "Local Certificate:\t%v\t\n", security.GetTls().LocalCertificate)
// fmt.Fprintf(w, "Remote Certificate:\t%v\t\n", security.GetTls().RemoteCertificate)
case *zpb.Security_Other:
fmt.Fprintf(w, "Security Model:\t%v\t\n", "Other")
fmt.Fprintf(w, "Name:\t%v\t\n", security.GetOther().GetName())
// fmt.Fprintf(w, "Value:\t%v\t\n", security.GetOther().Value)
default:
return fmt.Errorf("Unexpected security model type %T", x)
}
w.Flush()
}
return nil
}
var channelzSocketCmd = &cobra.Command{
Use: "socket <id>",
Short: "Display socket states in a human readable way.",
Args: cobra.ExactArgs(1),
RunE: channelzSocketCommandRunWithError,
}
func channelzServersCommandRunWithError(cmd *cobra.Command, args []string) error {
var servers = transport.Servers(startIDFlag, maxResultsFlag)
// Print as JSON
if jsonOutputFlag {
return printObjectAsJSON(servers)
}
// Print as table
fmt.Fprintln(w, "Server ID\tListen Addresses\tCalls(Started/Succeeded/Failed)\tLast Call Started\t")
for _, server := range servers {
var listenAddresses []string
for _, socketRef := range server.GetListenSocket() {
socket := transport.Socket(socketRef.SocketId)
listenAddresses = append(listenAddresses, prettyAddress(socket.GetLocal()))
}
fmt.Fprintf(
w, "%v\t%v\t%v/%v/%v\t%v\t\n",
server.GetRef().GetServerId(),
listenAddresses,
server.GetData().GetCallsStarted(),
server.GetData().GetCallsSucceeded(),
server.GetData().GetCallsFailed(),
prettyTime(server.GetData().GetLastCallStartedTimestamp()),
)
}
w.Flush()
return nil
}
var channelzServersCmd = &cobra.Command{
Use: "servers",
Short: "List servers in a human readable way.",
Args: cobra.NoArgs,
RunE: channelzServersCommandRunWithError,
}
func channelzServerCommandRunWithError(cmd *cobra.Command, args []string) error {
serverID, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("Invalid server ID %v", serverID)
}
selected := transport.Server(serverID)
// Print as JSON
if jsonOutputFlag {
return printObjectAsJSON(selected)
}
// Print as table
var listenAddresses []string
for _, socketRef := range selected.GetListenSocket() {
socket := transport.Socket(socketRef.GetSocketId())
listenAddresses = append(listenAddresses, prettyAddress(socket.GetLocal()))
}
fmt.Fprintf(w, "Server Id:\t%v\t\n", selected.GetRef().GetServerId())
fmt.Fprintf(w, "Listen Addresses:\t%v\t\n", listenAddresses)
fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted())
fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded())
fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed())
fmt.Fprintf(w, "Last Call Started:\t%v\t\n", prettyTime(selected.GetData().GetLastCallStartedTimestamp()))
w.Flush()
if sockets := transport.ServerSocket(selected.GetRef().GetServerId(), startIDFlag, maxResultsFlag); len(sockets) > 0 {
// Print socket list
fmt.Println("---")
printSockets(sockets)
}
return nil
}
var channelzServerCmd = &cobra.Command{
Use: "server <id>",
Short: "Display the server state in a human readable way.",
Args: cobra.ExactArgs(1),
RunE: channelzServerCommandRunWithError,
}
var channelzCmd = &cobra.Command{
Use: "channelz",
Short: "Display gRPC states in a human readable way.",
Args: cobra.NoArgs,
}
func init() {
rootCmd.AddCommand(channelzCmd)
channelzChannelsCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of output channels")
channelzChannelsCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start channel ID")
channelzServerCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of the output sockets")
channelzServerCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start server socket ID")
channelzServersCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of output servers")
channelzServersCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start server ID")
channelzCmd.PersistentFlags().BoolVarP(&jsonOutputFlag, "json", "o", false, "Whether to print the result as JSON")
channelzCmd.AddCommand(channelzChannelCmd)
channelzCmd.AddCommand(channelzChannelsCmd)
channelzCmd.AddCommand(channelzSubchannelCmd)
channelzCmd.AddCommand(channelzSocketCmd)
channelzCmd.AddCommand(channelzServersCmd)
channelzCmd.AddCommand(channelzServerCmd)
}
================================================
FILE: cmd/config/config.go
================================================
package config
import (
"errors"
"io"
"os"
"path"
"runtime"
"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"
"gopkg.in/yaml.v2"
)
// SecurityType is the enum type of available security modes
type SecurityType string
const (
// TypeInsecure is the insecure security mode and it is the default value
TypeInsecure SecurityType = "insecure"
// TypeTLS is the TLS security mode, which requires caller to provide
// credentials to connect to peer
TypeTLS = "tls"
)
// The environment variable name of getting the server configs
const grpcdebugServerConfigEnvName = "GRPCDEBUG_CONFIG"
// ServerConfig is the configuration for how to connect to a target
type ServerConfig struct {
RealAddress string `yaml:"real_address"`
Security SecurityType `yaml:"security"`
CredentialFile string `yaml:"credential_file"`
ServerNameOverride string `yaml:"server_name_override"`
}
type grpcdebugConfig struct {
Servers map[string]ServerConfig `yaml:"servers"`
}
func loadServerConfigsFromFile(path string) map[string]ServerConfig {
file, err := os.Open(path)
if err != nil {
panic(err)
}
bytes, err := io.ReadAll(file)
if err != nil {
panic(err)
}
var config grpcdebugConfig
err = yaml.Unmarshal(bytes, &config)
if err != nil {
panic(err)
}
verbose.Debugf("Loaded grpcdebug config from %v: %v", path, config)
return config.Servers
}
// userConfigDir is copied here, so we can support Go v1.12
func userConfigDir() (string, error) {
var dir string
switch runtime.GOOS {
case "windows":
dir = os.Getenv("AppData")
if dir == "" {
return "", errors.New("%AppData% is not defined")
}
case "darwin", "ios":
dir = os.Getenv("HOME")
if dir == "" {
return "", errors.New("$HOME is not defined")
}
dir += "/Library/Application Support"
case "plan9":
dir = os.Getenv("home")
if dir == "" {
return "", errors.New("$home is not defined")
}
dir += "/lib"
default: // Unix
dir = os.Getenv("XDG_CONFIG_HOME")
if dir == "" {
dir = os.Getenv("HOME")
if dir == "" {
return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined")
}
dir += "/.config"
}
}
return dir, nil
}
func loadServerConfigs() map[string]ServerConfig {
if value := os.Getenv(grpcdebugServerConfigEnvName); value != "" {
return loadServerConfigsFromFile(value)
}
// Try to load from work directory, if exists
if _, err := os.Stat("./grpcdebug_config.yaml"); err == nil {
return loadServerConfigsFromFile("./grpcdebug_config.yaml")
}
// Try to load from user config directory, if exists
dir, _ := userConfigDir()
defaultUserConfig := path.Join(dir, "grpcdebug_config.yaml")
if _, err := os.Stat(defaultUserConfig); err == nil {
return loadServerConfigsFromFile(defaultUserConfig)
}
return nil
}
// GetServerConfig returns a connect configuration for the given target
func GetServerConfig(target string) ServerConfig {
for pattern, config := range loadServerConfigs() {
// TODO(lidiz): support wildcards
if pattern == target {
if config.RealAddress == "" {
config.RealAddress = pattern
}
return config
}
}
return ServerConfig{RealAddress: target}
}
================================================
FILE: cmd/health.go
================================================
package cmd
import (
"fmt"
"sort"
"github.com/grpc-ecosystem/grpcdebug/cmd/transport"
"github.com/spf13/cobra"
)
var healthCmd = &cobra.Command{
Use: "health [service names]",
Short: "Check health status of the target service (default \"\").",
RunE: func(cmd *cobra.Command, args []string) error {
var services []string
// Ensure there's the overall health status
services = append(services, "")
services = append(services, args...)
// Sort alphabetically, and deduplicate inputs
sort.Strings(services)
j := 0
for i := 1; i < len(services); i++ {
if services[i] == services[j] {
continue
}
j++
services[j] = services[i]
}
services = services[:j+1]
// Print as table
for _, service := range services {
var serviceName string
if service == "" {
serviceName = "<Overall>"
} else {
serviceName = service
}
fmt.Fprintf(
w, "%v:\t%v\t\n",
serviceName,
transport.GetHealthStatus(service),
)
}
w.Flush()
return nil
},
}
func init() {
rootCmd.AddCommand(healthCmd)
}
================================================
FILE: cmd/root.go
================================================
// Defines the root command and global flags
package cmd
import (
"fmt"
"log"
"os"
"text/tabwriter"
"github.com/grpc-ecosystem/grpcdebug/cmd/config"
"github.com/grpc-ecosystem/grpcdebug/cmd/transport"
"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"
"github.com/spf13/cobra"
)
var verboseFlag, timestampFlag bool
var address, security, credFile, serverNameOverride string
// The table formater
var w = tabwriter.NewWriter(os.Stdout, 10, 0, 3, ' ', 0)
var rootUsageTemplate = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
grpcdebug <target address> [flags] {{ .CommandPath | ChildCommandPath }} <command>{{end}}{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "grpcdebug <target address> {{ .CommandPath | ChildCommandPath }} [command] --help" for more information about a command.{{end}}
`
var rootCmd = &cobra.Command{
Use: "grpcdebug",
Short: "grpcdebug is a gRPC service admin CLI",
}
func initConfig() {
if verboseFlag {
verbose.EnableDebugOutput()
}
c := config.GetServerConfig(address)
if credFile != "" {
c.CredentialFile = credFile
}
if serverNameOverride != "" {
c.ServerNameOverride = serverNameOverride
}
if security == "tls" {
c.Security = config.TypeTLS
if c.CredentialFile == "" {
rootCmd.Usage()
log.Fatalf("Please specify credential file under [tls] mode.")
}
} else if security != "insecure" {
rootCmd.Usage()
log.Fatalf("Unrecognized security mode: %v", security)
}
transport.Connect(c)
}
// ChildCommandPath used in template
func ChildCommandPath(path string) string {
if len(path) <= 10 {
return ""
}
return path[10:]
}
func init() {
cobra.AddTemplateFunc("ChildCommandPath", ChildCommandPath)
cobra.OnInitialize(initConfig)
rootCmd.SetUsageTemplate(rootUsageTemplate)
rootCmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Print verbose information for debugging")
rootCmd.PersistentFlags().BoolVarP(×tampFlag, "timestamp", "t", false, "Print timestamp as RFC3339 instead of human readable strings")
rootCmd.PersistentFlags().StringVar(&security, "security", "insecure", "Defines the type of credentials to use [tls, google-default, insecure]")
rootCmd.PersistentFlags().StringVar(&credFile, "credential_file", "", "Sets the path of the credential file; used in [tls] mode")
rootCmd.PersistentFlags().StringVar(&serverNameOverride, "server_name_override", "", "Overrides the peer server name if non empty; used in [tls] mode")
}
// Execute executes the root command.
func Execute() {
if len(os.Args) > 1 {
address = os.Args[1]
os.Args = os.Args[1:]
} else {
rootCmd.Usage()
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
================================================
FILE: cmd/transport/grpc.go
================================================
package transport
import (
"context"
"log"
"time"
csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
"github.com/grpc-ecosystem/grpcdebug/cmd/config"
"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"
"google.golang.org/grpc"
zpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
var conn *grpc.ClientConn
var channelzClient zpb.ChannelzClient
var csdsClient csdspb.ClientStatusDiscoveryServiceClient
var healthClient healthpb.HealthClient
const rpcTimeout = time.Second * 15
// Connect connects to the service at address and creates stubs
func Connect(c config.ServerConfig) {
verbose.Debugf("Connecting with %v", c)
var err error
var credOption grpc.DialOption
if c.CredentialFile != "" {
cred, err := credentials.NewClientTLSFromFile(c.CredentialFile, c.ServerNameOverride)
if err != nil {
log.Fatalf("failed to create credential: %v", err)
}
credOption = grpc.WithTransportCredentials(cred)
} else {
credOption = grpc.WithTransportCredentials(insecure.NewCredentials())
}
conn, err = grpc.NewClient(c.RealAddress, credOption)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
channelzClient = zpb.NewChannelzClient(conn)
csdsClient = csdspb.NewClientStatusDiscoveryServiceClient(conn)
healthClient = healthpb.NewHealthClient(conn)
}
// Channels returns all available channels
func Channels(startID, maxResults int64) []*zpb.Channel {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
channels, err := channelzClient.GetTopChannels(ctx, &zpb.GetTopChannelsRequest{StartChannelId: startID, MaxResults: maxResults})
if err != nil {
log.Fatalf("failed to fetch top channels: %v", err)
}
return channels.Channel
}
// Channel returns the channel with given channel ID
func Channel(channelID int64) *zpb.Channel {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
channel, err := channelzClient.GetChannel(ctx, &zpb.GetChannelRequest{ChannelId: channelID})
if err != nil {
log.Fatalf("failed to fetch channel id=%v: %v", channelID, err)
}
return channel.Channel
}
// Subchannel returns the queried subchannel
func Subchannel(subchannelID int64) *zpb.Subchannel {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
subchannel, err := channelzClient.GetSubchannel(ctx, &zpb.GetSubchannelRequest{SubchannelId: subchannelID})
if err != nil {
log.Fatalf("failed to fetch subchannel (id=%v): %v", subchannelID, err)
}
return subchannel.Subchannel
}
// Servers returns all available servers
func Servers(startID, maxResults int64) []*zpb.Server {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
servers, err := channelzClient.GetServers(ctx, &zpb.GetServersRequest{StartServerId: startID, MaxResults: maxResults})
if err != nil {
log.Fatalf("failed to fetch servers: %v", err)
}
return servers.Server
}
// Server returns a server
func Server(serverID int64) *zpb.Server {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
server, err := channelzClient.GetServer(ctx, &zpb.GetServerRequest{ServerId: serverID})
if err != nil {
log.Fatalf("failed to fetch server (id=%v): %v", serverID, err)
}
return server.Server
}
// Socket returns a socket
func Socket(socketID int64) *zpb.Socket {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
socket, err := channelzClient.GetSocket(ctx, &zpb.GetSocketRequest{SocketId: socketID})
if err != nil {
log.Fatalf("failed to fetch socket (id=%v): %v", socketID, err)
}
return socket.Socket
}
// ServerSocket returns all sockets of this server
func ServerSocket(serverID, startID, maxResults int64) []*zpb.Socket {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
var s []*zpb.Socket
serverSocketResp, err := channelzClient.GetServerSockets(
ctx,
&zpb.GetServerSocketsRequest{
ServerId: serverID,
StartSocketId: startID,
MaxResults: maxResults,
},
)
if err != nil {
log.Fatalf("failed to fetch server sockets (id=%v): %v", serverID, err)
}
for _, socketRef := range serverSocketResp.SocketRef {
s = append(s, Socket(socketRef.SocketId))
}
return s
}
// FetchClientStatus fetches the xDS resources status
func FetchClientStatus() *csdspb.ClientStatusResponse {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
resp, err := csdsClient.FetchClientStatus(ctx, &csdspb.ClientStatusRequest{})
if err != nil {
log.Fatalf("failed to fetch xds config: %v", err)
}
return resp
}
// GetHealthStatus fetches the health checking status of the service from peer
func GetHealthStatus(service string) string {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
resp, err := healthClient.Check(ctx, &healthpb.HealthCheckRequest{Service: service})
if err != nil {
verbose.Debugf("failed to fetch health status for \"%s\": %v", service, err)
return healthpb.HealthCheckResponse_SERVICE_UNKNOWN.String()
}
return resp.Status.String()
}
================================================
FILE: cmd/verbose/verbose.go
================================================
package verbose
import "log"
var enableDebugOutput = false
// EnableDebugOutput enables debugging output
func EnableDebugOutput() {
enableDebugOutput = true
}
// Debugf prints log if debugging is enabled
func Debugf(format string, v ...any) {
if enableDebugOutput {
log.Printf(format, v...)
}
}
================================================
FILE: cmd/xds.go
================================================
package cmd
import (
"fmt"
"sort"
"strings"
"github.com/grpc-ecosystem/grpcdebug/cmd/transport"
adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
"github.com/spf13/cobra"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
var (
xdsTypeFlag string
xdsScopeFlag string
)
func printProtoBufMessageAsJSON(m proto.Message) error {
option := protojson.MarshalOptions{
Multiline: true,
Indent: " ",
UseProtoNames: false,
UseEnumNumbers: false,
}
jsonbytes, err := option.Marshal(m)
if err != nil {
return err
}
fmt.Println(string(jsonbytes))
return nil
}
func priorityPerXdsConfig(x *csdspb.PerXdsConfig) int {
switch x.PerXdsConfig.(type) {
case *csdspb.PerXdsConfig_ListenerConfig:
return 0
case *csdspb.PerXdsConfig_RouteConfig:
return 1
case *csdspb.PerXdsConfig_ClusterConfig:
return 2
case *csdspb.PerXdsConfig_EndpointConfig:
return 3
default:
return 4
}
}
func sortPerXdsConfigs(clientStatus *csdspb.ClientStatusResponse) {
for _, cfg := range clientStatus.Config {
// XdsConfig is deprecated but we support it for backward compatibility with older servers
sort.Slice(cfg.XdsConfig, func(i, j int) bool {
return priorityPerXdsConfig(cfg.XdsConfig[i]) < priorityPerXdsConfig(cfg.XdsConfig[j])
})
}
}
func filterConfigsByScope(all []*csdspb.ClientConfig, scope string) ([]*csdspb.ClientConfig, error) {
if scope == "" {
return all, nil
}
var out []*csdspb.ClientConfig
for _, c := range all {
if c.ClientScope == scope {
out = append(out, c)
}
}
if len(out) == 0 {
return nil, fmt.Errorf("no ClientConfig matched scope=%q", scope)
}
return out, nil
}
func xdsConfigCommandRunWithError(cmd *cobra.Command, args []string) error {
clientStatus := transport.FetchClientStatus()
if len(clientStatus.Config) == 0 {
return fmt.Errorf("no ClientConfig returned")
}
sortPerXdsConfigs(clientStatus)
configs, err := filterConfigsByScope(clientStatus.Config, xdsScopeFlag)
if err != nil {
return err
}
if xdsTypeFlag == "" {
// Print whole response; if filtered by scope, print a shallow copy to reflect only filtered configs
if len(configs) == len(clientStatus.Config) {
return printProtoBufMessageAsJSON(clientStatus)
}
return printProtoBufMessageAsJSON(&csdspb.ClientStatusResponse{Config: configs})
}
// Parse --type and print resources for each selected config
wantXdsTypes := strings.Split(xdsTypeFlag, ",")
var wantLDS, wantRDS, wantCDS, wantEDS bool
for _, t := range wantXdsTypes {
switch strings.ToLower(t) {
case "lds":
wantLDS = true
case "rds":
wantRDS = true
case "cds":
wantCDS = true
case "eds":
wantEDS = true
}
}
multi := len(configs) > 1
for _, cfg := range configs {
if multi {
fmt.Printf("== client_scope: %q ==\n", cfg.ClientScope)
}
if len(cfg.GenericXdsConfigs) > 0 {
for _, g := range cfg.GenericXdsConfigs {
var m proto.Message
tokens := strings.Split(g.TypeUrl, ".")
switch tokens[len(tokens)-1] {
case "Listener":
if wantLDS {
m = g.GetXdsConfig()
}
case "RouteConfiguration":
if wantRDS {
m = g.GetXdsConfig()
}
case "Cluster":
if wantCDS {
m = g.GetXdsConfig()
}
case "ClusterLoadAssignment":
if wantEDS {
m = g.GetXdsConfig()
}
}
if m != nil {
if err := printProtoBufMessageAsJSON(m); err != nil {
return fmt.Errorf("Failed to print xDS config: %v", err)
}
}
}
} else {
// XdsConfig is deprecated but we support it for backward compatibility with older servers
for _, x := range cfg.XdsConfig {
var m proto.Message
switch x.PerXdsConfig.(type) {
case *csdspb.PerXdsConfig_ListenerConfig:
if wantLDS {
m = x.GetListenerConfig()
}
case *csdspb.PerXdsConfig_RouteConfig:
if wantRDS {
m = x.GetRouteConfig()
}
case *csdspb.PerXdsConfig_ClusterConfig:
if wantCDS {
m = x.GetClusterConfig()
}
case *csdspb.PerXdsConfig_EndpointConfig:
if wantEDS {
m = x.GetEndpointConfig()
}
}
if m != nil {
if err := printProtoBufMessageAsJSON(m); err != nil {
return fmt.Errorf("Failed to print xDS config: %v", err)
}
}
}
}
}
return nil
}
var xdsConfigCmd = &cobra.Command{
Use: "config",
Short: "Dump the operating xDS configs.",
RunE: xdsConfigCommandRunWithError,
Args: cobra.NoArgs,
}
type xdsResourceStatusEntry struct {
Scope string
Name string
Status adminpb.ClientResourceStatus
Version string
Type string
LastUpdated *timestamppb.Timestamp
}
func printStatusEntry(entry *xdsResourceStatusEntry, includeScope bool) {
if includeScope {
fmt.Fprintf(
w, "%v\t%v\t%v\t%v\t%v\t%v\t\n",
entry.Scope,
entry.Name,
entry.Status,
entry.Version,
entry.Type,
prettyTime(entry.LastUpdated),
)
return
}
fmt.Fprintf(
w, "%v\t%v\t%v\t%v\t%v\t\n",
entry.Name,
entry.Status,
entry.Version,
entry.Type,
prettyTime(entry.LastUpdated),
)
}
func xdsStatusCommandRunWithError(cmd *cobra.Command, args []string) error {
clientStatus := transport.FetchClientStatus()
if len(clientStatus.Config) == 0 {
return fmt.Errorf("no ClientConfig returned")
}
configs, err := filterConfigsByScope(clientStatus.Config, xdsScopeFlag)
if err != nil {
return err
}
includeScope := xdsScopeFlag == ""
if includeScope {
fmt.Fprintln(w, "Scope\tName\tStatus\tVersion\tType\tLastUpdated")
} else {
fmt.Fprintln(w, "Name\tStatus\tVersion\tType\tLastUpdated")
}
for _, config := range configs {
scope := config.ClientScope
for _, g := range config.GenericXdsConfigs {
entry := xdsResourceStatusEntry{
Scope: scope,
Name: g.Name,
Status: g.ClientStatus,
Version: g.VersionInfo,
Type: g.TypeUrl,
LastUpdated: g.LastUpdated,
}
printStatusEntry(&entry, includeScope)
}
if len(config.GenericXdsConfigs) == 0 {
// XdsConfig is deprecated but we support it for backward compatibility with older servers
for _, x := range config.XdsConfig {
switch x.PerXdsConfig.(type) {
case *csdspb.PerXdsConfig_ListenerConfig:
for _, dl := range x.GetListenerConfig().DynamicListeners {
e := xdsResourceStatusEntry{Scope: scope, Name: dl.Name, Status: dl.ClientStatus}
if s := dl.GetActiveState(); s != nil {
e.Version = s.VersionInfo
e.Type = s.Listener.TypeUrl
e.LastUpdated = s.LastUpdated
}
printStatusEntry(&e, includeScope)
}
case *csdspb.PerXdsConfig_RouteConfig:
for _, dr := range x.GetRouteConfig().DynamicRouteConfigs {
e := xdsResourceStatusEntry{
Scope: scope,
Status: dr.ClientStatus,
Version: dr.VersionInfo,
Type: dr.RouteConfig.TypeUrl,
LastUpdated: dr.LastUpdated,
}
if packed := dr.GetRouteConfig(); packed != nil {
var rc routepb.RouteConfiguration
if err := packed.UnmarshalTo(&rc); err != nil {
return err
}
e.Name = rc.Name
}
printStatusEntry(&e, includeScope)
}
case *csdspb.PerXdsConfig_ClusterConfig:
for _, dc := range x.GetClusterConfig().DynamicActiveClusters {
e := xdsResourceStatusEntry{
Scope: scope,
Status: dc.ClientStatus,
Version: dc.VersionInfo,
Type: dc.Cluster.TypeUrl,
LastUpdated: dc.LastUpdated,
}
if packed := dc.GetCluster(); packed != nil {
var c clusterpb.Cluster
if err := packed.UnmarshalTo(&c); err != nil {
return err
}
e.Name = c.Name
}
printStatusEntry(&e, includeScope)
}
case *csdspb.PerXdsConfig_EndpointConfig:
for _, de := range x.GetEndpointConfig().GetDynamicEndpointConfigs() {
e := xdsResourceStatusEntry{
Scope: scope,
Status: de.ClientStatus,
Version: de.VersionInfo,
Type: de.EndpointConfig.TypeUrl,
LastUpdated: de.LastUpdated,
}
if packed := de.GetEndpointConfig(); packed != nil {
var ep endpointpb.ClusterLoadAssignment
if err := packed.UnmarshalTo(&ep); err != nil {
return err
}
e.Name = ep.ClusterName
}
printStatusEntry(&e, includeScope)
}
}
}
}
}
w.Flush()
return nil
}
var xdsStatusCmd = &cobra.Command{
Use: "status",
Short: "Print the config synchronization status.",
RunE: xdsStatusCommandRunWithError,
}
var xdsCmd = &cobra.Command{
Use: "xds",
Short: "Fetch xDS related information.",
}
func init() {
xdsConfigCmd.Flags().StringVarP(&xdsTypeFlag, "type", "y", "", "Filters the wanted type of xDS config to print (separated by commas) (available types: LDS,RDS,CDS,EDS) (by default, print all)")
xdsConfigCmd.Flags().StringVarP(&xdsScopeFlag, "scope", "s", "", "Filter by client_scope when multiple ClientConfig are present")
xdsCmd.AddCommand(xdsConfigCmd)
xdsCmd.AddCommand(xdsStatusCmd)
rootCmd.AddCommand(xdsCmd)
}
================================================
FILE: cmd/xds_test.go
================================================
package cmd
import (
"strings"
"testing"
csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
)
func TestFilterConfigsByScope(t *testing.T) {
t.Parallel()
tests := []struct {
name string
configs []*csdspb.ClientConfig
scope string
wantLen int
wantErr bool
wantErrMsg string
}{
{
name: "empty scope returns all",
configs: []*csdspb.ClientConfig{
{ClientScope: "primary"},
{ClientScope: "fallback"},
},
scope: "",
wantLen: 2,
wantErr: false,
},
{
name: "filter by primary scope",
configs: []*csdspb.ClientConfig{
{ClientScope: "primary"},
{ClientScope: "fallback"},
},
scope: "primary",
wantLen: 1,
wantErr: false,
},
{
name: "filter by fallback scope",
configs: []*csdspb.ClientConfig{
{ClientScope: "primary"},
{ClientScope: "fallback"},
},
scope: "fallback",
wantLen: 1,
wantErr: false,
},
{
name: "no match returns error",
configs: []*csdspb.ClientConfig{
{ClientScope: "primary"},
{ClientScope: "fallback"},
},
scope: "nonexistent",
wantLen: 0,
wantErr: true,
wantErrMsg: "no ClientConfig matched scope=",
},
{
name: "multiple configs with same scope",
configs: []*csdspb.ClientConfig{
{ClientScope: "primary"},
{ClientScope: "primary"},
{ClientScope: "fallback"},
},
scope: "primary",
wantLen: 2,
wantErr: false,
},
{
name: "empty scope name in config",
configs: []*csdspb.ClientConfig{
{ClientScope: ""},
{ClientScope: "primary"},
},
scope: "",
wantLen: 2,
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
got, err := filterConfigsByScope(test.configs, test.scope)
if test.wantErr {
if err == nil {
t.Errorf("filterConfigsByScope() expected error but got none")
return
}
if !strings.Contains(err.Error(), test.wantErrMsg) {
t.Errorf("filterConfigsByScope() error = %v, want error containing %v", err, test.wantErrMsg)
}
return
}
if err != nil {
t.Errorf("filterConfigsByScope() unexpected error = %v", err)
return
}
if len(got) != test.wantLen {
t.Errorf("filterConfigsByScope() returned %d configs, want %d", len(got), test.wantLen)
}
// Verify filtered results actually match the scope
if test.scope != "" {
for _, c := range got {
if c.ClientScope != test.scope {
t.Errorf("filterConfigsByScope() returned config with scope %q, want %q", c.ClientScope, test.scope)
}
}
}
})
}
}
func TestSortPerXdsConfigs(t *testing.T) {
t.Parallel()
// Test that sortPerXdsConfigs doesn't panic with multiple configs
// XdsConfig is deprecated but we test it for backward compatibility
clientStatus := &csdspb.ClientStatusResponse{
Config: []*csdspb.ClientConfig{
{
ClientScope: "primary",
XdsConfig: []*csdspb.PerXdsConfig{
{PerXdsConfig: &csdspb.PerXdsConfig_ClusterConfig{}},
{PerXdsConfig: &csdspb.PerXdsConfig_ListenerConfig{}},
},
},
{
ClientScope: "fallback",
XdsConfig: []*csdspb.PerXdsConfig{
{PerXdsConfig: &csdspb.PerXdsConfig_EndpointConfig{}},
{PerXdsConfig: &csdspb.PerXdsConfig_RouteConfig{}},
},
},
},
}
// Should not panic
sortPerXdsConfigs(clientStatus)
// Verify first config is sorted: Listener(0) < Cluster(2)
if len(clientStatus.Config[0].XdsConfig) >= 2 {
p0 := priorityPerXdsConfig(clientStatus.Config[0].XdsConfig[0])
p1 := priorityPerXdsConfig(clientStatus.Config[0].XdsConfig[1])
if p0 > p1 {
t.Errorf("First config not sorted properly: priority[0]=%d > priority[1]=%d", p0, p1)
}
}
// Verify second config is sorted: Route(1) < Endpoint(3)
if len(clientStatus.Config[1].XdsConfig) >= 2 {
p0 := priorityPerXdsConfig(clientStatus.Config[1].XdsConfig[0])
p1 := priorityPerXdsConfig(clientStatus.Config[1].XdsConfig[1])
if p0 > p1 {
t.Errorf("Second config not sorted properly: priority[0]=%d > priority[1]=%d", p0, p1)
}
}
}
func TestSortPerXdsConfigsEmptyConfigs(t *testing.T) {
t.Parallel()
// Test with empty configs
clientStatus := &csdspb.ClientStatusResponse{
Config: []*csdspb.ClientConfig{},
}
sortPerXdsConfigs(clientStatus)
}
func TestSortPerXdsConfigsSingleConfig(t *testing.T) {
t.Parallel()
// Test backward compatibility with single config
// XdsConfig is deprecated but we test it for backward compatibility
clientStatus := &csdspb.ClientStatusResponse{
Config: []*csdspb.ClientConfig{
{
ClientScope: "",
XdsConfig: []*csdspb.PerXdsConfig{
{PerXdsConfig: &csdspb.PerXdsConfig_EndpointConfig{}},
{PerXdsConfig: &csdspb.PerXdsConfig_ListenerConfig{}},
{PerXdsConfig: &csdspb.PerXdsConfig_ClusterConfig{}},
},
},
},
}
sortPerXdsConfigs(clientStatus)
// Verify sorted: Listener(0) < Cluster(2) < Endpoint(3)
expected := []int{0, 2, 3}
for i, cfg := range clientStatus.Config[0].XdsConfig {
priority := priorityPerXdsConfig(cfg)
if priority != expected[i] {
t.Errorf("Config[%d] priority = %d, want %d", i, priority, expected[i])
}
}
}
================================================
FILE: go.mod
================================================
module github.com/grpc-ecosystem/grpcdebug
go 1.23.0
require (
github.com/dustin/go-humanize v1.0.1
github.com/envoyproxy/go-control-plane/contrib v1.32.4
github.com/envoyproxy/go-control-plane/envoy v1.32.4
github.com/golang/protobuf v1.5.4
github.com/spf13/cobra v1.8.1
google.golang.org/grpc v1.70.0
google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e
google.golang.org/protobuf v1.36.4
gopkg.in/yaml.v2 v2.4.0
)
require (
cel.dev/expr v0.19.0 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
================================================
FILE: go.sum
================================================
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/contrib v1.32.4 h1:/udV6s9xkDGe13WfrT2MHAxXTNDMBYBPxI1GkleCrmM=
github.com/envoyproxy/go-control-plane/contrib v1.32.4/go.mod h1:gkGYoY7plfQg7FPBDhyKtP1cDA9frFR/3YsCx8taRvI=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e h1:LGj5F6Z+enJcJFa6sjBMwmJ1gnjHpV60bKrQ2ass/aE=
google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e/go.mod h1:UxqwMHw3ntCGQS0LuHPmqkO+z9CyMtK1oN7xh6P+gw8=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: internal/testing/ca.pem
================================================
-----BEGIN CERTIFICATE-----
MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
Dfcog5wrJytaQ6UA0wE=
-----END CERTIFICATE-----
================================================
FILE: internal/testing/grpcdebug_config.yaml
================================================
servers:
dev:
real_address: localhost:50051
security: insecure
prod:
real_address: "localhost:50052"
security: tls
credential_file: ./internal/testing/ca.pem
server_name_override: "*.test.youtube.com"
"localhost:50052":
security: tls
credential_file: ./internal/testing/ca.pem
server_name_override: "*.test.youtube.com"
================================================
FILE: internal/testing/testserver/csds_config_dump.json
================================================
{
"config": [
{
"node": {
"id": "projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b431-c1d43e6ac0ab",
"cluster": "cluster",
"metadata": {
"INSTANCE_IP": "192.168.120.31",
"TRAFFICDIRECTOR_GCP_PROJECT_NUMBER": "1040920224690",
"TRAFFICDIRECTOR_NETWORK_NAME": "default"
},
"locality": {
"zone": "us-central1-a"
},
"userAgentName": "gRPC Java",
"userAgentVersion": "1.38.0-SNAPSHOT",
"clientFeatures": [
"envoy.lb.does_not_support_overprovisioning"
]
},
"xdsConfig": [
{
"listenerConfig": {
"versionInfo": "1617141154495058478",
"dynamicListeners": [
{
"name": "xds-test-server:1337",
"activeState": {
"versionInfo": "1617141154495058478",
"listener": {
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"apiListener": {
"apiListener": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"httpFilters": [
{
"name": "envoy.filters.http.fault",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault"
}
},
{
"name": "envoy.filters.http.router",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router",
"suppressEnvoyHeaders": true
}
}
],
"rds": {
"configSource": {
"ads": {},
"resourceApiVersion": "V3"
},
"routeConfigName": "URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337"
},
"statPrefix": "trafficdirector"
}
},
"name": "xds-test-server:1337"
},
"lastUpdated": "2021-03-31T01:20:33.144Z"
},
"clientStatus": "ACKED"
}
]
}
},
{
"routeConfig": {
"dynamicRouteConfigs": [
{
"versionInfo": "1617141154495058478",
"routeConfig": {
"@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"name": "URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337",
"virtualHosts": [
{
"domains": [
"xds-test-server:1337"
],
"routes": [
{
"match": {
"prefix": ""
},
"route": {
"cluster": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
"timeout": "30s",
"retryPolicy": {
"retryOn": "gateway-error",
"numRetries": 1,
"perTryTimeout": "30s"
}
}
}
]
}
]
},
"lastUpdated": "2021-03-31T01:20:33.302Z",
"clientStatus": "ACKED"
}
]
}
},
{
"clusterConfig": {
"versionInfo": "1617141154495058478",
"dynamicActiveClusters": [
{
"versionInfo": "1617141154495058478",
"cluster": {
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"circuitBreakers": {
"thresholds": [
{
"maxConnections": 2147483647,
"maxPendingRequests": 2147483647,
"maxRequests": 2147483647,
"maxRetries": 2147483647
}
]
},
"commonLbConfig": {
"healthyPanicThreshold": {
"value": 1
},
"localityWeightedLbConfig": {}
},
"connectTimeout": "30s",
"edsClusterConfig": {
"edsConfig": {
"ads": {},
"initialFetchTimeout": "15s",
"resourceApiVersion": "V3"
}
},
"http2ProtocolOptions": {
"maxConcurrentStreams": 100
},
"lrsServer": {
"self": {}
},
"metadata": {
"filterMetadata": {
"com.google.trafficdirector": {
"backend_service_name": "sergii-psm-test-backend-service"
}
}
},
"name": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
"type": "EDS"
},
"lastUpdated": "2021-03-31T01:20:33.853Z",
"clientStatus": "ACKED"
}
]
}
},
{
"endpointConfig": {
"dynamicEndpointConfigs": [
{
"versionInfo": "1",
"endpointConfig": {
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
"endpoints": [
{
"locality": {
"subZone": "jf:us-central1-a_7062512536751318190_neg"
},
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "192.168.120.26",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY"
}
],
"loadBalancingWeight": 100
}
]
},
"lastUpdated": "2021-03-31T01:20:33.936Z",
"clientStatus": "ACKED"
}
]
}
}
]
}
]
}
================================================
FILE: internal/testing/testserver/csds_config_dump_multi_scope.json
================================================
{
"config": [
{
"node": {
"id": "projects/1040920224690/networks/default/nodes/primary-node",
"cluster": "cluster",
"userAgentName": "gRPC Java",
"userAgentVersion": "1.60.0"
},
"clientScope": "primary",
"xdsConfig": [
{
"listenerConfig": {
"versionInfo": "1617141154495058478",
"dynamicListeners": [
{
"name": "primary-listener:1337",
"activeState": {
"versionInfo": "1617141154495058478",
"listener": {
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "primary-listener:1337"
},
"lastUpdated": "2021-03-31T01:20:33.144Z"
},
"clientStatus": "ACKED"
}
]
}
},
{
"clusterConfig": {
"versionInfo": "1617141154495058478",
"dynamicActiveClusters": [
{
"versionInfo": "1617141154495058478",
"cluster": {
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "primary-cluster",
"type": "EDS"
},
"lastUpdated": "2021-03-31T01:20:33.853Z",
"clientStatus": "ACKED"
}
]
}
}
]
},
{
"node": {
"id": "projects/1040920224690/networks/default/nodes/fallback-node",
"cluster": "cluster",
"userAgentName": "gRPC Java",
"userAgentVersion": "1.60.0"
},
"clientScope": "fallback",
"xdsConfig": [
{
"listenerConfig": {
"versionInfo": "1617141154495058479",
"dynamicListeners": [
{
"name": "fallback-listener:1338",
"activeState": {
"versionInfo": "1617141154495058479",
"listener": {
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "fallback-listener:1338"
},
"lastUpdated": "2021-03-31T01:25:33.144Z"
},
"clientStatus": "ACKED"
}
]
}
},
{
"clusterConfig": {
"versionInfo": "1617141154495058479",
"dynamicActiveClusters": [
{
"versionInfo": "1617141154495058479",
"cluster": {
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "fallback-cluster",
"type": "EDS"
},
"lastUpdated": "2021-03-31T01:25:33.853Z",
"clientStatus": "ACKED"
}
]
}
}
]
}
]
}
================================================
FILE: internal/testing/testserver/main.go
================================================
// Testserver mocking the responses of Channelz/CSDS/Health
package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"io"
"log"
"math/rand"
"net"
"os"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/channelz/service"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/health"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"google.golang.org/grpc/testdata"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/protobuf/encoding/protojson"
)
var (
servingPortFlag = flag.Int("serving", 10001, "the serving port")
adminPortFlag = flag.Int("admin", 50051, "the admin port")
secureAdminPortFlag = flag.Int("secure_admin", 50052, "the secure admin port")
healthFlag = flag.Bool("health", true, "the health checking status")
qpsFlag = flag.Int("qps", 10, "The size of the generated load against itself")
abortPercentageFlag = flag.Int("abort_percentage", 10, "The percentage of failed RPCs")
)
// Prepare the CSDS response
var csdsResponse csdspb.ClientStatusResponse
func init() {
file, err := os.Open("csds_config_dump.json")
if err != nil {
panic(err)
}
configDump, err := io.ReadAll(file)
if err != nil {
panic(err)
}
if err := protojson.Unmarshal([]byte(configDump), &csdsResponse); err != nil {
panic(err)
}
}
// Implements the Greeter service
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
if int(rand.Int31n(100)) <= *abortPercentageFlag {
return nil, status.Errorf(codes.Code(rand.Int31n(15)+1), "Fault injected")
}
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
// Implements the CSDS service
type mockCsdsServer struct {
csdspb.UnimplementedClientStatusDiscoveryServiceServer
}
func (*mockCsdsServer) FetchClientStatus(ctx context.Context, req *csdspb.ClientStatusRequest) (*csdspb.ClientStatusResponse, error) {
return &csdsResponse, nil
}
func setupAdminServer(s *grpc.Server) {
reflection.Register(s)
service.RegisterChannelzServiceToServer(s)
csdspb.RegisterClientStatusDiscoveryServiceServer(s, &mockCsdsServer{})
healthcheck := health.NewServer()
if *healthFlag {
healthcheck.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthcheck.SetServingStatus("helloworld.Greeter", healthpb.HealthCheckResponse_SERVING)
} else {
healthcheck.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
healthcheck.SetServingStatus("helloworld.Greeter", healthpb.HealthCheckResponse_NOT_SERVING)
}
healthpb.RegisterHealthServer(s, healthcheck)
}
func main() {
// Parse the flags
flag.Parse()
// Creates the primary server
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *servingPortFlag))
if err != nil {
panic(err)
}
defer lis.Close()
fmt.Printf("Serving Business Logic on :%d\n", *servingPortFlag)
cert, err := tls.LoadX509KeyPair(testdata.Path("server1.pem"), testdata.Path("server1.key"))
if err != nil {
log.Fatalf("failed to load key pair: %s", err)
}
s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert)))
pb.RegisterGreeterServer(s, &server{})
go s.Serve(lis)
defer s.Stop()
// Creates the admin server without credentials
insecureListener, err := net.Listen("tcp", fmt.Sprintf(":%d", *adminPortFlag))
if err != nil {
panic(err)
}
defer insecureListener.Close()
insecureAdminServer := grpc.NewServer()
setupAdminServer(insecureAdminServer)
go insecureAdminServer.Serve(insecureListener)
defer insecureAdminServer.Stop()
fmt.Printf("Serving Insecure Admin Services on :%d\n", *adminPortFlag)
// Creates the admin server with credentials
secureListener, err := net.Listen("tcp", fmt.Sprintf(":%d", *secureAdminPortFlag))
if err != nil {
panic(err)
}
defer secureListener.Close()
secureAdminServer := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert)))
setupAdminServer(secureAdminServer)
go secureAdminServer.Serve(secureListener)
defer secureAdminServer.Stop()
fmt.Printf("Serving Secure Admin Services on :%d\n", *secureAdminPortFlag)
// Creates a client to hydrate the primary server
creds, err := credentials.NewClientTLSFromFile(testdata.Path("ca.pem"), "*.test.youtube.com")
if err != nil {
panic(err)
}
conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", *servingPortFlag), grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer conn.Close()
greeterClient := pb.NewGreeterClient(conn)
for {
greeterClient.SayHello(context.Background(), &pb.HelloRequest{Name: "world"})
time.Sleep(time.Second / time.Duration(*qpsFlag))
}
}
================================================
FILE: main.go
================================================
package main
import (
cmd "github.com/grpc-ecosystem/grpcdebug/cmd"
// To parse Any protos, ProtoBuf requires the descriptors of the given message
// type to present in its descriptor pool. Otherwise, it will fail. Here we
// preload as much proto descriptors as possible, so the released binaries can
// have better forward compatibility.
_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/http/dynamo/v3"
_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/http/squash/v3"
_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/client_ssl_auth/v3"
_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/kafka_broker/v3"
_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/mysql_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha"
_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/rocketmq_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/metrics/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/overload/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/tap/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/config/trace/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/data/cluster/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/data/core/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/data/dns/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/data/tap/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/wasm/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/dynamic_forward_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/dynamic_forward_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/matching/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/tap/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/compressor/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/decompressor/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/compressor/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/decompressor/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/dependency/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/matcher/action/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/adaptive_concurrency/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/admission_control/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_lambda/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_request_signing/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/buffer/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cache/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cdn_loop/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/compressor/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/csrf/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/decompressor/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/dynamic_forward_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_proc/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_http1_bridge/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_http1_reverse_bridge/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_json_transcoder/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_stats/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_web/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gzip/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/header_to_metadata/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/health_check/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ip_tagging/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/kill_request/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/oauth2/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/on_demand/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/original_src/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/tap/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/http_inspector/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_dst/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_src/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/proxy_protocol/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/direct_response/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/dubbo_proxy/router/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/dubbo_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/echo/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ext_authz/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/local_ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/mongo_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/redis_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_cluster/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/router/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/zookeeper_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/dns_filter/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/allow_listed_routes/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/previous_routes/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/safe_cross_scheme/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/least_request/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/pick_first/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/ring_hash/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/round_robin/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/wrr_locality/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/socket_interface/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/rate_limit_descriptors/expr/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/host/omit_host_metadata/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/host/previous_hosts/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/priority/previous_priorities/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/stat_sinks/wasm/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/alts/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/proxy_protocol/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/raw_buffer/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/starttls/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tap/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/generic/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/http/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/tcp/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/udp/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/watchdog/profile_action/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/event_reporting/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/extension/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/health/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/load_stats/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/metrics/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/service/tap/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/type/metadata/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/type/tracing/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/type/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/watchdog/v3"
// Add the xDS resolver to allow resolving using a "xds:///" target.
_ "google.golang.org/grpc/xds"
)
func main() {
cmd.Execute()
}
gitextract_vgznyx4c/ ├── .github/ │ └── workflows/ │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd/ │ ├── channelz.go │ ├── config/ │ │ └── config.go │ ├── health.go │ ├── root.go │ ├── transport/ │ │ └── grpc.go │ ├── verbose/ │ │ └── verbose.go │ ├── xds.go │ └── xds_test.go ├── go.mod ├── go.sum ├── internal/ │ └── testing/ │ ├── ca.pem │ ├── grpcdebug_config.yaml │ └── testserver/ │ ├── csds_config_dump.json │ ├── csds_config_dump_multi_scope.json │ └── main.go └── main.go
SYMBOL INDEX (62 symbols across 10 files)
FILE: cmd/channelz.go
function prettyTime (line 24) | func prettyTime(ts *timestamppb.Timestamp) string {
function prettyAddress (line 34) | func prettyAddress(addr *zpb.Address) string {
function printChannelTraceEvents (line 42) | func printChannelTraceEvents(events []*zpb.ChannelTraceEvent) {
function printSockets (line 63) | func printSockets(sockets []*zpb.Socket) {
function printObjectAsJSON (line 84) | func printObjectAsJSON(data any) error {
function printCreationTimestamp (line 93) | func printCreationTimestamp(data *zpb.ChannelData) string {
function channelzChannelsCommandRunWithError (line 97) | func channelzChannelsCommandRunWithError(cmd *cobra.Command, args []stri...
function channelzChannelCommandRunWithError (line 132) | func channelzChannelCommandRunWithError(cmd *cobra.Command, args []strin...
function channelzSubchannelCommandRunWithError (line 190) | func channelzSubchannelCommandRunWithError(cmd *cobra.Command, args []st...
function channelzSocketCommandRunWithError (line 229) | func channelzSocketCommandRunWithError(cmd *cobra.Command, args []string...
function channelzServersCommandRunWithError (line 304) | func channelzServersCommandRunWithError(cmd *cobra.Command, args []strin...
function channelzServerCommandRunWithError (line 339) | func channelzServerCommandRunWithError(cmd *cobra.Command, args []string...
function init (line 383) | func init() {
FILE: cmd/config/config.go
type SecurityType (line 15) | type SecurityType
constant TypeInsecure (line 19) | TypeInsecure SecurityType = "insecure"
constant TypeTLS (line 22) | TypeTLS = "tls"
constant grpcdebugServerConfigEnvName (line 26) | grpcdebugServerConfigEnvName = "GRPCDEBUG_CONFIG"
type ServerConfig (line 29) | type ServerConfig struct
type grpcdebugConfig (line 36) | type grpcdebugConfig struct
function loadServerConfigsFromFile (line 40) | func loadServerConfigsFromFile(path string) map[string]ServerConfig {
function userConfigDir (line 59) | func userConfigDir() (string, error) {
function loadServerConfigs (line 95) | func loadServerConfigs() map[string]ServerConfig {
function GetServerConfig (line 113) | func GetServerConfig(target string) ServerConfig {
FILE: cmd/health.go
function init (line 49) | func init() {
FILE: cmd/root.go
function initConfig (line 54) | func initConfig() {
function ChildCommandPath (line 79) | func ChildCommandPath(path string) string {
function init (line 86) | func init() {
function Execute (line 99) | func Execute() {
FILE: cmd/transport/grpc.go
constant rpcTimeout (line 23) | rpcTimeout = time.Second * 15
function Connect (line 26) | func Connect(c config.ServerConfig) {
function Channels (line 49) | func Channels(startID, maxResults int64) []*zpb.Channel {
function Channel (line 60) | func Channel(channelID int64) *zpb.Channel {
function Subchannel (line 71) | func Subchannel(subchannelID int64) *zpb.Subchannel {
function Servers (line 82) | func Servers(startID, maxResults int64) []*zpb.Server {
function Server (line 93) | func Server(serverID int64) *zpb.Server {
function Socket (line 104) | func Socket(socketID int64) *zpb.Socket {
function ServerSocket (line 115) | func ServerSocket(serverID, startID, maxResults int64) []*zpb.Socket {
function FetchClientStatus (line 137) | func FetchClientStatus() *csdspb.ClientStatusResponse {
function GetHealthStatus (line 148) | func GetHealthStatus(service string) string {
FILE: cmd/verbose/verbose.go
function EnableDebugOutput (line 8) | func EnableDebugOutput() {
function Debugf (line 13) | func Debugf(format string, v ...any) {
FILE: cmd/xds.go
function printProtoBufMessageAsJSON (line 26) | func printProtoBufMessageAsJSON(m proto.Message) error {
function priorityPerXdsConfig (line 41) | func priorityPerXdsConfig(x *csdspb.PerXdsConfig) int {
function sortPerXdsConfigs (line 56) | func sortPerXdsConfigs(clientStatus *csdspb.ClientStatusResponse) {
function filterConfigsByScope (line 65) | func filterConfigsByScope(all []*csdspb.ClientConfig, scope string) ([]*...
function xdsConfigCommandRunWithError (line 81) | func xdsConfigCommandRunWithError(cmd *cobra.Command, args []string) err...
type xdsResourceStatusEntry (line 190) | type xdsResourceStatusEntry struct
function printStatusEntry (line 199) | func printStatusEntry(entry *xdsResourceStatusEntry, includeScope bool) {
function xdsStatusCommandRunWithError (line 222) | func xdsStatusCommandRunWithError(cmd *cobra.Command, args []string) err...
function init (line 339) | func init() {
FILE: cmd/xds_test.go
function TestFilterConfigsByScope (line 10) | func TestFilterConfigsByScope(t *testing.T) {
function TestSortPerXdsConfigs (line 117) | func TestSortPerXdsConfigs(t *testing.T) {
function TestSortPerXdsConfigsEmptyConfigs (line 162) | func TestSortPerXdsConfigsEmptyConfigs(t *testing.T) {
function TestSortPerXdsConfigsSingleConfig (line 171) | func TestSortPerXdsConfigsSingleConfig(t *testing.T) {
FILE: internal/testing/testserver/main.go
function init (line 46) | func init() {
type server (line 61) | type server struct
method SayHello (line 65) | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*...
type mockCsdsServer (line 73) | type mockCsdsServer struct
method FetchClientStatus (line 77) | func (*mockCsdsServer) FetchClientStatus(ctx context.Context, req *csd...
function setupAdminServer (line 81) | func setupAdminServer(s *grpc.Server) {
function main (line 96) | func main() {
FILE: main.go
function main (line 171) | func main() {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
{
"path": ".github/workflows/release.yml",
"chars": 1387,
"preview": "name: Release\n\non:\n release:\n types: [published]\n\njobs:\n release:\n name: Release grpcdebug\n runs-on: ubuntu-l"
},
{
"path": ".gitignore",
"chars": 269,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 19916,
"preview": "# grpcdebug\n[](https://goreportcard"
},
{
"path": "cmd/channelz.go",
"chars": 14356,
"preview": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/g"
},
{
"path": "cmd/config/config.go",
"chars": 3172,
"preview": "package config\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\n\t\"github.com/grpc-ecosystem/grpcdebug/cmd/verbose\"\n\t\""
},
{
"path": "cmd/health.go",
"chars": 1057,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/grpc-ecosystem/grpcdebug/cmd/transport\"\n\t\"github.com/spf13/cobra\"\n)\n\n"
},
{
"path": "cmd/root.go",
"chars": 3405,
"preview": "// Defines the root command and global flags\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"text/tabwriter\"\n\n\t\"github.com/"
},
{
"path": "cmd/transport/grpc.go",
"chars": 5274,
"preview": "package transport\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\tcsdspb \"github.com/envoyproxy/go-control-plane/envoy/service/sta"
},
{
"path": "cmd/verbose/verbose.go",
"chars": 304,
"preview": "package verbose\n\nimport \"log\"\n\nvar enableDebugOutput = false\n\n// EnableDebugOutput enables debugging output\nfunc EnableD"
},
{
"path": "cmd/xds.go",
"chars": 9467,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/grpc-ecosystem/grpcdebug/cmd/transport\"\n\n\tadminpb \"github."
},
{
"path": "cmd/xds_test.go",
"chars": 5161,
"preview": "package cmd\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tcsdspb \"github.com/envoyproxy/go-control-plane/envoy/service/status/v3\"\n)\n"
},
{
"path": "go.mod",
"chars": 1571,
"preview": "module github.com/grpc-ecosystem/grpcdebug\n\ngo 1.23.0\n\nrequire (\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/envoy"
},
{
"path": "go.sum",
"chars": 8255,
"preview": "cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=\ncel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxx"
},
{
"path": "internal/testing/ca.pem",
"chars": 855,
"preview": "-----BEGIN CERTIFICATE-----\nMIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIEwpTb21"
},
{
"path": "internal/testing/grpcdebug_config.yaml",
"chars": 362,
"preview": "servers:\n dev:\n real_address: localhost:50051\n security: insecure\n prod:\n real_address: \"localhost:50052\"\n "
},
{
"path": "internal/testing/testserver/csds_config_dump.json",
"chars": 7396,
"preview": "{\n \"config\": [\n {\n \"node\": {\n \"id\": \"projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b4"
},
{
"path": "internal/testing/testserver/csds_config_dump_multi_scope.json",
"chars": 2980,
"preview": "{\n \"config\": [\n {\n \"node\": {\n \"id\": \"projects/1040920224690/networks/default/nodes/primary-node\",\n "
},
{
"path": "internal/testing/testserver/main.go",
"chars": 5086,
"preview": "// Testserver mocking the responses of Channelz/CSDS/Health\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"flag\"\n\t\"fm"
},
{
"path": "main.go",
"chars": 13658,
"preview": "package main\n\nimport (\n\tcmd \"github.com/grpc-ecosystem/grpcdebug/cmd\"\n\n\t// To parse Any protos, ProtoBuf requires the de"
}
]
About this extraction
This page contains the full source code of the grpc-ecosystem/grpcdebug GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (112.6 KB), approximately 32.7k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.