Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented JSON distributed trace converter #335

Merged
merged 4 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions v3/newrelic/trace_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"testing"

Expand Down Expand Up @@ -46,6 +47,47 @@ type TraceContextTestCase struct {
} `json:"intrinsics"`
}

func TestJSONDTHeaders(t *testing.T) {
type testcase struct {
in string
out http.Header
err bool
}

for i, test := range []testcase{
{"", http.Header{}, false},
{"{}", http.Header{}, false},
{" invalid ", http.Header{}, true},
{`"foo"`, http.Header{}, true},
{`{"foo": "bar"}`, http.Header{
"Foo": {"bar"},
}, false},
{`{
"foo": "bar",
"baz": "quux",
"multiple": [
"alpha",
"beta",
"gamma"
]
}`, http.Header{
"Foo": {"bar"},
"Baz": {"quux"},
"Multiple": {"alpha", "beta", "gamma"},
}, false},
} {
h, err := DistributedTraceHeadersFromJSON(test.in)

if err != nil {
if !test.err {
t.Errorf("case %d: %v: error expected but not generated", i, test.in)
}
} else if !reflect.DeepEqual(test.out, h) {
t.Errorf("case %d, %v -> %v but expected %v", i, test.in, h, test.out)
}
}
}

func TestCrossAgentW3CTraceContext(t *testing.T) {
var tcs []TraceContextTestCase

Expand Down
89 changes: 89 additions & 0 deletions v3/newrelic/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package newrelic

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -269,6 +271,93 @@ func (txn *Transaction) AcceptDistributedTraceHeaders(t TransportType, hdrs http
txn.thread.logAPIError(txn.thread.AcceptDistributedTraceHeaders(t, hdrs), "accept trace payload", nil)
}

//
// AcceptDistributedTraceHeadersFromJSON works just like AcceptDistributedTraceHeaders(), except
// that it takes the header data as a JSON string à la DistributedTraceHeadersFromJSON(). Additionally
// (unlike AcceptDistributedTraceHeaders()) it returns an error if it was unable to successfully
// convert the JSON string to http headers. There is no guarantee that the header data found in JSON
// is correct beyond conforming to the expected types and syntax.
//
func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, jsondata string) error {
hdrs, err := DistributedTraceHeadersFromJSON(jsondata)
if err != nil {
return err
}
txn.AcceptDistributedTraceHeaders(t, hdrs)
return nil
}

//
// DistributedTraceHeadersFromJSON takes a set of distributed trace headers as a JSON-encoded string
// and emits a http.Header value suitable for passing on to the
// txn.AcceptDistributedTraceHeaders() function.
//
// This is a convenience function provided for cases where you receive the trace header data
// already as a JSON string and want to avoid manually converting that to an http.Header.
// It helps facilitate handling of headers passed to your Go application from components written in other
// languages which may natively handle these header values as JSON strings.
//
// For example, given the input string
// `{"traceparent": "frob", "tracestate": "blorfl", "newrelic": "xyzzy"}`
// This will emit an http.Header value with headers "traceparent", "tracestate", and "newrelic".
// Specifically:
// http.Header{
// "Traceparent": {"frob"},
// "Tracestate": {"blorfl"},
// "Newrelic": {"xyzzy"},
// }
//
// The JSON string must be a single object whose values may be strings or arrays of strings.
// These are translated directly to http headers with singleton or multiple values.
// In the case of multiple string values, these are translated to a multi-value HTTP
// header. For example:
// `{"traceparent": "12345", "colors": ["red", "green", "blue"]}`
// which produces
// http.Header{
// "Traceparent": {"12345"},
// "Colors": {"red", "green", "blue"},
// }
// (Note that the HTTP headers are capitalized.)
//
func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err error) {
var raw interface{}
hdrs = http.Header{}
if jsondata == "" {
RichVanderwal marked this conversation as resolved.
Show resolved Hide resolved
return
}
err = json.Unmarshal([]byte(jsondata), &raw)
if err != nil {
return
}

switch d := raw.(type) {
case map[string]interface{}:
for k, v := range d {
switch hval := v.(type) {
case string:
hdrs.Set(k, hval)
case []interface{}:
for _, subval := range hval {
switch sval := subval.(type) {
case string:
hdrs.Add(k, sval)
default:
err = fmt.Errorf("the JSON object must have only strings or arrays of strings")
return
}
}
default:
err = fmt.Errorf("the JSON object must have only strings or arrays of strings")
return
}
}
default:
err = fmt.Errorf("the JSON string must consist of only a single object")
return
}
return
}

// Application returns the Application which started the transaction.
func (txn *Transaction) Application() *Application {
if nil == txn {
Expand Down