Full Code of grpc-ecosystem/grpcdebug for AI

main 9609af11b247 cached
20 files
112.6 KB
32.7k tokens
62 symbols
1 requests
Download .txt
Repository: grpc-ecosystem/grpcdebug
Branch: main
Commit: 9609af11b247
Files: 20
Total size: 112.6 KB

Directory structure:
gitextract_vgznyx4c/

├── .github/
│   └── workflows/
│       └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── cmd/
│   ├── channelz.go
│   ├── config/
│   │   └── config.go
│   ├── health.go
│   ├── root.go
│   ├── transport/
│   │   └── grpc.go
│   ├── verbose/
│   │   └── verbose.go
│   ├── xds.go
│   └── xds_test.go
├── go.mod
├── go.sum
├── internal/
│   └── testing/
│       ├── ca.pem
│       ├── grpcdebug_config.yaml
│       └── testserver/
│           ├── csds_config_dump.json
│           ├── csds_config_dump_multi_scope.json
│           └── main.go
└── main.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  release:
    types: [published]

jobs:
  release:
    name: Release grpcdebug
    runs-on: ubuntu-latest
    strategy:
      matrix:
        goos: [linux, darwin, windows]
        goarch: [386, amd64, arm64]
        exclude:
          - goos: darwin
            goarch: 386

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Go
        uses: actions/setup-go@v2

      - name: Prepare build directory
        run: |
          mkdir -p build/
          cp README.md build/
          cp LICENSE build/
      - name: Build
        env:
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ matrix.goarch }}
        run: |
          go build -trimpath -o $GITHUB_WORKSPACE/build
      - name: Create package
        id: package
        run: |
          PACKAGE_NAME=grpcdebug.${GITHUB_REF#refs/tags/}.${{ matrix.goos }}.${{ matrix.goarch }}.tar.gz
          tar -czvf $PACKAGE_NAME -C build .
          echo ::set-output name=name::${PACKAGE_NAME}
      - name: Upload asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ github.event.release.upload_url }}
          asset_path: ./${{ steps.package.outputs.name }}
          asset_name: ${{ steps.package.outputs.name }}
          asset_content_type: application/gzip


