Skip to content

Commit

Permalink
getting started with new header
Browse files Browse the repository at this point in the history
  • Loading branch information
iamemilio committed Nov 14, 2023
1 parent 91a21cb commit b8cb6ac
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 10 deletions.
1 change: 1 addition & 0 deletions v3/internal/cat/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ const (
NewRelicTxnName = "X-Newrelic-Transaction"
NewRelicAppDataName = "X-Newrelic-App-Data"
NewRelicSyntheticsName = "X-Newrelic-Synthetics"
NewRelicSyntheticsInfo = "X-Newrelic-Synthecics-Info"
)
106 changes: 100 additions & 6 deletions v3/internal/cat/synthetics.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,30 @@ type SyntheticsHeader struct {
MonitorID string
}

// SyntheticsInfo represents a decoded synthetics info payload.
type SyntheticsInfo struct {
Version int
Type string
Initiator string
Attributes map[string]string
}

var (
errInvalidSyntheticsJSON = errors.New("invalid synthetics JSON")
errInvalidSyntheticsVersion = errors.New("version is not a float64")
errInvalidSyntheticsAccountID = errors.New("account ID is not a float64")
errInvalidSyntheticsResourceID = errors.New("synthetics resource ID is not a string")
errInvalidSyntheticsJobID = errors.New("synthetics job ID is not a string")
errInvalidSyntheticsMonitorID = errors.New("synthetics monitor ID is not a string")
errInvalidSyntheticsJSON = errors.New("invalid synthetics JSON")
errInvalidSyntheticsInfoJSON = errors.New("invalid synthetics info JSON")
errInvalidSyntheticsVersion = errors.New("version is not a float64")
errInvalidSyntheticsAccountID = errors.New("account ID is not a float64")
errInvalidSyntheticsResourceID = errors.New("synthetics resource ID is not a string")
errInvalidSyntheticsJobID = errors.New("synthetics job ID is not a string")
errInvalidSyntheticsMonitorID = errors.New("synthetics monitor ID is not a string")
errInvalidSyntheticsInfoVersion = errors.New("synthetics info version is not a float64")
errMissingSyntheticsInfoVersion = errors.New("synthetics info version is missing from JSON object")
errInvalidSyntheticsInfoType = errors.New("synthetics info type is not a string")
errMissingSyntheticsInfoType = errors.New("synthetics info type is missing from JSON object")
errInvalidSyntheticsInfoInitiator = errors.New("synthetics info initiator is not a string")
errMissingSyntheticsInfoInitiator = errors.New("synthetics info initiator is missing from JSON object")
errInvalidSyntheticsInfoAttributes = errors.New("synthetics info attributes is not a map")
errInvalidSyntheticsInfoAttributeVal = errors.New("synthetics info keys and values must be strings")
)

type errUnexpectedSyntheticsVersion int
Expand Down Expand Up @@ -83,3 +100,80 @@ func (s *SyntheticsHeader) UnmarshalJSON(data []byte) error {

return nil
}

const (
versionKey = "version"
typeKey = "type"
initiatorKey = "initiator"
attributesKey = "attributes"
)

// UnmarshalJSON unmarshalls a SyntheticsInfo from raw JSON.
func (s *SyntheticsInfo) UnmarshalJSON(data []byte) error {
var v any

if err := json.Unmarshal(data, &v); err != nil {
return err
}

m, ok := v.(map[string]any)
if !ok {
return errInvalidSyntheticsInfoJSON
}

version, ok := m[versionKey]
if !ok {
return errMissingSyntheticsInfoVersion
}

versionFloat, ok := version.(float64)
if !ok {
return errInvalidSyntheticsInfoVersion
}

s.Version = int(versionFloat)
if s.Version != 1 {
return errUnexpectedSyntheticsVersion(s.Version)
}

infoType, ok := m[typeKey]
if !ok {
return errMissingSyntheticsInfoType
}

s.Type, ok = infoType.(string)
if !ok {
return errInvalidSyntheticsInfoType
}

initiator, ok := m[initiatorKey]
if !ok {
return errMissingSyntheticsInfoInitiator
}

s.Initiator, ok = initiator.(string)
if !ok {
return errInvalidSyntheticsInfoInitiator
}

attrs, ok := m[attributesKey]
if ok {
attrMap, ok := attrs.(map[string]any)
if !ok {
return errInvalidSyntheticsInfoAttributes
}
for k, v := range attrMap {
val, ok := v.(string)
if !ok {
return errInvalidSyntheticsInfoAttributeVal
}
if s.Attributes == nil {
s.Attributes = map[string]string{k: val}
} else {
s.Attributes[k] = val
}
}
}

return nil
}
127 changes: 127 additions & 0 deletions v3/internal/cat/synthetics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cat

