File: trace_context_test.go

package info (click to toggle)
golang-github-newrelic-go-agent 3.15.2-9
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 8,356 kB
  • sloc: sh: 65; makefile: 6
file content (367 lines) | stat: -rw-r--r-- 10,850 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package newrelic

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"reflect"
	"strings"
	"testing"

	"github.com/newrelic/go-agent/v3/internal"
	"github.com/newrelic/go-agent/v3/internal/crossagent"
)

type fieldExpect struct {
	Exact      map[string]interface{} `json:"exact,omitempty"`
	Expected   []string               `json:"expected,omitempty"`
	Unexpected []string               `json:"unexpected,omitempty"`
	NotEqual   map[string]interface{} `json:"notequal,omitempty"`
	Vendors    []string               `json:"vendors,omitempty"`
}

type TraceContextTestCase struct {
	TestName          string              `json:"test_name"`
	TrustedAccountKey string              `json:"trusted_account_key"`
	AccountID         string              `json:"account_id"`
	WebTransaction    bool                `json:"web_transaction"`
	RaisesException   bool                `json:"raises_exception"`
	ForceSampledTrue  bool                `json:"force_sampled_true"`
	SpanEventsEnabled bool                `json:"span_events_enabled"`
	TxnEventsEnabled  bool                `json:"transaction_events_enabled"`
	TransportType     string              `json:"transport_type"`
	InboundHeaders    []map[string]string `json:"inbound_headers"`
	OutboundPayloads  []fieldExpect       `json:"outbound_payloads,omitempty"`
	ExpectedMetrics   [][2]interface{}    `json:"expected_metrics"`
	Intrinsics        struct {
		TargetEvents     []string     `json:"target_events"`
		Common           *fieldExpect `json:"common,omitempty"`
		Transaction      *fieldExpect `json:"Transaction,omitempty"`
		Span             *fieldExpect `json:"Span,omitempty"`
		TransactionError *fieldExpect `json:"TransactionError,omitempty"`
	} `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

	data, err := crossagent.ReadFile("distributed_tracing/trace_context.json")
	if err != nil {
		t.Fatal(err)
	}

	if err := json.Unmarshal(data, &tcs); nil != err {
		t.Fatal(err)
	}

	for _, tc := range tcs {
		t.Run(tc.TestName, func(t *testing.T) {
			if tc.TestName == "spans_disabled_in_child" || tc.TestName == "spans_disabled_root" {
				t.Skip("spec change caused failing test, skipping")
				return
			}
			runW3CTestCase(t, tc)
		})
	}
}

func runW3CTestCase(t *testing.T, tc TraceContextTestCase) {
	configCallback := func(cfg *Config) {
		cfg.CrossApplicationTracer.Enabled = false
		cfg.DistributedTracer.Enabled = true
		cfg.SpanEvents.Enabled = tc.SpanEventsEnabled
		cfg.TransactionEvents.Enabled = tc.TxnEventsEnabled
	}

	app := testApp(func(reply *internal.ConnectReply) {
		reply.AccountID = tc.AccountID
		reply.AppID = "456"
		reply.PrimaryAppID = "456"
		reply.TrustedAccountKey = tc.TrustedAccountKey
		reply.SetSampleEverything()

	}, configCallback, t)

	txn := app.StartTransaction("hello")
	if tc.WebTransaction {
		txn.SetWebRequestHTTP(nil)
	}

	// If the tests wants us to have an error, give 'em an error
	if tc.RaisesException {
		txn.NoticeError(errors.New("my error message"))
	}

	// If there are no inbound payloads, invoke Accept on an empty inbound payload.
	if nil == tc.InboundHeaders {
		txn.AcceptDistributedTraceHeaders(getTransportType(tc.TransportType), nil)
	}

	txn.AcceptDistributedTraceHeaders(getTransportType(tc.TransportType), headersFromStringMap(tc.InboundHeaders))

	// Call create each time an outbound payload appears in the testcase
	for _, expect := range tc.OutboundPayloads {
		hdrs := http.Header{}
		txn.InsertDistributedTraceHeaders(hdrs)
		assertTestCaseOutboundHeaders(expect, t, hdrs)
	}

	txn.End()

	// create WantMetrics and assert
	var wantMetrics []internal.WantMetric
	for _, metric := range tc.ExpectedMetrics {
		wantMetrics = append(wantMetrics,
			internal.WantMetric{Name: metric[0].(string), Scope: "", Forced: nil, Data: nil})
	}
	app.ExpectMetricsPresent(t, wantMetrics)

	// Add extra fields that are not listed in the JSON file so that we can
	// always do exact intrinsic set match.

	extraTxnFields := &fieldExpect{Expected: []string{"name"}}
	if tc.WebTransaction {
		extraTxnFields.Expected = append(extraTxnFields.Expected, "nr.apdexPerfZone")
	}

	extraSpanFields := &fieldExpect{
		Expected: []string{"name", "transaction.name", "category", "nr.entryPoint"},
	}

	// There is a single test with an error (named "exception"), so these
	// error expectations can be hard coded.
	extraErrorFields := &fieldExpect{
		Expected: []string{"parent.type", "parent.account", "parent.app",
			"parent.transportType", "error.message", "transactionName",
			"parent.transportDuration", "error.class", "spanId"},
	}

	for _, value := range tc.Intrinsics.TargetEvents {
		switch value {
		case "Transaction":
			assertW3CTestCaseIntrinsics(t,
				app.ExpectTxnEvents,
				tc.Intrinsics.Common,
				tc.Intrinsics.Transaction,
				extraTxnFields)
		case "Span":
			assertW3CTestCaseIntrinsics(t,
				app.ExpectSpanEvents,
				tc.Intrinsics.Common,
				tc.Intrinsics.Span,
				extraSpanFields)

		case "TransactionError":
			assertW3CTestCaseIntrinsics(t,
				app.ExpectErrorEvents,
				tc.Intrinsics.Common,
				tc.Intrinsics.TransactionError,
				extraErrorFields)
		}
	}
}

// getTransport ensures that our transport names match cross agent test values.
func getTransportType(transport string) TransportType {
	switch TransportType(transport) {
	case TransportHTTP, TransportHTTPS, TransportKafka, TransportJMS, TransportIronMQ, TransportAMQP,
		TransportQueue, TransportOther:
		return TransportType(transport)
	default:
		return TransportUnknown
	}
}

func headersFromStringMap(hdrs []map[string]string) http.Header {
	httpHdrs := http.Header{}
	for _, entry := range hdrs {
		for k, v := range entry {
			httpHdrs.Add(k, v)
		}
	}
	return httpHdrs
}

func assertTestCaseOutboundHeaders(expect fieldExpect, t *testing.T, hdrs http.Header) {
	p := make(map[string]string)

	// prepare traceparent header
	pHdr := hdrs.Get("traceparent")
	pSplit := strings.Split(pHdr, "-")
	if len(pSplit) != 4 {
		t.Error("incorrect traceparent header created ", pHdr)
		return
	}
	p["traceparent.version"] = pSplit[0]
	p["traceparent.trace_id"] = pSplit[1]
	p["traceparent.parent_id"] = pSplit[2]
	p["traceparent.trace_flags"] = pSplit[3]

	// prepare tracestate header
	sHdr := hdrs.Get("tracestate")
	sSplit := strings.Split(sHdr, "-")
	if len(sSplit) >= 9 {
		p["tracestate.tenant_id"] = strings.Split(sHdr, "@")[0]
		p["tracestate.version"] = strings.Split(sSplit[0], "=")[1]
		p["tracestate.parent_type"] = sSplit[1]
		p["tracestate.parent_account_id"] = sSplit[2]
		p["tracestate.parent_application_id"] = sSplit[3]
		p["tracestate.span_id"] = sSplit[4]
		p["tracestate.transaction_id"] = sSplit[5]
		p["tracestate.sampled"] = sSplit[6]
		p["tracestate.priority"] = sSplit[7]
		p["tracestate.timestamp"] = sSplit[8]
	}

	// prepare newrelic header
	nHdr := hdrs.Get("newrelic")
	decoded, err := base64.StdEncoding.DecodeString(nHdr)
	if err != nil {
		t.Error("failure to decode newrelic header: ", err)
	}
	nrPayload := struct {
		Version [2]int  `json:"v"`
		Data    payload `json:"d"`
	}{}
	if err := json.Unmarshal(decoded, &nrPayload); nil != err {
		t.Error("unable to unmarshall newrelic header: ", err)
	}
	p["newrelic.v"] = fmt.Sprintf("%v", nrPayload.Version)
	p["newrelic.d.ac"] = nrPayload.Data.Account
	p["newrelic.d.ap"] = nrPayload.Data.App
	p["newrelic.d.id"] = nrPayload.Data.ID
	p["newrelic.d.pr"] = fmt.Sprintf("%v", nrPayload.Data.Priority)
	p["newrelic.d.ti"] = fmt.Sprintf("%v", nrPayload.Data.Timestamp)
	p["newrelic.d.tr"] = nrPayload.Data.TracedID
	p["newrelic.d.tx"] = nrPayload.Data.TransactionID
	p["newrelic.d.ty"] = nrPayload.Data.Type
	if *nrPayload.Data.Sampled {
		p["newrelic.d.sa"] = "1"
	} else {
		p["newrelic.d.sa"] = "0"
	}

	// Affirm that the exact values are in the payload.
	for k, v := range expect.Exact {
		var exp string
		switch val := v.(type) {
		case bool:
			if val {
				exp = "1"
			} else {
				exp = "0"
			}
		case string:
			exp = val
		default:
			exp = fmt.Sprintf("%v", val)
		}
		if val := p[k]; val != exp {
			t.Errorf("expected outbound payload wrong value for key %s, expected=%s, actual=%s", k, exp, val)
		}
	}

	// Affirm that the expected values are in the actual payload.
	for _, e := range expect.Expected {
		if val := p[e]; val == "" {
			t.Errorf("expected outbound payload missing key %s", e)
		}
	}

	// Affirm that the unexpected values are not in the actual payload.
	for _, e := range expect.Unexpected {
		if val := p[e]; val != "" {
			t.Errorf("expected outbound payload contains key %s", e)
		}
	}

	// Affirm that not equal values are not equal in the actual payload
	for k, v := range expect.NotEqual {
		exp := fmt.Sprintf("%v", v)
		if val := p[k]; val == exp {
			t.Errorf("expected outbound payload has equal value for key %s, value=%s", k, val)
		}
	}

	// Affirm that the correct vendors are included in the actual payload
	for _, e := range expect.Vendors {
		if !strings.Contains(sHdr, e) {
			t.Errorf("expected outbound payload does not contain vendor %s, tracestate=%s", e, sHdr)
		}
	}
	if sHdr != "" {
		// when the tracestate header is non-empty, ensure that no extraneous
		// vendors appear
		if cnt := strings.Count(sHdr, "="); cnt != len(expect.Vendors)+1 {
			t.Errorf("expected outbound payload has wrong number of vendors, tracestate=%s", sHdr)
		}
	}
}

func assertW3CTestCaseIntrinsics(t internal.Validator,
	expect func(internal.Validator, []internal.WantEvent),
	fields ...*fieldExpect) {

	intrinsics := map[string]interface{}{}
	for _, f := range fields {
		f.add(intrinsics)
	}
	expect(t, []internal.WantEvent{{Intrinsics: intrinsics}})
}

func (fe *fieldExpect) add(intrinsics map[string]interface{}) {
	if nil != fe {
		for k, v := range fe.Exact {
			intrinsics[k] = v
		}
		for _, v := range fe.Expected {
			intrinsics[v] = internal.MatchAnything
		}
	}
}