================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# grpcdebug
[![Go Report
Card](https://goreportcard.com/badge/github.com/grpc-ecosystem/grpcdebug)](https://goreportcard.com/report/github.com/grpc-ecosystem/grpcdebug)

grpcdebug is a command line interface focusing on simplifying the debugging
process of gRPC applications. grpcdebug fetches the internal states of the gRPC
library from the application via gRPC protocol and provide a human-friendly UX
to browse them. Currently, it supports Channelz/Health Checking/CSDS (aka. admin
services). In other words, it can fetch statistics about how many RPCs has being
sent or failed on a given gRPC channel, it can inspect address resolution
results, it can dump the in-effective xDS configuration that directs the routing
of RPCs.

If you are looking for a tool to send gRPC requests and interact with a gRPC
server, please checkout https://github.com/fullstorydev/grpcurl.

```
grpcdebug is an gRPC service admin CLI

Usage:
  grpcdebug <target address> [flags] <command>

Available Commands:
  channelz    Display gRPC states in a human readable way.
  health      Check health status of the target service (default "").
  help        Help about any command
  xds         Fetch xDS related information.

Flags:
      --credential_file string        Sets the path of the credential file; used in [tls] mode
  -h, --help                          help for grpcdebug
      --security string               Defines the type of credentials to use [tls, google-default, insecure] (default "insecure")
      --server_name_override string   Overrides the peer server name if non empty; used in [tls] mode
  -t, --timestamp                     Print timestamp as RFC3339 instead of human readable strings
  -v, --verbose                       Print verbose information for debugging

Use "grpcdebug <target address>  [command] --help" for more information about a command.
```

## Table of Contents
- [grpcdebug](#grpcdebug)
  - [Table of Contents](#table-of-contents)
  - [Installation](#installation)
    - [Use Compiled Binaries](#use-compiled-binaries)
    - [Compile From Source](#compile-from-source)
  - [Quick Start](#quick-start)
    - [Connect & Security](#connect--security)
      - [Insecure Connection](#insecure-connection)
      - [TLS Connection - Flags](#tls-connection---flags)
      - [Server Connection Config](#server-connection-config)
    - [Health](#health)
    - [Channelz](#channelz)
      - [Usage 1: Raw Channelz Output](#usage-1-raw-channelz-output)
      - [Usage 2: List Client Channels](#usage-2-list-client-channels)
      - [Usage 3: List Servers](#usage-3-list-servers)
      - [Usage 4: Inspect a Channel](#usage-4-inspect-a-channel)
      - [Usage 5: Inspect a Subchannel](#usage-5-inspect-a-subchannel)
      - [Usage 6: Inspect a Socket](#usage-6-inspect-a-socket)
      - [Usage 7: Inspect a Server](#usage-7-inspect-a-server)
      - [Usage 8: Pagination](#usage-8-pagination)
    - [Debug xDS](#debug-xds)
      - [Usage 1: xDS Resources Overview](#usage-1-xds-resources-overview)
      - [Usage 2: Dump xDS Configs](#usage-2-dump-xds-configs)
      - [Usage 3: Filter xDS Configs](#usage-3-filter-xds-configs)
  - [Admin Services](#admin-services)
    - [gRPC Java:](#grpc-java)
    - [gRPC Go:](#grpc-go)
    - [gRPC C++:](#grpc-c)

## Installation

### Use Compiled Binaries

The download links of the binaries can be found at
https://github.com/grpc-ecosystem/grpcdebug/releases. You can find the
precompiled artifacts for `macOS`/`Linux`/`Windows`.

### Compile From Source

Minimum Golang Version 1.22. Official Golang install guide:
https://golang.org/doc/install.

You can install the `grpcdebug` tool using command:

```shell
go install -v github.com/grpc-ecosystem/grpcdebug@latest
```

You can check your Golang version with:

```shell
go version
```

Don't forget to add Golang binaries to your `PATH`:

```shell
export PATH=$PATH:$(go env GOPATH)/bin
```

## Quick Start

If certain commands are confusing, please try to use `-h` to get more context.
Suggestions and ideas are welcome, please post them to
https://github.com/grpc-ecosystem/grpcdebug/issues!

If you haven't got your gRPC application instrumented, feel free to try out the
mocking `testserver` which implemented admin services.

```shell
cd internal/testing/testserver
go run main.go
# Serving Business Logic on :10001
# Serving Insecure Admin Services on :50051
# Serving Secure Admin Services on :50052
# ...
```

### Connect & Security

#### Insecure Connection

To connect to a gRPC endpoint without any credentials, we don't use any special
flags. If the local network can connect to the given gRPC endpoint, it should
just work. For example, if I have a gRPC application exposing admin services at
`localhost:50051`:

```shell
grpcdebug localhost:50051 channelz channels
```

#### TLS Connection - Flags

One way to establish a TLS connection with grpcdebug is by specifying the
credentials via command line flags. For example:

```shell
grpcdebug localhost:50052 --security=tls --credential_file=./internal/testing/ca.pem --server_name_override="*.test.youtube.com" channelz channels
```

#### Server Connection Config

Alternatively, like OpenSSH clients, you can specify the security settings in a
`grpcdebug_config.yaml` file. grpcdebug CLI will find matching connection config and
then use it to connect.

```yaml
servers:
  "pattern string":
    real_address: string
    security: insecure/tls
    credential_file: string
    server_name_override: string
```

Here is an example config file
[grpcdebug_config.yaml](internal/testing/grpcdebug_config.yaml).

Each server config can have the following settings:

* Pattern: the string right after `Server ` which dictates if this rule should
  apply;
* RealAddress: if present, override the given target address, which allows
  giving nicknames/aliases to frequently used addresses;
* Security: allows `insecure` or `tls`, expecting more in the future;
* CredentialFile: path to the credential file;
* ServerNameOverride: override the hostname, which is useful for local reproductions to
  comply with the certificates' common name requirement.

grpcdebug searches the config file in the following order:

1. Check if the environment variable `GRPCDEBUG_CONFIG` is set, if so, load from the
   given path;
2. Try to load the `grpcdebug_config.yaml` file in the current working directory;
3. Try to load the `grpcdebug_config.yaml` file in the user config directory (Linux:
   `$HOME/.config`, macOS: `$HOME/Library/Application Support`, Windows:
   `%AppData%`, see
   [`os.UserConfigDir()`](https://golang.org/pkg/os/#UserConfigDir)).

For example, we can connect to our mock test server's secure admin port via:

```shell
GRPCDEBUG_CONFIG=internal/testing/grpcdebug_config.yaml grpcdebug localhost:50052 channelz channels
# Or
GRPCDEBUG_CONFIG=internal/testing/grpcdebug_config.yaml grpcdebug prod channelz channels
```

### Health

grpcdebug can be used to fetch the health checking status of a peer gRPC
application (see
[health.proto](https://github.com/grpc/grpc/blob/master/src/proto/grpc/health/v1/health.proto)).
gRPC's health checking works at the service-level, meaning services registered on
the same gRPC server may have different health statuses. The health status of
service `""` is used to represent the overall health status of the gRPC
application.

To simply fetch the overall health status:

```shell
grpcdebug localhost:50051 health
# <Overall>:  SERVING
# or
# <Overall>:  NOT_SERVING
```

Or fetch individual service's health status:

```shell
grpcdebug localhost:50051 health helloworld.Greeter
# <Overall>:            SERVING
# helloworld.Greeter:   SERVING
```

### Channelz

[Channelz](https://github.com/grpc/proposal/blob/master/A14-channelz.md) is a
channel tracing library that allows applications to remotely query gRPC internal
debug information. Also, Channelz has a web interface (see
[gdebug](https://github.com/grpc/grpc-experiments/tree/master/gdebug)).
grpcdebug is able to fetch information and present it in a more readable way.

Generally, you wil start with either the `servers` or `channels` command and
then work down to the details.

#### Usage 1: Raw Channelz Output

For all Channelz commands, you can add `--json` to get the raw Channelz output.

```shell
grpcdebug localhost:50051 channelz servers --json
#[
#  {
#    "ref": {
#      "server_id": 2,
#      "name": "ServerImpl{logId=2, transportServer=NettyServer{logId=1, addresses=[0.0.0.0/0.0.0.0:50051]}}"
#    },
#    "data": {
#      "calls_started": 3,
#      "calls_succeeded": 2,
#      "last_call_started_timestamp": {
#        "seconds": 1680220688,
#        "nanos": 444000000
#      }
#    },
#    "listen_socket": [
#      {
#        "socket_id": 3,
#        "name": "ListenSocket{logId=3, channel=[id: 0x05f9f16c, L:/0:0:0:0:0:0:0:0%0:50051]}"
#      }
#    ]
#  }
#]

```

#### Usage 2: List Client Channels

```shell
grpcdebug localhost:50051 channelz channels
# Channel ID   Target            State     Calls(Started/Succeeded/Failed)   Created Time
# 7            localhost:10001   READY     5136/4631/505                     8 minutes ago
```

#### Usage 3: List Servers

```shell
grpcdebug localhost:50051 channelz servers
# Server ID   Listen Addresses   Calls(Started/Succeeded/Failed)   Last Call Started
# 1           [:::10001]         2852/2530/322                     now
# 2           [:::50051]         29/28/0                           now
# 3           [:::50052]         4/4/0                             26 seconds ago
```

#### Usage 4: Inspect a Channel

You can identify a channel via the Channel ID.

```shell
grpcdebug localhost:50051 channelz channel 7
# Channel ID:        7
# Target:            localhost:10001
# State:             READY
# Calls Started:     3976
# Calls Succeeded:   3520
# Calls Failed:      456
# Created Time:      6 minutes ago
# ---
# Subchannel ID   Target            State     Calls(Started/Succeeded/Failed)   CreatedTime
# 8               localhost:10001   READY     3976/3520/456                     6 minutes ago
# ---
# Severity   Time            Child Ref                      Description
# CT_INFO    6 minutes ago                                  Channel Created
# CT_INFO    6 minutes ago                                  Resolver state updated: {Addresses:[{Addr:localhost:10001 ServerName: Attributes:<nil> Type:0 Metadata:<nil>}] ServiceConfig:<nil> Attributes:<nil>} (resolver returned new addresses)
# CT_INFO    6 minutes ago                                  Channel switches to new LB policy "pick_first"
# CT_INFO    6 minutes ago   subchannel(subchannel_id:8 )   Subchannel(id:8) created
# CT_INFO    6 minutes ago                                  Channel Connectivity change to CONNECTING
# CT_INFO    6 minutes ago                                  Channel Connectivity change to READY
```

#### Usage 5: Inspect a Subchannel

```shell
grpcdebug localhost:50051 channelz subchannel 8
# Subchannel ID:     8
# Target:            localhost:10001
# State:             READY
# Calls Started:     4490
# Calls Succeeded:   3966
# Calls Failed:      524
# Created Time:      7 minutes ago
# ---
# Socket ID   Local->Remote          Streams(Started/Succeeded/Failed)   Messages(Sent/Received)
# 9           ::1:47436->::1:10001   4490/4490/0                         4490/3966
```

#### Usage 6: Inspect a Socket

```shell
grpcdebug localhost:50051 channelz socket 9
# Socket ID:                       9
# Address:                         ::1:47436->::1:10001
# Streams Started:                 4807
# Streams Succeeded:               4807
# Streams Failed:                  0
# Messages Sent:                   4807
# Messages Received:               4243
# Keep Alives Sent:                0
# Last Local Stream Created:       now
# Last Remote Stream Created:      a long while ago
# Last Message Sent Created:       now
# Last Message Received Created:   now
# Local Flow Control Window:       65535
# Remote Flow Control Window:      65535
# ---
# Socket Options Name   Value
# SO_LINGER             [type.googleapis.com/grpc.channelz.v1.SocketOptionLinger]:{duration:{}}
# SO_RCVTIMEO           [type.googleapis.com/grpc.channelz.v1.SocketOptionTimeout]:{duration:{}}
# SO_SNDTIMEO           [type.googleapis.com/grpc.channelz.v1.SocketOptionTimeout]:{duration:{}}
# TCP_INFO              [type.googleapis.com/grpc.channelz.v1.SocketOptionTcpInfo]:{tcpi_state:1  tcpi_options:7  tcpi_rto:204000  tcpi_ato:40000  tcpi_snd_mss:32768  tcpi_rcv_mss:1093  tcpi_last_data_sent:16  tcpi_last_data_recv:16  tcpi_last_ack_recv:16  tcpi_pmtu:65536  tcpi_rcv_ssthresh:65476  tcpi_rtt:192  tcpi_rttvar:153  tcpi_snd_ssthresh:2147483647  tcpi_snd_cwnd:10  tcpi_advmss:65464  tcpi_reordering:3}
# ---
# Security Model:   TLS
# Standard Name:    TLS_AES_128_GCM_SHA256
```

#### Usage 7: Inspect a Server

```shell
grpcdebug localhost:50051 channelz server 1
# Server Id:           1
# Listen Addresses:    [:::10001]
# Calls Started:       5250
# Calls Succeeded:     4647
# Calls Failed:        603
# Last Call Started:   now
# ---
# Socket ID   Local->Remote          Streams(Started/Succeeded/Failed)   Messages(Sent/Received)
# 10          ::1:10001->::1:47436   5250/5250/0                         4647/5250
```

#### Usage 8: Pagination

In production, there may be thousands of clients/servers/sockets. It would be very noisy to print all of them at once, so Channelz supports pagination through `start_id` and `max_results`

```shell
grpcdebug localhost:50051 channelz servers --start_id=0 --max_results=1
# Server ID   Listen Addresses   Calls(Started/Succeeded/Failed)   Last Call Started
# 1           [:::10001]         2852/2530/322                     now
grpcdebug localhost:50051 channelz servers --start_id=2 --max_results=2
# Server ID   Listen Addresses   Calls(Started/Succeeded/Failed)   Last Call Started
# 2           [:::50051]         29/28/0                           now
# 3           [:::50052]         4/4/0                             26 seconds ago
```

It works similarly for printing channels via `channelz channels` and printing server sockets via `channelz server`.

### Debug xDS

[xDS](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration)
is a data plane configuration API commonly used in service mesh projects. It's
created by Envoy, used by Istio, Traffic Director, and gRPC.

#### Usage 1: xDS Resources Overview

The xDS resources status might be `REQUESTED`/`DOES_NOT_EXIST`/`ACKED`/`NACKED` (see
[config_dump.proto](https://github.com/envoyproxy/envoy/blob/b0ce15c96cebd89cf391869e49017325cd7faaa8/api/envoy/admin/v3/config_dump.proto#L22)).
This view is intended for a quick scan if a configuration is propagated from the
service mesh control plane.

```shell
grpcdebug localhost:50051 xds status
# Name                                                                   Status    Version               Type                                                                 LastUpdated
# xds-test-server:1337                                                   ACKED     1617141154495058478   type.googleapis.com/envoy.config.listener.v3.Listener                2 days ago
# URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337   ACKED     1617141154495058478   type.googleapis.com/envoy.config.route.v3.RouteConfiguration         2 days ago
# cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229        ACKED     1617141154495058478   type.googleapis.com/envoy.config.cluster.v3.Cluster                  2 days ago
# cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229        ACKED     1                     type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment   2 days ago
```

#### Usage 2: Dump xDS Configs

```shell
grpcdebug localhost:50051 xds config
# {
#   "config":  [
#     {
#       "node":  {
#         "id":  "projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b431-c1d43e6ac0ab",
#         "cluster":  "cluster",
#         "metadata":  {
#           "INSTANCE_IP":  "192.168.120.31",
#           "TRAFFICDIRECTOR_GCP_PROJECT_NUMBER":  "1040920224690",
#           "TRAFFICDIRECTOR_NETWORK_NAME":  "default"
#         },
# ...
```

For an example config dump, see
[csds_config_dump.json](internal/testing/testserver/csds_config_dump.json).

#### Usage 3: Filter xDS Configs

The dumped xDS config can be quite verbose, if I only interested in certain xDS
type, grpcdebug can only print the selected section.

```shell
grpcdebug localhost:50051 xds config --type=eds
# {
#   "dynamicEndpointConfigs":  [
#     {
#       "versionInfo":  "1",
#       "endpointConfig":  {
#         "@type":  "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
#         "clusterName":  "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
#         "endpoints":  [
#           {
#             "locality":  {
#               "subZone":  "jf:us-central1-a_7062512536751318190_neg"
#             },
#             "lbEndpoints":  [
#               {
#                 "endpoint":  {
#                   "address":  {
#                     "socketAddress":  {
#                       "address":  "192.168.120.26",
#                       "portValue":  8080
#                     }
#                   }
#                 },
#                 "healthStatus":  "HEALTHY"
#               }
#             ],
#             "loadBalancingWeight":  100
#           }
#         ]
#       },
#       "lastUpdated":  "2021-03-31T01:20:33.936Z",
#       "clientStatus":  "ACKED"
#     }
#   ]
# }
```

## Admin Services

### gRPC Java:

```diff
--- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java
+++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java
@@ -18,6 +18,7 @@ package io.grpc.examples.helloworld;

 import io.grpc.Server;
 import io.grpc.ServerBuilder;
+import io.grpc.services.AdminInterface;
 import io.grpc.stub.StreamObserver;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
@@ -36,6 +37,7 @@ public class HelloWorldServer {
     int port = 50051;
     server = ServerBuilder.forPort(port)
         .addService(new GreeterImpl())
+        .addServices(AdminInterface.getStandardServices())
         .build()
         .start();
     logger.info("Server started, listening on " + port);
```


### gRPC Go:

```diff
--- a/examples/helloworld/greeter_server/main.go
+++ b/examples/helloworld/greeter_server/main.go
@@ -27,6 +27,7 @@ import (
        "net"

        "google.golang.org/grpc"
+       "google.golang.org/grpc/admin"
        pb "google.golang.org/grpc/examples/helloworld/helloworld"
 )

@@ -51,6 +52,11 @@ func main() {
                log.Fatalf("failed to listen: %v", err)
        }
        s := grpc.NewServer()
+       cleanup, err := admin.Register(s)
+       if err != nil {
+               log.Fatalf("failed to register admin: %v", err)
+       }
+       defer cleanup()
        pb.RegisterGreeterServer(s, &server{})
        if err := s.Serve(lis); err != nil {
                log.Fatalf("failed to serve: %v", err)
```


### gRPC C++:

```diff
--- a/examples/cpp/helloworld/greeter_server.cc
+++ b/examples/cpp/helloworld/greeter_server.cc
@@ -20,6 +20,7 @@
 #include <memory>
 #include <string>

+#include <grpcpp/ext/admin_services.h>
 #include <grpcpp/ext/proto_server_reflection_plugin.h>
 #include <grpcpp/grpcpp.h>
 #include <grpcpp/health_check_service_interface.h>
@@ -60,6 +61,7 @@ void RunServer() {
   // Register "service" as the instance through which we'll communicate with
   // clients. In this case it corresponds to an *synchronous* service.
   builder.RegisterService(&service);
+  grpc::AddAdminServices(&builder);
   // Finally assemble the server.
   std::unique_ptr<Server> server(builder.BuildAndStart());
   std::cout << "Server listening on " << server_address << std::endl;
```



================================================
FILE: cmd/channelz.go
================================================
package cmd

import (
	"encoding/json"
	"fmt"
	"net"
	"strconv"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/grpc-ecosystem/grpcdebug/cmd/transport"
	"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"
	"github.com/spf13/cobra"
	zpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)

var (
	jsonOutputFlag bool
	startIDFlag    int64
	maxResultsFlag int64
)

func prettyTime(ts *timestamppb.Timestamp) string {
	if ts == nil || (ts.Seconds == 0 && ts.Nanos == 0) {
		return ""
	}
	if timestampFlag {
		return ts.AsTime().Format(time.RFC3339Nano)
	}
	return humanize.Time(ts.AsTime())
}

func prettyAddress(addr *zpb.Address) string {
	if ipPort := addr.GetTcpipAddress(); ipPort != nil {
		address := net.TCPAddr{IP: net.IP(ipPort.IpAddress), Port: int(ipPort.Port)}
		return address.String()
	}
	panic(fmt.Sprintf("Address type not supported for %s", addr))
}

func printChannelTraceEvents(events []*zpb.ChannelTraceEvent) {
	fmt.Fprintln(w, "Severity\tTime\tChild Ref\tDescription\t")
	for _, event := range events {
		var childRef string
		switch event.ChildRef.(type) {
		case *zpb.ChannelTraceEvent_SubchannelRef:
			childRef = fmt.Sprintf("subchannel(%v)", event.GetSubchannelRef())
		case *zpb.ChannelTraceEvent_ChannelRef:
			childRef = fmt.Sprintf("channel(%v)", event.GetChannelRef())
		}
		fmt.Fprintf(
			w, "%v\t%v\t%v\t%v\t\n",
			event.Severity,
			prettyTime(event.Timestamp),
			childRef,
			event.Description,
		)
	}
	w.Flush()
}

func printSockets(sockets []*zpb.Socket) {
	fmt.Fprintln(w, "Socket ID\tLocal->Remote\tStreams(Started/Succeeded/Failed)\tMessages(Sent/Received)\t")
	for _, socket := range sockets {
		if socket.GetRef() == nil || socket.GetData() == nil {
			verbose.Debugf("failed to print socket: %s", socket)
			continue
		}
		fmt.Fprintf(
			w, "%v\t%v\t%v/%v/%v\t%v/%v\t\n",
			socket.Ref.SocketId,
			fmt.Sprintf("%v->%v", prettyAddress(socket.Local), prettyAddress(socket.Remote)),
			socket.Data.StreamsStarted,
			socket.Data.StreamsSucceeded,
			socket.Data.StreamsFailed,
			socket.Data.MessagesSent,
			socket.Data.MessagesReceived,
		)
	}
	w.Flush()
}

func printObjectAsJSON(data any) error {
	json, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return err
	}
	fmt.Println(string(json))
	return nil
}

func printCreationTimestamp(data *zpb.ChannelData) string {
	return prettyTime(data.GetTrace().GetCreationTimestamp())
}

func channelzChannelsCommandRunWithError(cmd *cobra.Command, args []string) error {
	var channels = transport.Channels(startIDFlag, maxResultsFlag)
	// Print as JSON
	if jsonOutputFlag {
		return printObjectAsJSON(channels)
	}
	// Print as table
	fmt.Fprintln(w, "Channel ID\tTarget\tState\tCalls(Started/Succeeded/Failed)\tCreated Time\t")
	for _, channel := range channels {
		if channel.GetRef() == nil || channel.GetData() == nil {
			verbose.Debugf("failed to print channel: %s", channel)
			continue
		}
		fmt.Fprintf(
			w, "%v\t%v\t%v\t%v/%v/%v\t%v\t\n",
			channel.Ref.ChannelId,
			channel.Data.Target,
			channel.Data.GetState().GetState(),
			channel.Data.CallsStarted,
			channel.Data.CallsSucceeded,
			channel.Data.CallsFailed,
			printCreationTimestamp(channel.Data),
		)
	}
	w.Flush()
	return nil
}

var channelzChannelsCmd = &cobra.Command{
	Use:   "channels",
	Short: "List client channels for the target application.",
	Args:  cobra.NoArgs,
	RunE:  channelzChannelsCommandRunWithError,
}

func channelzChannelCommandRunWithError(cmd *cobra.Command, args []string) error {
	id, err := strconv.ParseInt(args[0], 10, 64)
	if err != nil {
		return fmt.Errorf("Failed to parse ID=%v: %v", args[0], err)
	}
	selected := transport.Channel(id)
	// Print as JSON
	if jsonOutputFlag {
		return printObjectAsJSON(selected)
	}
	// Print as table
	// Print Channel information
	fmt.Fprintf(w, "Channel ID:\t%v\t\n", selected.GetRef().GetChannelId())
	fmt.Fprintf(w, "Target:\t%v\t\n", selected.GetData().GetTarget())
	fmt.Fprintf(w, "State:\t%v\t\n", selected.GetData().GetState().GetState())
	fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted())
	fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded())
	fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed())
	fmt.Fprintf(w, "Created Time:\t%v\t\n", printCreationTimestamp(selected.GetData()))
	w.Flush()
	// Print Subchannel list
	if len(selected.GetSubchannelRef()) > 0 {
		fmt.Println("---")
		fmt.Fprintln(w, "Subchannel ID\tTarget\tState\tCalls(Started/Succeeded/Failed)\tCreatedTime\t")
		for _, subchannelRef := range selected.GetSubchannelRef() {
			var subchannel = transport.Subchannel(subchannelRef.GetSubchannelId())
			if subchannel.GetRef() == nil || subchannel.GetData() == nil {
				verbose.Debugf("failed to print subchannel: %s", subchannel)
				continue
			}
			fmt.Fprintf(
				w, "%v\t%.50s\t%v\t%v/%v/%v\t%v\t\n",
				subchannel.Ref.SubchannelId,
				subchannel.Data.Target,
				subchannel.Data.State.State,
				subchannel.Data.CallsStarted,
				subchannel.Data.CallsSucceeded,
				subchannel.Data.CallsFailed,
				printCreationTimestamp(subchannel.Data),
			)
		}
		w.Flush()
	}
	// Print channel trace events
	if len(selected.GetData().GetTrace().GetEvents()) != 0 {
		fmt.Println("---")
		printChannelTraceEvents(selected.Data.Trace.Events)
	}
	return nil
}

var channelzChannelCmd = &cobra.Command{
	Use:   "channel <channel id or URL>",
	Short: "Display channel states in a human readable way.",
	Args:  cobra.ExactArgs(1),
	RunE:  channelzChannelCommandRunWithError,
}

func channelzSubchannelCommandRunWithError(cmd *cobra.Command, args []string) error {
	id, err := strconv.ParseInt(args[0], 10, 64)
	if err != nil {
		return fmt.Errorf("Failed to parse ID=%v: %v", args[0], err)
	}
	selected := transport.Subchannel(id)
	// Print as JSON
	if jsonOutputFlag {
		return printObjectAsJSON(selected)
	}
	// Print as table
	// Print Subchannel information
	fmt.Fprintf(w, "Subchannel ID:\t%v\t\n", selected.GetRef().GetSubchannelId())
	fmt.Fprintf(w, "Target:\t%v\t\n", selected.GetData().GetTarget())
	fmt.Fprintf(w, "State:\t%v\t\n", selected.GetData().GetState().GetState())
	fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted())
	fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded())
	fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed())
	fmt.Fprintf(w, "Created Time:\t%v\t\n", printCreationTimestamp(selected.GetData()))
	w.Flush()
	if len(selected.SocketRef) > 0 {
		// Print socket list
		fmt.Println("---")
		var sockets []*zpb.Socket
		for _, socketRef := range selected.GetSocketRef() {
			sockets = append(sockets, transport.Socket(socketRef.GetSocketId()))
		}
		printSockets(sockets)
	}
	return nil
}

var channelzSubchannelCmd = &cobra.Command{
	Use:   "subchannel <id>",
	Short: "Display subchannel states in a human readable way.",
	Args:  cobra.ExactArgs(1),
	RunE:  channelzSubchannelCommandRunWithError,
}

func channelzSocketCommandRunWithError(cmd *cobra.Command, args []string) error {
	socketID, err := strconv.ParseInt(args[0], 10, 64)
	if err != nil {
		return fmt.Errorf("Invalid socket ID %v", socketID)
	}
	selected := transport.Socket(socketID)
	// Print as JSON
	if jsonOutputFlag {
		return printObjectAsJSON(selected)
	}
	// Print as table
	// Print Socket information
	fmt.Fprintf(w, "Socket ID:\t%v\t\n", selected.GetRef().GetSocketId())
	fmt.Fprintf(w, "Address:\t%v\t\n", fmt.Sprintf("%v->%v", prettyAddress(selected.GetLocal()), prettyAddress(selected.GetRemote())))
	fmt.Fprintf(w, "Streams Started:\t%v\t\n", selected.GetData().GetStreamsStarted())
	fmt.Fprintf(w, "Streams Succeeded:\t%v\t\n", selected.GetData().GetStreamsSucceeded())
	fmt.Fprintf(w, "Streams Failed:\t%v\t\n", selected.GetData().GetStreamsFailed())
	fmt.Fprintf(w, "Messages Sent:\t%v\t\n", selected.GetData().GetMessagesSent())
	fmt.Fprintf(w, "Messages Received:\t%v\t\n", selected.GetData().GetMessagesReceived())
	fmt.Fprintf(w, "Keep Alives Sent:\t%v\t\n", selected.GetData().GetKeepAlivesSent())
	fmt.Fprintf(w, "Last Local Stream Created:\t%v\t\n", prettyTime(selected.GetData().GetLastLocalStreamCreatedTimestamp()))
	fmt.Fprintf(w, "Last Remote Stream Created:\t%v\t\n", prettyTime(selected.GetData().GetLastRemoteStreamCreatedTimestamp()))
	fmt.Fprintf(w, "Last Message Sent Created:\t%v\t\n", prettyTime(selected.GetData().GetLastMessageSentTimestamp()))
	fmt.Fprintf(w, "Last Message Received Created:\t%v\t\n", prettyTime(selected.GetData().GetLastMessageReceivedTimestamp()))
	fmt.Fprintf(w, "Local Flow Control Window:\t%v\t\n", selected.GetData().GetLocalFlowControlWindow().GetValue())
	fmt.Fprintf(w, "Remote Flow Control Window:\t%v\t\n", selected.GetData().GetRemoteFlowControlWindow().GetValue())
	w.Flush()
	if len(selected.GetData().GetOption()) > 0 {
		fmt.Println("---")
		fmt.Fprintln(w, "Socket Options Name\tValue\t")
		for _, option := range selected.GetData().GetOption() {
			if option.GetValue() != "" {
				// Prefer human readable value than the Any proto
				fmt.Fprintf(w, "%v\t%v\t\n", option.GetName(), option.GetValue())
			} else {
				fmt.Fprintf(w, "%v\t%v\t\n", option.GetName(), option.GetAdditional())
			}
		}
		w.Flush()
	}
	// Print security information
	if security := selected.GetSecurity(); security != nil {
		fmt.Println("---")
		switch x := security.Model.(type) {
		case *zpb.Security_Tls_:
			fmt.Fprintf(w, "Security Model:\t%v\t\n", "TLS")
			switch y := security.GetTls().GetCipherSuite().(type) {
			case *zpb.Security_Tls_StandardName:
				fmt.Fprintf(w, "Standard Name:\t%v\t\n", security.GetTls().GetStandardName())
			case *zpb.Security_Tls_OtherName:
				fmt.Fprintf(w, "Other Name:\t%v\t\n", security.GetTls().GetOtherName())
			default:
				return fmt.Errorf("Unexpected Cipher suite name type %T", y)
			}
			// fmt.Fprintf(w, "Local Certificate:\t%v\t\n", security.GetTls().LocalCertificate)
			// fmt.Fprintf(w, "Remote Certificate:\t%v\t\n", security.GetTls().RemoteCertificate)
		case *zpb.Security_Other:
			fmt.Fprintf(w, "Security Model:\t%v\t\n", "Other")
			fmt.Fprintf(w, "Name:\t%v\t\n", security.GetOther().GetName())
			// fmt.Fprintf(w, "Value:\t%v\t\n", security.GetOther().Value)
		default:
			return fmt.Errorf("Unexpected security model type %T", x)
		}
		w.Flush()
	}
	return nil
}

var channelzSocketCmd = &cobra.Command{
	Use:   "socket <id>",
	Short: "Display socket states in a human readable way.",
	Args:  cobra.ExactArgs(1),
	RunE:  channelzSocketCommandRunWithError,
}

func channelzServersCommandRunWithError(cmd *cobra.Command, args []string) error {
	var servers = transport.Servers(startIDFlag, maxResultsFlag)
	// Print as JSON
	if jsonOutputFlag {
		return printObjectAsJSON(servers)
	}
	// Print as table
	fmt.Fprintln(w, "Server ID\tListen Addresses\tCalls(Started/Succeeded/Failed)\tLast Call Started\t")
	for _, server := range servers {
		var listenAddresses []string
		for _, socketRef := range server.GetListenSocket() {
			socket := transport.Socket(socketRef.SocketId)
			listenAddresses = append(listenAddresses, prettyAddress(socket.GetLocal()))
		}
		fmt.Fprintf(
			w, "%v\t%v\t%v/%v/%v\t%v\t\n",
			server.GetRef().GetServerId(),
			listenAddresses,
			server.GetData().GetCallsStarted(),
			server.GetData().GetCallsSucceeded(),
			server.GetData().GetCallsFailed(),
			prettyTime(server.GetData().GetLastCallStartedTimestamp()),
		)
	}
	w.Flush()
	return nil
}

var channelzServersCmd = &cobra.Command{
	Use:   "servers",
	Short: "List servers in a human readable way.",
	Args:  cobra.NoArgs,
	RunE:  channelzServersCommandRunWithError,
}

func channelzServerCommandRunWithError(cmd *cobra.Command, args []string) error {
	serverID, err := strconv.ParseInt(args[0], 10, 64)
	if err != nil {
		return fmt.Errorf("Invalid server ID %v", serverID)
	}
	selected := transport.Server(serverID)
	// Print as JSON
	if jsonOutputFlag {
		return printObjectAsJSON(selected)
	}
	// Print as table
	var listenAddresses []string
	for _, socketRef := range selected.GetListenSocket() {
		socket := transport.Socket(socketRef.GetSocketId())
		listenAddresses = append(listenAddresses, prettyAddress(socket.GetLocal()))
	}
	fmt.Fprintf(w, "Server Id:\t%v\t\n", selected.GetRef().GetServerId())
	fmt.Fprintf(w, "Listen Addresses:\t%v\t\n", listenAddresses)
	fmt.Fprintf(w, "Calls Started:\t%v\t\n", selected.GetData().GetCallsStarted())
	fmt.Fprintf(w, "Calls Succeeded:\t%v\t\n", selected.GetData().GetCallsSucceeded())
	fmt.Fprintf(w, "Calls Failed:\t%v\t\n", selected.GetData().GetCallsFailed())
	fmt.Fprintf(w, "Last Call Started:\t%v\t\n", prettyTime(selected.GetData().GetLastCallStartedTimestamp()))
	w.Flush()
	if sockets := transport.ServerSocket(selected.GetRef().GetServerId(), startIDFlag, maxResultsFlag); len(sockets) > 0 {
		// Print socket list
		fmt.Println("---")
		printSockets(sockets)
	}
	return nil
}

var channelzServerCmd = &cobra.Command{
	Use:   "server <id>",
	Short: "Display the server state in a human readable way.",
	Args:  cobra.ExactArgs(1),
	RunE:  channelzServerCommandRunWithError,
}

var channelzCmd = &cobra.Command{
	Use:   "channelz",
	Short: "Display gRPC states in a human readable way.",
	Args:  cobra.NoArgs,
}

func init() {
	rootCmd.AddCommand(channelzCmd)
	channelzChannelsCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of output channels")
	channelzChannelsCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start channel ID")
	channelzServerCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of the output sockets")
	channelzServerCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start server socket ID")
	channelzServersCmd.Flags().Int64VarP(&maxResultsFlag, "max_results", "m", 100, "The maximum number of output servers")
	channelzServersCmd.Flags().Int64VarP(&startIDFlag, "start_id", "s", 0, "The start server ID")
	channelzCmd.PersistentFlags().BoolVarP(&jsonOutputFlag, "json", "o", false, "Whether to print the result as JSON")
	channelzCmd.AddCommand(channelzChannelCmd)
	channelzCmd.AddCommand(channelzChannelsCmd)
	channelzCmd.AddCommand(channelzSubchannelCmd)
	channelzCmd.AddCommand(channelzSocketCmd)
	channelzCmd.AddCommand(channelzServersCmd)
	channelzCmd.AddCommand(channelzServerCmd)
}


================================================
FILE: cmd/config/config.go
================================================
package config

import (
	"errors"
	"io"
	"os"
	"path"
	"runtime"

	"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"
	"gopkg.in/yaml.v2"
)

// SecurityType is the enum type of available security modes
type SecurityType string

const (
	// TypeInsecure is the insecure security mode and it is the default value
	TypeInsecure SecurityType = "insecure"
	// TypeTLS is the TLS security mode, which requires caller to provide
	// credentials to connect to peer
	TypeTLS = "tls"
)

// The environment variable name of getting the server configs
const grpcdebugServerConfigEnvName = "GRPCDEBUG_CONFIG"

// ServerConfig is the configuration for how to connect to a target
type ServerConfig struct {
	RealAddress        string       `yaml:"real_address"`
	Security           SecurityType `yaml:"security"`
	CredentialFile     string       `yaml:"credential_file"`
	ServerNameOverride string       `yaml:"server_name_override"`
}

type grpcdebugConfig struct {
	Servers map[string]ServerConfig `yaml:"servers"`
}

func loadServerConfigsFromFile(path string) map[string]ServerConfig {
	file, err := os.Open(path)
	if err != nil {
		panic(err)
	}
	bytes, err := io.ReadAll(file)
	if err != nil {
		panic(err)
	}
	var config grpcdebugConfig
	err = yaml.Unmarshal(bytes, &config)
	if err != nil {
		panic(err)
	}
	verbose.Debugf("Loaded grpcdebug config from %v: %v", path, config)
	return config.Servers
}

// userConfigDir is copied here, so we can support Go v1.12
func userConfigDir() (string, error) {
	var dir string
	switch runtime.GOOS {
	case "windows":
		dir = os.Getenv("AppData")
		if dir == "" {
			return "", errors.New("%AppData% is not defined")
		}

	case "darwin", "ios":
		dir = os.Getenv("HOME")
		if dir == "" {
			return "", errors.New("$HOME is not defined")
		}
		dir += "/Library/Application Support"

	case "plan9":
		dir = os.Getenv("home")
		if dir == "" {
			return "", errors.New("$home is not defined")
		}
		dir += "/lib"

	default: // Unix
		dir = os.Getenv("XDG_CONFIG_HOME")
		if dir == "" {
			dir = os.Getenv("HOME")
			if dir == "" {
				return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined")
			}
			dir += "/.config"
		}
	}
	return dir, nil
}

func loadServerConfigs() map[string]ServerConfig {
	if value := os.Getenv(grpcdebugServerConfigEnvName); value != "" {
		return loadServerConfigsFromFile(value)
	}
	// Try to load from work directory, if exists
	if _, err := os.Stat("./grpcdebug_config.yaml"); err == nil {
		return loadServerConfigsFromFile("./grpcdebug_config.yaml")
	}
	// Try to load from user config directory, if exists
	dir, _ := userConfigDir()
	defaultUserConfig := path.Join(dir, "grpcdebug_config.yaml")
	if _, err := os.Stat(defaultUserConfig); err == nil {
		return loadServerConfigsFromFile(defaultUserConfig)
	}
	return nil
}

// GetServerConfig returns a connect configuration for the given target
func GetServerConfig(target string) ServerConfig {
	for pattern, config := range loadServerConfigs() {
		// TODO(lidiz): support wildcards
		if pattern == target {
			if config.RealAddress == "" {
				config.RealAddress = pattern
			}
			return config
		}
	}
	return ServerConfig{RealAddress: target}
}


================================================
FILE: cmd/health.go
================================================
package cmd

import (
	"fmt"
	"sort"

	"github.com/grpc-ecosystem/grpcdebug/cmd/transport"
	"github.com/spf13/cobra"
)

var healthCmd = &cobra.Command{
	Use:   "health [service names]",
	Short: "Check health status of the target service (default \"\").",
	RunE: func(cmd *cobra.Command, args []string) error {
		var services []string
		// Ensure there's the overall health status
		services = append(services, "")
		services = append(services, args...)
		// Sort alphabetically, and deduplicate inputs
		sort.Strings(services)
		j := 0
		for i := 1; i < len(services); i++ {
			if services[i] == services[j] {
				continue
			}
			j++
			services[j] = services[i]
		}
		services = services[:j+1]
		// Print as table
		for _, service := range services {
			var serviceName string
			if service == "" {
				serviceName = "<Overall>"
			} else {
				serviceName = service
			}
			fmt.Fprintf(
				w, "%v:\t%v\t\n",
				serviceName,
				transport.GetHealthStatus(service),
			)
		}
		w.Flush()
		return nil
	},
}

func init() {
	rootCmd.AddCommand(healthCmd)
}


================================================
FILE: cmd/root.go
================================================
// Defines the root command and global flags

package cmd

import (
	"fmt"
	"log"
	"os"
	"text/tabwriter"

	"github.com/grpc-ecosystem/grpcdebug/cmd/config"
	"github.com/grpc-ecosystem/grpcdebug/cmd/transport"
	"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"

	"github.com/spf13/cobra"
)

var verboseFlag, timestampFlag bool
var address, security, credFile, serverNameOverride string

// The table formater
var w = tabwriter.NewWriter(os.Stdout, 10, 0, 3, ' ', 0)

var rootUsageTemplate = `Usage:{{if .Runnable}}
  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
  grpcdebug <target address> [flags] {{ .CommandPath | ChildCommandPath }} <command>{{end}}{{if gt (len .Aliases) 0}}

Aliases:
  {{.NameAndAliases}}{{end}}{{if .HasExample}}

Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}

Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}

Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}

Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}

Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}

Use "grpcdebug <target address> {{ .CommandPath | ChildCommandPath }} [command] --help" for more information about a command.{{end}}
`

var rootCmd = &cobra.Command{
	Use:   "grpcdebug",
	Short: "grpcdebug is a gRPC service admin CLI",
}

func initConfig() {
	if verboseFlag {
		verbose.EnableDebugOutput()
	}
	c := config.GetServerConfig(address)
	if credFile != "" {
		c.CredentialFile = credFile
	}
	if serverNameOverride != "" {
		c.ServerNameOverride = serverNameOverride
	}
	if security == "tls" {
		c.Security = config.TypeTLS
		if c.CredentialFile == "" {
			rootCmd.Usage()
			log.Fatalf("Please specify credential file under [tls] mode.")
		}
	} else if security != "insecure" {
		rootCmd.Usage()
		log.Fatalf("Unrecognized security mode: %v", security)
	}
	transport.Connect(c)
}

// ChildCommandPath used in template
func ChildCommandPath(path string) string {
	if len(path) <= 10 {
		return ""
	}
	return path[10:]
}

func init() {
	cobra.AddTemplateFunc("ChildCommandPath", ChildCommandPath)
	cobra.OnInitialize(initConfig)
	rootCmd.SetUsageTemplate(rootUsageTemplate)

	rootCmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Print verbose information for debugging")
	rootCmd.PersistentFlags().BoolVarP(&timestampFlag, "timestamp", "t", false, "Print timestamp as RFC3339 instead of human readable strings")
	rootCmd.PersistentFlags().StringVar(&security, "security", "insecure", "Defines the type of credentials to use [tls, google-default, insecure]")
	rootCmd.PersistentFlags().StringVar(&credFile, "credential_file", "", "Sets the path of the credential file; used in [tls] mode")
	rootCmd.PersistentFlags().StringVar(&serverNameOverride, "server_name_override", "", "Overrides the peer server name if non empty; used in [tls] mode")
}

// Execute executes the root command.
func Execute() {
	if len(os.Args) > 1 {
		address = os.Args[1]
		os.Args = os.Args[1:]
	} else {
		rootCmd.Usage()
		os.Exit(1)
	}
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}


================================================
FILE: cmd/transport/grpc.go
================================================
package transport

import (
	"context"
	"log"
	"time"

	csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
	"github.com/grpc-ecosystem/grpcdebug/cmd/config"
	"github.com/grpc-ecosystem/grpcdebug/cmd/verbose"
	"google.golang.org/grpc"
	zpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

var conn *grpc.ClientConn
var channelzClient zpb.ChannelzClient
var csdsClient csdspb.ClientStatusDiscoveryServiceClient
var healthClient healthpb.HealthClient

const rpcTimeout = time.Second * 15

// Connect connects to the service at address and creates stubs
func Connect(c config.ServerConfig) {
	verbose.Debugf("Connecting with %v", c)
	var err error
	var credOption grpc.DialOption
	if c.CredentialFile != "" {
		cred, err := credentials.NewClientTLSFromFile(c.CredentialFile, c.ServerNameOverride)
		if err != nil {
			log.Fatalf("failed to create credential: %v", err)
		}
		credOption = grpc.WithTransportCredentials(cred)
	} else {
		credOption = grpc.WithTransportCredentials(insecure.NewCredentials())
	}
	conn, err = grpc.NewClient(c.RealAddress, credOption)
	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}
	channelzClient = zpb.NewChannelzClient(conn)
	csdsClient = csdspb.NewClientStatusDiscoveryServiceClient(conn)
	healthClient = healthpb.NewHealthClient(conn)
}

// Channels returns all available channels
func Channels(startID, maxResults int64) []*zpb.Channel {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	channels, err := channelzClient.GetTopChannels(ctx, &zpb.GetTopChannelsRequest{StartChannelId: startID, MaxResults: maxResults})
	if err != nil {
		log.Fatalf("failed to fetch top channels: %v", err)
	}
	return channels.Channel
}

// Channel returns the channel with given channel ID
func Channel(channelID int64) *zpb.Channel {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	channel, err := channelzClient.GetChannel(ctx, &zpb.GetChannelRequest{ChannelId: channelID})
	if err != nil {
		log.Fatalf("failed to fetch channel id=%v: %v", channelID, err)
	}
	return channel.Channel
}

// Subchannel returns the queried subchannel
func Subchannel(subchannelID int64) *zpb.Subchannel {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	subchannel, err := channelzClient.GetSubchannel(ctx, &zpb.GetSubchannelRequest{SubchannelId: subchannelID})
	if err != nil {
		log.Fatalf("failed to fetch subchannel (id=%v): %v", subchannelID, err)
	}
	return subchannel.Subchannel
}

// Servers returns all available servers
func Servers(startID, maxResults int64) []*zpb.Server {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	servers, err := channelzClient.GetServers(ctx, &zpb.GetServersRequest{StartServerId: startID, MaxResults: maxResults})
	if err != nil {
		log.Fatalf("failed to fetch servers: %v", err)
	}
	return servers.Server
}

// Server returns a server
func Server(serverID int64) *zpb.Server {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	server, err := channelzClient.GetServer(ctx, &zpb.GetServerRequest{ServerId: serverID})
	if err != nil {
		log.Fatalf("failed to fetch server (id=%v): %v", serverID, err)
	}
	return server.Server
}

// Socket returns a socket
func Socket(socketID int64) *zpb.Socket {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	socket, err := channelzClient.GetSocket(ctx, &zpb.GetSocketRequest{SocketId: socketID})
	if err != nil {
		log.Fatalf("failed to fetch socket (id=%v): %v", socketID, err)
	}
	return socket.Socket
}

// ServerSocket returns all sockets of this server
func ServerSocket(serverID, startID, maxResults int64) []*zpb.Socket {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	var s []*zpb.Socket
	serverSocketResp, err := channelzClient.GetServerSockets(
		ctx,
		&zpb.GetServerSocketsRequest{
			ServerId:      serverID,
			StartSocketId: startID,
			MaxResults:    maxResults,
		},
	)
	if err != nil {
		log.Fatalf("failed to fetch server sockets (id=%v): %v", serverID, err)
	}
	for _, socketRef := range serverSocketResp.SocketRef {
		s = append(s, Socket(socketRef.SocketId))
	}
	return s
}

// FetchClientStatus fetches the xDS resources status
func FetchClientStatus() *csdspb.ClientStatusResponse {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	resp, err := csdsClient.FetchClientStatus(ctx, &csdspb.ClientStatusRequest{})
	if err != nil {
		log.Fatalf("failed to fetch xds config: %v", err)
	}
	return resp
}

// GetHealthStatus fetches the health checking status of the service from peer
func GetHealthStatus(service string) string {
	ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
	defer cancel()
	resp, err := healthClient.Check(ctx, &healthpb.HealthCheckRequest{Service: service})
	if err != nil {
		verbose.Debugf("failed to fetch health status for \"%s\": %v", service, err)
		return healthpb.HealthCheckResponse_SERVICE_UNKNOWN.String()
	}
	return resp.Status.String()
}


================================================
FILE: cmd/verbose/verbose.go
================================================
package verbose

import "log"

var enableDebugOutput = false

// EnableDebugOutput enables debugging output
func EnableDebugOutput() {
	enableDebugOutput = true
}

// Debugf prints log if debugging is enabled
func Debugf(format string, v ...any) {
	if enableDebugOutput {
		log.Printf(format, v...)
	}
}


================================================
FILE: cmd/xds.go
================================================
package cmd

import (
	"fmt"
	"sort"
	"strings"

	"github.com/grpc-ecosystem/grpcdebug/cmd/transport"

	adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
	clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
	routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
	"github.com/spf13/cobra"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

var (
	xdsTypeFlag  string
	xdsScopeFlag string
)

func printProtoBufMessageAsJSON(m proto.Message) error {
	option := protojson.MarshalOptions{
		Multiline:      true,
		Indent:         "  ",
		UseProtoNames:  false,
		UseEnumNumbers: false,
	}
	jsonbytes, err := option.Marshal(m)
	if err != nil {
		return err
	}
	fmt.Println(string(jsonbytes))
	return nil
}

func priorityPerXdsConfig(x *csdspb.PerXdsConfig) int {
	switch x.PerXdsConfig.(type) {
	case *csdspb.PerXdsConfig_ListenerConfig:
		return 0
	case *csdspb.PerXdsConfig_RouteConfig:
		return 1
	case *csdspb.PerXdsConfig_ClusterConfig:
		return 2
	case *csdspb.PerXdsConfig_EndpointConfig:
		return 3
	default:
		return 4
	}
}

func sortPerXdsConfigs(clientStatus *csdspb.ClientStatusResponse) {
	for _, cfg := range clientStatus.Config {
		// XdsConfig is deprecated but we support it for backward compatibility with older servers
		sort.Slice(cfg.XdsConfig, func(i, j int) bool {
			return priorityPerXdsConfig(cfg.XdsConfig[i]) < priorityPerXdsConfig(cfg.XdsConfig[j])
		})
	}
}

func filterConfigsByScope(all []*csdspb.ClientConfig, scope string) ([]*csdspb.ClientConfig, error) {
	if scope == "" {
		return all, nil
	}
	var out []*csdspb.ClientConfig
	for _, c := range all {
		if c.ClientScope == scope {
			out = append(out, c)
		}
	}
	if len(out) == 0 {
		return nil, fmt.Errorf("no ClientConfig matched scope=%q", scope)
	}
	return out, nil
}

func xdsConfigCommandRunWithError(cmd *cobra.Command, args []string) error {
	clientStatus := transport.FetchClientStatus()
	if len(clientStatus.Config) == 0 {
		return fmt.Errorf("no ClientConfig returned")
	}
	sortPerXdsConfigs(clientStatus)

	configs, err := filterConfigsByScope(clientStatus.Config, xdsScopeFlag)
	if err != nil {
		return err
	}

	if xdsTypeFlag == "" {
		// Print whole response; if filtered by scope, print a shallow copy to reflect only filtered configs
		if len(configs) == len(clientStatus.Config) {
			return printProtoBufMessageAsJSON(clientStatus)
		}
		return printProtoBufMessageAsJSON(&csdspb.ClientStatusResponse{Config: configs})
	}

	// Parse --type and print resources for each selected config
	wantXdsTypes := strings.Split(xdsTypeFlag, ",")
	var wantLDS, wantRDS, wantCDS, wantEDS bool
	for _, t := range wantXdsTypes {
		switch strings.ToLower(t) {
		case "lds":
			wantLDS = true
		case "rds":
			wantRDS = true
		case "cds":
			wantCDS = true
		case "eds":
			wantEDS = true
		}
	}

	multi := len(configs) > 1
	for _, cfg := range configs {
		if multi {
			fmt.Printf("== client_scope: %q ==\n", cfg.ClientScope)
		}
		if len(cfg.GenericXdsConfigs) > 0 {
			for _, g := range cfg.GenericXdsConfigs {
				var m proto.Message
				tokens := strings.Split(g.TypeUrl, ".")
				switch tokens[len(tokens)-1] {
				case "Listener":
					if wantLDS {
						m = g.GetXdsConfig()
					}
				case "RouteConfiguration":
					if wantRDS {
						m = g.GetXdsConfig()
					}
				case "Cluster":
					if wantCDS {
						m = g.GetXdsConfig()
					}
				case "ClusterLoadAssignment":
					if wantEDS {
						m = g.GetXdsConfig()
					}
				}
				if m != nil {
					if err := printProtoBufMessageAsJSON(m); err != nil {
						return fmt.Errorf("Failed to print xDS config: %v", err)
					}
				}
			}
		} else {
			// XdsConfig is deprecated but we support it for backward compatibility with older servers
			for _, x := range cfg.XdsConfig {
				var m proto.Message
				switch x.PerXdsConfig.(type) {
				case *csdspb.PerXdsConfig_ListenerConfig:
					if wantLDS {
						m = x.GetListenerConfig()
					}
				case *csdspb.PerXdsConfig_RouteConfig:
					if wantRDS {
						m = x.GetRouteConfig()
					}
				case *csdspb.PerXdsConfig_ClusterConfig:
					if wantCDS {
						m = x.GetClusterConfig()
					}
				case *csdspb.PerXdsConfig_EndpointConfig:
					if wantEDS {
						m = x.GetEndpointConfig()
					}
				}
				if m != nil {
					if err := printProtoBufMessageAsJSON(m); err != nil {
						return fmt.Errorf("Failed to print xDS config: %v", err)
					}
				}
			}
		}
	}
	return nil
}

var xdsConfigCmd = &cobra.Command{
	Use:   "config",
	Short: "Dump the operating xDS configs.",
	RunE:  xdsConfigCommandRunWithError,
	Args:  cobra.NoArgs,
}

type xdsResourceStatusEntry struct {
	Scope       string
	Name        string
	Status      adminpb.ClientResourceStatus
	Version     string
	Type        string
	LastUpdated *timestamppb.Timestamp
}

func printStatusEntry(entry *xdsResourceStatusEntry, includeScope bool) {
	if includeScope {
		fmt.Fprintf(
			w, "%v\t%v\t%v\t%v\t%v\t%v\t\n",
			entry.Scope,
			entry.Name,
			entry.Status,
			entry.Version,
			entry.Type,
			prettyTime(entry.LastUpdated),
		)
		return
	}
	fmt.Fprintf(
		w, "%v\t%v\t%v\t%v\t%v\t\n",
		entry.Name,
		entry.Status,
		entry.Version,
		entry.Type,
		prettyTime(entry.LastUpdated),
	)
}

func xdsStatusCommandRunWithError(cmd *cobra.Command, args []string) error {
	clientStatus := transport.FetchClientStatus()
	if len(clientStatus.Config) == 0 {
		return fmt.Errorf("no ClientConfig returned")
	}
	configs, err := filterConfigsByScope(clientStatus.Config, xdsScopeFlag)
	if err != nil {
		return err
	}

	includeScope := xdsScopeFlag == ""
	if includeScope {
		fmt.Fprintln(w, "Scope\tName\tStatus\tVersion\tType\tLastUpdated")
	} else {
		fmt.Fprintln(w, "Name\tStatus\tVersion\tType\tLastUpdated")
	}

	for _, config := range configs {
		scope := config.ClientScope
		for _, g := range config.GenericXdsConfigs {
			entry := xdsResourceStatusEntry{
				Scope:       scope,
				Name:        g.Name,
				Status:      g.ClientStatus,
				Version:     g.VersionInfo,
				Type:        g.TypeUrl,
				LastUpdated: g.LastUpdated,
			}
			printStatusEntry(&entry, includeScope)
		}
		if len(config.GenericXdsConfigs) == 0 {
			// XdsConfig is deprecated but we support it for backward compatibility with older servers
			for _, x := range config.XdsConfig {
				switch x.PerXdsConfig.(type) {
				case *csdspb.PerXdsConfig_ListenerConfig:
					for _, dl := range x.GetListenerConfig().DynamicListeners {
						e := xdsResourceStatusEntry{Scope: scope, Name: dl.Name, Status: dl.ClientStatus}
						if s := dl.GetActiveState(); s != nil {
							e.Version = s.VersionInfo
							e.Type = s.Listener.TypeUrl
							e.LastUpdated = s.LastUpdated
						}
						printStatusEntry(&e, includeScope)
					}
				case *csdspb.PerXdsConfig_RouteConfig:
					for _, dr := range x.GetRouteConfig().DynamicRouteConfigs {
						e := xdsResourceStatusEntry{
							Scope:       scope,
							Status:      dr.ClientStatus,
							Version:     dr.VersionInfo,
							Type:        dr.RouteConfig.TypeUrl,
							LastUpdated: dr.LastUpdated,
						}
						if packed := dr.GetRouteConfig(); packed != nil {
							var rc routepb.RouteConfiguration
							if err := packed.UnmarshalTo(&rc); err != nil {
								return err
							}
							e.Name = rc.Name
						}
						printStatusEntry(&e, includeScope)
					}
				case *csdspb.PerXdsConfig_ClusterConfig:
					for _, dc := range x.GetClusterConfig().DynamicActiveClusters {
						e := xdsResourceStatusEntry{
							Scope:       scope,
							Status:      dc.ClientStatus,
							Version:     dc.VersionInfo,
							Type:        dc.Cluster.TypeUrl,
							LastUpdated: dc.LastUpdated,
						}
						if packed := dc.GetCluster(); packed != nil {
							var c clusterpb.Cluster
							if err := packed.UnmarshalTo(&c); err != nil {
								return err
							}
							e.Name = c.Name
						}
						printStatusEntry(&e, includeScope)
					}
				case *csdspb.PerXdsConfig_EndpointConfig:
					for _, de := range x.GetEndpointConfig().GetDynamicEndpointConfigs() {
						e := xdsResourceStatusEntry{
							Scope:       scope,
							Status:      de.ClientStatus,
							Version:     de.VersionInfo,
							Type:        de.EndpointConfig.TypeUrl,
							LastUpdated: de.LastUpdated,
						}
						if packed := de.GetEndpointConfig(); packed != nil {
							var ep endpointpb.ClusterLoadAssignment
							if err := packed.UnmarshalTo(&ep); err != nil {
								return err
							}
							e.Name = ep.ClusterName
						}
						printStatusEntry(&e, includeScope)
					}
				}
			}
		}
	}
	w.Flush()
	return nil
}

var xdsStatusCmd = &cobra.Command{
	Use:   "status",
	Short: "Print the config synchronization status.",
	RunE:  xdsStatusCommandRunWithError,
}

var xdsCmd = &cobra.Command{
	Use:   "xds",
	Short: "Fetch xDS related information.",
}

func init() {
	xdsConfigCmd.Flags().StringVarP(&xdsTypeFlag, "type", "y", "", "Filters the wanted type of xDS config to print (separated by commas) (available types: LDS,RDS,CDS,EDS) (by default, print all)")
	xdsConfigCmd.Flags().StringVarP(&xdsScopeFlag, "scope", "s", "", "Filter by client_scope when multiple ClientConfig are present")
	xdsCmd.AddCommand(xdsConfigCmd)
	xdsCmd.AddCommand(xdsStatusCmd)
	rootCmd.AddCommand(xdsCmd)
}


================================================
FILE: cmd/xds_test.go
================================================
package cmd

import (
	"strings"
	"testing"

	csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
)

func TestFilterConfigsByScope(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name       string
		configs    []*csdspb.ClientConfig
		scope      string
		wantLen    int
		wantErr    bool
		wantErrMsg string
	}{
		{
			name: "empty scope returns all",
			configs: []*csdspb.ClientConfig{
				{ClientScope: "primary"},
				{ClientScope: "fallback"},
			},
			scope:   "",
			wantLen: 2,
			wantErr: false,
		},
		{
			name: "filter by primary scope",
			configs: []*csdspb.ClientConfig{
				{ClientScope: "primary"},
				{ClientScope: "fallback"},
			},
			scope:   "primary",
			wantLen: 1,
			wantErr: false,
		},
		{
			name: "filter by fallback scope",
			configs: []*csdspb.ClientConfig{
				{ClientScope: "primary"},
				{ClientScope: "fallback"},
			},
			scope:   "fallback",
			wantLen: 1,
			wantErr: false,
		},
		{
			name: "no match returns error",
			configs: []*csdspb.ClientConfig{
				{ClientScope: "primary"},
				{ClientScope: "fallback"},
			},
			scope:      "nonexistent",
			wantLen:    0,
			wantErr:    true,
			wantErrMsg: "no ClientConfig matched scope=",
		},
		{
			name: "multiple configs with same scope",
			configs: []*csdspb.ClientConfig{
				{ClientScope: "primary"},
				{ClientScope: "primary"},
				{ClientScope: "fallback"},
			},
			scope:   "primary",
			wantLen: 2,
			wantErr: false,
		},
		{
			name: "empty scope name in config",
			configs: []*csdspb.ClientConfig{
				{ClientScope: ""},
				{ClientScope: "primary"},
			},
			scope:   "",
			wantLen: 2,
			wantErr: false,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			got, err := filterConfigsByScope(test.configs, test.scope)
			if test.wantErr {
				if err == nil {
					t.Errorf("filterConfigsByScope() expected error but got none")
					return
				}
				if !strings.Contains(err.Error(), test.wantErrMsg) {
					t.Errorf("filterConfigsByScope() error = %v, want error containing %v", err, test.wantErrMsg)
				}
				return
			}
			if err != nil {
				t.Errorf("filterConfigsByScope() unexpected error = %v", err)
				return
			}
			if len(got) != test.wantLen {
				t.Errorf("filterConfigsByScope() returned %d configs, want %d", len(got), test.wantLen)
			}
			// Verify filtered results actually match the scope
			if test.scope != "" {
				for _, c := range got {
					if c.ClientScope != test.scope {
						t.Errorf("filterConfigsByScope() returned config with scope %q, want %q", c.ClientScope, test.scope)
					}
				}
			}
		})
	}
}

func TestSortPerXdsConfigs(t *testing.T) {
	t.Parallel()
	// Test that sortPerXdsConfigs doesn't panic with multiple configs
	// XdsConfig is deprecated but we test it for backward compatibility
	clientStatus := &csdspb.ClientStatusResponse{
		Config: []*csdspb.ClientConfig{
			{
				ClientScope: "primary",
				XdsConfig: []*csdspb.PerXdsConfig{
					{PerXdsConfig: &csdspb.PerXdsConfig_ClusterConfig{}},
					{PerXdsConfig: &csdspb.PerXdsConfig_ListenerConfig{}},
				},
			},
			{
				ClientScope: "fallback",
				XdsConfig: []*csdspb.PerXdsConfig{
					{PerXdsConfig: &csdspb.PerXdsConfig_EndpointConfig{}},
					{PerXdsConfig: &csdspb.PerXdsConfig_RouteConfig{}},
				},
			},
		},
	}

	// Should not panic
	sortPerXdsConfigs(clientStatus)

	// Verify first config is sorted: Listener(0) < Cluster(2)
	if len(clientStatus.Config[0].XdsConfig) >= 2 {
		p0 := priorityPerXdsConfig(clientStatus.Config[0].XdsConfig[0])
		p1 := priorityPerXdsConfig(clientStatus.Config[0].XdsConfig[1])
		if p0 > p1 {
			t.Errorf("First config not sorted properly: priority[0]=%d > priority[1]=%d", p0, p1)
		}
	}

	// Verify second config is sorted: Route(1) < Endpoint(3)
	if len(clientStatus.Config[1].XdsConfig) >= 2 {
		p0 := priorityPerXdsConfig(clientStatus.Config[1].XdsConfig[0])
		p1 := priorityPerXdsConfig(clientStatus.Config[1].XdsConfig[1])
		if p0 > p1 {
			t.Errorf("Second config not sorted properly: priority[0]=%d > priority[1]=%d", p0, p1)
		}
	}
}

func TestSortPerXdsConfigsEmptyConfigs(t *testing.T) {
	t.Parallel()
	// Test with empty configs
	clientStatus := &csdspb.ClientStatusResponse{
		Config: []*csdspb.ClientConfig{},
	}
	sortPerXdsConfigs(clientStatus)
}

func TestSortPerXdsConfigsSingleConfig(t *testing.T) {
	t.Parallel()
	// Test backward compatibility with single config
	// XdsConfig is deprecated but we test it for backward compatibility
	clientStatus := &csdspb.ClientStatusResponse{
		Config: []*csdspb.ClientConfig{
			{
				ClientScope: "",
				XdsConfig: []*csdspb.PerXdsConfig{
					{PerXdsConfig: &csdspb.PerXdsConfig_EndpointConfig{}},
					{PerXdsConfig: &csdspb.PerXdsConfig_ListenerConfig{}},
					{PerXdsConfig: &csdspb.PerXdsConfig_ClusterConfig{}},
				},
			},
		},
	}

	sortPerXdsConfigs(clientStatus)

	// Verify sorted: Listener(0) < Cluster(2) < Endpoint(3)
	expected := []int{0, 2, 3}
	for i, cfg := range clientStatus.Config[0].XdsConfig {
		priority := priorityPerXdsConfig(cfg)
		if priority != expected[i] {
			t.Errorf("Config[%d] priority = %d, want %d", i, priority, expected[i])
		}
	}
}


================================================
FILE: go.mod
================================================
module github.com/grpc-ecosystem/grpcdebug

go 1.23.0

require (
	github.com/dustin/go-humanize v1.0.1
	github.com/envoyproxy/go-control-plane/contrib v1.32.4
	github.com/envoyproxy/go-control-plane/envoy v1.32.4
	github.com/golang/protobuf v1.5.4
	github.com/spf13/cobra v1.8.1
	google.golang.org/grpc v1.70.0
	google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e
	google.golang.org/protobuf v1.36.4
	gopkg.in/yaml.v2 v2.4.0
)

require (
	cel.dev/expr v0.19.0 // indirect
	cloud.google.com/go/compute/metadata v0.5.2 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
	github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 // indirect
	github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/kr/pretty v0.3.1 // indirect
	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
	github.com/prometheus/client_model v0.6.1 // indirect
	github.com/rogpeppe/go-internal v1.13.1 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
	golang.org/x/net v0.34.0 // indirect
	golang.org/x/oauth2 v0.24.0 // indirect
	golang.org/x/sync v0.10.0 // indirect
	golang.org/x/sys v0.29.0 // indirect
	golang.org/x/text v0.21.0 // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)


================================================
FILE: go.sum
================================================
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/contrib v1.32.4 h1:/udV6s9xkDGe13WfrT2MHAxXTNDMBYBPxI1GkleCrmM=
github.com/envoyproxy/go-control-plane/contrib v1.32.4/go.mod h1:gkGYoY7plfQg7FPBDhyKtP1cDA9frFR/3YsCx8taRvI=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e h1:LGj5F6Z+enJcJFa6sjBMwmJ1gnjHpV60bKrQ2ass/aE=
google.golang.org/grpc/examples v0.0.0-20241106195202-b3393d95a74e/go.mod h1:UxqwMHw3ntCGQS0LuHPmqkO+z9CyMtK1oN7xh6P+gw8=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: internal/testing/ca.pem
================================================
-----BEGIN CERTIFICATE-----
MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
Dfcog5wrJytaQ6UA0wE=
-----END CERTIFICATE-----


================================================
FILE: internal/testing/grpcdebug_config.yaml
================================================
servers:
  dev:
    real_address: localhost:50051
    security: insecure
  prod:
    real_address: "localhost:50052"
    security: tls
    credential_file: ./internal/testing/ca.pem
    server_name_override: "*.test.youtube.com"
  "localhost:50052":
    security: tls
    credential_file: ./internal/testing/ca.pem
    server_name_override: "*.test.youtube.com"


================================================
FILE: internal/testing/testserver/csds_config_dump.json
================================================
{
  "config": [
    {
      "node": {
        "id": "projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b431-c1d43e6ac0ab",
        "cluster": "cluster",
        "metadata": {
            "INSTANCE_IP": "192.168.120.31",
            "TRAFFICDIRECTOR_GCP_PROJECT_NUMBER": "1040920224690",
            "TRAFFICDIRECTOR_NETWORK_NAME": "default"
          },
        "locality": {
          "zone": "us-central1-a"
        },
        "userAgentName": "gRPC Java",
        "userAgentVersion": "1.38.0-SNAPSHOT",
        "clientFeatures": [
          "envoy.lb.does_not_support_overprovisioning"
        ]
      },
      "xdsConfig": [
        {
          "listenerConfig": {
            "versionInfo": "1617141154495058478",
            "dynamicListeners": [
              {
                "name": "xds-test-server:1337",
                "activeState": {
                  "versionInfo": "1617141154495058478",
                  "listener": {
                    "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
                    "apiListener": {
                      "apiListener": {
                        "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
                        "httpFilters": [
                          {
                            "name": "envoy.filters.http.fault",
                            "typedConfig": {
                              "@type": "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault"
                            }
                          },
                          {
                            "name": "envoy.filters.http.router",
                            "typedConfig": {
                              "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router",
                              "suppressEnvoyHeaders": true
                            }
                          }
                        ],
                        "rds": {
                          "configSource": {
                            "ads": {},
                            "resourceApiVersion": "V3"
                          },
                          "routeConfigName": "URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337"
                        },
                        "statPrefix": "trafficdirector"
                      }
                    },
                    "name": "xds-test-server:1337"
                  },
                  "lastUpdated": "2021-03-31T01:20:33.144Z"
                },
                "clientStatus": "ACKED"
              }
            ]
          }
        },
        {
          "routeConfig": {
            "dynamicRouteConfigs": [
              {
                "versionInfo": "1617141154495058478",
                "routeConfig": {
                  "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
                  "name": "URL_MAP/1040920224690_sergii-psm-test-url-map_0_xds-test-server:1337",
                  "virtualHosts": [
                    {
                      "domains": [
                        "xds-test-server:1337"
                      ],
                      "routes": [
                        {
                          "match": {
                            "prefix": ""
                          },
                          "route": {
                            "cluster": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
                            "timeout": "30s",
                            "retryPolicy": {
                              "retryOn": "gateway-error",
                              "numRetries": 1,
                              "perTryTimeout": "30s"
                            }
                          }
                        }
                      ]
                    }
                  ]
                },
                "lastUpdated": "2021-03-31T01:20:33.302Z",
                "clientStatus": "ACKED"
              }
            ]
          }
        },
        {
          "clusterConfig": {
            "versionInfo": "1617141154495058478",
            "dynamicActiveClusters": [
              {
                "versionInfo": "1617141154495058478",
                "cluster": {
                  "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
                  "circuitBreakers": {
                    "thresholds": [
                      {
                        "maxConnections": 2147483647,
                        "maxPendingRequests": 2147483647,
                        "maxRequests": 2147483647,
                        "maxRetries": 2147483647
                      }
                    ]
                  },
                  "commonLbConfig": {
                    "healthyPanicThreshold": {
                      "value": 1
                    },
                    "localityWeightedLbConfig": {}
                  },
                  "connectTimeout": "30s",
                  "edsClusterConfig": {
                    "edsConfig": {
                      "ads": {},
                      "initialFetchTimeout": "15s",
                      "resourceApiVersion": "V3"
                    }
                  },
                  "http2ProtocolOptions": {
                    "maxConcurrentStreams": 100
                  },
                  "lrsServer": {
                    "self": {}
                  },
                  "metadata": {
                    "filterMetadata": {
                      "com.google.trafficdirector": {
                        "backend_service_name": "sergii-psm-test-backend-service"
                      }
                    }
                  },
                  "name": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
                  "type": "EDS"
                },
                "lastUpdated": "2021-03-31T01:20:33.853Z",
                "clientStatus": "ACKED"
              }
            ]
          }
        },
        {
          "endpointConfig": {
            "dynamicEndpointConfigs": [
              {
                "versionInfo": "1",
                "endpointConfig": {
                  "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
                  "clusterName": "cloud-internal-istio:cloud_mp_1040920224690_6530603179561593229",
                  "endpoints": [
                    {
                      "locality": {
                        "subZone": "jf:us-central1-a_7062512536751318190_neg"
                      },
                      "lbEndpoints": [
                        {
                          "endpoint": {
                            "address": {
                              "socketAddress": {
                                "address": "192.168.120.26",
                                "portValue": 8080
                              }
                            }
                          },
                          "healthStatus": "HEALTHY"
                        }
                      ],
                      "loadBalancingWeight": 100
                    }
                  ]
                },
                "lastUpdated": "2021-03-31T01:20:33.936Z",
                "clientStatus": "ACKED"
              }
            ]
          }
        }
      ]
    }
  ]
}


================================================
FILE: internal/testing/testserver/csds_config_dump_multi_scope.json
================================================
{
  "config": [
    {
      "node": {
        "id": "projects/1040920224690/networks/default/nodes/primary-node",
        "cluster": "cluster",
        "userAgentName": "gRPC Java",
        "userAgentVersion": "1.60.0"
      },
      "clientScope": "primary",
      "xdsConfig": [
        {
          "listenerConfig": {
            "versionInfo": "1617141154495058478",
            "dynamicListeners": [
              {
                "name": "primary-listener:1337",
                "activeState": {
                  "versionInfo": "1617141154495058478",
                  "listener": {
                    "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
                    "name": "primary-listener:1337"
                  },
                  "lastUpdated": "2021-03-31T01:20:33.144Z"
                },
                "clientStatus": "ACKED"
              }
            ]
          }
        },
        {
          "clusterConfig": {
            "versionInfo": "1617141154495058478",
            "dynamicActiveClusters": [
              {
                "versionInfo": "1617141154495058478",
                "cluster": {
                  "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
                  "name": "primary-cluster",
                  "type": "EDS"
                },
                "lastUpdated": "2021-03-31T01:20:33.853Z",
                "clientStatus": "ACKED"
              }
            ]
          }
        }
      ]
    },
    {
      "node": {
        "id": "projects/1040920224690/networks/default/nodes/fallback-node",
        "cluster": "cluster",
        "userAgentName": "gRPC Java",
        "userAgentVersion": "1.60.0"
      },
      "clientScope": "fallback",
      "xdsConfig": [
        {
          "listenerConfig": {
            "versionInfo": "1617141154495058479",
            "dynamicListeners": [
              {
                "name": "fallback-listener:1338",
                "activeState": {
                  "versionInfo": "1617141154495058479",
                  "listener": {
                    "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
                    "name": "fallback-listener:1338"
                  },
                  "lastUpdated": "2021-03-31T01:25:33.144Z"
                },
                "clientStatus": "ACKED"
              }
            ]
          }
        },
        {
          "clusterConfig": {
            "versionInfo": "1617141154495058479",
            "dynamicActiveClusters": [
              {
                "versionInfo": "1617141154495058479",
                "cluster": {
                  "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
                  "name": "fallback-cluster",
                  "type": "EDS"
                },
                "lastUpdated": "2021-03-31T01:25:33.853Z",
                "clientStatus": "ACKED"
              }
            ]
          }
        }
      ]
    }
  ]
}


================================================
FILE: internal/testing/testserver/main.go
================================================
// Testserver mocking the responses of Channelz/CSDS/Health
package main

import (
	"context"
	"crypto/tls"
	"flag"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net"
	"os"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/channelz/service"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/reflection"
	"google.golang.org/grpc/status"
	"google.golang.org/grpc/testdata"

	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	csdspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
	healthpb "google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/protobuf/encoding/protojson"
)

var (
	servingPortFlag     = flag.Int("serving", 10001, "the serving port")
	adminPortFlag       = flag.Int("admin", 50051, "the admin port")
	secureAdminPortFlag = flag.Int("secure_admin", 50052, "the secure admin port")
	healthFlag          = flag.Bool("health", true, "the health checking status")
	qpsFlag             = flag.Int("qps", 10, "The size of the generated load against itself")
	abortPercentageFlag = flag.Int("abort_percentage", 10, "The percentage of failed RPCs")
)

// Prepare the CSDS response
var csdsResponse csdspb.ClientStatusResponse

func init() {
	file, err := os.Open("csds_config_dump.json")
	if err != nil {
		panic(err)
	}
	configDump, err := io.ReadAll(file)
	if err != nil {
		panic(err)
	}
	if err := protojson.Unmarshal([]byte(configDump), &csdsResponse); err != nil {
		panic(err)
	}
}

// Implements the Greeter service
type server struct {
	pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	if int(rand.Int31n(100)) <= *abortPercentageFlag {
		return nil, status.Errorf(codes.Code(rand.Int31n(15)+1), "Fault injected")
	}
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

// Implements the CSDS service
type mockCsdsServer struct {
	csdspb.UnimplementedClientStatusDiscoveryServiceServer
}

func (*mockCsdsServer) FetchClientStatus(ctx context.Context, req *csdspb.ClientStatusRequest) (*csdspb.ClientStatusResponse, error) {
	return &csdsResponse, nil
}

func setupAdminServer(s *grpc.Server) {
	reflection.Register(s)
	service.RegisterChannelzServiceToServer(s)
	csdspb.RegisterClientStatusDiscoveryServiceServer(s, &mockCsdsServer{})
	healthcheck := health.NewServer()
	if *healthFlag {
		healthcheck.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
		healthcheck.SetServingStatus("helloworld.Greeter", healthpb.HealthCheckResponse_SERVING)
	} else {
		healthcheck.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
		healthcheck.SetServingStatus("helloworld.Greeter", healthpb.HealthCheckResponse_NOT_SERVING)
	}
	healthpb.RegisterHealthServer(s, healthcheck)
}

func main() {
	// Parse the flags
	flag.Parse()
	// Creates the primary server
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *servingPortFlag))
	if err != nil {
		panic(err)
	}
	defer lis.Close()
	fmt.Printf("Serving Business Logic on :%d\n", *servingPortFlag)
	cert, err := tls.LoadX509KeyPair(testdata.Path("server1.pem"), testdata.Path("server1.key"))
	if err != nil {
		log.Fatalf("failed to load key pair: %s", err)
	}
	s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert)))
	pb.RegisterGreeterServer(s, &server{})
	go s.Serve(lis)
	defer s.Stop()
	// Creates the admin server without credentials
	insecureListener, err := net.Listen("tcp", fmt.Sprintf(":%d", *adminPortFlag))
	if err != nil {
		panic(err)
	}
	defer insecureListener.Close()
	insecureAdminServer := grpc.NewServer()
	setupAdminServer(insecureAdminServer)
	go insecureAdminServer.Serve(insecureListener)
	defer insecureAdminServer.Stop()
	fmt.Printf("Serving Insecure Admin Services on :%d\n", *adminPortFlag)
	// Creates the admin server with credentials
	secureListener, err := net.Listen("tcp", fmt.Sprintf(":%d", *secureAdminPortFlag))
	if err != nil {
		panic(err)
	}
	defer secureListener.Close()
	secureAdminServer := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert)))
	setupAdminServer(secureAdminServer)
	go secureAdminServer.Serve(secureListener)
	defer secureAdminServer.Stop()
	fmt.Printf("Serving Secure Admin Services on :%d\n", *secureAdminPortFlag)
	// Creates a client to hydrate the primary server
	creds, err := credentials.NewClientTLSFromFile(testdata.Path("ca.pem"), "*.test.youtube.com")
	if err != nil {
		panic(err)
	}
	conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", *servingPortFlag), grpc.WithTransportCredentials(creds))
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	greeterClient := pb.NewGreeterClient(conn)
	for {
		greeterClient.SayHello(context.Background(), &pb.HelloRequest{Name: "world"})
		time.Sleep(time.Second / time.Duration(*qpsFlag))
	}
}


================================================
FILE: main.go
================================================
package main

import (
	cmd "github.com/grpc-ecosystem/grpcdebug/cmd"

	// To parse Any protos, ProtoBuf requires the descriptors of the given message
	// type to present in its descriptor pool. Otherwise, it will fail. Here we
	// preload as much proto descriptors as possible, so the released binaries can
	// have better forward compatibility.
	_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/http/dynamo/v3"
	_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/http/squash/v3"
	_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/client_ssl_auth/v3"
	_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/kafka_broker/v3"
	_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/mysql_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha"
	_ "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/network/rocketmq_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/metrics/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/overload/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/tap/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/config/trace/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/data/cluster/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/data/core/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/data/dns/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/data/tap/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/wasm/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/dynamic_forward_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/dynamic_forward_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/matching/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/tap/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/compressor/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/decompressor/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/compressor/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/decompressor/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/dependency/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/matcher/action/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/adaptive_concurrency/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/admission_control/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_lambda/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_request_signing/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/buffer/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cache/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cdn_loop/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/compressor/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/csrf/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/decompressor/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/dynamic_forward_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_proc/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_http1_bridge/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_http1_reverse_bridge/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_json_transcoder/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_stats/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_web/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gzip/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/header_to_metadata/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/health_check/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ip_tagging/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/kill_request/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/oauth2/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/on_demand/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/original_src/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/tap/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/http_inspector/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_dst/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_src/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/proxy_protocol/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/direct_response/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/dubbo_proxy/router/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/dubbo_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/echo/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ext_authz/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/local_ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/mongo_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/redis_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_cluster/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/router/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/zookeeper_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/dns_filter/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/allow_listed_routes/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/previous_routes/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/internal_redirect/safe_cross_scheme/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/least_request/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/pick_first/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/ring_hash/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/round_robin/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/wrr_locality/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/socket_interface/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/rate_limit_descriptors/expr/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/host/omit_host_metadata/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/host/previous_hosts/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/priority/previous_priorities/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/stat_sinks/wasm/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/alts/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/proxy_protocol/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/raw_buffer/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/starttls/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tap/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/generic/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/http/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/tcp/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/udp/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/watchdog/profile_action/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/event_reporting/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/extension/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/health/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/load_stats/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/metrics/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/service/tap/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/type/metadata/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/type/tracing/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/type/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/watchdog/v3"

	// Add the xDS resolver to allow resolving using a "xds:///" target.
	_ "google.golang.org/grpc/xds"
)

func main() {
	cmd.Execute()
}
Download .txt
gitextract_vgznyx4c/

├── .github/
│   └── workflows/
│       └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── cmd/
│   ├── channelz.go
│   ├── config/
│   │   └── config.go
│   ├── health.go
│   ├── root.go
│   ├── transport/
│   │   └── grpc.go
│   ├── verbose/
│   │   └── verbose.go
│   ├── xds.go
│   └── xds_test.go
├── go.mod
├── go.sum
├── internal/
│   └── testing/
│       ├── ca.pem
│       ├── grpcdebug_config.yaml
│       └── testserver/
│           ├── csds_config_dump.json
│           ├── csds_config_dump_multi_scope.json
│           └── main.go
└── main.go
Download .txt
SYMBOL INDEX (62 symbols across 10 files)

FILE: cmd/channelz.go
  function prettyTime (line 24) | func prettyTime(ts *timestamppb.Timestamp) string {
  function prettyAddress (line 34) | func prettyAddress(addr *zpb.Address) string {
  function printChannelTraceEvents (line 42) | func printChannelTraceEvents(events []*zpb.ChannelTraceEvent) {
  function printSockets (line 63) | func printSockets(sockets []*zpb.Socket) {
  function printObjectAsJSON (line 84) | func printObjectAsJSON(data any) error {
  function printCreationTimestamp (line 93) | func printCreationTimestamp(data *zpb.ChannelData) string {
  function channelzChannelsCommandRunWithError (line 97) | func channelzChannelsCommandRunWithError(cmd *cobra.Command, args []stri...
  function channelzChannelCommandRunWithError (line 132) | func channelzChannelCommandRunWithError(cmd *cobra.Command, args []strin...
  function channelzSubchannelCommandRunWithError (line 190) | func channelzSubchannelCommandRunWithError(cmd *cobra.Command, args []st...
  function channelzSocketCommandRunWithError (line 229) | func channelzSocketCommandRunWithError(cmd *cobra.Command, args []string...
  function channelzServersCommandRunWithError (line 304) | func channelzServersCommandRunWithError(cmd *cobra.Command, args []strin...
  function channelzServerCommandRunWithError (line 339) | func channelzServerCommandRunWithError(cmd *cobra.Command, args []string...
  function init (line 383) | func init() {

FILE: cmd/config/config.go
  type SecurityType (line 15) | type SecurityType
  constant TypeInsecure (line 19) | TypeInsecure SecurityType = "insecure"
  constant TypeTLS (line 22) | TypeTLS = "tls"
  constant grpcdebugServerConfigEnvName (line 26) | grpcdebugServerConfigEnvName = "GRPCDEBUG_CONFIG"
  type ServerConfig (line 29) | type ServerConfig struct
  type grpcdebugConfig (line 36) | type grpcdebugConfig struct
  function loadServerConfigsFromFile (line 40) | func loadServerConfigsFromFile(path string) map[string]ServerConfig {
  function userConfigDir (line 59) | func userConfigDir() (string, error) {
  function loadServerConfigs (line 95) | func loadServerConfigs() map[string]ServerConfig {
  function GetServerConfig (line 113) | func GetServerConfig(target string) ServerConfig {

FILE: cmd/health.go
  function init (line 49) | func init() {

FILE: cmd/root.go
  function initConfig (line 54) | func initConfig() {
  function ChildCommandPath (line 79) | func ChildCommandPath(path string) string {
  function init (line 86) | func init() {
  function Execute (line 99) | func Execute() {

FILE: cmd/transport/grpc.go
  constant rpcTimeout (line 23) | rpcTimeout = time.Second * 15
  function Connect (line 26) | func Connect(c config.ServerConfig) {
  function Channels (line 49) | func Channels(startID, maxResults int64) []*zpb.Channel {
  function Channel (line 60) | func Channel(channelID int64) *zpb.Channel {
  function Subchannel (line 71) | func Subchannel(subchannelID int64) *zpb.Subchannel {
  function Servers (line 82) | func Servers(startID, maxResults int64) []*zpb.Server {
  function Server (line 93) | func Server(serverID int64) *zpb.Server {
  function Socket (line 104) | func Socket(socketID int64) *zpb.Socket {
  function ServerSocket (line 115) | func ServerSocket(serverID, startID, maxResults int64) []*zpb.Socket {
  function FetchClientStatus (line 137) | func FetchClientStatus() *csdspb.ClientStatusResponse {
  function GetHealthStatus (line 148) | func GetHealthStatus(service string) string {

FILE: cmd/verbose/verbose.go
  function EnableDebugOutput (line 8) | func EnableDebugOutput() {
  function Debugf (line 13) | func Debugf(format string, v ...any) {

FILE: cmd/xds.go
  function printProtoBufMessageAsJSON (line 26) | func printProtoBufMessageAsJSON(m proto.Message) error {
  function priorityPerXdsConfig (line 41) | func priorityPerXdsConfig(x *csdspb.PerXdsConfig) int {
  function sortPerXdsConfigs (line 56) | func sortPerXdsConfigs(clientStatus *csdspb.ClientStatusResponse) {
  function filterConfigsByScope (line 65) | func filterConfigsByScope(all []*csdspb.ClientConfig, scope string) ([]*...
  function xdsConfigCommandRunWithError (line 81) | func xdsConfigCommandRunWithError(cmd *cobra.Command, args []string) err...
  type xdsResourceStatusEntry (line 190) | type xdsResourceStatusEntry struct
  function printStatusEntry (line 199) | func printStatusEntry(entry *xdsResourceStatusEntry, includeScope bool) {
  function xdsStatusCommandRunWithError (line 222) | func xdsStatusCommandRunWithError(cmd *cobra.Command, args []string) err...
  function init (line 339) | func init() {

FILE: cmd/xds_test.go
  function TestFilterConfigsByScope (line 10) | func TestFilterConfigsByScope(t *testing.T) {
  function TestSortPerXdsConfigs (line 117) | func TestSortPerXdsConfigs(t *testing.T) {
  function TestSortPerXdsConfigsEmptyConfigs (line 162) | func TestSortPerXdsConfigsEmptyConfigs(t *testing.T) {
  function TestSortPerXdsConfigsSingleConfig (line 171) | func TestSortPerXdsConfigsSingleConfig(t *testing.T) {

FILE: internal/testing/testserver/main.go
  function init (line 46) | func init() {
  type server (line 61) | type server struct
    method SayHello (line 65) | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*...
  type mockCsdsServer (line 73) | type mockCsdsServer struct
    method FetchClientStatus (line 77) | func (*mockCsdsServer) FetchClientStatus(ctx context.Context, req *csd...
  function setupAdminServer (line 81) | func setupAdminServer(s *grpc.Server) {
  function main (line 96) | func main() {

FILE: main.go
  function main (line 171) | func main() {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
  {
    "path": ".github/workflows/release.yml",
    "chars": 1387,
    "preview": "name: Release\n\non:\n  release:\n    types: [published]\n\njobs:\n  release:\n    name: Release grpcdebug\n    runs-on: ubuntu-l"
  },
  {
    "path": ".gitignore",
    "chars": 269,
    "preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 19916,
    "preview": "# grpcdebug\n[![Go Report\nCard](https://goreportcard.com/badge/github.com/grpc-ecosystem/grpcdebug)](https://goreportcard"
  },
  {
    "path": "cmd/channelz.go",
    "chars": 14356,
    "preview": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/g"
  },
  {
    "path": "cmd/config/config.go",
    "chars": 3172,
    "preview": "package config\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\n\t\"github.com/grpc-ecosystem/grpcdebug/cmd/verbose\"\n\t\""
  },
  {
    "path": "cmd/health.go",
    "chars": 1057,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/grpc-ecosystem/grpcdebug/cmd/transport\"\n\t\"github.com/spf13/cobra\"\n)\n\n"
  },
  {
    "path": "cmd/root.go",
    "chars": 3405,
    "preview": "// Defines the root command and global flags\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"text/tabwriter\"\n\n\t\"github.com/"
  },
  {
    "path": "cmd/transport/grpc.go",
    "chars": 5274,
    "preview": "package transport\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\tcsdspb \"github.com/envoyproxy/go-control-plane/envoy/service/sta"
  },
  {
    "path": "cmd/verbose/verbose.go",
    "chars": 304,
    "preview": "package verbose\n\nimport \"log\"\n\nvar enableDebugOutput = false\n\n// EnableDebugOutput enables debugging output\nfunc EnableD"
  },
  {
    "path": "cmd/xds.go",
    "chars": 9467,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/grpc-ecosystem/grpcdebug/cmd/transport\"\n\n\tadminpb \"github."
  },
  {
    "path": "cmd/xds_test.go",
    "chars": 5161,
    "preview": "package cmd\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tcsdspb \"github.com/envoyproxy/go-control-plane/envoy/service/status/v3\"\n)\n"
  },
  {
    "path": "go.mod",
    "chars": 1571,
    "preview": "module github.com/grpc-ecosystem/grpcdebug\n\ngo 1.23.0\n\nrequire (\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/envoy"
  },
  {
    "path": "go.sum",
    "chars": 8255,
    "preview": "cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=\ncel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxx"
  },
  {
    "path": "internal/testing/ca.pem",
    "chars": 855,
    "preview": "-----BEGIN CERTIFICATE-----\nMIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIEwpTb21"
  },
  {
    "path": "internal/testing/grpcdebug_config.yaml",
    "chars": 362,
    "preview": "servers:\n  dev:\n    real_address: localhost:50051\n    security: insecure\n  prod:\n    real_address: \"localhost:50052\"\n   "
  },
  {
    "path": "internal/testing/testserver/csds_config_dump.json",
    "chars": 7396,
    "preview": "{\n  \"config\": [\n    {\n      \"node\": {\n        \"id\": \"projects/1040920224690/networks/default/nodes/5cc9170c-d5b4-4061-b4"
  },
  {
    "path": "internal/testing/testserver/csds_config_dump_multi_scope.json",
    "chars": 2980,
    "preview": "{\n  \"config\": [\n    {\n      \"node\": {\n        \"id\": \"projects/1040920224690/networks/default/nodes/primary-node\",\n      "
  },
  {
    "path": "internal/testing/testserver/main.go",
    "chars": 5086,
    "preview": "// Testserver mocking the responses of Channelz/CSDS/Health\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"flag\"\n\t\"fm"
  },
  {
    "path": "main.go",
    "chars": 13658,
    "preview": "package main\n\nimport (\n\tcmd \"github.com/grpc-ecosystem/grpcdebug/cmd\"\n\n\t// To parse Any protos, ProtoBuf requires the de"
  }
]

About this extraction

This page contains the full source code of the grpc-ecosystem/grpcdebug GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (112.6 KB), approximately 32.7k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!