Skip to content

Commit

Permalink
Add reflection support (#68)
Browse files Browse the repository at this point in the history
* [skip ci] reflection support WIP

* [skip ci] fix test and move the test server to internal

* [skip ci] test GetMethodDescFromReflect

* [skip ci] reflection run test

* [skip ci] remove println

* [skip ci] more reflect tests

* [skip ci] fix spelling

* [skip ci] reflect metadata

* [skip ci] reflect metadata usage and docs

* Update influx output to include protoset and account for reflection
  • Loading branch information
bojand committed Feb 5, 2019
1 parent 0215b2c commit 063c563
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 235 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -32,6 +32,7 @@ Options:
-call A fully-qualified method name in 'package.Service/method' or 'package.Service.Method' format.
-i Comma separated list of proto import paths. The current working directory and the directory
of the protocol buffer file are automatically added to the import list.
-rmd Reflect metadata as stringified JSON used only for reflection request.

-cacert File containing trusted root certificates for verifying the server.
-cert File containing client certificate (public key), to present to the server. Must also provide -key option.
Expand Down
65 changes: 33 additions & 32 deletions cmd/ghz/config.go
Expand Up @@ -31,38 +31,39 @@ func (d Duration) String() string {

// Config for the run.
type config struct {
Proto string `json:"proto" toml:"proto" yaml:"proto"`
Protoset string `json:"protoset" toml:"protoset" yaml:"protoset"`
Call string `json:"call" toml:"call" yaml:"call" required:"true"`
RootCert string `json:"cacert" toml:"cacert" yaml:"cacert"`
Cert string `json:"cert" toml:"cert" yaml:"cert"`
Key string `json:"key" toml:"key" yaml:"key"`
SkipTLSVerify bool `json:"skipTLS" toml:"skipTLS" yaml:"skipTLS"`
CName string `json:"cname" toml:"cname" yaml:"cname"`
Authority string `json:"authority" toml:"authority" yaml:"authority"`
Insecure bool `json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty"`
N uint `json:"n" toml:"n" yaml:"n" default:"200"`
C uint `json:"c" toml:"c" yaml:"c" default:"50"`
QPS uint `json:"q" toml:"q" yaml:"q"`
Z Duration `json:"z" toml:"z" yaml:"z"`
X Duration `json:"x" toml:"x" yaml:"x"`
Timeout uint `json:"t" toml:"t" yaml:"t" default:"20"`
Data interface{} `json:"d,omitempty" toml:"d,omitempty" yaml:"d,omitempty"`
DataPath string `json:"D" toml:"D" yaml:"D"`
BinData []byte `json:"-" toml:"-" yaml:"-"`
BinDataPath string `json:"B" toml:"B" yaml:"B"`
Metadata *map[string]string `json:"m,omitempty" toml:"m,omitempty" yaml:"m,omitempty"`
MetadataPath string `json:"M" toml:"M" yaml:"M"`
SI Duration `json:"si" toml:"si" yaml:"si"`
Output string `json:"o" toml:"o" yaml:"o"`
Format string `json:"O" toml:"O" yaml:"O"`
Host string `json:"host" toml:"host" yaml:"host"`
DialTimeout uint `json:"T" toml:"T" yaml:"T" default:"10"`
KeepaliveTime uint `json:"L" toml:"L" yaml:"L"`
CPUs uint `json:"cpus" toml:"cpus" yaml:"cpus"`
ImportPaths []string `json:"i,omitempty" toml:"i,omitempty" yaml:"i,omitempty"`
Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"`
Tags *map[string]string `json:"tags,omitempty" toml:"tags,omitempty" yaml:"tags,omitempty"`
Proto string `json:"proto" toml:"proto" yaml:"proto"`
Protoset string `json:"protoset" toml:"protoset" yaml:"protoset"`
Call string `json:"call" toml:"call" yaml:"call" required:"true"`
RootCert string `json:"cacert" toml:"cacert" yaml:"cacert"`
Cert string `json:"cert" toml:"cert" yaml:"cert"`
Key string `json:"key" toml:"key" yaml:"key"`
SkipTLSVerify bool `json:"skipTLS" toml:"skipTLS" yaml:"skipTLS"`
CName string `json:"cname" toml:"cname" yaml:"cname"`
Authority string `json:"authority" toml:"authority" yaml:"authority"`
Insecure bool `json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty"`
N uint `json:"n" toml:"n" yaml:"n" default:"200"`
C uint `json:"c" toml:"c" yaml:"c" default:"50"`
QPS uint `json:"q" toml:"q" yaml:"q"`
Z Duration `json:"z" toml:"z" yaml:"z"`
X Duration `json:"x" toml:"x" yaml:"x"`
Timeout uint `json:"t" toml:"t" yaml:"t" default:"20"`
Data interface{} `json:"d,omitempty" toml:"d,omitempty" yaml:"d,omitempty"`
DataPath string `json:"D" toml:"D" yaml:"D"`
BinData []byte `json:"-" toml:"-" yaml:"-"`
BinDataPath string `json:"B" toml:"B" yaml:"B"`
Metadata *map[string]string `json:"m,omitempty" toml:"m,omitempty" yaml:"m,omitempty"`
MetadataPath string `json:"M" toml:"M" yaml:"M"`
SI Duration `json:"si" toml:"si" yaml:"si"`
Output string `json:"o" toml:"o" yaml:"o"`
Format string `json:"O" toml:"O" yaml:"O"`
Host string `json:"host" toml:"host" yaml:"host"`
DialTimeout uint `json:"T" toml:"T" yaml:"T" default:"10"`
KeepaliveTime uint `json:"L" toml:"L" yaml:"L"`
CPUs uint `json:"cpus" toml:"cpus" yaml:"cpus"`
ImportPaths []string `json:"i,omitempty" toml:"i,omitempty" yaml:"i,omitempty"`
Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"`
Tags *map[string]string `json:"tags,omitempty" toml:"tags,omitempty" yaml:"tags,omitempty"`
ReflectMetadata *map[string]string `json:"rmd,omitempty" toml:"rmd,omitempty" yaml:"rmd,omitempty"`
}

// UnmarshalJSON is our custom implementation to handle the Duration fields
Expand Down
78 changes: 45 additions & 33 deletions cmd/ghz/main.go
Expand Up @@ -49,6 +49,7 @@ var (
md = flag.String("m", "", "Request metadata as stringified JSON.")
mdPath = flag.String("M", "", "File path for call metadata JSON file. Examples: /home/user/metadata.json or ./metadata.json.")
si = flag.Duration("si", 0, "Interval for stream requests between message sends.")
rmd = flag.String("rmd", "", "Reflect metadata as stringified JSON used only for reflection request.")

output = flag.String("o", "", "Output path. If none provided stdout is used.")
format = flag.String("O", "", "Output format. If none provided, a summary is printed.")
Expand All @@ -74,6 +75,7 @@ Options:
-call A fully-qualified method name in 'package.Service/Method' or 'package.Service.Method' format.
-i Comma separated list of proto import paths. The current working directory and the directory
of the protocol buffer file are automatically added to the import list.
-rmd Reflect metadata as stringified JSON used only for reflection request.
-cacert File containing trusted root certificates for verifying the server.
-cert File containing client certificate (public key), to present to the server. Must also provide -key option.
Expand Down Expand Up @@ -192,6 +194,7 @@ func main() {
runner.WithMetadata(cfg.Metadata),
runner.WithTags(cfg.Tags),
runner.WithStreamInterval(time.Duration(cfg.SI)),
runner.WithReflectionMetadata(cfg.ReflectMetadata),
)

if strings.TrimSpace(cfg.MetadataPath) != "" {
Expand Down Expand Up @@ -292,43 +295,52 @@ func createConfigFromArgs() (*config, error) {
*tags = strings.TrimSpace(*tags)
if *tags != "" {
if err := json.Unmarshal([]byte(*tags), &tagsMap); err != nil {
return nil, fmt.Errorf("Error unmarshaling tags '%v': %v", *md, err.Error())
return nil, fmt.Errorf("Error unmarshaling tags '%v': %v", *tags, err.Error())
}
}

var rmdMap map[string]string
*rmd = strings.TrimSpace(*rmd)
if *rmd != "" {
if err := json.Unmarshal([]byte(*rmd), &rmdMap); err != nil {
return nil, fmt.Errorf("Error unmarshaling reflection metadata '%v': %v", *rmd, err.Error())
}
}

cfg := &config{
Host: host,
Proto: *proto,
Protoset: *protoset,
Call: *call,
RootCert: *cacert,
Cert: *cert,
Key: *key,
SkipTLSVerify: *skipVerify,
Insecure: *insecure,
Authority: *authority,
CName: *cname,
N: *n,
C: *c,
QPS: *q,
Z: Duration(*z),
X: Duration(*x),
Timeout: *t,
Data: dataObj,
DataPath: *dataPath,
BinData: binaryData,
BinDataPath: *binPath,
Metadata: &metadata,
MetadataPath: *mdPath,
SI: Duration(*si),
Output: *output,
Format: *format,
ImportPaths: iPaths,
DialTimeout: *ct,
KeepaliveTime: *kt,
CPUs: *cpus,
Name: *name,
Tags: &tagsMap,
Host: host,
Proto: *proto,
Protoset: *protoset,
Call: *call,
RootCert: *cacert,
Cert: *cert,
Key: *key,
SkipTLSVerify: *skipVerify,
Insecure: *insecure,
Authority: *authority,
CName: *cname,
N: *n,
C: *c,
QPS: *q,
Z: Duration(*z),
X: Duration(*x),
Timeout: *t,
Data: dataObj,
DataPath: *dataPath,
BinData: binaryData,
BinDataPath: *binPath,
Metadata: &metadata,
MetadataPath: *mdPath,
SI: Duration(*si),
Output: *output,
Format: *format,
ImportPaths: iPaths,
DialTimeout: *ct,
KeepaliveTime: *kt,
CPUs: *cpus,
Name: *name,
Tags: &tagsMap,
ReflectMetadata: &rmdMap,
}

return cfg, nil
Expand Down
19 changes: 12 additions & 7 deletions runner/common_test.go → internal/common.go
@@ -1,19 +1,24 @@
package runner
package internal

import (
"net"
"strconv"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"

"github.com/bojand/ghz/internal/helloworld"
)

var port string
var localhost string
// TestPort is the port
var TestPort string

func startServer(secure bool) (*helloworld.Greeter, *grpc.Server, error) {
// TestLocalhost is the localhost
var TestLocalhost string

// StartServer starts server
func StartServer(secure bool) (*helloworld.Greeter, *grpc.Server, error) {
lis, err := net.Listen("tcp", ":0")
if err != nil {
return nil, nil, err
Expand All @@ -33,10 +38,10 @@ func startServer(secure bool) (*helloworld.Greeter, *grpc.Server, error) {

gs := helloworld.NewGreeter()
helloworld.RegisterGreeterServer(s, gs)
// reflection.Register(s)
reflection.Register(s)

port = strconv.Itoa(lis.Addr().(*net.TCPAddr).Port)
localhost = "localhost:" + port
TestPort = strconv.Itoa(lis.Addr().(*net.TCPAddr).Port)
TestLocalhost = "localhost:" + TestPort

go func() {
s.Serve(lis)
Expand Down
7 changes: 6 additions & 1 deletion printer/printer.go
Expand Up @@ -125,7 +125,12 @@ func (rp *ReportPrinter) getInfluxTags(addErrors bool) string {

options := rp.Report.Options

s = append(s, fmt.Sprintf(`proto="%v"`, options.Proto))
if options.Proto != "" {
s = append(s, fmt.Sprintf(`proto="%v"`, options.Proto))
} else if options.Protoset != "" {
s = append(s, fmt.Sprintf(`Protoset="%v"`, options.Protoset))
}

s = append(s, fmt.Sprintf(`call="%v"`, options.Call))
s = append(s, fmt.Sprintf(`host="%v"`, options.Host))
s = append(s, fmt.Sprintf("n=%v", options.N))
Expand Down
2 changes: 1 addition & 1 deletion printer/printer_test.go
Expand Up @@ -22,7 +22,7 @@ func TestPrinter_getInfluxLine(t *testing.T) {
{
"empty",
runner.Report{},
`ghz_run,proto="",call="",host="",n=0,c=0,qps=0,z=0,timeout=0,dial_timeout=0,keepalive=0,data="null",metadata="",tags="",errors=0,has_errors=false count=0,total=0,average=0,fastest=0,slowest=0,rps=0.00,errors=0 0`,
`ghz_run,call="",host="",n=0,c=0,qps=0,z=0,timeout=0,dial_timeout=0,keepalive=0,data="null",metadata="",tags="",errors=0,has_errors=false count=0,total=0,average=0,fastest=0,slowest=0,rps=0.00,errors=0 0`,
},
{
"basic",
Expand Down
27 changes: 27 additions & 0 deletions protodesc/protodesc.go
Expand Up @@ -11,6 +11,9 @@ import (
"github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/desc/protoparse"
"github.com/jhump/protoreflect/grpcreflect"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

var errNoMethodNameSpecified = errors.New("no method name specified")
Expand Down Expand Up @@ -66,6 +69,20 @@ func GetMethodDescFromProtoSet(call, protoset string) (*desc.MethodDescriptor, e
return getMethodDesc(call, resolved)
}

// GetMethodDescFromReflect gets method descriptor for the call from reflection using client
func GetMethodDescFromReflect(call string, client *grpcreflect.Client) (*desc.MethodDescriptor, error) {
call = strings.Replace(call, "/", ".", -1)
file, err := client.FileContainingSymbol(call)
if err != nil || file == nil {
return nil, reflectionSupport(err)
}

files := map[string]*desc.FileDescriptor{}
files[file.GetName()] = file

return getMethodDesc(call, files)
}

func getMethodDesc(call string, files map[string]*desc.FileDescriptor) (*desc.MethodDescriptor, error) {
svc, mth, err := parseServiceMethod(call)
if err != nil {
Expand Down Expand Up @@ -162,3 +179,13 @@ func parseServiceMethod(svcAndMethod string) (string, string, error) {
func newInvalidMethodNameError(svcAndMethod string) error {
return fmt.Errorf("method name must be package.Service.Method or package.Service/Method: %q", svcAndMethod)
}

func reflectionSupport(err error) error {
if err == nil {
return nil
}
if stat, ok := status.FromError(err); ok && stat.Code() == codes.Unimplemented {
return errors.New("server does not support the reflection API")
}
return err
}
75 changes: 75 additions & 0 deletions protodesc/protodesc_test.go
@@ -1,9 +1,16 @@
package protodesc

import (
"context"
"fmt"
"testing"

"github.com/bojand/ghz/internal"
"github.com/jhump/protoreflect/grpcreflect"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
)

func TestProtodesc_GetMethodDescFromProto(t *testing.T) {
Expand Down Expand Up @@ -113,3 +120,71 @@ func testParseServiceMethodError(t *testing.T, svcAndMethod string) {
_, _, err := parseServiceMethod(svcAndMethod)
assert.Error(t, err)
}

func TestProtodesc_GetMethodDescFromReflect(t *testing.T) {

_, s, err := internal.StartServer(false)

if err != nil {
assert.FailNow(t, err.Error())
}

defer s.Stop()

t.Run("test known call", func(t *testing.T) {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
ctx := context.Background()
conn, err := grpc.DialContext(ctx, internal.TestLocalhost, opts...)
assert.NoError(t, err)

md := make(metadata.MD)

refCtx := metadata.NewOutgoingContext(ctx, md)

refClient := grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(conn))

mtd, err := GetMethodDescFromReflect("helloworld.Greeter.SayHello", refClient)
fmt.Println(mtd)
assert.NoError(t, err)
assert.NotNil(t, mtd)
assert.Equal(t, "SayHello", mtd.GetName())
})

t.Run("test known call with /", func(t *testing.T) {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
ctx := context.Background()
conn, err := grpc.DialContext(ctx, internal.TestLocalhost, opts...)
assert.NoError(t, err)

md := make(metadata.MD)

refCtx := metadata.NewOutgoingContext(ctx, md)

refClient := grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(conn))

mtd, err := GetMethodDescFromReflect("helloworld.Greeter/SayHello", refClient)
assert.NoError(t, err)
assert.NotNil(t, mtd)
assert.Equal(t, "SayHello", mtd.GetName())
})

t.Run("test unknown known call", func(t *testing.T) {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
ctx := context.Background()
conn, err := grpc.DialContext(ctx, internal.TestLocalhost, opts...)
assert.NoError(t, err)

md := make(metadata.MD)

refCtx := metadata.NewOutgoingContext(ctx, md)

refClient := grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(conn))

mtd, err := GetMethodDescFromReflect("helloworld.Greeter/SayHelloAsdf", refClient)
assert.Error(t, err)
assert.Nil(t, mtd)
})
}

0 comments on commit 063c563

Please sign in to comment.