import (
"encoding/json"
"fmt"
"testing"
)

Expand Down Expand Up @@ -118,3 +119,129 @@ func TestSyntheticsUnmarshalValid(t *testing.T) {
}
}
}

func TestSyntheticsInfoUnmarshal(t *testing.T) {
type testCase struct {
name string
json string
syntheticsInfo SyntheticsInfo
expectedError error
}

testCases := []testCase{
{
name: "missing type field",
json: `{"version":1,"initiator":"cli"}`,
syntheticsInfo: SyntheticsInfo{},
expectedError: errMissingSyntheticsInfoType,
},
{
name: "invalid type field",
json: `{"version":1,"initiator":"cli","type":1}`,
syntheticsInfo: SyntheticsInfo{},
expectedError: errInvalidSyntheticsInfoType,
},
{
name: "missing initiator field",
json: `{"version":1,"type":"scheduled"}`,
syntheticsInfo: SyntheticsInfo{},
expectedError: errMissingSyntheticsInfoInitiator,
},
{
name: "invalid initiator field",
json: `{"version":1,"initiator":1,"type":"scheduled"}`,
syntheticsInfo: SyntheticsInfo{},
expectedError: errInvalidSyntheticsInfoInitiator,
},
{
name: "missing version field",
json: `{"type":"scheduled"}`,
syntheticsInfo: SyntheticsInfo{},
expectedError: errMissingSyntheticsInfoVersion,
},
{
name: "invalid version field",
json: `{"version":"1","initiator":"cli","type":"scheduled"}`,
syntheticsInfo: SyntheticsInfo{},
expectedError: errInvalidSyntheticsInfoVersion,
},
{
name: "valid synthetics info",
json: `{"version":1,"type":"scheduled","initiator":"cli"}`,
syntheticsInfo: SyntheticsInfo{
Version: 1,
Type: "scheduled",
Initiator: "cli",
},
expectedError: nil,
},
{
name: "valid synthetics info with attributes",
json: `{"version":1,"type":"scheduled","initiator":"cli","attributes":{"hi":"hello"}}`,
syntheticsInfo: SyntheticsInfo{
Version: 1,
Type: "scheduled",
Initiator: "cli",
Attributes: map[string]string{"hi": "hello"},
},
expectedError: nil,
},
{
name: "valid synthetics info with invalid attributes",
json: `{"version":1,"type":"scheduled","initiator":"cli","attributes":{"hi":1}}`,
syntheticsInfo: SyntheticsInfo{
Version: 1,
Type: "scheduled",
Initiator: "cli",
Attributes: nil,
},
expectedError: errInvalidSyntheticsInfoAttributeVal,
},
}

for _, testCase := range testCases {
syntheticsInfo := SyntheticsInfo{}
err := syntheticsInfo.UnmarshalJSON([]byte(testCase.json))
if testCase.expectedError == nil {
if err != nil {
recordError(t, testCase.name, fmt.Sprintf("expected synthetics info to unmarshal without error, but got error: %v", err))
}

expect := testCase.syntheticsInfo
if expect.Version != syntheticsInfo.Version {
recordError(t, testCase.name, fmt.Sprintf(`expected version "%d", but got "%d"`, expect.Version, syntheticsInfo.Version))
}

if expect.Type != syntheticsInfo.Type {
recordError(t, testCase.name, fmt.Sprintf(`expected version "%s", but got "%s"`, expect.Type, syntheticsInfo.Type))
}

if expect.Initiator != syntheticsInfo.Initiator {
recordError(t, testCase.name, fmt.Sprintf(`expected version "%s", but got "%s"`, expect.Initiator, syntheticsInfo.Initiator))
}

if len(expect.Attributes) != 0 {
if len(syntheticsInfo.Attributes) == 0 {
recordError(t, testCase.name, fmt.Sprintf(`expected attribute array to have %d elements, but it only had %d`, len(expect.Attributes), len(syntheticsInfo.Attributes)))
}
for ek, ev := range expect.Attributes {
v, ok := syntheticsInfo.Attributes[ek]
if !ok {
recordError(t, testCase.name, fmt.Sprintf(`expected attributes to contain key "%s", but it did not`, ek))
}
if ev != v {
recordError(t, testCase.name, fmt.Sprintf(`expected attributes to contain "%s":"%s", but it contained "%s":"%s"`, ek, ev, ek, v))
}
}
}
} else {
if err != testCase.expectedError {
recordError(t, testCase.name, fmt.Sprintf(`expected synthetics info to unmarshal with error "%v", but got "%v"`, testCase.expectedError, err))
}
}
}
}

