From bb7fe4f155cf1e769209c8ca8b020233f7a0b384 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Wed, 30 Jun 2021 14:22:35 -0700 Subject: [PATCH 1/4] implemented JSON distributed trace converter --- v3/go.mod | 4 +- v3/newrelic/trace_context_test.go | 42 ++++++++++++++++++ v3/newrelic/transaction.go | 72 +++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 2ada3801e..92bc2eef5 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -3,6 +3,6 @@ module github.com/newrelic/go-agent/v3 go 1.7 require ( - github.com/golang/protobuf v1.3.3 - google.golang.org/grpc v1.27.0 + github.com/golang/protobuf v1.4.3 + google.golang.org/grpc v1.39.0 ) diff --git a/v3/newrelic/trace_context_test.go b/v3/newrelic/trace_context_test.go index 3aeccc10a..7dc6dd9cb 100644 --- a/v3/newrelic/trace_context_test.go +++ b/v3/newrelic/trace_context_test.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net/http" + "reflect" "strings" "testing" @@ -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"}`, map[string][]string{ + "Foo": {"bar"}, + }, false}, + {`{ + "foo": "bar", + "baz": "quux", + "multiple": [ + "alpha", + "beta", + "gamma" + ] + }`, map[string][]string{ + "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 diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index 60c3e9cee..bbc5b9a49 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -4,6 +4,8 @@ package newrelic import ( + "encoding/json" + "fmt" "net/http" "net/url" "strings" @@ -269,6 +271,76 @@ 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. +// +// 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". +// +// 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. +// +// 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. +// +func DistributedTraceHeadersFromJson(jsondata string) (hdrs http.Header, err error) { + var raw interface{} + hdrs = http.Header{} + if jsondata == "" { + 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("JSON object must have only strings or arrays of strings.") + return + } + } + default: + err = fmt.Errorf("JSON object must have only strings or arrays of strings.") + return + } + } + default: + err = fmt.Errorf("JSON string must be a single object.") + return + } + return +} + // Application returns the Application which started the transaction. func (txn *Transaction) Application() *Application { if nil == txn { From b89a93e07c4e2866870822da712deac17d042cad Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Wed, 30 Jun 2021 16:05:01 -0700 Subject: [PATCH 2/4] style cleanup, corrected go.mod versions --- v3/go.mod | 4 ++-- v3/newrelic/trace_context_test.go | 8 ++++---- v3/newrelic/transaction.go | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 92bc2eef5..2ada3801e 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -3,6 +3,6 @@ module github.com/newrelic/go-agent/v3 go 1.7 require ( - github.com/golang/protobuf v1.4.3 - google.golang.org/grpc v1.39.0 + github.com/golang/protobuf v1.3.3 + google.golang.org/grpc v1.27.0 ) diff --git a/v3/newrelic/trace_context_test.go b/v3/newrelic/trace_context_test.go index 7dc6dd9cb..e054219b1 100644 --- a/v3/newrelic/trace_context_test.go +++ b/v3/newrelic/trace_context_test.go @@ -47,7 +47,7 @@ type TraceContextTestCase struct { } `json:"intrinsics"` } -func TestJsonDTHeaders(t *testing.T) { +func TestJSONDTHeaders(t *testing.T) { type testcase struct { in string out http.Header @@ -59,7 +59,7 @@ func TestJsonDTHeaders(t *testing.T) { {"{}", http.Header{}, false}, {" invalid ", http.Header{}, true}, {`"foo"`, http.Header{}, true}, - {`{"foo": "bar"}`, map[string][]string{ + {`{"foo": "bar"}`, http.Header{ "Foo": {"bar"}, }, false}, {`{ @@ -70,13 +70,13 @@ func TestJsonDTHeaders(t *testing.T) { "beta", "gamma" ] - }`, map[string][]string{ + }`, http.Header{ "Foo": {"bar"}, "Baz": {"quux"}, "Multiple": {"alpha", "beta", "gamma"}, }, false}, } { - h, err := DistributedTraceHeadersFromJson(test.in) + h, err := DistributedTraceHeadersFromJSON(test.in) if err != nil { if !test.err { diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index bbc5b9a49..306a3de12 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -272,14 +272,14 @@ func (txn *Transaction) AcceptDistributedTraceHeaders(t TransportType, hdrs http } // -// AcceptDistributedTraceHeadersFromJson() works just like AcceptDistributedTraceHeaders(), except -// that it takes the header data as a JSON string à la DistributedTraceHeadersFromJson(). Additionally +// 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) +func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, jsondata string) error { + hdrs, err := DistributedTraceHeadersFromJSON(jsondata) if err != nil { return err } @@ -288,7 +288,7 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJson(t TransportType, j } // -// DistributedTraceHeadersFromJson() takes a set of distributed trace headers as a JSON-encoded string +// 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. // @@ -302,7 +302,7 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJson(t TransportType, j // 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. // -func DistributedTraceHeadersFromJson(jsondata string) (hdrs http.Header, err error) { +func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err error) { var raw interface{} hdrs = http.Header{} if jsondata == "" { @@ -325,17 +325,17 @@ func DistributedTraceHeadersFromJson(jsondata string) (hdrs http.Header, err err case string: hdrs.Add(k, sval) default: - err = fmt.Errorf("JSON object must have only strings or arrays of strings.") + err = fmt.Errorf("the JSON object must have only strings or arrays of strings") return } } default: - err = fmt.Errorf("JSON object must have only strings or arrays of strings.") + err = fmt.Errorf("the JSON object must have only strings or arrays of strings") return } } default: - err = fmt.Errorf("JSON string must be a single object.") + err = fmt.Errorf("the JSON string must consist of only a single object") return } return From 2c20aed6f46cb52cd53db7597933b0015d64e033 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Wed, 30 Jun 2021 16:31:12 -0700 Subject: [PATCH 3/4] updated documentation --- v3/newrelic/transaction.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index 306a3de12..34ea3932f 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -292,15 +292,33 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, j // and emits a http.Header value suitable for passing on to the // txn.AcceptDistributedTraceHeaders() function. // +// This helps facilitate 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"}, +// } // // 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. // // 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{} From f6642c602fd406fe3f276ed789a93c88ae1a62fb Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Wed, 30 Jun 2021 16:33:48 -0700 Subject: [PATCH 4/4] updated documentation --- v3/newrelic/transaction.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index 34ea3932f..82e738ed1 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -292,7 +292,9 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, j // and emits a http.Header value suitable for passing on to the // txn.AcceptDistributedTraceHeaders() function. // -// This helps facilitate headers passed to your Go application from components written in other +// 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 @@ -305,9 +307,6 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, j // "Newrelic": {"xyzzy"}, // } // -// 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. -// // 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