Skip to content

Commit

Permalink
feat(testing): Implement EXPECT_SPAN_EVENTS_LIKE (#717)
Browse files Browse the repository at this point in the history
Adds a new feature (`EXPECT_SPAN_EVENTS_LIKE`) for the
`integration_runner` which allows testing for the existence of a set of
spans in the actual spans created. It differs from `EXPECT_SPAN_EVENTS`
in that it is not necessary to list ALL expected spans. For tests
involving PHP frameworks the number of generated spans can be in the
hundreds so it is not practical to use `EXPECT_SPAN_EVENTS` and thus
`EXPECT_SPAN_EVENTS_LIKE` was added.

Here is an example:

```
/*EXPECT_SPAN_EVENTS_LIKE
[
  [
    {
        "category": "generic",
        "type": "Span",
        "guid": "??",
        "traceId": "??",
        "transactionId": "??",
        "name": "WebTransaction\/Action\/dispatch",
        "timestamp": "??",
        "duration": "??",
        "priority": "??",
        "sampled": true,
        "nr.entryPoint": true,
        "transaction.name": "WebTransaction\/Action\/dispatch"
    },
    {},
    {
        "response.headers.contentType": "text\/html",
        "http.statusCode": 200,
        "response.statusCode": 200,
        "httpResponseCode": "200",
        "request.uri": "\/dispatch",
        "request.method": "GET",
        "request.headers.host": "127.0.0.1"
    }
  ],
  [
    {
        "category": "generic",
        "type": "Span",
        "guid": "??",
        "traceId": "??",
        "transactionId": "??",
        "name": "Custom\/App\\Http\\Controllers\\JobOne::dispatch",
        "timestamp": "??",
        "duration": "??",
        "priority": "??",
        "sampled": true,
        "parentId": "??"
    },
    {},
    {
        "code.lineno": "??",
        "code.namespace": "App\\Http\\Controllers\\JobOne",
        "code.filepath": "??",
        "code.function": "dispatch"
    }
  ]
]
*/
```

---------

Co-authored-by: Michal Nowacki <[email protected]>
  • Loading branch information
mfulb and lavarou authored Aug 22, 2023
1 parent 7fd56ae commit 7d72a0b
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/newrelic/integration/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (
"EXPECT_CUSTOM_EVENTS": parseCustomEvents,
"EXPECT_ERROR_EVENTS": parseErrorEvents,
"EXPECT_SPAN_EVENTS": parseSpanEvents,
"EXPECT_SPAN_EVENTS_LIKE": parseSpanEventsLike,
"EXPECT_LOG_EVENTS": parseLogEvents,
"EXPECT_METRICS": parseMetrics,
"EXPECT": parseExpect,
Expand Down Expand Up @@ -211,6 +212,10 @@ func parseSpanEvents(test *Test, content []byte) error {
test.spanEvents = content
return nil
}
func parseSpanEventsLike(test *Test, content []byte) error {
test.spanEventsLike = content
return nil
}
func parseLogEvents(test *Test, content []byte) error {
test.logEvents = content
return nil
Expand Down
121 changes: 121 additions & 0 deletions src/newrelic/integration/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Test struct {
customEvents []byte
errorEvents []byte
spanEvents []byte
spanEventsLike []byte
logEvents []byte
metrics []byte
slowSQLs []byte
Expand Down Expand Up @@ -315,6 +316,122 @@ func (t *Test) compareResponseHeaders() {
}
}

// Handling EXPECT_SPAN_EVENTS_LIKE is different than normal payload compare so
// a different function is required
func (t *Test) compareSpanEventsLike(harvest *newrelic.Harvest) {
// convert array of expected spans JSON to interface representation
var x2 interface{}

if nil == t.spanEventsLike {
return
}

if err := json.Unmarshal(t.spanEventsLike, &x2); nil != err {
t.Fatal(fmt.Errorf("unable to parse expected spans like json for fuzzy matching: %v", err))
return
}

// expected will be represented as an array of "interface{}"
// each element will be the internal representation of the JSON
// for each expected span
// this is needed for the call to isFuzzyMatch Recursive later
// when comparing to the actual spans in their internal representation
expected := x2.([]interface{})

// now parse actual span data JSON into interface representation
var x1 interface{}

es := *harvest.SpanEvents
id := newrelic.AgentRunID("?? agent run id")
actualJSON, err := es.Data(id, time.Now())
if nil != err {
t.Fatal(fmt.Errorf("unable to access span event JSON data: %v", err))
return
}

// scrub actual spans
scrubjson := ScrubLineNumbers(actualJSON)
scrubjson = ScrubFilename(scrubjson, t.Path)
scrubjson = ScrubHost(scrubjson)

// parse to internal format
if err := json.Unmarshal(scrubjson, &x1); nil != err {
t.Fatal(fmt.Errorf("unable to parse actual spans like json for fuzzy matching: %v", err))
return
}

// expect x1 to be of type "[]interface {}" which wraps the entire span event data
// within this generic array there will be:
// - a string of the form "?? agent run id"
// - a map container "events_seen" and "reservoir_size"
// - an array of arrays containing:
// - map containing main span data
// - map for attributes(?)
// - map of CLM data

// test initial type is as expected
switch x1.(type) {
case []interface{}:
default:
t.Fatal(errors.New("span event data json doesnt match expected format"))
return
}

// expect array of len 3
v2, _ := x1.([]interface{})
if 3 != len(v2) {
t.Fatal(errors.New("span event data json doesnt match expected format - expected 3 elements"))
return
}

// get array of actual spans from 3rd element
actual := v2[2].([]interface{})

// check if expected JSON is present in actual data
// will call isFuzzyMatchRecursive with "interface" representations
numMatched := 0
haveMatched := make([]bool, len(expected))
for i := 0; i < len(expected); i++ {
haveMatched[i] = false
}

for i := 0; i < len(actual); i++ {
// check each expected span (interface representation) against current actual span
// only iterate over unmatched expected spans
for j := 0; j < len(expected); j++ {
if haveMatched[j] {
continue
}

err := isFuzzyMatchRecursive(expected[j], actual[i])
if nil == err {
haveMatched[j] = true
numMatched++
break
}
}

if len(expected) == numMatched {
break
}
}

for j := 0; j < len(expected); j++ {
if !haveMatched[j] {
actualPretty := bytes.Buffer{}
json.Indent(&actualPretty, actualJSON, "", " ")
expectedJSON, _ := json.Marshal(expected[j])

t.Fail(ComparisonFailure{
Name: fmt.Sprintf("matching span event data like: unmatched expected span"),
Expect: string(expectedJSON),
Actual: actualPretty.String(),
})
return
}
}
}

func (t *Test) comparePayload(expected json.RawMessage, pc newrelic.PayloadCreator, isMetrics bool) {
if nil == expected {
// No expected output has been specified: Anything passes.
Expand Down Expand Up @@ -451,6 +568,10 @@ func (t *Test) Compare(harvest *newrelic.Harvest) {
return
}

// check for any "expected spans like"
t.compareSpanEventsLike(harvest)

// check remaining payloads
t.comparePayload(t.analyticEvents, harvest.TxnEvents, false)
t.comparePayload(t.customEvents, harvest.CustomEvents, false)
t.comparePayload(t.errorEvents, harvest.ErrorEvents, false)
Expand Down

0 comments on commit 7d72a0b

Please sign in to comment.