func recordError(t *testing.T, test, err string) {
t.Errorf("%s: %s", test, err)
}
5 changes: 5 additions & 0 deletions v3/newrelic/cross_process_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func metadataToHTTPHeader(metadata crossProcessMetadata) http.Header {

if metadata.Synthetics != "" {
header.Add(cat.NewRelicSyntheticsName, metadata.Synthetics)

// This header will only be present when the `X-NewRelic-Synthetics` header is present
if metadata.SyntheticsInfo != "" {

}
}

return header
Expand Down
48 changes: 45 additions & 3 deletions v3/newrelic/txn_cross_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,26 @@ type txnCrossProcess struct {
ReferringPathHash string
ReferringTxnGUID string
Synthetics *cat.SyntheticsHeader
SyntheticsInfo *cat.SyntheticsInfo

// The encoded synthetics header received as part of the request headers, if
// any. By storing this here, we avoid needing to marshal the invariant
// Synthetics struct above each time an external segment is created.
SyntheticsHeader string

// The encoded synthetics info header received as part of the request headers, if
// any. By storing this here, we avoid needing to marshal the invariant
// Synthetics struct above each time an external segment is created.
SyntheticsInfoHeader string
}

// crossProcessMetadata represents the metadata that must be transmitted with
// an external request for CAT to work.
type crossProcessMetadata struct {
ID string
TxnData string
Synthetics string
ID string
TxnData string
Synthetics string
SyntheticsInfo string
}

// Init initialises a txnCrossProcess based on the given application connect
Expand Down Expand Up @@ -247,6 +254,11 @@ func (txp *txnCrossProcess) handleInboundRequestHeaders(metadata crossProcessMet
if err := txp.handleInboundRequestEncodedSynthetics(metadata.Synthetics); err != nil {
return err
}
if metadata.SyntheticsInfo != "" {
if err := txp.handleInboundRequestEncodedSyntheticsInfo(metadata.SyntheticsInfo); err != nil {
return err
}
}
}

return nil
Expand Down Expand Up @@ -338,6 +350,36 @@ func (txp *txnCrossProcess) handleInboundRequestSynthetics(raw []byte) error {
return nil
}

func (txp *txnCrossProcess) handleInboundRequestEncodedSyntheticsInfo(encoded string) error {
raw, err := deobfuscate(encoded, txp.EncodingKey)
if err != nil {
return err
}

if err := txp.handleInboundRequestSyntheticsInfo(raw); err != nil {
return err
}

txp.SyntheticsInfoHeader = encoded
return nil
}

func (txp *txnCrossProcess) handleInboundRequestSyntheticsInfo(raw []byte) error {
synthetics := &cat.SyntheticsInfo{}
if err := json.Unmarshal(raw, synthetics); err != nil {
return err
}

// The specced behaviour here if the account isn't trusted is to disable the
// synthetics handling, but not CAT in general, so we won't return an error
// here.
if txp.IsSynthetics() {
txp.SyntheticsInfo = synthetics
}

return nil
}

func (txp *txnCrossProcess) outboundID() (string, error) {
return obfuscate(txp.CrossProcessID, txp.EncodingKey)
}
Expand Down
16 changes: 15 additions & 1 deletion v3/newrelic/txn_cross_process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ func (req *request) withSynthetics(account int, encodingKey string) *request {
}

req.Header.Add(cat.NewRelicSyntheticsName, string(obfuscated))

return req.withSyntheticsInfo("cli", "scheduled", encodingKey)
}

func (req *request) withSyntheticsInfo(initiator, synthType, encodingKey string) *request {
header := fmt.Sprintf(`{"version":1,"type":"%s","initiator":"%s"}`, synthType, initiator)
obfuscated, err := obfuscate([]byte(header), []byte(encodingKey))
if err != nil {
panic(err)
}

req.Header.Add(cat.NewRelicSyntheticsInfo, string(obfuscated))
return req
}

Expand Down Expand Up @@ -168,14 +180,16 @@ func TestTxnCrossProcessInit(t *testing.T) {
id := ""
txnData := ""
synthetics := ""
syntheticsInfo := ""
if tc.req != nil {
id = tc.req.Header.Get(cat.NewRelicIDName)
txnData = tc.req.Header.Get(cat.NewRelicTxnName)
synthetics = tc.req.Header.Get(cat.NewRelicSyntheticsName)
syntheticsInfo = tc.req.Header.Get(cat.NewRelicSyntheticsInfo)
}

actual.Init(tc.enabled, false, tc.reply)
err := actual.handleInboundRequestHeaders(crossProcessMetadata{id, txnData, synthetics})
err := actual.handleInboundRequestHeaders(crossProcessMetadata{id, txnData, synthetics, syntheticsInfo})

if tc.expectedError == false && err != nil {
t.Errorf("%s: unexpected error returned from Init: %v", tc.name, err)
Expand Down

0 comments on commit b8cb6ac

Please sign in to comment.