github.com/newrelic/go-agent@v3.26.0+incompatible/_integrations/nrawssdk/v1/nrawssdk_test.go (about) 1 // Copyright 2020 New Relic Corporation. All rights reserved. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package nrawssdk 5 6 import ( 7 "bytes" 8 "errors" 9 "io/ioutil" 10 "net/http" 11 "testing" 12 13 "github.com/aws/aws-sdk-go/aws" 14 "github.com/aws/aws-sdk-go/aws/credentials" 15 "github.com/aws/aws-sdk-go/aws/request" 16 "github.com/aws/aws-sdk-go/aws/session" 17 "github.com/aws/aws-sdk-go/service/dynamodb" 18 "github.com/aws/aws-sdk-go/service/lambda" 19 newrelic "github.com/newrelic/go-agent" 20 "github.com/newrelic/go-agent/internal" 21 "github.com/newrelic/go-agent/internal/integrationsupport" 22 ) 23 24 func testApp() integrationsupport.ExpectApp { 25 return integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, integrationsupport.DTEnabledCfgFn) 26 } 27 28 type fakeTransport struct{} 29 30 func (t fakeTransport) RoundTrip(r *http.Request) (*http.Response, error) { 31 return &http.Response{ 32 Status: "200 OK", 33 StatusCode: 200, 34 Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), 35 Header: http.Header{ 36 "X-Amzn-Requestid": []string{requestID}, 37 }, 38 }, nil 39 } 40 41 type fakeCreds struct{} 42 43 func (c *fakeCreds) Retrieve() (credentials.Value, error) { 44 return credentials.Value{}, nil 45 } 46 func (c *fakeCreds) IsExpired() bool { return false } 47 48 func newSession() *session.Session { 49 r := "us-west-2" 50 ses := session.New() 51 ses.Config.Credentials = credentials.NewCredentials(&fakeCreds{}) 52 ses.Config.HTTPClient.Transport = &fakeTransport{} 53 ses.Config.Region = &r 54 return ses 55 } 56 57 const ( 58 requestID = "testing request id" 59 txnName = "aws-txn" 60 ) 61 62 var ( 63 genericSpan = internal.WantEvent{ 64 Intrinsics: map[string]interface{}{ 65 "name": "OtherTransaction/Go/" + txnName, 66 "sampled": true, 67 "category": "generic", 68 "priority": internal.MatchAnything, 69 "guid": internal.MatchAnything, 70 "transactionId": internal.MatchAnything, 71 "nr.entryPoint": true, 72 "traceId": internal.MatchAnything, 73 }, 74 UserAttributes: map[string]interface{}{}, 75 AgentAttributes: map[string]interface{}{}, 76 } 77 externalSpan = internal.WantEvent{ 78 Intrinsics: map[string]interface{}{ 79 "name": "External/lambda.us-west-2.amazonaws.com/http/POST", 80 "sampled": true, 81 "category": "http", 82 "priority": internal.MatchAnything, 83 "guid": internal.MatchAnything, 84 "transactionId": internal.MatchAnything, 85 "traceId": internal.MatchAnything, 86 "parentId": internal.MatchAnything, 87 "component": "http", 88 "span.kind": "client", 89 }, 90 UserAttributes: map[string]interface{}{}, 91 AgentAttributes: map[string]interface{}{ 92 "aws.operation": "Invoke", 93 "aws.region": "us-west-2", 94 "aws.requestId": requestID, 95 "http.method": "POST", 96 "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", 97 }, 98 } 99 externalSpanNoRequestID = internal.WantEvent{ 100 Intrinsics: map[string]interface{}{ 101 "name": "External/lambda.us-west-2.amazonaws.com/http/POST", 102 "sampled": true, 103 "category": "http", 104 "priority": internal.MatchAnything, 105 "guid": internal.MatchAnything, 106 "transactionId": internal.MatchAnything, 107 "traceId": internal.MatchAnything, 108 "parentId": internal.MatchAnything, 109 "component": "http", 110 "span.kind": "client", 111 }, 112 UserAttributes: map[string]interface{}{}, 113 AgentAttributes: map[string]interface{}{ 114 "aws.operation": "Invoke", 115 "aws.region": "us-west-2", 116 "http.method": "POST", 117 "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", 118 }, 119 } 120 datastoreSpan = internal.WantEvent{ 121 Intrinsics: map[string]interface{}{ 122 "name": "Datastore/statement/DynamoDB/thebesttable/DescribeTable", 123 "sampled": true, 124 "category": "datastore", 125 "priority": internal.MatchAnything, 126 "guid": internal.MatchAnything, 127 "transactionId": internal.MatchAnything, 128 "traceId": internal.MatchAnything, 129 "parentId": internal.MatchAnything, 130 "component": "DynamoDB", 131 "span.kind": "client", 132 }, 133 UserAttributes: map[string]interface{}{}, 134 AgentAttributes: map[string]interface{}{ 135 "aws.operation": "DescribeTable", 136 "aws.region": "us-west-2", 137 "aws.requestId": requestID, 138 "db.collection": "thebesttable", 139 "db.statement": "'DescribeTable' on 'thebesttable' using 'DynamoDB'", 140 "peer.address": "dynamodb.us-west-2.amazonaws.com:unknown", 141 "peer.hostname": "dynamodb.us-west-2.amazonaws.com", 142 }, 143 } 144 145 txnMetrics = []internal.WantMetric{ 146 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, 147 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, 148 {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, 149 {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, 150 {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, 151 {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, 152 } 153 externalMetrics = append([]internal.WantMetric{ 154 {Name: "External/all", Scope: "", Forced: true, Data: nil}, 155 {Name: "External/allOther", Scope: "", Forced: true, Data: nil}, 156 {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: nil}, 157 {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: nil}, 158 }, txnMetrics...) 159 datastoreMetrics = append([]internal.WantMetric{ 160 {Name: "Datastore/DynamoDB/all", Scope: "", Forced: true, Data: nil}, 161 {Name: "Datastore/DynamoDB/allOther", Scope: "", Forced: true, Data: nil}, 162 {Name: "Datastore/all", Scope: "", Forced: true, Data: nil}, 163 {Name: "Datastore/allOther", Scope: "", Forced: true, Data: nil}, 164 {Name: "Datastore/instance/DynamoDB/dynamodb.us-west-2.amazonaws.com/unknown", Scope: "", Forced: false, Data: nil}, 165 {Name: "Datastore/operation/DynamoDB/DescribeTable", Scope: "", Forced: false, Data: nil}, 166 {Name: "Datastore/statement/DynamoDB/thebesttable/DescribeTable", Scope: "", Forced: false, Data: nil}, 167 {Name: "Datastore/statement/DynamoDB/thebesttable/DescribeTable", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: nil}, 168 }, txnMetrics...) 169 ) 170 171 func TestInstrumentRequestExternal(t *testing.T) { 172 app := testApp() 173 txn := app.StartTransaction(txnName, nil, nil) 174 175 client := lambda.New(newSession()) 176 input := &lambda.InvokeInput{ 177 ClientContext: aws.String("MyApp"), 178 FunctionName: aws.String("non-existent-function"), 179 InvocationType: aws.String("Event"), 180 LogType: aws.String("Tail"), 181 Payload: []byte("{}"), 182 } 183 184 req, out := client.InvokeRequest(input) 185 InstrumentHandlers(&req.Handlers) 186 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) 187 188 err := req.Send() 189 if nil != err { 190 t.Error(err) 191 } 192 if 200 != *out.StatusCode { 193 t.Error("wrong status code on response", out.StatusCode) 194 } 195 196 txn.End() 197 198 app.ExpectMetrics(t, externalMetrics) 199 app.ExpectSpanEvents(t, []internal.WantEvent{ 200 genericSpan, externalSpan}) 201 } 202 203 func TestInstrumentRequestDatastore(t *testing.T) { 204 app := testApp() 205 txn := app.StartTransaction(txnName, nil, nil) 206 207 client := dynamodb.New(newSession()) 208 input := &dynamodb.DescribeTableInput{ 209 TableName: aws.String("thebesttable"), 210 } 211 212 req, _ := client.DescribeTableRequest(input) 213 InstrumentHandlers(&req.Handlers) 214 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) 215 216 err := req.Send() 217 if nil != err { 218 t.Error(err) 219 } 220 221 txn.End() 222 223 app.ExpectMetrics(t, datastoreMetrics) 224 app.ExpectSpanEvents(t, []internal.WantEvent{ 225 genericSpan, datastoreSpan}) 226 } 227 228 func TestInstrumentRequestExternalNoTxn(t *testing.T) { 229 client := lambda.New(newSession()) 230 input := &lambda.InvokeInput{ 231 ClientContext: aws.String("MyApp"), 232 FunctionName: aws.String("non-existent-function"), 233 InvocationType: aws.String("Event"), 234 LogType: aws.String("Tail"), 235 Payload: []byte("{}"), 236 } 237 238 req, out := client.InvokeRequest(input) 239 InstrumentHandlers(&req.Handlers) 240 241 err := req.Send() 242 if nil != err { 243 t.Error(err) 244 } 245 if 200 != *out.StatusCode { 246 t.Error("wrong status code on response", out.StatusCode) 247 } 248 } 249 250 func TestInstrumentRequestDatastoreNoTxn(t *testing.T) { 251 client := dynamodb.New(newSession()) 252 input := &dynamodb.DescribeTableInput{ 253 TableName: aws.String("thebesttable"), 254 } 255 256 req, _ := client.DescribeTableRequest(input) 257 InstrumentHandlers(&req.Handlers) 258 259 err := req.Send() 260 if nil != err { 261 t.Error(err) 262 } 263 } 264 265 func TestInstrumentSessionExternal(t *testing.T) { 266 app := testApp() 267 txn := app.StartTransaction(txnName, nil, nil) 268 269 ses := newSession() 270 InstrumentHandlers(&ses.Handlers) 271 client := lambda.New(ses) 272 273 input := &lambda.InvokeInput{ 274 ClientContext: aws.String("MyApp"), 275 FunctionName: aws.String("non-existent-function"), 276 InvocationType: aws.String("Event"), 277 LogType: aws.String("Tail"), 278 Payload: []byte("{}"), 279 } 280 281 req, out := client.InvokeRequest(input) 282 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) 283 284 err := req.Send() 285 if nil != err { 286 t.Error(err) 287 } 288 if 200 != *out.StatusCode { 289 t.Error("wrong status code on response", out.StatusCode) 290 } 291 292 txn.End() 293 294 app.ExpectMetrics(t, externalMetrics) 295 app.ExpectSpanEvents(t, []internal.WantEvent{ 296 genericSpan, externalSpan}) 297 } 298 299 func TestInstrumentSessionDatastore(t *testing.T) { 300 app := testApp() 301 txn := app.StartTransaction(txnName, nil, nil) 302 303 ses := newSession() 304 InstrumentHandlers(&ses.Handlers) 305 client := dynamodb.New(ses) 306 307 input := &dynamodb.DescribeTableInput{ 308 TableName: aws.String("thebesttable"), 309 } 310 311 req, _ := client.DescribeTableRequest(input) 312 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) 313 314 err := req.Send() 315 if nil != err { 316 t.Error(err) 317 } 318 319 txn.End() 320 321 app.ExpectMetrics(t, datastoreMetrics) 322 app.ExpectSpanEvents(t, []internal.WantEvent{ 323 genericSpan, datastoreSpan}) 324 } 325 326 func TestInstrumentSessionExternalNoTxn(t *testing.T) { 327 ses := newSession() 328 InstrumentHandlers(&ses.Handlers) 329 client := lambda.New(ses) 330 331 input := &lambda.InvokeInput{ 332 ClientContext: aws.String("MyApp"), 333 FunctionName: aws.String("non-existent-function"), 334 InvocationType: aws.String("Event"), 335 LogType: aws.String("Tail"), 336 Payload: []byte("{}"), 337 } 338 339 req, out := client.InvokeRequest(input) 340 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, nil) 341 342 err := req.Send() 343 if nil != err { 344 t.Error(err) 345 } 346 if 200 != *out.StatusCode { 347 t.Error("wrong status code on response", out.StatusCode) 348 } 349 } 350 351 func TestInstrumentSessionDatastoreNoTxn(t *testing.T) { 352 ses := newSession() 353 InstrumentHandlers(&ses.Handlers) 354 client := dynamodb.New(ses) 355 356 input := &dynamodb.DescribeTableInput{ 357 TableName: aws.String("thebesttable"), 358 } 359 360 req, _ := client.DescribeTableRequest(input) 361 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, nil) 362 363 err := req.Send() 364 if nil != err { 365 t.Error(err) 366 } 367 } 368 369 func TestInstrumentSessionExternalTxnNotInCtx(t *testing.T) { 370 app := testApp() 371 txn := app.StartTransaction(txnName, nil, nil) 372 373 ses := newSession() 374 InstrumentHandlers(&ses.Handlers) 375 client := lambda.New(ses) 376 377 input := &lambda.InvokeInput{ 378 ClientContext: aws.String("MyApp"), 379 FunctionName: aws.String("non-existent-function"), 380 InvocationType: aws.String("Event"), 381 LogType: aws.String("Tail"), 382 Payload: []byte("{}"), 383 } 384 385 req, out := client.InvokeRequest(input) 386 387 err := req.Send() 388 if nil != err { 389 t.Error(err) 390 } 391 if 200 != *out.StatusCode { 392 t.Error("wrong status code on response", out.StatusCode) 393 } 394 395 txn.End() 396 397 app.ExpectMetrics(t, txnMetrics) 398 } 399 400 func TestInstrumentSessionDatastoreTxnNotInCtx(t *testing.T) { 401 app := testApp() 402 txn := app.StartTransaction(txnName, nil, nil) 403 404 ses := newSession() 405 InstrumentHandlers(&ses.Handlers) 406 client := dynamodb.New(ses) 407 408 input := &dynamodb.DescribeTableInput{ 409 TableName: aws.String("thebesttable"), 410 } 411 412 req, _ := client.DescribeTableRequest(input) 413 414 err := req.Send() 415 if nil != err { 416 t.Error(err) 417 } 418 419 txn.End() 420 421 app.ExpectMetrics(t, txnMetrics) 422 } 423 424 func TestDoublyInstrumented(t *testing.T) { 425 hs := &request.Handlers{} 426 if found := hs.Send.Len(); 0 != found { 427 t.Error("unexpected number of Send handlers found:", found) 428 } 429 430 InstrumentHandlers(hs) 431 if found := hs.Send.Len(); 2 != found { 432 t.Error("unexpected number of Send handlers found:", found) 433 } 434 435 InstrumentHandlers(hs) 436 if found := hs.Send.Len(); 2 != found { 437 t.Error("unexpected number of Send handlers found:", found) 438 } 439 } 440 441 type firstFailingTransport struct { 442 failing bool 443 } 444 445 func (t *firstFailingTransport) RoundTrip(r *http.Request) (*http.Response, error) { 446 if t.failing { 447 t.failing = false 448 return nil, errors.New("Oops this failed") 449 } 450 return &http.Response{ 451 Status: "200 OK", 452 StatusCode: 200, 453 Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), 454 Header: http.Header{ 455 "X-Amzn-Requestid": []string{requestID}, 456 }, 457 }, nil 458 } 459 460 func TestRetrySend(t *testing.T) { 461 app := testApp() 462 txn := app.StartTransaction(txnName, nil, nil) 463 464 ses := newSession() 465 ses.Config.HTTPClient.Transport = &firstFailingTransport{failing: true} 466 467 client := lambda.New(ses) 468 input := &lambda.InvokeInput{ 469 ClientContext: aws.String("MyApp"), 470 FunctionName: aws.String("non-existent-function"), 471 InvocationType: aws.String("Event"), 472 LogType: aws.String("Tail"), 473 Payload: []byte("{}"), 474 } 475 476 req, out := client.InvokeRequest(input) 477 InstrumentHandlers(&req.Handlers) 478 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) 479 480 err := req.Send() 481 if nil != err { 482 t.Error(err) 483 } 484 if 200 != *out.StatusCode { 485 t.Error("wrong status code on response", out.StatusCode) 486 } 487 488 txn.End() 489 490 app.ExpectMetrics(t, externalMetrics) 491 app.ExpectSpanEvents(t, []internal.WantEvent{ 492 genericSpan, externalSpanNoRequestID, externalSpan}) 493 } 494 495 func TestRequestSentTwice(t *testing.T) { 496 app := testApp() 497 txn := app.StartTransaction(txnName, nil, nil) 498 499 client := lambda.New(newSession()) 500 input := &lambda.InvokeInput{ 501 ClientContext: aws.String("MyApp"), 502 FunctionName: aws.String("non-existent-function"), 503 InvocationType: aws.String("Event"), 504 LogType: aws.String("Tail"), 505 Payload: []byte("{}"), 506 } 507 508 req, out := client.InvokeRequest(input) 509 InstrumentHandlers(&req.Handlers) 510 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) 511 512 firstErr := req.Send() 513 if nil != firstErr { 514 t.Error(firstErr) 515 } 516 if 200 != *out.StatusCode { 517 t.Error("wrong status code on response", out.StatusCode) 518 } 519 520 secondErr := req.Send() 521 if nil != secondErr { 522 t.Error(secondErr) 523 } 524 if 200 != *out.StatusCode { 525 t.Error("wrong status code on response", out.StatusCode) 526 } 527 528 txn.End() 529 530 app.ExpectMetrics(t, []internal.WantMetric{ 531 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, 532 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, 533 {Name: "External/all", Scope: "", Forced: true, Data: []float64{2}}, 534 {Name: "External/allOther", Scope: "", Forced: true, Data: []float64{2}}, 535 {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: []float64{2}}, 536 {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: []float64{2}}, 537 {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, 538 {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, 539 {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, 540 {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, 541 }) 542 app.ExpectSpanEvents(t, []internal.WantEvent{ 543 genericSpan, externalSpan, externalSpan}) 544 } 545 546 type noRequestIDTransport struct{} 547 548 func (t *noRequestIDTransport) RoundTrip(r *http.Request) (*http.Response, error) { 549 return &http.Response{ 550 Status: "200 OK", 551 StatusCode: 200, 552 Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), 553 }, nil 554 } 555 556 func TestNoRequestIDFound(t *testing.T) { 557 app := testApp() 558 txn := app.StartTransaction(txnName, nil, nil) 559 560 ses := newSession() 561 ses.Config.HTTPClient.Transport = &noRequestIDTransport{} 562 563 client := lambda.New(ses) 564 input := &lambda.InvokeInput{ 565 ClientContext: aws.String("MyApp"), 566 FunctionName: aws.String("non-existent-function"), 567 InvocationType: aws.String("Event"), 568 LogType: aws.String("Tail"), 569 Payload: []byte("{}"), 570 } 571 572 req, out := client.InvokeRequest(input) 573 InstrumentHandlers(&req.Handlers) 574 req.HTTPRequest = newrelic.RequestWithTransactionContext(req.HTTPRequest, txn) 575 576 err := req.Send() 577 if nil != err { 578 t.Error(err) 579 } 580 if 200 != *out.StatusCode { 581 t.Error("wrong status code on response", out.StatusCode) 582 } 583 584 txn.End() 585 586 app.ExpectMetrics(t, externalMetrics) 587 app.ExpectSpanEvents(t, []internal.WantEvent{ 588 genericSpan, externalSpanNoRequestID}) 589 }