├── .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 ├── go.mod ├── go.sum ├── internal └── testing │ ├── ca.pem │ ├── grpcdebug_config.yaml │ └── testserver │ ├── csds_config_dump.json │ └── main.go └── main.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Release grpcdebug 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | goos: [linux, darwin, windows] 14 | goarch: [386, amd64, arm64] 15 | exclude: 16 | - goos: darwin 17 | goarch: 386 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v2 25 | 26 | - name: Prepare build directory 27 | run: | 28 | mkdir -p build/ 29 | cp README.md build/ 30 | cp LICENSE build/ 31 | - name: Build 32 | env: 33 | GOOS: ${{ matrix.goos }} 34 | GOARCH: ${{ matrix.goarch }} 35 | run: | 36 | go build -trimpath -o $GITHUB_WORKSPACE/build 37 | - name: Create package 38 | id: package 39 | run: | 40 | PACKAGE_NAME=grpcdebug.${GITHUB_REF#refs/tags/}.${{ matrix.goos }}.${{ matrix.goarch }}.tar.gz 41 | tar -czvf $PACKAGE_NAME -C build . 42 | echo ::set-output name=name::${PACKAGE_NAME} 43 | - name: Upload asset 44 | uses: actions/upload-release-asset@v1 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | with: 48 | upload_url: ${{ github.event.release.upload_url }} 49 | asset_path: ./${{ steps.package.outputs.name }} 50 | asset_name: ${{ steps.package.outputs.name }} 51 | asset_content_type: application/gzip 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grpcdebug 2 | [![Go Report 3 | Card](https://goreportcard.com/badge/github.com/grpc-ecosystem/grpcdebug)](https://goreportcard.com/report/github.com/grpc-ecosystem/grpcdebug) 4 | 5 | grpcdebug is a command line interface focusing on simplifying the debugging 6 | process of gRPC applications. grpcdebug fetches the internal states of the gRPC 7 | library from the application via gRPC protocol and provide a human-friendly UX 8 | to browse them. Currently, it supports Channelz/Health Checking/CSDS (aka. admin 9 | services). In other words, it can fetch statistics about how many RPCs has being 10 | sent or failed on a given gRPC channel, it can inspect address resolution 11 | results, it can dump the in-effective xDS configuration that directs the routing 12 | of RPCs. 13 | 14 | If you are looking for a tool to send gRPC requests and interact with a gRPC 15 | server, please checkout https://github.com/fullstorydev/grpcurl. 16 | 17 | ``` 18 | grpcdebug is an gRPC service admin CLI 19 | 20 | Usage: 21 | grpcdebug [flags] 22 | 23 | Available Commands: 24 | channelz Display gRPC states in a human readable way. 25 | health Check health status of the target service (default ""). 26 | help Help about any command 27 | xds Fetch xDS related information. 28 | 29 | Flags: 30 | --credential_file string Sets the path of the credential file; used in [tls] mode 31 | -h, --help help for grpcdebug 32 | --security string Defines the type of credentials to use [tls, google-default, insecure] (default "insecure") 33 | --server_name_override string Overrides the peer server name if non empty; used in [tls] mode 34 | -t, --timestamp Print timestamp as RFC3339 instead of human readable strings 35 | -v, --verbose Print verbose information for debugging 36 | 37 | Use "grpcdebug [command] --help" for more information about a command. 38 | ``` 39 | 40 | ## Table of Contents 41 | - [grpcdebug](#grpcdebug) 42 | - [Table of Contents](#table-of-contents) 43 | - [Installation](#installation) 44 | - [Use Compiled Binaries](#use-compiled-binaries) 45 | - [Compile From Source](#compile-from-source) 46 | - [Quick Start](#quick-start) 47 | - [Connect & Security](#connect--security) 48 | - [Insecure Connection](#insecure-connection) 49 | - [TLS Connection - Flags](#tls-connection---flags) 50 | - [Server Connection Config](#server-connection-config) 51 | - [Health](#health) 52 | - [Channelz](#channelz) 53 | - [Usage 1: Raw Channelz Output](#usage-1-raw-channelz-output) 54 | - [Usage 2: List Client Channels](#usage-2-list-client-channels) 55 | - [Usage 3: List Servers](#usage-3-list-servers) 56 | - [Usage 4: Inspect a Channel](#usage-4-inspect-a-channel) 57 | - [Usage 5: Inspect a Subchannel](#usage-5-inspect-a-subchannel) 58 | - [Usage 6: Inspect a Socket](#usage-6-inspect-a-socket) 59 | - [Usage 7: Inspect a Server](#usage-7-inspect-a-server) 60 | - [Usage 8: Pagination](#usage-8-pagination) 61 | - [Debug xDS](#debug-xds) 62 | - [Usage 1: xDS Resources Overview](#usage-1-xds-resources-overview) 63 | - [Usage 2: Dump xDS Configs](#usage-2-dump-xds-configs) 64 | - [Usage 3: Filter xDS Configs](#usage-3-filter-xds-configs) 65 | - [Admin Services](#admin-services) 66 | - [gRPC Java:](#grpc-java) 67 | - [gRPC Go:](#grpc-go) 68 | - [gRPC C++:](#grpc-c) 69 | 70 | ## Installation 71 | 72 | ### Use Compiled Binaries 73 | 74 | The download links of the binaries can be found at 75 | https://github.com/grpc-ecosystem/grpcdebug/releases. You can find the 76 | precompiled artifacts for `macOS`/`Linux`/`Windows`. 77 | 78 | ### Compile From Source 79 | 80 | Minimum Golang Version 1.22. Official Golang install guide: 81 | https://golang.org/doc/install. 82 | 83 | You can install the `grpcdebug` tool using command: 84 | 85 | ```shell 86 | go install -v github.com/grpc-ecosystem/grpcdebug@latest 87 | ``` 88 | 89 | You can check your Golang version with: 90 | 91 | ```shell 92 | go version 93 | ``` 94 | 95 | Don't forget to add Golang binaries to your `PATH`: 96 | 97 | ```shell 98 | export PATH=$PATH:$(go env GOPATH)/bin 99 | ``` 100 | 101 | ## Quick Start 102 | 103 | If certain commands are confusing, please try to use `-h` to get more context. 104 | Suggestions and ideas are welcome, please post them to 105 | https://github.com/grpc-ecosystem/grpcdebug/issues! 106 | 107 | If you haven't got your gRPC application instrumented, feel free to try out the 108 | mocking `testserver` which implemented admin services. 109 | 110 | ```shell 111 | cd internal/testing/testserver 112 | go run main.go 113 | # Serving Business Logic on :10001 114 | # Serving Insecure Admin Services on :50051 115 | # Serving Secure Admin Services on :50052 116 | # ... 117 | ``` 118 | 119 | ### Connect & Security 120 | 121 | #### Insecure Connection 122 | 123 | To connect to a gRPC endpoint without any credentials, we don't use any special 124 | flags. If the local network can connect to the given gRPC endpoint, it should 125 | just work. For example, if I have a gRPC application exposing admin services at 126 | `localhost:50051`: 127 | 128 | ```shell 129 | grpcdebug localhost:50051 channelz channels 130 | ``` 131 | 132 | #### TLS Connection - Flags 133 | 134 | One way to establish a TLS connection with grpcdebug is by specifying the 135 | credentials via command line flags. For example: 136 | 137 | ```shell 138 | grpcdebug localhost:50052 --security=tls --credential_file=./internal/testing/ca.pem --server_name_override="*.test.youtube.com" channelz channels 139 | ``` 140 | 141 | #### Server Connection Config 142 | 143 | Alternatively, like OpenSSH clients, you can specify the security settings in a 144 | `grpcdebug_config.yaml` file. grpcdebug CLI will find matching connection config and 145 | then use it to connect. 146 | 147 | ```yaml 148 | servers: 149 | "pattern string": 150 | real_address: string 151 | security: insecure/tls 152 | credential_file: string 153 | server_name_override: string 154 | ``` 155 | 156 | Here is an example config file 157 | [grpcdebug_config.yaml](internal/testing/grpcdebug_config.yaml). 158 | 159 | Each server config can have the following settings: 160 | 161 | * Pattern: the string right after `Server ` which dictates if this rule should 162 | apply; 163 | * RealAddress: if present, override the given target address, which allows 164 | giving nicknames/aliases to frequently used addresses; 165 | * Security: allows `insecure` or `tls`, expecting more in the future; 166 | * CredentialFile: path to the credential file; 167 | * ServerNameOverride: override the hostname, which is useful for local reproductions to 168 | comply with the certificates' common name requirement. 169 | 170 | grpcdebug searches the config file in the following order: 171 | 172 | 1. Check if the environment variable `GRPCDEBUG_CONFIG` is set, if so, load from the 173 | given path; 174 | 2. Try to load the `grpcdebug_config.yaml` file in the current working directory; 175 | 3. Try to load the `grpcdebug_config.yaml` file in the user config directory (Linux: 176 | `$HOME/.config`, macOS: `$HOME/Library/Application Support`, Windows: 177 | `%AppData%`, see 178 | [`os.UserConfigDir()`](https://golang.org/pkg/os/#UserConfigDir)). 179 | 180 | For example, we can connect to our mock test server's secure admin port via: 181 | 182 | ```shell 183 | GRPCDEBUG_CONFIG=internal/testing/grpcdebug_config.yaml grpcdebug localhost:50052 channelz channels 184 | # Or 185 | GRPCDEBUG_CONFIG=internal/testing/grpcdebug_config.yaml grpcdebug prod channelz channels 186 | ``` 187 | 188 | ### Health 189 | 190 | grpcdebug can be used to fetch the health checking status of a peer gRPC 191 | application (see 192 | [health.proto](https://github.com/grpc/grpc/blob/master/src/proto/grpc/health/v1/health.proto)). 193 | gRPC's health checking works at the service-level, meaning services registered on 194 | the same gRPC server may have different health statuses. The health status of 195 | service `""` is used to represent the overall health status of the gRPC 196 | application. 197 | 198 | To simply fetch the overall health status: 199 | 200 | ```shell 201 | grpcdebug localhost:50051 health 202 | # : SERVING 203 | # or 204 | # : NOT_SERVING 205 | ``` 206 | 207 | Or fetch individual service's health status: 208 | 209 | ```shell 210 | grpcdebug localhost:50051 health helloworld.Greeter 211 | # : SERVING 212 | # helloworld.Greeter: SERVING 213 | ``` 214 | 215 | ### Channelz 216 | 217 | [Channelz](https://github.com/grpc/proposal/blob/master/A14-channelz.md) is a 218 | channel tracing library that allows applications to remotely query gRPC internal 219 | debug information. Also, Channelz has a web interface (see 220 | [gdebug](https://github.com/grpc/grpc-experiments/tree/master/gdebug)). 221 | grpcdebug is able to fetch information and present it in a more readable way. 222 | 223 | Generally, you wil start with either the `servers` or `channels` command and 224 | then work down to the details. 225 | 226 | #### Usage 1: Raw Channelz Output 227 | 228 | For all Channelz commands, you can add `--json` to get the raw Channelz output. 229 | 230 | ```shell 231 | grpcdebug localhost:50051 channelz servers --json 232 | #[ 233 | # { 234 | # "ref": { 235 | # "server_id": 2, 236 | # "name": "ServerImpl{logId=2, transportServer=NettyServer{logId=1, addresses=[0.0.0.0/0.0.0.0:50051]}}" 237 | # }, 238 | # "data": { 239 | # "calls_started": 3, 240 | # "calls_succeeded": 2, 241 | # "last_call_started_timestamp": { 242 | # "seconds": 1680220688, 243 | # "nanos": 444000000 244 | # } 245 | # }, 246 | # "listen_socket": [ 247 | # { 248 | # "socket_id": 3, 249 | # "name": "ListenSocket{logId=3, channel=[id: 0x05f9f16c, L:/0:0:0:0:0:0:0:0%0:50051]}" 250 | # } 251 | # ] 252 | # } 253 | #] 254 | 255 | ``` 256 | 257 | #### Usage 2: List Client Channels 258 | 259 | ```shell 260 | grpcdebug localhost:50051 channelz channels 261 | # Channel ID Target State Calls(Started/Succeeded/Failed) Created Time 262 | # 7 localhost:10001 READY 5136/4631/505 8 minutes ago 263 | ``` 264 | 265 | #### Usage 3: List Servers 266 | 267 | ```shell 268 | grpcdebug localhost:50051 channelz servers 269 | # Server ID Listen Addresses Calls(Started/Succeeded/Failed) Last Call Started 270 | # 1 [:::10001] 2852/2530/322 now 271 | # 2 [:::50051] 29/28/0 now 272 | # 3 [:::50052] 4/4/0 26 seconds ago 273 | ``` 274 | 275 | #### Usage 4: Inspect a Channel 276 | 277 | You can identify a channel via the Channel ID. 278 | 279 | ```shell 280 | grpcdebug localhost:50051 channelz channel 7 281 | # Channel ID: 7 282 | # Target: localhost:10001 283 | # State: READY 284 | # Calls Started: 3976 285 | # Calls Succeeded: 3520 286 | # Calls Failed: 456 287 | # Created Time: 6 minutes ago 288 | # --- 289 | # Subchannel ID Target State Calls(Started/Succeeded/Failed) CreatedTime 290 | # 8 localhost:10001 READY 3976/3520/456 6 minutes ago 291 | # --- 292 | # Severity Time Child Ref Description 293 | # CT_INFO 6 minutes ago Channel Created 294 | # CT_INFO 6 minutes ago Resolver state updated: {Addresses:[{Addr:localhost:10001 ServerName: Attributes: Type:0 Metadata:}] ServiceConfig: Attributes:} (resolver returned new addresses) 295 | # CT_INFO 6 minutes ago Channel switches to new LB policy "pick_first" 296 | # CT_INFO 6 minutes ago subchannel(subchannel_id:8 ) Subchannel(id:8) created 297 | # CT_INFO 6 minutes ago Channel Connectivity change to CONNECTING 298 | # CT_INFO 6 minutes ago Channel Connectivity change to READY 299 | ``` 300 | 301 | #### Usage 5: Inspect a Subchannel 302 | 303 | ```shell 304 | grpcdebug localhost:50051 channelz subchannel 8 305 | # Subchannel ID: 8 306 | # Target: localhost:10001 307 | # State: READY 308 | # Calls Started: 4490 309 | # Calls Succeeded: 3966 310 | # Calls Failed: 524 311 | # Created Time: 7 minutes ago 312 | # --- 313 | # Socket ID Local->Remote Streams(Started/Succeeded/Failed) Messages(Sent/Received) 314 | # 9 ::1:47436->::1:10001 4490/4490/0 4490/3966 315 | ``` 316 | 317 | #### Usage 6: Inspect a Socket 318 | 319 | ```shell 320 | grpcdebug localhost:50051 channelz socket 9 321 | # Socket ID: 9 322 | # Address: ::1:47436->::1:10001 323 | # Streams Started: 4807 324 | # Streams Succeeded: 4807 325 | # Streams Failed: 0 326 | # Messages Sent: 4807 327 | # Messages Received: 4243 328 | # Keep Alives Sent: 0 329 | # Last Local Stream Created: now 330 | # Last Remote Stream Created: a long while ago 331 | # Last Message Sent Created: now 332 | # Last Message Received Created: now 333 | # Local Flow Control Window: 65535 334 | # Remote Flow Control Window: 65535 335 | # --- 336 | # Socket Options Name Value 337 | # SO_LINGER [type.googleapis.com/grpc.channelz.v1.SocketOptionLinger]:{duration:{}} 338 | # SO_RCVTIMEO [type.googleapis.com/grpc.channelz.v1.SocketOptionTimeout]:{duration:{}} 339 | # SO_SNDTIMEO [type.googleapis.com/grpc.channelz.v1.SocketOptionTimeout]:{duration:{}} 340 | # 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} 341 | # --- 342 | # Security Model: TLS 343 | # Standard Name: TLS_AES_128_GCM_SHA256 344 | ``` 345 | 346 | #### Usage 7: Inspect a Server 347 | 348 | ```shell 349 | grpcdebug localhost:50051 channelz server 1 350 | # Server Id: 1 351 | # Listen Addresses: [:::10001] 352 | # Calls Started: 5250 353 | # Calls Succeeded: 4647 354 | # Calls Failed: 603 355 | # Last Call Started: now 356 | # --- 357 | # Socket ID Local->Remote Streams(Started/Succeeded/Failed) Messages(Sent/Received) 358 | # 10 ::1:10001->::1:47436 5250/5250/0 4647/5250 359 | ``` 360 | 361 | #### Usage 8: Pagination 362 | 363 | 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` 364 | 365 | ```shell 366 | grpcdebug localhost:50051 channelz servers --start_id=0 --max_results=1 367 | # Server ID Listen Addresses Calls(Started/Succeeded/Failed) Last Call Started 368 | # 1 [:::10001] 2852/2530/322 now 369 | grpcdebug localhost:50051 channelz servers --start_id=2 --max_results=2 370 | # Server ID Listen Addresses Calls(Started/Succeeded/Failed) Last Call Started 371 | # 2 [:::50051] 29/28/0 now 372 | # 3 [:::50052] 4/4/0 26 seconds ago 373 | ``` 374 | 375 | It works similarly for printing channels via `channelz channels` and printing server sockets via `channelz server`. 376 | 377 | ### Debug xDS 378 | 379 | [xDS](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration) 380 | is a data plane configuration API commonly used in service mesh projects. It's 381 | created by Envoy, used by Istio, Traffic Director, and gRPC. 382 | 383 | #### Usage 1: xDS Resources Overview 384 | 385 | The xDS resources status might be `REQUESTED`/`DOES_NOT_EXIST`/`ACKED`/`NACKED` (see 386 | [config_dump.proto](https://github.com/envoyproxy/envoy/blob/b0ce15c96cebd89cf391869e49017325cd7faaa8/api/envoy/admin/v3/config_dump.proto#L22)). 387 | This view is intended for a quick scan if a configuration is propagated from the 388 | service mesh control plane. 389 | 390 | ```shell 391 | grpcdebug localhost:50051 xds status 392 | # Name Status Version Type LastUpdated 393 | # xds-test-server:1337 ACKED 1617141154495058478 type.googleapis.com/envoy.config.listener.v3.Listener 2 days ago 394 | # 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 395 | # cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229 ACKED 1617141154495058478 type.googleapis.com/envoy.config.cluster.v3.Cluster 2 days ago 396 | # cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229 ACKED 1 type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment 2 days ago 397 | ``` 398 | 399 | #### Usage 2: Dump xDS Configs 400 | 401 | ```shell 402 | grpcdebug localhost:50051 xds config 403 | # { 404 | # "config": [ 405 | # { 406 | # "node": { 407 | # "id": "projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b431-c1d43e6ac0ab", 408 | # "cluster": "cluster", 409 | # "metadata": { 410 | # "INSTANCE_IP": "192.168.120.31", 411 | # "TRAFFICDIRECTOR_GCP_PROJECT_NUMBER": "1040920224690", 412 | # "TRAFFICDIRECTOR_NETWORK_NAME": "default" 413 | # }, 414 | # ... 415 | ``` 416 | 417 | For an example config dump, see 418 | [csds_config_dump.json](internal/testing/testserver/csds_config_dump.json). 419 | 420 | #### Usage 3: Filter xDS Configs 421 | 422 | The dumped xDS config can be quite verbose, if I only interested in certain xDS 423 | type, grpcdebug can only print the selected section. 424 | 425 | ```shell 426 | grpcdebug localhost:50051 xds config --type=eds 427 | # { 428 | # "dynamicEndpointConfigs": [ 429 | # { 430 | # "versionInfo": "1", 431 | # "endpointConfig": { 432 | # "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 433 | # "clusterName": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229", 434 | # "endpoints": [ 435 | # { 436 | # "locality": { 437 | # "subZone": "jf:us-central1-a_7062512536751318190_neg" 438 | # }, 439 | # "lbEndpoints": [ 440 | # { 441 | # "endpoint": { 442 | # "address": { 443 | # "socketAddress": { 444 | # "address": "192.168.120.26", 445 | # "portValue": 8080 446 | # } 447 | # } 448 | # }, 449 | # "healthStatus": "HEALTHY" 450 | # } 451 | # ], 452 | # "loadBalancingWeight": 100 453 | # } 454 | # ] 455 | # }, 456 | # "lastUpdated": "2021-03-31T01:20:33.936Z", 457 | # "clientStatus": "ACKED" 458 | # } 459 | # ] 460 | # } 461 | ``` 462 | 463 | ## Admin Services 464 | 465 | ### gRPC Java: 466 | 467 | ```diff 468 | --- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java 469 | +++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java 470 | @@ -18,6 +18,7 @@ package io.grpc.examples.helloworld; 471 | 472 | import io.grpc.Server; 473 | import io.grpc.ServerBuilder; 474 | +import io.grpc.services.AdminInterface; 475 | import io.grpc.stub.StreamObserver; 476 | import java.io.IOException; 477 | import java.util.concurrent.TimeUnit; 478 | @@ -36,6 +37,7 @@ public class HelloWorldServer { 479 | int port = 50051; 480 | server = ServerBuilder.forPort(port) 481 | .addService(new GreeterImpl()) 482 | + .addServices(AdminInterface.getStandardServices()) 483 | .build() 484 | .start(); 485 | logger.info("Server started, listening on " + port); 486 | ``` 487 | 488 | 489 | ### gRPC Go: 490 | 491 | ```diff 492 | --- a/examples/helloworld/greeter_server/main.go 493 | +++ b/examples/helloworld/greeter_server/main.go 494 | @@ -27,6 +27,7 @@ import ( 495 | "net" 496 | 497 | "google.golang.org/grpc" 498 | + "google.golang.org/grpc/admin" 499 | pb "google.golang.org/grpc/examples/helloworld/helloworld" 500 | ) 501 | 502 | @@ -51,6 +52,11 @@ func main() { 503 | log.Fatalf("failed to listen: %v", err) 504 | } 505 | s := grpc.NewServer() 506 | + cleanup, err := admin.Register(s) 507 | + if err != nil { 508 | + log.Fatalf("failed to register admin: %v", err) 509 | + } 510 | + defer cleanup() 511 | pb.RegisterGreeterServer(s, &server{}) 512 | if err := s.Serve(lis); err != nil { 513 | log.Fatalf("failed to serve: %v", err) 514 | ``` 515 | 516 | 517 | ### gRPC C++: 518 | 519 | ```diff 520 | --- a/examples/cpp/helloworld/greeter_server.cc 521 | +++ b/examples/cpp/helloworld/greeter_server.cc 522 | @@ -20,6 +20,7 @@ 523 | #include 524 | #include 525 | 526 | +#include 527 | #include 528 | #include 529 | #include 530 | @@ -60,6 +61,7 @@ void RunServer() { 531 | // Register "service" as the instance through which we'll communicate with 532 | // clients. In this case it corresponds to an *synchronous* service. 533 | builder.RegisterService(&service); 534 | + grpc::AddAdminServices(&builder); 535 | // Finally assemble the server. 536 | std::unique_ptr server(builder.BuildAndStart()); 537 | std::cout << "Server listening on " << server_address << std::endl; 538 | ``` 539 | 540 | -------------------------------------------------------------------------------- /cmd/channelz.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | 9 | "github.com/dustin/go-humanize" 10 | "github.com/golang/protobuf/ptypes" 11 | timestamppb "github.com/golang/protobuf/ptypes/timestamp" 12 | "github.com/grpc-ecosystem/grpcdebug/cmd/transport" 13 | "github.com/grpc-ecosystem/grpcdebug/cmd/verbose" 14 | "github.com/spf13/cobra" 15 | zpb "google.golang.org/grpc/channelz/grpc_channelz_v1" 16 | ) 17 | 18 | var ( 19 | jsonOutputFlag bool 20 | startIDFlag int64 21 | maxResultsFlag int64 22 | ) 23 | 24 | func prettyTime(ts *timestamppb.Timestamp) string { 25 | if ts == nil || (ts.Seconds == 0 && ts.Nanos == 0) { 26 | return "" 27 | } 28 | if timestampFlag { 29 | return ptypes.TimestampString(ts) 30 | } 31 | t, _ := ptypes.Timestamp(ts) 32 | return humanize.Time(t) 33 | } 34 | 35 | func prettyAddress(addr *zpb.Address) string { 36 | if ipPort := addr.GetTcpipAddress(); ipPort != nil { 37 | address := net.TCPAddr{IP: net.IP(ipPort.IpAddress), Port: int(ipPort.Port)} 38 | return address.String() 39 | } 40 | panic(fmt.Sprintf("Address type not supported for %s", addr)) 41 | } 42 | 43 | func printChannelTraceEvents(events []*zpb.ChannelTraceEvent) { 44 | fmt.Fprintln(w, "Severity\tTime\tChild Ref\tDescription\t") 45 | for _, event := range events { 46 | var childRef string 47 | switch event.ChildRef.(type) { 48 | case *zpb.ChannelTraceEvent_SubchannelRef: 49 | childRef = fmt.Sprintf("subchannel(%v)", event.GetSubchannelRef()) 50 | case *zpb.ChannelTraceEvent_ChannelRef: 51 | childRef = fmt.Sprintf("channel(%v)", event.GetChannelRef()) 52 | } 53 | fmt.Fprintf( 54 | w, "%v\t%v\t%v\t%v\t\n", 55 | event.Severity, 56 | prettyTime(event.Timestamp), 57 | childRef, 58 | event.Description, 59 | ) 60 | } 61 | w.Flush() 62 | } 63 | 64 | func printSockets(sockets []*zpb.Socket) { 65 | fmt.Fprintln(w, "Socket ID\tLocal->Remote\tStreams(Started/Succeeded/Failed)\tMessages(Sent/Received)\t") 66 | for _, socket := range sockets { 67 | if socket.GetRef() == nil || socket.GetData() == nil { 68 | verbose.Debugf("failed to print socket: %s", socket) 69 | continue 70 | } 71 | fmt.Fprintf( 72 | w, "%v\t%v\t%v/%v/%v\t%v/%v\t\n", 73 | socket.Ref.SocketId, 74 | fmt.Sprintf("%v->%v", prettyAddress(socket.Local), prettyAddress(socket.Remote)), 75 | socket.Data.StreamsStarted, 76 | socket.Data.StreamsSucceeded, 77 | socket.Data.StreamsFailed, 78 | socket.Data.MessagesSent, 79 | socket.Data.MessagesReceived, 80 | ) 81 | } 82 | w.Flush() 83 | } 84 | 85 | func printObjectAsJSON(data interface{}) error { 86 | json, err := json.MarshalIndent(data, "", " ") 87 | if err != nil { 88 | return err 89 | } 90 | fmt.Println(string(json)) 91 | return nil 92 | } 93 | 94 | func printCreationTimestamp(data *zpb.ChannelData) string { 95 | return prettyTime(data.GetTrace().GetCreationTimestamp()) 96 | } 97 | 98 | func channelzChannelsCommandRunWithError(cmd *cobra.Command, args []string) error { 99 | var channels = transport.Channels(startIDFlag, maxResultsFlag) 100 | // Print as JSON 101 | if jsonOutputFlag { 102 | return printObjectAsJSON(channels) 103 | } 104 | // Print as table 105 | fmt.Fprintln(w, "Channel ID\tTarget\tState\tCalls(Started/Succeeded/Failed)\tCreated Time\t") 106 | for _, channel := range channels { 107 | if channel.GetRef() == nil || channel.GetData() == nil { 108 | verbose.Debugf("failed to print channel: %s", channel) 109 | continue 110 | } 111 | fmt.Fprintf( 112 | w, "%v\t%v\t%v\t%v/%v/%v\t%v\t\n", 113 | channel.Ref.ChannelId, 114 | channel.Data.Target, 115 | channel.Data.GetState().GetState(), 116 | channel.Data.CallsStarted, 117 | channel.Data.CallsSucceeded, 118 | channel.Data.CallsFailed, 119 | printCreationTimestamp(channel.Data), 120 | ) 121 | } 122 | w.Flush() 123 | return nil 124 | } 125 | 126 | var channelzChannelsCmd = &cobra.Command{ 127 | Use: "channels", 128 | Short: "List client channels for the target application.", 129 | Args: cobra.NoArgs, 130 | RunE: channelzChannelsCommandRunWithError, 131 | } 132 | 133 | func channelzChannelCommandRunWithError(cmd *cobra.Command, args []string) error { 134 | id, err := strconv.ParseInt(args[0], 10, 64) 135 | if err != nil { 136 | return fmt.Errorf("Failed to parse ID=%v: %v", args[0], err) 137 | } 138 | selected := transport.Channel(id) 139 | // Print as JSON 140 | if jsonOutputFlag { 141 | return printObjectAsJSON(selected) 142 | } 143 | // Print as table 144 | // Print Channel information 145 | fmt.Fprintf(w, "Channel ID:\t%v\t\n", selected.GetRef().GetChannelId()) 146 | fmt.Fprintf(w, "Target:\t%v\t\n", selected.GetData().GetTarget()) 147 | fmt.Fprintf(w, "State:\t%v\t\n", selected.GetData().GetState().GetState()) 148 | fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted()) 149 | fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded()) 150 | fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed()) 151 | fmt.Fprintf(w, "Created Time:\t%v\t\n", printCreationTimestamp(selected.GetData())) 152 | w.Flush() 153 | // Print Subchannel list 154 | if len(selected.GetSubchannelRef()) > 0 { 155 | fmt.Println("---") 156 | fmt.Fprintln(w, "Subchannel ID\tTarget\tState\tCalls(Started/Succeeded/Failed)\tCreatedTime\t") 157 | for _, subchannelRef := range selected.GetSubchannelRef() { 158 | var subchannel = transport.Subchannel(subchannelRef.GetSubchannelId()) 159 | if subchannel.GetRef() == nil || subchannel.GetData() == nil { 160 | verbose.Debugf("failed to print subchannel: %s", subchannel) 161 | continue 162 | } 163 | fmt.Fprintf( 164 | w, "%v\t%.50s\t%v\t%v/%v/%v\t%v\t\n", 165 | subchannel.Ref.SubchannelId, 166 | subchannel.Data.Target, 167 | subchannel.Data.State.State, 168 | subchannel.Data.CallsStarted, 169 | subchannel.Data.CallsSucceeded, 170 | subchannel.Data.CallsFailed, 171 | printCreationTimestamp(subchannel.Data), 172 | ) 173 | } 174 | w.Flush() 175 | } 176 | // Print channel trace events 177 | if len(selected.GetData().GetTrace().GetEvents()) != 0 { 178 | fmt.Println("---") 179 | printChannelTraceEvents(selected.Data.Trace.Events) 180 | } 181 | return nil 182 | } 183 | 184 | var channelzChannelCmd = &cobra.Command{ 185 | Use: "channel ", 186 | Short: "Display channel states in a human readable way.", 187 | Args: cobra.ExactArgs(1), 188 | RunE: channelzChannelCommandRunWithError, 189 | } 190 | 191 | func channelzSubchannelCommandRunWithError(cmd *cobra.Command, args []string) error { 192 | id, err := strconv.ParseInt(args[0], 10, 64) 193 | if err != nil { 194 | return fmt.Errorf("Failed to parse ID=%v: %v", args[0], err) 195 | } 196 | selected := transport.Subchannel(id) 197 | // Print as JSON 198 | if jsonOutputFlag { 199 | return printObjectAsJSON(selected) 200 | } 201 | // Print as table 202 | // Print Subchannel information 203 | fmt.Fprintf(w, "Subchannel ID:\t%v\t\n", selected.GetRef().GetSubchannelId()) 204 | fmt.Fprintf(w, "Target:\t%v\t\n", selected.GetData().GetTarget()) 205 | fmt.Fprintf(w, "State:\t%v\t\n", selected.GetData().GetState().GetState()) 206 | fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted()) 207 | fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded()) 208 | fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed()) 209 | fmt.Fprintf(w, "Created Time:\t%v\t\n", printCreationTimestamp(selected.GetData())) 210 | w.Flush() 211 | if len(selected.SocketRef) > 0 { 212 | // Print socket list 213 | fmt.Println("---") 214 | var sockets []*zpb.Socket 215 | for _, socketRef := range selected.GetSocketRef() { 216 | sockets = append(sockets, transport.Socket(socketRef.GetSocketId())) 217 | } 218 | printSockets(sockets) 219 | } 220 | return nil 221 | } 222 | 223 | var channelzSubchannelCmd = &cobra.Command{ 224 | Use: "subchannel ", 225 | Short: "Display subchannel states in a human readable way.", 226 | Args: cobra.ExactArgs(1), 227 | RunE: channelzSubchannelCommandRunWithError, 228 | } 229 | 230 | func channelzSocketCommandRunWithError(cmd *cobra.Command, args []string) error { 231 | socketID, err := strconv.ParseInt(args[0], 10, 64) 232 | if err != nil { 233 | return fmt.Errorf("Invalid socket ID %v", socketID) 234 | } 235 | selected := transport.Socket(socketID) 236 | // Print as JSON 237 | if jsonOutputFlag { 238 | return printObjectAsJSON(selected) 239 | } 240 | // Print as table 241 | // Print Socket information 242 | fmt.Fprintf(w, "Socket ID:\t%v\t\n", selected.GetRef().GetSocketId()) 243 | fmt.Fprintf(w, "Address:\t%v\t\n", fmt.Sprintf("%v->%v", prettyAddress(selected.GetLocal()), prettyAddress(selected.GetRemote()))) 244 | fmt.Fprintf(w, "Streams Started:\t%v\t\n", selected.GetData().GetStreamsStarted()) 245 | fmt.Fprintf(w, "Streams Succeeded:\t%v\t\n", selected.GetData().GetStreamsSucceeded()) 246 | fmt.Fprintf(w, "Streams Failed:\t%v\t\n", selected.GetData().GetStreamsFailed()) 247 | fmt.Fprintf(w, "Messages Sent:\t%v\t\n", selected.GetData().GetMessagesSent()) 248 | fmt.Fprintf(w, "Messages Received:\t%v\t\n", selected.GetData().GetMessagesReceived()) 249 | fmt.Fprintf(w, "Keep Alives Sent:\t%v\t\n", selected.GetData().GetKeepAlivesSent()) 250 | fmt.Fprintf(w, "Last Local Stream Created:\t%v\t\n", prettyTime(selected.GetData().GetLastLocalStreamCreatedTimestamp())) 251 | fmt.Fprintf(w, "Last Remote Stream Created:\t%v\t\n", prettyTime(selected.GetData().GetLastRemoteStreamCreatedTimestamp())) 252 | fmt.Fprintf(w, "Last Message Sent Created:\t%v\t\n", prettyTime(selected.GetData().GetLastMessageSentTimestamp())) 253 | fmt.Fprintf(w, "Last Message Received Created:\t%v\t\n", prettyTime(selected.GetData().GetLastMessageReceivedTimestamp())) 254 | fmt.Fprintf(w, "Local Flow Control Window:\t%v\t\n", selected.GetData().GetLocalFlowControlWindow().GetValue()) 255 | fmt.Fprintf(w, "Remote Flow Control Window:\t%v\t\n", selected.GetData().GetRemoteFlowControlWindow().GetValue()) 256 | w.Flush() 257 | if len(selected.GetData().GetOption()) > 0 { 258 | fmt.Println("---") 259 | fmt.Fprintln(w, "Socket Options Name\tValue\t") 260 | for _, option := range selected.GetData().GetOption() { 261 | if option.GetValue() != "" { 262 | // Prefer human readable value than the Any proto 263 | fmt.Fprintf(w, "%v\t%v\t\n", option.GetName(), option.GetValue()) 264 | } else { 265 | fmt.Fprintf(w, "%v\t%v\t\n", option.GetName(), option.GetAdditional()) 266 | } 267 | } 268 | w.Flush() 269 | } 270 | // Print security information 271 | if security := selected.GetSecurity(); security != nil { 272 | fmt.Println("---") 273 | switch x := security.Model.(type) { 274 | case *zpb.Security_Tls_: 275 | fmt.Fprintf(w, "Security Model:\t%v\t\n", "TLS") 276 | switch y := security.GetTls().GetCipherSuite().(type) { 277 | case *zpb.Security_Tls_StandardName: 278 | fmt.Fprintf(w, "Standard Name:\t%v\t\n", security.GetTls().GetStandardName()) 279 | case *zpb.Security_Tls_OtherName: 280 | fmt.Fprintf(w, "Other Name:\t%v\t\n", security.GetTls().GetOtherName()) 281 | default: 282 | return fmt.Errorf("Unexpected Cipher suite name type %T", y) 283 | } 284 | // fmt.Fprintf(w, "Local Certificate:\t%v\t\n", security.GetTls().LocalCertificate) 285 | // fmt.Fprintf(w, "Remote Certificate:\t%v\t\n", security.GetTls().RemoteCertificate) 286 | case *zpb.Security_Other: 287 | fmt.Fprintf(w, "Security Model:\t%v\t\n", "Other") 288 | fmt.Fprintf(w, "Name:\t%v\t\n", security.GetOther().GetName()) 289 | // fmt.Fprintf(w, "Value:\t%v\t\n", security.GetOther().Value) 290 | default: 291 | return fmt.Errorf("Unexpected security model type %T", x) 292 | } 293 | w.Flush() 294 | } 295 | return nil 296 | } 297 | 298 | var channelzSocketCmd = &cobra.Command{ 299 | Use: "socket ", 300 | Short: "Display socket states in a human readable way.", 301 | Args: cobra.ExactArgs(1), 302 | RunE: channelzSocketCommandRunWithError, 303 | } 304 | 305 | func channelzServersCommandRunWithError(cmd *cobra.Command, args []string) error { 306 | var servers = transport.Servers(startIDFlag, maxResultsFlag) 307 | // Print as JSON 308 | if jsonOutputFlag { 309 | return printObjectAsJSON(servers) 310 | } 311 | // Print as table 312 | fmt.Fprintln(w, "Server ID\tListen Addresses\tCalls(Started/Succeeded/Failed)\tLast Call Started\t") 313 | for _, server := range servers { 314 | var listenAddresses []string 315 | for _, socketRef := range server.GetListenSocket() { 316 | socket := transport.Socket(socketRef.SocketId) 317 | listenAddresses = append(listenAddresses, prettyAddress(socket.GetLocal())) 318 | } 319 | fmt.Fprintf( 320 | w, "%v\t%v\t%v/%v/%v\t%v\t\n", 321 | server.GetRef().GetServerId(), 322 | listenAddresses, 323 | server.GetData().GetCallsStarted(), 324 | server.GetData().GetCallsSucceeded(), 325 | server.GetData().GetCallsFailed(), 326 | prettyTime(server.GetData().GetLastCallStartedTimestamp()), 327 | ) 328 | } 329 | w.Flush() 330 | return nil 331 | } 332 | 333 | var channelzServersCmd = &cobra.Command{ 334 | Use: "servers", 335 | Short: "List servers in a human readable way.", 336 | Args: cobra.NoArgs, 337 | RunE: channelzServersCommandRunWithError, 338 | } 339 | 340 | func channelzServerCommandRunWithError(cmd *cobra.Command, args []string) error { 341 | serverID, err := strconv.ParseInt(args[0], 10, 64) 342 | if err != nil { 343 | return fmt.Errorf("Invalid server ID %v", serverID) 344 | } 345 | selected := transport.Server(serverID) 346 | // Print as JSON 347 | if jsonOutputFlag { 348 | return printObjectAsJSON(selected) 349 | } 350 | // Print as table 351 | var listenAddresses []string 352 | for _, socketRef := range selected.GetListenSocket() { 353 | socket := transport.Socket(socketRef.GetSocketId()) 354 | listenAddresses = append(listenAddresses, prettyAddress(socket.GetLocal())) 355 | } 356 | fmt.Fprintf(w, "Server Id:\t%v\t\n", selected.GetRef().GetServerId()) 357 | fmt.Fprintf(w, "Listen Addresses:\t%v\t\n", listenAddresses) 358 | fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted()) 359 | fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded()) 360 | fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed()) 361 | fmt.Fprintf(w, "Last Call Started:\t%v\t\n", prettyTime(selected.GetData().GetLastCallStartedTimestamp())) 362 | w.Flush() 363 | if sockets := transport.ServerSocket(selected.GetRef().GetServerId(), startIDFlag, maxResultsFlag); len(sockets) > 0 { 364 | // Print socket list 365 | fmt.Println("---") 366 | printSockets(sockets) 367 | } 368 | return nil 369 | } 370 | 371 | var channelzServerCmd = &cobra.Command{ 372 | Use: "server ", 373 | Short: "Display the server state in a human readable way.", 374 | Args: cobra.ExactArgs(1), 375 | RunE: channelzServerCommandRunWithError, 376 | } 377 | 378 | var channelzCmd = &cobra.Command{ 379 | Use: "channelz", 380 | Short: "Display gRPC states in a human readable way.", 381 | Args: cobra.NoArgs, 382 | } 383 | 384 | func init() { 385 | rootCmd.AddCommand(channelzCmd) 386 | channelzChannelsCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of output channels") 387 | channelzChannelsCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start channel ID") 388 | channelzServerCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of the output sockets") 389 | channelzServerCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start server socket ID") 390 | channelzServersCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of output servers") 391 | channelzServersCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start server ID") 392 | channelzCmd.PersistentFlags().BoolVarP(&jsonOutputFlag, "json", "o", false, "Whether to print the result as JSON") 393 | channelzCmd.AddCommand(channelzChannelCmd) 394 | channelzCmd.AddCommand(channelzChannelsCmd) 395 | channelzCmd.AddCommand(channelzSubchannelCmd) 396 | channelzCmd.AddCommand(channelzSocketCmd) 397 | channelzCmd.AddCommand(channelzServersCmd) 398 | channelzCmd.AddCommand(channelzServerCmd) 399 | } 400 | -------------------------------------------------------------------------------- /cmd/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "runtime" 9 | 10 | "github.com/grpc-ecosystem/grpcdebug/cmd/verbose" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // SecurityType is the enum type of available security modes 15 | type SecurityType string 16 | 17 | const ( 18 | // TypeInsecure is the insecure security mode and it is the default value 19 | TypeInsecure SecurityType = "insecure" 20 | // TypeTLS is the TLS security mode, which requires caller to provide 21 | // credentials to connect to peer 22 | TypeTLS = "tls" 23 | ) 24 | 25 | // The environment variable name of getting the server configs 26 | const grpcdebugServerConfigEnvName = "GRPCDEBUG_CONFIG" 27 | 28 | // ServerConfig is the configuration for how to connect to a target 29 | type ServerConfig struct { 30 | RealAddress string `yaml:"real_address"` 31 | Security SecurityType `yaml:"security"` 32 | CredentialFile string `yaml:"credential_file"` 33 | ServerNameOverride string `yaml:"server_name_override"` 34 | } 35 | 36 | type grpcdebugConfig struct { 37 | Servers map[string]ServerConfig `yaml:"servers"` 38 | } 39 | 40 | func loadServerConfigsFromFile(path string) map[string]ServerConfig { 41 | file, err := os.Open(path) 42 | if err != nil { 43 | panic(err) 44 | } 45 | bytes, err := ioutil.ReadAll(file) 46 | if err != nil { 47 | panic(err) 48 | } 49 | var config grpcdebugConfig 50 | err = yaml.Unmarshal(bytes, &config) 51 | if err != nil { 52 | panic(err) 53 | } 54 | verbose.Debugf("Loaded grpcdebug config from %v: %v", path, config) 55 | return config.Servers 56 | } 57 | 58 | // userConfigDir is copied here, so we can support Go v1.12 59 | func userConfigDir() (string, error) { 60 | var dir string 61 | switch runtime.GOOS { 62 | case "windows": 63 | dir = os.Getenv("AppData") 64 | if dir == "" { 65 | return "", errors.New("%AppData% is not defined") 66 | } 67 | 68 | case "darwin", "ios": 69 | dir = os.Getenv("HOME") 70 | if dir == "" { 71 | return "", errors.New("$HOME is not defined") 72 | } 73 | dir += "/Library/Application Support" 74 | 75 | case "plan9": 76 | dir = os.Getenv("home") 77 | if dir == "" { 78 | return "", errors.New("$home is not defined") 79 | } 80 | dir += "/lib" 81 | 82 | default: // Unix 83 | dir = os.Getenv("XDG_CONFIG_HOME") 84 | if dir == "" { 85 | dir = os.Getenv("HOME") 86 | if dir == "" { 87 | return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined") 88 | } 89 | dir += "/.config" 90 | } 91 | } 92 | return dir, nil 93 | } 94 | 95 | func loadServerConfigs() map[string]ServerConfig { 96 | if value := os.Getenv(grpcdebugServerConfigEnvName); value != "" { 97 | return loadServerConfigsFromFile(value) 98 | } 99 | // Try to load from work directory, if exists 100 | if _, err := os.Stat("./grpcdebug_config.yaml"); err == nil { 101 | return loadServerConfigsFromFile("./grpcdebug_config.yaml") 102 | } 103 | // Try to load from user config directory, if exists 104 | dir, _ := userConfigDir() 105 | defaultUserConfig := path.Join(dir, "grpcdebug_config.yaml") 106 | if _, err := os.Stat(defaultUserConfig); err == nil { 107 | return loadServerConfigsFromFile(defaultUserConfig) 108 | } 109 | return nil 110 | } 111 | 112 | // GetServerConfig returns a connect configuration for the given target 113 | func GetServerConfig(target string) ServerConfig { 114 | for pattern, config := range loadServerConfigs() { 115 | // TODO(lidiz): support wildcards 116 | if pattern == target { 117 | if config.RealAddress == "" { 118 | config.RealAddress = pattern 119 | } 120 | return config 121 | } 122 | } 123 | return ServerConfig{RealAddress: target} 124 | } 125 | -------------------------------------------------------------------------------- /cmd/health.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/grpc-ecosystem/grpcdebug/cmd/transport" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var healthCmd = &cobra.Command{ 12 | Use: "health [service names]", 13 | Short: "Check health status of the target service (default \"\").", 14 | RunE: func(cmd *cobra.Command, args []string) error { 15 | var services []string 16 | // Ensure there's the overall health status 17 | services = append(services, "") 18 | services = append(services, args...) 19 | // Sort alphabetically, and deduplicate inputs 20 | sort.Strings(services) 21 | j := 0 22 | for i := 1; i < len(services); i++ { 23 | if services[i] == services[j] { 24 | continue 25 | } 26 | j++ 27 | services[j] = services[i] 28 | } 29 | services = services[:j+1] 30 | // Print as table 31 | for _, service := range services { 32 | var serviceName string 33 | if service == "" { 34 | serviceName = "" 35 | } else { 36 | serviceName = service 37 | } 38 | fmt.Fprintf( 39 | w, "%v:\t%v\t\n", 40 | serviceName, 41 | transport.GetHealthStatus(service), 42 | ) 43 | } 44 | w.Flush() 45 | return nil 46 | }, 47 | } 48 | 49 | func init() { 50 | rootCmd.AddCommand(healthCmd) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Defines the root command and global flags 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "os" 9 | "text/tabwriter" 10 | 11 | "github.com/grpc-ecosystem/grpcdebug/cmd/config" 12 | "github.com/grpc-ecosystem/grpcdebug/cmd/transport" 13 | "github.com/grpc-ecosystem/grpcdebug/cmd/verbose" 14 | 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | var verboseFlag, timestampFlag bool 19 | var address, security, credFile, serverNameOverride string 20 | 21 | // The table formater 22 | var w = tabwriter.NewWriter(os.Stdout, 10, 0, 3, ' ', 0) 23 | 24 | var rootUsageTemplate = `Usage:{{if .Runnable}} 25 | {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} 26 | grpcdebug [flags] {{ .CommandPath | ChildCommandPath }} {{end}}{{if gt (len .Aliases) 0}} 27 | 28 | Aliases: 29 | {{.NameAndAliases}}{{end}}{{if .HasExample}} 30 | 31 | Examples: 32 | {{.Example}}{{end}}{{if .HasAvailableSubCommands}} 33 | 34 | Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} 35 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} 36 | 37 | Flags: 38 | {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} 39 | 40 | Global Flags: 41 | {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} 42 | 43 | Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} 44 | {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} 45 | 46 | Use "grpcdebug {{ .CommandPath | ChildCommandPath }} [command] --help" for more information about a command.{{end}} 47 | ` 48 | 49 | var rootCmd = &cobra.Command{ 50 | Use: "grpcdebug", 51 | Short: "grpcdebug is a gRPC service admin CLI", 52 | } 53 | 54 | func initConfig() { 55 | if verboseFlag { 56 | verbose.EnableDebugOutput() 57 | } 58 | c := config.GetServerConfig(address) 59 | if credFile != "" { 60 | c.CredentialFile = credFile 61 | } 62 | if serverNameOverride != "" { 63 | c.ServerNameOverride = serverNameOverride 64 | } 65 | if security == "tls" { 66 | c.Security = config.TypeTLS 67 | if c.CredentialFile == "" { 68 | rootCmd.Usage() 69 | log.Fatalf("Please specify credential file under [tls] mode.") 70 | } 71 | } else if security != "insecure" { 72 | rootCmd.Usage() 73 | log.Fatalf("Unrecognized security mode: %v", security) 74 | } 75 | transport.Connect(c) 76 | } 77 | 78 | // ChildCommandPath used in template 79 | func ChildCommandPath(path string) string { 80 | if len(path) <= 10 { 81 | return "" 82 | } 83 | return path[10:] 84 | } 85 | 86 | func init() { 87 | cobra.AddTemplateFunc("ChildCommandPath", ChildCommandPath) 88 | cobra.OnInitialize(initConfig) 89 | rootCmd.SetUsageTemplate(rootUsageTemplate) 90 | 91 | rootCmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Print verbose information for debugging") 92 | rootCmd.PersistentFlags().BoolVarP(×tampFlag, "timestamp", "t", false, "Print timestamp as RFC3339 instead of human readable strings") 93 | rootCmd.PersistentFlags().StringVar(&security, "security", "insecure", "Defines the type of credentials to use [tls, google-default, insecure]") 94 | rootCmd.PersistentFlags().StringVar(&credFile, "credential_file", "", "Sets the path of the credential file; used in [tls] mode") 95 | rootCmd.PersistentFlags().StringVar(&serverNameOverride, "server_name_override", "", "Overrides the peer server name if non empty; used in [tls] mode") 96 | } 97 | 98 | // Execute executes the root command. 99 | func Execute() { 100 | if len(os.Args) > 1 { 101 | address = os.Args[1] 102 | os.Args = os.Args[1:] 103 | } else { 104 | rootCmd.Usage() 105 | os.Exit(1) 106 | } 107 | if err := rootCmd.Execute(); err != nil { 108 | fmt.Println(err) 109 | os.Exit(1) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /cmd/transport/grpc.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 9 | "github.com/grpc-ecosystem/grpcdebug/cmd/config" 10 | "github.com/grpc-ecosystem/grpcdebug/cmd/verbose" 11 | "google.golang.org/grpc" 12 | zpb "google.golang.org/grpc/channelz/grpc_channelz_v1" 13 | "google.golang.org/grpc/credentials" 14 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 15 | ) 16 | 17 | var conn *grpc.ClientConn 18 | var channelzClient zpb.ChannelzClient 19 | var csdsClient csdspb.ClientStatusDiscoveryServiceClient 20 | var healthClient healthpb.HealthClient 21 | 22 | const connectionTimeout = time.Second * 5 23 | const rpcTimeout = time.Second * 15 24 | 25 | // Connect connects to the service at address and creates stubs 26 | func Connect(c config.ServerConfig) { 27 | verbose.Debugf("Connecting with %v", c) 28 | var err error 29 | var credOption grpc.DialOption 30 | if c.CredentialFile != "" { 31 | cred, err := credentials.NewClientTLSFromFile(c.CredentialFile, c.ServerNameOverride) 32 | if err != nil { 33 | log.Fatalf("failed to create credential: %v", err) 34 | } 35 | credOption = grpc.WithTransportCredentials(cred) 36 | } else { 37 | credOption = grpc.WithInsecure() 38 | } 39 | // Dial, wait for READY, with a timeout. 40 | ctx, cancel := context.WithTimeout(context.Background(), connectionTimeout) 41 | defer cancel() 42 | conn, err = grpc.DialContext(ctx, c.RealAddress, credOption, grpc.WithBlock()) 43 | if err != nil { 44 | log.Fatalf("failed to connect: %v", err) 45 | } 46 | channelzClient = zpb.NewChannelzClient(conn) 47 | csdsClient = csdspb.NewClientStatusDiscoveryServiceClient(conn) 48 | healthClient = healthpb.NewHealthClient(conn) 49 | } 50 | 51 | // Channels returns all available channels 52 | func Channels(startID, maxResults int64) []*zpb.Channel { 53 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 54 | defer cancel() 55 | channels, err := channelzClient.GetTopChannels(ctx, &zpb.GetTopChannelsRequest{StartChannelId: startID, MaxResults: maxResults}) 56 | if err != nil { 57 | log.Fatalf("failed to fetch top channels: %v", err) 58 | } 59 | return channels.Channel 60 | } 61 | 62 | // Channel returns the channel with given channel ID 63 | func Channel(channelID int64) *zpb.Channel { 64 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 65 | defer cancel() 66 | channel, err := channelzClient.GetChannel(ctx, &zpb.GetChannelRequest{ChannelId: channelID}) 67 | if err != nil { 68 | log.Fatalf("failed to fetch channel id=%v: %v", channelID, err) 69 | } 70 | return channel.Channel 71 | } 72 | 73 | // Subchannel returns the queried subchannel 74 | func Subchannel(subchannelID int64) *zpb.Subchannel { 75 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 76 | defer cancel() 77 | subchannel, err := channelzClient.GetSubchannel(ctx, &zpb.GetSubchannelRequest{SubchannelId: subchannelID}) 78 | if err != nil { 79 | log.Fatalf("failed to fetch subchannel (id=%v): %v", subchannelID, err) 80 | } 81 | return subchannel.Subchannel 82 | } 83 | 84 | // Servers returns all available servers 85 | func Servers(startID, maxResults int64) []*zpb.Server { 86 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 87 | defer cancel() 88 | servers, err := channelzClient.GetServers(ctx, &zpb.GetServersRequest{StartServerId: startID, MaxResults: maxResults}) 89 | if err != nil { 90 | log.Fatalf("failed to fetch servers: %v", err) 91 | } 92 | return servers.Server 93 | } 94 | 95 | // Server returns a server 96 | func Server(serverID int64) *zpb.Server { 97 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 98 | defer cancel() 99 | server, err := channelzClient.GetServer(ctx, &zpb.GetServerRequest{ServerId: serverID}) 100 | if err != nil { 101 | log.Fatalf("failed to fetch server (id=%v): %v", serverID, err) 102 | } 103 | return server.Server 104 | } 105 | 106 | // Socket returns a socket 107 | func Socket(socketID int64) *zpb.Socket { 108 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 109 | defer cancel() 110 | socket, err := channelzClient.GetSocket(ctx, &zpb.GetSocketRequest{SocketId: socketID}) 111 | if err != nil { 112 | log.Fatalf("failed to fetch socket (id=%v): %v", socketID, err) 113 | } 114 | return socket.Socket 115 | } 116 | 117 | // ServerSocket returns all sockets of this server 118 | func ServerSocket(serverID, startID, maxResults int64) []*zpb.Socket { 119 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 120 | defer cancel() 121 | var s []*zpb.Socket 122 | serverSocketResp, err := channelzClient.GetServerSockets( 123 | ctx, 124 | &zpb.GetServerSocketsRequest{ 125 | ServerId: serverID, 126 | StartSocketId: startID, 127 | MaxResults: maxResults, 128 | }, 129 | ) 130 | if err != nil { 131 | log.Fatalf("failed to fetch server sockets (id=%v): %v", serverID, err) 132 | } 133 | for _, socketRef := range serverSocketResp.SocketRef { 134 | s = append(s, Socket(socketRef.SocketId)) 135 | } 136 | return s 137 | } 138 | 139 | // FetchClientStatus fetches the xDS resources status 140 | func FetchClientStatus() *csdspb.ClientStatusResponse { 141 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 142 | defer cancel() 143 | resp, err := csdsClient.FetchClientStatus(ctx, &csdspb.ClientStatusRequest{}) 144 | if err != nil { 145 | log.Fatalf("failed to fetch xds config: %v", err) 146 | } 147 | return resp 148 | } 149 | 150 | // GetHealthStatus fetches the health checking status of the service from peer 151 | func GetHealthStatus(service string) string { 152 | ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) 153 | defer cancel() 154 | resp, err := healthClient.Check(ctx, &healthpb.HealthCheckRequest{Service: service}) 155 | if err != nil { 156 | verbose.Debugf("failed to fetch health status for \"%s\": %v", service, err) 157 | return healthpb.HealthCheckResponse_SERVICE_UNKNOWN.String() 158 | } 159 | return resp.Status.String() 160 | } 161 | -------------------------------------------------------------------------------- /cmd/verbose/verbose.go: -------------------------------------------------------------------------------- 1 | package verbose 2 | 3 | import "log" 4 | 5 | var enableDebugOutput = false 6 | 7 | // EnableDebugOutput enables debugging output 8 | func EnableDebugOutput() { 9 | enableDebugOutput = true 10 | } 11 | 12 | // Debugf prints log if debugging is enabled 13 | func Debugf(format string, v ...interface{}) { 14 | if enableDebugOutput { 15 | log.Printf(format, v...) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cmd/xds.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/grpc-ecosystem/grpcdebug/cmd/transport" 9 | 10 | adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3" 11 | clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 12 | endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 13 | routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 14 | csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 15 | "github.com/golang/protobuf/ptypes" 16 | timestamppb "github.com/golang/protobuf/ptypes/timestamp" 17 | "github.com/spf13/cobra" 18 | "google.golang.org/protobuf/encoding/protojson" 19 | "google.golang.org/protobuf/proto" 20 | ) 21 | 22 | var xdsTypeFlag string 23 | 24 | func printProtoBufMessageAsJSON(m proto.Message) error { 25 | option := protojson.MarshalOptions{ 26 | Multiline: true, 27 | Indent: " ", 28 | UseProtoNames: false, 29 | UseEnumNumbers: false, 30 | } 31 | jsonbytes, err := option.Marshal(m) 32 | if err != nil { 33 | return err 34 | } 35 | fmt.Println(string(jsonbytes)) 36 | return nil 37 | } 38 | 39 | func priorityPerXdsConfig(x *csdspb.PerXdsConfig) int { 40 | switch x.PerXdsConfig.(type) { 41 | case *csdspb.PerXdsConfig_ListenerConfig: 42 | return 0 43 | case *csdspb.PerXdsConfig_RouteConfig: 44 | return 1 45 | case *csdspb.PerXdsConfig_ClusterConfig: 46 | return 2 47 | case *csdspb.PerXdsConfig_EndpointConfig: 48 | return 3 49 | default: 50 | return 4 51 | } 52 | } 53 | 54 | func sortPerXdsConfigs(clientStatus *csdspb.ClientStatusResponse) { 55 | sort.Slice(clientStatus.Config[0].XdsConfig, func(i, j int) bool { 56 | return priorityPerXdsConfig(clientStatus.Config[0].XdsConfig[i]) < priorityPerXdsConfig(clientStatus.Config[0].XdsConfig[j]) 57 | }) 58 | } 59 | 60 | func xdsConfigCommandRunWithError(cmd *cobra.Command, args []string) error { 61 | clientStatus := transport.FetchClientStatus() 62 | if len(clientStatus.Config) != 1 { 63 | return fmt.Errorf("Received unexpected number of ClientConfig %v", len(clientStatus.Config)) 64 | } 65 | if xdsTypeFlag == "" { 66 | // No filters, just print the whole thing 67 | sortPerXdsConfigs(clientStatus) 68 | return printProtoBufMessageAsJSON(clientStatus) 69 | } 70 | // Parse flags 71 | wantXdsTypes := strings.Split(xdsTypeFlag, ",") 72 | var wantLDS, wantRDS, wantCDS, wantEDS bool 73 | for _, wantXdsType := range wantXdsTypes { 74 | switch strings.ToLower(wantXdsType) { 75 | case "lds": 76 | wantLDS = true 77 | case "rds": 78 | wantRDS = true 79 | case "cds": 80 | wantCDS = true 81 | case "eds": 82 | wantEDS = true 83 | } 84 | } 85 | // Filter the CSDS output 86 | for _, genericXdsConfig := range clientStatus.Config[0].GenericXdsConfigs { 87 | var printSubject proto.Message 88 | tokens := strings.Split(genericXdsConfig.TypeUrl, ".") 89 | switch tokens[len(tokens)-1] { 90 | case "Listener": 91 | if wantLDS { 92 | printSubject = genericXdsConfig.GetXdsConfig() 93 | } 94 | case "RouteConfiguration": 95 | if wantRDS { 96 | printSubject = genericXdsConfig.GetXdsConfig() 97 | } 98 | case "Cluster": 99 | if wantCDS { 100 | printSubject = genericXdsConfig.GetXdsConfig() 101 | } 102 | case "ClusterLoadAssignment": 103 | if wantEDS { 104 | printSubject = genericXdsConfig.GetXdsConfig() 105 | } 106 | } 107 | if printSubject != nil { 108 | err := printProtoBufMessageAsJSON(printSubject) 109 | if err != nil { 110 | return fmt.Errorf("Failed to print xDS config: %v", err) 111 | } 112 | } 113 | } 114 | if len(clientStatus.Config[0].GenericXdsConfigs) == 0 { 115 | for _, xdsConfig := range clientStatus.Config[0].XdsConfig { 116 | var printSubject proto.Message 117 | switch xdsConfig.PerXdsConfig.(type) { 118 | case *csdspb.PerXdsConfig_ListenerConfig: 119 | if wantLDS { 120 | printSubject = xdsConfig.GetListenerConfig() 121 | } 122 | case *csdspb.PerXdsConfig_RouteConfig: 123 | if wantRDS { 124 | printSubject = xdsConfig.GetRouteConfig() 125 | } 126 | case *csdspb.PerXdsConfig_ClusterConfig: 127 | if wantCDS { 128 | printSubject = xdsConfig.GetClusterConfig() 129 | } 130 | case *csdspb.PerXdsConfig_EndpointConfig: 131 | if wantEDS { 132 | printSubject = xdsConfig.GetEndpointConfig() 133 | } 134 | } 135 | if printSubject != nil { 136 | err := printProtoBufMessageAsJSON(printSubject) 137 | if err != nil { 138 | return fmt.Errorf("Failed to print xDS config: %v", err) 139 | } 140 | } 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | var xdsConfigCmd = &cobra.Command{ 147 | Use: "config", 148 | Short: "Dump the operating xDS configs.", 149 | RunE: xdsConfigCommandRunWithError, 150 | Args: cobra.NoArgs, 151 | } 152 | 153 | type xdsResourceStatusEntry struct { 154 | Name string 155 | Status adminpb.ClientResourceStatus 156 | Version string 157 | Type string 158 | LastUpdated *timestamppb.Timestamp 159 | } 160 | 161 | func printStatusEntry(entry *xdsResourceStatusEntry) { 162 | fmt.Fprintf( 163 | w, "%v\t%v\t%v\t%v\t%v\t\n", 164 | entry.Name, 165 | entry.Status, 166 | entry.Version, 167 | entry.Type, 168 | prettyTime(entry.LastUpdated), 169 | ) 170 | } 171 | 172 | func xdsStatusCommandRunWithError(cmd *cobra.Command, args []string) error { 173 | clientStatus := transport.FetchClientStatus() 174 | if len(clientStatus.Config) != 1 { 175 | return fmt.Errorf("Received unexpected number of ClientConfig %v", len(clientStatus.Config)) 176 | } 177 | 178 | fmt.Fprintln(w, "Name\tStatus\tVersion\tType\tLastUpdated") 179 | config := clientStatus.Config[0] 180 | for _, genericXdsConfig := range config.GenericXdsConfigs { 181 | entry := xdsResourceStatusEntry{ 182 | Name: genericXdsConfig.Name, 183 | Status: genericXdsConfig.ClientStatus, 184 | Version: genericXdsConfig.VersionInfo, 185 | Type: genericXdsConfig.TypeUrl, 186 | LastUpdated: genericXdsConfig.LastUpdated, 187 | } 188 | printStatusEntry(&entry) 189 | } 190 | if len(config.GenericXdsConfigs) == 0 { 191 | for _, xdsConfig := range config.XdsConfig { 192 | switch xdsConfig.PerXdsConfig.(type) { 193 | case *csdspb.PerXdsConfig_ListenerConfig: 194 | for _, dynamicListener := range xdsConfig.GetListenerConfig().DynamicListeners { 195 | entry := xdsResourceStatusEntry{ 196 | Name: dynamicListener.Name, 197 | Status: dynamicListener.ClientStatus, 198 | } 199 | if state := dynamicListener.GetActiveState(); state != nil { 200 | entry.Version = state.VersionInfo 201 | entry.Type = state.Listener.TypeUrl 202 | entry.LastUpdated = state.LastUpdated 203 | } 204 | printStatusEntry(&entry) 205 | } 206 | case *csdspb.PerXdsConfig_RouteConfig: 207 | for _, dynamicRouteConfig := range xdsConfig.GetRouteConfig().DynamicRouteConfigs { 208 | entry := xdsResourceStatusEntry{ 209 | Status: dynamicRouteConfig.ClientStatus, 210 | Version: dynamicRouteConfig.VersionInfo, 211 | Type: dynamicRouteConfig.RouteConfig.TypeUrl, 212 | LastUpdated: dynamicRouteConfig.LastUpdated, 213 | } 214 | if packed := dynamicRouteConfig.GetRouteConfig(); packed != nil { 215 | var routeConfig routepb.RouteConfiguration 216 | if err := ptypes.UnmarshalAny(packed, &routeConfig); err != nil { 217 | return err 218 | } 219 | entry.Name = routeConfig.Name 220 | } 221 | printStatusEntry(&entry) 222 | } 223 | case *csdspb.PerXdsConfig_ClusterConfig: 224 | for _, dynamicCluster := range xdsConfig.GetClusterConfig().DynamicActiveClusters { 225 | entry := xdsResourceStatusEntry{ 226 | Status: dynamicCluster.ClientStatus, 227 | Version: dynamicCluster.VersionInfo, 228 | Type: dynamicCluster.Cluster.TypeUrl, 229 | LastUpdated: dynamicCluster.LastUpdated, 230 | } 231 | if packed := dynamicCluster.GetCluster(); packed != nil { 232 | var cluster clusterpb.Cluster 233 | if err := ptypes.UnmarshalAny(packed, &cluster); err != nil { 234 | return err 235 | } 236 | entry.Name = cluster.Name 237 | } 238 | printStatusEntry(&entry) 239 | } 240 | case *csdspb.PerXdsConfig_EndpointConfig: 241 | for _, dynamicEndpoint := range xdsConfig.GetEndpointConfig().GetDynamicEndpointConfigs() { 242 | entry := xdsResourceStatusEntry{ 243 | Status: dynamicEndpoint.ClientStatus, 244 | Version: dynamicEndpoint.VersionInfo, 245 | Type: dynamicEndpoint.EndpointConfig.TypeUrl, 246 | LastUpdated: dynamicEndpoint.LastUpdated, 247 | } 248 | if packed := dynamicEndpoint.GetEndpointConfig(); packed != nil { 249 | var endpoint endpointpb.ClusterLoadAssignment 250 | if err := ptypes.UnmarshalAny(packed, &endpoint); err != nil { 251 | return err 252 | } 253 | entry.Name = endpoint.ClusterName 254 | } 255 | printStatusEntry(&entry) 256 | } 257 | } 258 | } 259 | } 260 | w.Flush() 261 | return nil 262 | } 263 | 264 | var xdsStatusCmd = &cobra.Command{ 265 | Use: "status", 266 | Short: "Print the config synchronization status.", 267 | RunE: xdsStatusCommandRunWithError, 268 | } 269 | 270 | var xdsCmd = &cobra.Command{ 271 | Use: "xds", 272 | Short: "Fetch xDS related information.", 273 | } 274 | 275 | func init() { 276 | 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)") 277 | xdsCmd.AddCommand(xdsConfigCmd) 278 | xdsCmd.AddCommand(xdsStatusCmd) 279 | rootCmd.AddCommand(xdsCmd) 280 | } 281 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grpc-ecosystem/grpcdebug 2 | 3 | go 1.22.7 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/dustin/go-humanize v1.0.1 9 | github.com/envoyproxy/go-control-plane v0.13.4 10 | github.com/golang/protobuf v1.5.4 11 | github.com/spf13/cobra v1.8.1 12 | google.golang.org/grpc v1.68.0 13 | google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e 14 | google.golang.org/protobuf v1.35.2 15 | gopkg.in/yaml.v2 v2.4.0 16 | ) 17 | 18 | require ( 19 | cel.dev/expr v0.16.2 // indirect 20 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 21 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 24 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 25 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 26 | github.com/kr/text v0.2.0 // indirect 27 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 28 | github.com/prometheus/client_model v0.6.1 // indirect 29 | github.com/rogpeppe/go-internal v1.13.1 // indirect 30 | github.com/spf13/pflag v1.0.5 // indirect 31 | golang.org/x/net v0.30.0 // indirect 32 | golang.org/x/oauth2 v0.23.0 // indirect 33 | golang.org/x/sync v0.8.0 // indirect 34 | golang.org/x/sys v0.26.0 // indirect 35 | golang.org/x/text v0.19.0 // indirect 36 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect 37 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.16.2 h1:RwRhoH17VhAu9U5CMvMhH1PDVgf0tuz9FT+24AfMLfU= 2 | cel.dev/expr v0.16.2/go.mod h1:gXngZQMkWJoSbE8mOzehJlXQyubn/Vg0vR9/F3W7iw8= 3 | cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= 4 | cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= 5 | github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= 6 | github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= 10 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 12 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 13 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 14 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 15 | github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= 16 | github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= 17 | github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= 18 | github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= 19 | github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= 20 | github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= 21 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 22 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 23 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 24 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 25 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 26 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 27 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 28 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 29 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 30 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 31 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 32 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 33 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 34 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 35 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 36 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 37 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 38 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 39 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 40 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 41 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 42 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 43 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 44 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 45 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 46 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 47 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 48 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 49 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 50 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 51 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 52 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 53 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 54 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= 55 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= 56 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= 57 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 58 | google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= 59 | google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= 60 | google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e h1:LGj5F6Z+enJcJFa6sjBMwmJ1gnjHpV60bKrQ2ass/aE= 61 | google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e/go.mod h1:UxqwMHw3ntCGQS0LuHPmqkO+z9CyMtK1oN7xh6P+gw8= 62 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 63 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 64 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 67 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 68 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 69 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 70 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 71 | -------------------------------------------------------------------------------- /internal/testing/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla 5 | Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 6 | YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT 7 | BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 8 | +L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu 9 | g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd 10 | Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV 11 | HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau 12 | sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m 13 | oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG 14 | Dfcog5wrJytaQ6UA0wE= 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /internal/testing/grpcdebug_config.yaml: -------------------------------------------------------------------------------- 1 | servers: 2 | dev: 3 | real_address: localhost:50051 4 | security: insecure 5 | prod: 6 | real_address: "localhost:50052" 7 | security: tls 8 | credential_file: ./internal/testing/ca.pem 9 | server_name_override: "*.test.youtube.com" 10 | "localhost:50052": 11 | security: tls 12 | credential_file: ./internal/testing/ca.pem 13 | server_name_override: "*.test.youtube.com" 14 | -------------------------------------------------------------------------------- /internal/testing/testserver/csds_config_dump.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": [ 3 | { 4 | "node": { 5 | "id": "projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b431-c1d43e6ac0ab", 6 | "cluster": "cluster", 7 | "metadata": { 8 | "INSTANCE_IP": "192.168.120.31", 9 | "TRAFFICDIRECTOR_GCP_PROJECT_NUMBER": "1040920224690", 10 | "TRAFFICDIRECTOR_NETWORK_NAME": "default" 11 | }, 12 | "locality": { 13 | "zone": "us-central1-a" 14 | }, 15 | "userAgentName": "gRPC Java", 16 | "userAgentVersion": "1.38.0-SNAPSHOT", 17 | "clientFeatures": [ 18 | "envoy.lb.does_not_support_overprovisioning" 19 | ] 20 | }, 21 | "xdsConfig": [ 22 | { 23 | "listenerConfig": { 24 | "versionInfo": "1617141154495058478", 25 | "dynamicListeners": [ 26 | { 27 | "name": "xds-test-server:1337", 28 | "activeState": { 29 | "versionInfo": "1617141154495058478", 30 | "listener": { 31 | "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", 32 | "apiListener": { 33 | "apiListener": { 34 | "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", 35 | "httpFilters": [ 36 | { 37 | "name": "envoy.filters.http.fault", 38 | "typedConfig": { 39 | "@type": "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault" 40 | } 41 | }, 42 | { 43 | "name": "envoy.filters.http.router", 44 | "typedConfig": { 45 | "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router", 46 | "suppressEnvoyHeaders": true 47 | } 48 | } 49 | ], 50 | "rds": { 51 | "configSource": { 52 | "ads": {}, 53 | "resourceApiVersion": "V3" 54 | }, 55 | "routeConfigName": "URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337" 56 | }, 57 | "statPrefix": "trafficdirector" 58 | } 59 | }, 60 | "name": "xds-test-server:1337" 61 | }, 62 | "lastUpdated": "2021-03-31T01:20:33.144Z" 63 | }, 64 | "clientStatus": "ACKED" 65 | } 66 | ] 67 | } 68 | }, 69 | { 70 | "routeConfig": { 71 | "dynamicRouteConfigs": [ 72 | { 73 | "versionInfo": "1617141154495058478", 74 | "routeConfig": { 75 | "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 76 | "name": "URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337", 77 | "virtualHosts": [ 78 | { 79 | "domains": [ 80 | "xds-test-server:1337" 81 | ], 82 | "routes": [ 83 | { 84 | "match": { 85 | "prefix": "" 86 | }, 87 | "route": { 88 | "cluster": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229", 89 | "timeout": "30s", 90 | "retryPolicy": { 91 | "retryOn": "gateway-error", 92 | "numRetries": 1, 93 | "perTryTimeout": "30s" 94 | } 95 | } 96 | } 97 | ] 98 | } 99 | ] 100 | }, 101 | "lastUpdated": "2021-03-31T01:20:33.302Z", 102 | "clientStatus": "ACKED" 103 | } 104 | ] 105 | } 106 | }, 107 | { 108 | "clusterConfig": { 109 | "versionInfo": "1617141154495058478", 110 | "dynamicActiveClusters": [ 111 | { 112 | "versionInfo": "1617141154495058478", 113 | "cluster": { 114 | "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", 115 | "circuitBreakers": { 116 | "thresholds": [ 117 | { 118 | "maxConnections": 2147483647, 119 | "maxPendingRequests": 2147483647, 120 | "maxRequests": 2147483647, 121 | "maxRetries": 2147483647 122 | } 123 | ] 124 | }, 125 | "commonLbConfig": { 126 | "healthyPanicThreshold": { 127 | "value": 1 128 | }, 129 | "localityWeightedLbConfig": {} 130 | }, 131 | "connectTimeout": "30s", 132 | "edsClusterConfig": { 133 | "edsConfig": { 134 | "ads": {}, 135 | "initialFetchTimeout": "15s", 136 | "resourceApiVersion": "V3" 137 | } 138 | }, 139 | "http2ProtocolOptions": { 140 | "maxConcurrentStreams": 100 141 | }, 142 | "lrsServer": { 143 | "self": {} 144 | }, 145 | "metadata": { 146 | "filterMetadata": { 147 | "com.google.trafficdirector": { 148 | "backend_service_name": "sergii-psm-test-backend-service" 149 | } 150 | } 151 | }, 152 | "name": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229", 153 | "type": "EDS" 154 | }, 155 | "lastUpdated": "2021-03-31T01:20:33.853Z", 156 | "clientStatus": "ACKED" 157 | } 158 | ] 159 | } 160 | }, 161 | { 162 | "endpointConfig": { 163 | "dynamicEndpointConfigs": [ 164 | { 165 | "versionInfo": "1", 166 | "endpointConfig": { 167 | "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 168 | "clusterName": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229", 169 | "endpoints": [ 170 | { 171 | "locality": { 172 | "subZone": "jf:us-central1-a_7062512536751318190_neg" 173 | }, 174 | "lbEndpoints": [ 175 | { 176 | "endpoint": { 177 | "address": { 178 | "socketAddress": { 179 | "address": "192.168.120.26", 180 | "portValue": 8080 181 | } 182 | } 183 | }, 184 | "healthStatus": "HEALTHY" 185 | } 186 | ], 187 | "loadBalancingWeight": 100 188 | } 189 | ] 190 | }, 191 | "lastUpdated": "2021-03-31T01:20:33.936Z", 192 | "clientStatus": "ACKED" 193 | } 194 | ] 195 | } 196 | } 197 | ] 198 | } 199 | ] 200 | } 201 | -------------------------------------------------------------------------------- /internal/testing/testserver/main.go: -------------------------------------------------------------------------------- 1 | // Testserver mocking the responses of Channelz/CSDS/Health 2 | package main 3 | 4 | import ( 5 | "context" 6 | "crypto/tls" 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "math/rand" 12 | "net" 13 | "os" 14 | "time" 15 | 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/channelz/service" 18 | "google.golang.org/grpc/codes" 19 | "google.golang.org/grpc/credentials" 20 | "google.golang.org/grpc/health" 21 | "google.golang.org/grpc/reflection" 22 | "google.golang.org/grpc/testdata" 23 | 24 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3" 25 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" 26 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 27 | csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 28 | pb "google.golang.org/grpc/examples/helloworld/helloworld" 29 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 30 | "google.golang.org/protobuf/encoding/protojson" 31 | ) 32 | 33 | var ( 34 | servingPortFlag = flag.Int("serving", 10001, "the serving port") 35 | adminPortFlag = flag.Int("admin", 50051, "the admin port") 36 | secureAdminPortFlag = flag.Int("secure_admin", 50052, "the secure admin port") 37 | healthFlag = flag.Bool("health", true, "the health checking status") 38 | qpsFlag = flag.Int("qps", 10, "The size of the generated load against itself") 39 | abortPercentageFlag = flag.Int("abort_percentage", 10, "The percentage of failed RPCs") 40 | ) 41 | 42 | // Prepare the CSDS response 43 | var csdsResponse csdspb.ClientStatusResponse 44 | 45 | func init() { 46 | file, err := os.Open("csds_config_dump.json") 47 | if err != nil { 48 | panic(err) 49 | } 50 | configDump, err := ioutil.ReadAll(file) 51 | if err != nil { 52 | panic(err) 53 | } 54 | if err := protojson.Unmarshal([]byte(configDump), &csdsResponse); err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | // Implements the Greeter service 60 | type server struct { 61 | pb.UnimplementedGreeterServer 62 | } 63 | 64 | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 65 | 66 | if int(rand.Int31n(100)) <= *abortPercentageFlag { 67 | return nil, grpc.Errorf(codes.Code(rand.Int31n(15)+1), "Fault injected") 68 | } 69 | return &pb.HelloReply{Message: "Hello " + in.Name}, nil 70 | } 71 | 72 | // Implements the CSDS service 73 | type mockCsdsServer struct { 74 | csdspb.UnimplementedClientStatusDiscoveryServiceServer 75 | } 76 | 77 | func (*mockCsdsServer) FetchClientStatus(ctx context.Context, req *csdspb.ClientStatusRequest) (*csdspb.ClientStatusResponse, error) { 78 | return &csdsResponse, nil 79 | } 80 | 81 | func setupAdminServer(s *grpc.Server) { 82 | reflection.Register(s) 83 | service.RegisterChannelzServiceToServer(s) 84 | csdspb.RegisterClientStatusDiscoveryServiceServer(s, &mockCsdsServer{}) 85 | healthcheck := health.NewServer() 86 | if *healthFlag { 87 | healthcheck.SetServingStatus("", healthpb.HealthCheckResponse_SERVING) 88 | healthcheck.SetServingStatus("helloworld.Greeter", healthpb.HealthCheckResponse_SERVING) 89 | } else { 90 | healthcheck.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING) 91 | healthcheck.SetServingStatus("helloworld.Greeter", healthpb.HealthCheckResponse_NOT_SERVING) 92 | } 93 | healthpb.RegisterHealthServer(s, healthcheck) 94 | } 95 | 96 | func main() { 97 | // Parse the flags 98 | flag.Parse() 99 | // Seed the RNG 100 | rand.Seed(time.Now().UnixNano()) 101 | // Creates the primary server 102 | lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *servingPortFlag)) 103 | if err != nil { 104 | panic(err) 105 | } 106 | defer lis.Close() 107 | fmt.Printf("Serving Business Logic on :%d\n", *servingPortFlag) 108 | cert, err := tls.LoadX509KeyPair(testdata.Path("server1.pem"), testdata.Path("server1.key")) 109 | if err != nil { 110 | log.Fatalf("failed to load key pair: %s", err) 111 | } 112 | s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert))) 113 | pb.RegisterGreeterServer(s, &server{}) 114 | go s.Serve(lis) 115 | defer s.Stop() 116 | // Creates the admin server without credentials 117 | insecureListener, err := net.Listen("tcp", fmt.Sprintf(":%d", *adminPortFlag)) 118 | if err != nil { 119 | panic(err) 120 | } 121 | defer insecureListener.Close() 122 | insecureAdminServer := grpc.NewServer() 123 | setupAdminServer(insecureAdminServer) 124 | go insecureAdminServer.Serve(insecureListener) 125 | defer insecureAdminServer.Stop() 126 | fmt.Printf("Serving Insecure Admin Services on :%d\n", *adminPortFlag) 127 | // Creates the admin server with credentials 128 | secureListener, err := net.Listen("tcp", fmt.Sprintf(":%d", *secureAdminPortFlag)) 129 | if err != nil { 130 | panic(err) 131 | } 132 | defer secureListener.Close() 133 | secureAdminServer := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert))) 134 | setupAdminServer(secureAdminServer) 135 | go secureAdminServer.Serve(secureListener) 136 | defer secureAdminServer.Stop() 137 | fmt.Printf("Serving Secure Admin Services on :%d\n", *secureAdminPortFlag) 138 | // Creates a client to hydrate the primary server 139 | creds, err := credentials.NewClientTLSFromFile(testdata.Path("ca.pem"), "*.test.youtube.com") 140 | if err != nil { 141 | panic(err) 142 | } 143 | conn, err := grpc.Dial(fmt.Sprintf("localhost:%d", *servingPortFlag), grpc.WithTransportCredentials(creds)) 144 | if err != nil { 145 | panic(err) 146 | } 147 | defer conn.Close() 148 | greeterClient := pb.NewGreeterClient(conn) 149 | for { 150 | greeterClient.SayHello(context.Background(), &pb.HelloRequest{Name: "world"}) 151 | time.Sleep(time.Second / time.Duration(*qpsFlag)) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | cmd "github.com/grpc-ecosystem/grpcdebug/cmd" 5 | 6 | // To parse Any protos, ProtoBuf requires the descriptors of the given message 7 | // type to present in its descriptor pool. Otherwise, it will fail. Here we 8 | // preload as much proto descriptors as possible, so the released binaries can 9 | // have better forward compatibility. 10 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/http/dynamo/v3" 11 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/http/squash/v3" 12 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/client_ssl_auth/v3" 13 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/kafka_broker/v3" 14 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/mysql_proxy/v3" 15 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha" 16 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/rocketmq_proxy/v3" 17 | _ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/upstreams/http/v3" 18 | _ "github.com/envoyproxy/go-control-plane/envoy/admin/v3" 19 | _ "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" 20 | _ "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" 21 | _ "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 22 | _ "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3" 23 | _ "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 24 | _ "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 25 | _ "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v3" 26 | _ "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 27 | _ "github.com/envoyproxy/go-control-plane/envoy/config/metrics/v3" 28 | _ "github.com/envoyproxy/go-control-plane/envoy/config/overload/v3" 29 | _ "github.com/envoyproxy/go-control-plane/envoy/config/ratelimit/v3" 30 | _ "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" 31 | _ "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 32 | _ "github.com/envoyproxy/go-control-plane/envoy/config/tap/v3" 33 | _ "github.com/envoyproxy/go-control-plane/envoy/config/trace/v3" 34 | _ "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v3" 35 | _ "github.com/envoyproxy/go-control-plane/envoy/data/cluster/v3" 36 | _ "github.com/envoyproxy/go-control-plane/envoy/data/core/v3" 37 | _ "github.com/envoyproxy/go-control-plane/envoy/data/dns/v3" 38 | _ "github.com/envoyproxy/go-control-plane/envoy/data/tap/v3" 39 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3" 40 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3" 41 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/wasm/v3" 42 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3" 43 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/dynamic_forward_proxy/v3" 44 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/dynamic_forward_proxy/v3" 45 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/matching/v3" 46 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3" 47 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/tap/v3" 48 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/compressor/v3" 49 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/decompressor/v3" 50 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/compressor/v3" 51 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/decompressor/v3" 52 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/dependency/v3" 53 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3" 54 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/matcher/action/v3" 55 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/adaptive_concurrency/v3" 56 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/admission_control/v3" 57 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_lambda/v3" 58 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_request_signing/v3" 59 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/buffer/v3" 60 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cache/v3" 61 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cdn_loop/v3" 62 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/compressor/v3" 63 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3" 64 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/csrf/v3" 65 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/decompressor/v3" 66 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/dynamic_forward_proxy/v3" 67 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3" 68 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_proc/v3" 69 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3" 70 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_http1_bridge/v3" 71 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_http1_reverse_bridge/v3" 72 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_json_transcoder/v3" 73 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_stats/v3" 74 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_web/v3" 75 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gzip/v3" 76 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/header_to_metadata/v3" 77 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/health_check/v3" 78 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ip_tagging/v3" 79 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" 80 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/kill_request/v3" 81 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3" 82 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3" 83 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/oauth2/v3" 84 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/on_demand/v3" 85 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/original_src/v3" 86 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ratelimit/v3" 87 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" 88 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" 89 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/tap/v3" 90 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3" 91 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/http_inspector/v3" 92 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_dst/v3" 93 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_src/v3" 94 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/proxy_protocol/v3" 95 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3" 96 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/direct_response/v3" 97 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/dubbo_proxy/router/v3" 98 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/dubbo_proxy/v3" 99 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/echo/v3" 100 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ext_authz/v3" 101 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 102 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/local_ratelimit/v3" 103 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/mongo_proxy/v3" 104 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ratelimit/v3" 105 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3" 106 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/redis_proxy/v3" 107 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_cluster/v3" 108 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3" 109 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" 110 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3" 111 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/router/v3" 112 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/v3" 113 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3" 114 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/zookeeper_proxy/v3" 115 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/dns_filter/v3" 116 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3" 117 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/allow_listed_routes/v3" 118 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/previous_routes/v3" 119 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/safe_cross_scheme/v3" 120 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/least_request/v3" 121 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/pick_first/v3" 122 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/ring_hash/v3" 123 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/round_robin/v3" 124 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/wrr_locality/v3" 125 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/socket_interface/v3" 126 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/rate_limit_descriptors/expr/v3" 127 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/host/omit_host_metadata/v3" 128 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/priority/previous_priorities/v3" 129 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/stat_sinks/wasm/v3" 130 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/alts/v3" 131 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/proxy_protocol/v3" 132 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3" 133 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/raw_buffer/v3" 134 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/starttls/v3" 135 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tap/v3" 136 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" 137 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3" 138 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/watchdog/profile_action/v3" 139 | _ "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3" 140 | _ "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" 141 | _ "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3" 142 | _ "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 143 | _ "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3" 144 | _ "github.com/envoyproxy/go-control-plane/envoy/service/event_reporting/v3" 145 | _ "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 146 | _ "github.com/envoyproxy/go-control-plane/envoy/service/extension/v3" 147 | _ "github.com/envoyproxy/go-control-plane/envoy/service/health/v3" 148 | _ "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3" 149 | _ "github.com/envoyproxy/go-control-plane/envoy/service/load_stats/v3" 150 | _ "github.com/envoyproxy/go-control-plane/envoy/service/metrics/v3" 151 | _ "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3" 152 | _ "github.com/envoyproxy/go-control-plane/envoy/service/route/v3" 153 | _ "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3" 154 | _ "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3" 155 | _ "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 156 | _ "github.com/envoyproxy/go-control-plane/envoy/service/tap/v3" 157 | _ "github.com/envoyproxy/go-control-plane/envoy/service/trace/v3" 158 | _ "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" 159 | _ "github.com/envoyproxy/go-control-plane/envoy/type/metadata/v3" 160 | _ "github.com/envoyproxy/go-control-plane/envoy/type/tracing/v3" 161 | _ "github.com/envoyproxy/go-control-plane/envoy/type/v3" 162 | _ "github.com/envoyproxy/go-control-plane/envoy/watchdog/v3" 163 | 164 | // Add the xDS resolver to allow resolving using a "xds:///" target. 165 | _ "google.golang.org/grpc/xds" 166 | ) 167 | 168 | func main() { 169 | cmd.Execute() 170 | } 171 | --------------------------------------------------------------------------------