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

getting started with new header #816

Merged
merged 1 commit into from
Dec 14, 2023
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
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
Loading