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 [![Go Report Card](https://goreportcard.com/badge/github.com/grpc-ecosystem/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 [flags] 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 [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 # : SERVING # or # : NOT_SERVING ``` Or fetch individual service's health status: ```shell grpcdebug localhost:50051 health helloworld.Greeter # : 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: Type:0 Metadata:}] ServiceConfig: Attributes:} (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 #include +#include #include #include #include @@ -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(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 ", 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 ", 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 ", 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 ", 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 = "" } 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 [flags] {{ .CommandPath | ChildCommandPath }} {{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 {{ .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() }