Skip to content

Commit

Permalink
Add support for call template data. Addresses #20. (#21)
Browse files Browse the repository at this point in the history
* Initial commit for adding template data and metadata support

* do not error when failing to execute the template

* add timestamps

* add call template data tests and update readme

* improve comment
  • Loading branch information
bojand committed Jul 20, 2018
1 parent 6b6d32c commit bfc217c
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 32 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ Options:

Alternatively all settings can be set via `ghz.json` file if present in the same path as the `ghz` executable. A custom configuration file can be specified using `-config` option.

## Call Template Data

Data and metadata can specify [template actions](https://golang.org/pkg/text/template/) that will be parsed and evaluated at every request. Each request gets a new instance of the data. The available variables / actions are:

```go
// call template data
type callTemplateData struct {
RequestNumber int64 // unique incrememnted request number for each request
FullyQualifiedName string // fully-qualified name of the method call
MethodName string // shorter call method name
ServiceName string // the service name
InputName string // name of the input message type
OutputName string // name of the output message type
IsClientStreaming bool // whether this call is client streaming
IsServerStreaming bool // whether this call is server streaming
Timestamp string // timestamp of the call in RFC3339 format
TimestampUnix int64 // timestamp of the call as unix time
}
```

This can be useful to inject variable information into the data or metadata payload for each request, such as timestamp or unique request number. See examples below.

## Examples

A simple unary call:
Expand All @@ -72,6 +94,12 @@ A simple unary call:
ghz -proto ./greeter.proto -call helloworld.Greeter.SayHello -d '{"name":"Joe"}' 0.0.0.0:50051
```

A simple unary call with metadata using template actions:

```sh
ghz -proto ./greeter.proto -call helloworld.Greeter.SayHello -d '{"name":"Joe"}' -m '{"trace_id":"{{.RequestNumber}}","timestamp":"{{.TimestampUnix}}"}' 0.0.0.0:50051
```

Custom number of requests and concurrency:

```sh
Expand Down Expand Up @@ -119,11 +147,17 @@ Example `ghz.json`
"d": {
"name": "Joe"
},
"m": {
"foo": "bar",
"trace_id": "{{.RequestNumber}}",
"timestamp": "{{.TimestampUnix}}"
},
"i": [
"/path/to/protos"
],
"n": 4000,
"c": 40,
"x": "10s",
"host": "0.0.0.0:50051"
}
```
Expand Down
81 changes: 81 additions & 0 deletions call_template_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package ghz

import (
"bytes"
"encoding/json"
"text/template"
"time"

"github.com/jhump/protoreflect/desc"
)

// call template data
type callTemplateData struct {
RequestNumber int64 // unique incrememnted request number for each request
FullyQualifiedName string // fully-qualified name of the method call
MethodName string // shorter call method name
ServiceName string // the service name
InputName string // name of the input message type
OutputName string // name of the output message type
IsClientStreaming bool // whether this call is client streaming
IsServerStreaming bool // whether this call is server streaming
Timestamp string // timestamp of the call in RFC3339 format
TimestampUnix int64 // timestamp of the call as unix time
}

// newCallTemplateData returns new call template data
func newCallTemplateData(mtd *desc.MethodDescriptor, reqNum int64) *callTemplateData {
now := time.Now()

return &callTemplateData{
RequestNumber: reqNum,
FullyQualifiedName: mtd.GetFullyQualifiedName(),
MethodName: mtd.GetName(),
ServiceName: mtd.GetService().GetName(),
InputName: mtd.GetInputType().GetName(),
OutputName: mtd.GetOutputType().GetName(),
IsClientStreaming: mtd.IsClientStreaming(),
IsServerStreaming: mtd.IsServerStreaming(),
Timestamp: now.Format(time.RFC3339),
TimestampUnix: now.Unix(),
}
}

func (td *callTemplateData) execute(data string) (*bytes.Buffer, error) {
t := template.Must(template.New("call_template_data").Parse(data))
var tpl bytes.Buffer
err := t.Execute(&tpl, td)
return &tpl, err
}

func (td *callTemplateData) executeData(data string) (interface{}, error) {
input := []byte(data)
tpl, err := td.execute(data)
if err == nil {
input = tpl.Bytes()
}

var dataMap interface{}
err = json.Unmarshal(input, &dataMap)
if err != nil {
return nil, err
}

return dataMap, nil
}

func (td *callTemplateData) executeMetadata(metadata string) (*map[string]string, error) {
input := []byte(metadata)
tpl, err := td.execute(metadata)
if err == nil {
input = tpl.Bytes()
}

var mdMap map[string]string
err = json.Unmarshal(input, &mdMap)
if err != nil {
return nil, err
}

return &mdMap, nil
}
120 changes: 120 additions & 0 deletions call_template_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package ghz

import (
"testing"

"github.com/bojand/ghz/protodesc"
"github.com/stretchr/testify/assert"
)

func TestCallTemplateData_New(t *testing.T) {
md, err := protodesc.GetMethodDescFromProto("helloworld.Greeter/SayHello", "./testdata/greeter.proto", []string{})
assert.NoError(t, err)
assert.NotNil(t, md)

ctd := newCallTemplateData(md, 100)

assert.NotNil(t, ctd)
assert.Equal(t, int64(100), ctd.RequestNumber)
assert.Equal(t, "helloworld.Greeter.SayHello", ctd.FullyQualifiedName)
assert.Equal(t, "SayHello", ctd.MethodName)
assert.Equal(t, "Greeter", ctd.ServiceName)
assert.Equal(t, "HelloRequest", ctd.InputName)
assert.Equal(t, "HelloReply", ctd.OutputName)
assert.Equal(t, false, ctd.IsClientStreaming)
assert.Equal(t, false, ctd.IsServerStreaming)
assert.NotEmpty(t, ctd.Timestamp)
assert.NotZero(t, ctd.TimestampUnix)
}

func TestCallTemplateData_ExecuteData(t *testing.T) {
md, err := protodesc.GetMethodDescFromProto("helloworld.Greeter/SayHello", "./testdata/greeter.proto", []string{})
assert.NoError(t, err)
assert.NotNil(t, md)

ctd := newCallTemplateData(md, 200)

assert.NotNil(t, ctd)

var tests = []struct {
name string
in string
expected interface{}
expectError bool
}{
{"no template",
`{"name":"bob"}`,
map[string]interface{}{"name": "bob"},
false,
},
{"with template",
`{"name":"{{.RequestNumber}} bob {{.FullyQualifiedName}} {{.MethodName}} {{.ServiceName}} {{.InputName}} {{.OutputName}} {{.IsClientStreaming}} {{.IsServerStreaming}}"}`,
map[string]interface{}{"name": "200 bob helloworld.Greeter.SayHello SayHello Greeter HelloRequest HelloReply false false"},
false,
},
{"with unknown action",
`{"name":"asdf {{.Something}} {{.MethodName}} bob"}`,
map[string]interface{}{"name": "asdf {{.Something}} {{.MethodName}} bob"},
false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, err := ctd.executeData(tt.in)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}

assert.Equal(t, tt.expected, r)
})
}
}

func TestCallTemplateData_ExecuteMetadata(t *testing.T) {
md, err := protodesc.GetMethodDescFromProto("helloworld.Greeter/SayHello", "./testdata/greeter.proto", []string{})
assert.NoError(t, err)
assert.NotNil(t, md)

ctd := newCallTemplateData(md, 200)

assert.NotNil(t, ctd)

var tests = []struct {
name string
in string
expected interface{}
expectError bool
}{
{"no template",
`{"trace_id":"asdf"}`,
&map[string]string{"trace_id": "asdf"},
false,
},
{"with template",
`{"trace_id":"{{.RequestNumber}} asdf {{.FullyQualifiedName}} {{.MethodName}} {{.ServiceName}} {{.InputName}} {{.OutputName}} {{.IsClientStreaming}} {{.IsServerStreaming}}"}`,
&map[string]string{"trace_id": "200 asdf helloworld.Greeter.SayHello SayHello Greeter HelloRequest HelloReply false false"},
false,
},
{"with unknown action",
`{"trace_id":"asdf {{.Something}} {{.MethodName}} bob"}`,
&map[string]string{"trace_id": "asdf {{.Something}} {{.MethodName}} bob"},
false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, err := ctd.executeMetadata(tt.in)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}

assert.Equal(t, tt.expected, r)
})
}
}

0 comments on commit bfc217c

Please sign in to comment.