github.com/newrelic/go-agent@v3.26.0+incompatible/_integrations/nrlambda/handler_test.go (about) 1 // Copyright 2020 New Relic Corporation. All rights reserved. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package nrlambda 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "errors" 11 "strings" 12 "testing" 13 14 "github.com/aws/aws-lambda-go/events" 15 "github.com/aws/aws-lambda-go/lambdacontext" 16 newrelic "github.com/newrelic/go-agent" 17 "github.com/newrelic/go-agent/internal" 18 ) 19 20 func dataShouldContain(tb testing.TB, data map[string]json.RawMessage, keys ...string) { 21 if h, ok := tb.(interface { 22 Helper() 23 }); ok { 24 h.Helper() 25 } 26 if len(data) != len(keys) { 27 tb.Errorf("data key length mismatch, expected=%v got=%v", 28 len(keys), len(data)) 29 return 30 } 31 for _, k := range keys { 32 _, ok := data[k] 33 if !ok { 34 tb.Errorf("data does not contain key %v", k) 35 } 36 } 37 } 38 39 func testApp(getenv func(string) string, t *testing.T) newrelic.Application { 40 if nil == getenv { 41 getenv = func(string) string { return "" } 42 } 43 cfg := newConfigInternal(getenv) 44 45 app, err := newrelic.NewApplication(cfg) 46 if nil != err { 47 t.Fatal(err) 48 } 49 internal.HarvestTesting(app, nil) 50 return app 51 } 52 53 func distributedTracingEnabled(key string) string { 54 switch key { 55 case "NEW_RELIC_ACCOUNT_ID": 56 return "1" 57 case "NEW_RELIC_TRUSTED_ACCOUNT_KEY": 58 return "1" 59 case "NEW_RELIC_PRIMARY_APPLICATION_ID": 60 return "1" 61 default: 62 return "" 63 } 64 } 65 66 func TestColdStart(t *testing.T) { 67 originalHandler := func(c context.Context) {} 68 app := testApp(nil, t) 69 wrapped := Wrap(originalHandler, app) 70 w := wrapped.(*wrappedHandler) 71 w.functionName = "functionName" 72 buf := &bytes.Buffer{} 73 w.writer = buf 74 75 ctx := context.Background() 76 lctx := &lambdacontext.LambdaContext{ 77 AwsRequestID: "request-id", 78 InvokedFunctionArn: "function-arn", 79 } 80 ctx = lambdacontext.NewContext(ctx, lctx) 81 82 resp, err := wrapped.Invoke(ctx, nil) 83 if nil != err || string(resp) != "null" { 84 t.Error("unexpected response", err, string(resp)) 85 } 86 app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ 87 Intrinsics: map[string]interface{}{ 88 "name": "OtherTransaction/Go/functionName", 89 "guid": internal.MatchAnything, 90 "priority": internal.MatchAnything, 91 "sampled": internal.MatchAnything, 92 "traceId": internal.MatchAnything, 93 }, 94 UserAttributes: map[string]interface{}{}, 95 AgentAttributes: map[string]interface{}{ 96 "aws.requestId": "request-id", 97 "aws.lambda.arn": "function-arn", 98 "aws.lambda.coldStart": true, 99 }, 100 }}) 101 metadata, data, err := internal.ParseServerlessPayload(buf.Bytes()) 102 if err != nil { 103 t.Error(err) 104 } 105 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") 106 if v := string(metadata["arn"]); v != `"function-arn"` { 107 t.Error(metadata) 108 } 109 110 // Invoke the handler again to test the cold-start attribute absence. 111 buf = &bytes.Buffer{} 112 w.writer = buf 113 internal.HarvestTesting(app, nil) 114 resp, err = wrapped.Invoke(ctx, nil) 115 if nil != err || string(resp) != "null" { 116 t.Error("unexpected response", err, string(resp)) 117 } 118 app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ 119 Intrinsics: map[string]interface{}{ 120 "name": "OtherTransaction/Go/functionName", 121 "guid": internal.MatchAnything, 122 "priority": internal.MatchAnything, 123 "sampled": internal.MatchAnything, 124 "traceId": internal.MatchAnything, 125 }, 126 UserAttributes: map[string]interface{}{}, 127 AgentAttributes: map[string]interface{}{ 128 "aws.requestId": "request-id", 129 "aws.lambda.arn": "function-arn", 130 }, 131 }}) 132 metadata, data, err = internal.ParseServerlessPayload(buf.Bytes()) 133 if err != nil { 134 t.Error(err) 135 } 136 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") 137 if v := string(metadata["arn"]); v != `"function-arn"` { 138 t.Error(metadata) 139 } 140 } 141 142 func TestErrorCapture(t *testing.T) { 143 returnError := errors.New("problem") 144 originalHandler := func() error { return returnError } 145 app := testApp(nil, t) 146 wrapped := Wrap(originalHandler, app) 147 w := wrapped.(*wrappedHandler) 148 w.functionName = "functionName" 149 buf := &bytes.Buffer{} 150 w.writer = buf 151 152 resp, err := wrapped.Invoke(context.Background(), nil) 153 if err != returnError || string(resp) != "" { 154 t.Error(err, string(resp)) 155 } 156 app.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ 157 {Name: "OtherTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, 158 {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, 159 {Name: "OtherTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, 160 {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, 161 // Error metrics test the error capture. 162 {Name: "Errors/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, 163 {Name: "Errors/allOther", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, 164 {Name: "Errors/OtherTransaction/Go/functionName", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, 165 {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, 166 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, 167 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, 168 {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, 169 }) 170 app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ 171 Intrinsics: map[string]interface{}{ 172 "name": "OtherTransaction/Go/functionName", 173 "guid": internal.MatchAnything, 174 "priority": internal.MatchAnything, 175 "sampled": internal.MatchAnything, 176 "traceId": internal.MatchAnything, 177 }, 178 UserAttributes: map[string]interface{}{}, 179 AgentAttributes: map[string]interface{}{ 180 "aws.lambda.coldStart": true, 181 }, 182 }}) 183 _, data, err := internal.ParseServerlessPayload(buf.Bytes()) 184 if err != nil { 185 t.Error(err) 186 } 187 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data", 188 "error_event_data", "error_data") 189 } 190 191 func TestWrapNilApp(t *testing.T) { 192 originalHandler := func() (int, error) { 193 return 123, nil 194 } 195 wrapped := Wrap(originalHandler, nil) 196 ctx := context.Background() 197 resp, err := wrapped.Invoke(ctx, nil) 198 if nil != err || string(resp) != "123" { 199 t.Error("unexpected response", err, string(resp)) 200 } 201 } 202 203 func TestSetWebRequest(t *testing.T) { 204 originalHandler := func(events.APIGatewayProxyRequest) {} 205 app := testApp(nil, t) 206 wrapped := Wrap(originalHandler, app) 207 w := wrapped.(*wrappedHandler) 208 w.functionName = "functionName" 209 buf := &bytes.Buffer{} 210 w.writer = buf 211 212 req := events.APIGatewayProxyRequest{ 213 Headers: map[string]string{ 214 "X-Forwarded-Port": "4000", 215 "X-Forwarded-Proto": "HTTPS", 216 }, 217 } 218 reqbytes, err := json.Marshal(req) 219 if err != nil { 220 t.Error("unable to marshal json", err) 221 } 222 223 resp, err := wrapped.Invoke(context.Background(), reqbytes) 224 if err != nil { 225 t.Error(err, string(resp)) 226 } 227 app.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ 228 {Name: "Apdex", Scope: "", Forced: true, Data: nil}, 229 {Name: "Apdex/Go/functionName", Scope: "", Forced: false, Data: nil}, 230 {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, 231 {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, 232 {Name: "WebTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, 233 {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, 234 {Name: "WebTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, 235 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, 236 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, 237 }) 238 app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ 239 Intrinsics: map[string]interface{}{ 240 "name": "WebTransaction/Go/functionName", 241 "nr.apdexPerfZone": "S", 242 "guid": internal.MatchAnything, 243 "priority": internal.MatchAnything, 244 "sampled": internal.MatchAnything, 245 "traceId": internal.MatchAnything, 246 }, 247 UserAttributes: map[string]interface{}{}, 248 AgentAttributes: map[string]interface{}{ 249 "aws.lambda.coldStart": true, 250 "request.uri": "//:4000", 251 }, 252 }}) 253 _, data, err := internal.ParseServerlessPayload(buf.Bytes()) 254 if err != nil { 255 t.Error(err) 256 } 257 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") 258 } 259 260 func makePayload(app newrelic.Application) string { 261 txn := app.StartTransaction("hello", nil, nil) 262 return txn.CreateDistributedTracePayload().Text() 263 } 264 265 func TestDistributedTracing(t *testing.T) { 266 originalHandler := func(events.APIGatewayProxyRequest) {} 267 app := testApp(distributedTracingEnabled, t) 268 wrapped := Wrap(originalHandler, app) 269 w := wrapped.(*wrappedHandler) 270 w.functionName = "functionName" 271 buf := &bytes.Buffer{} 272 w.writer = buf 273 274 req := events.APIGatewayProxyRequest{ 275 Headers: map[string]string{ 276 "X-Forwarded-Port": "4000", 277 "X-Forwarded-Proto": "HTTPS", 278 newrelic.DistributedTracePayloadHeader: makePayload(app), 279 }, 280 } 281 reqbytes, err := json.Marshal(req) 282 if err != nil { 283 t.Error("unable to marshal json", err) 284 } 285 286 resp, err := wrapped.Invoke(context.Background(), reqbytes) 287 if err != nil { 288 t.Error(err, string(resp)) 289 } 290 app.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ 291 {Name: "Apdex", Scope: "", Forced: true, Data: nil}, 292 {Name: "Apdex/Go/functionName", Scope: "", Forced: false, Data: nil}, 293 {Name: "DurationByCaller/App/1/1/HTTPS/all", Scope: "", Forced: false, Data: nil}, 294 {Name: "DurationByCaller/App/1/1/HTTPS/allWeb", Scope: "", Forced: false, Data: nil}, 295 {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, 296 {Name: "Supportability/DistributedTrace/AcceptPayload/Success", Scope: "", Forced: true, Data: nil}, 297 {Name: "TransportDuration/App/1/1/HTTPS/all", Scope: "", Forced: false, Data: nil}, 298 {Name: "TransportDuration/App/1/1/HTTPS/allWeb", Scope: "", Forced: false, Data: nil}, 299 {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, 300 {Name: "WebTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, 301 {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, 302 {Name: "WebTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, 303 }) 304 app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ 305 Intrinsics: map[string]interface{}{ 306 "name": "WebTransaction/Go/functionName", 307 "nr.apdexPerfZone": "S", 308 "parent.account": "1", 309 "parent.app": "1", 310 "parent.transportType": "HTTPS", 311 "parent.type": "App", 312 "guid": internal.MatchAnything, 313 "parent.transportDuration": internal.MatchAnything, 314 "parentId": internal.MatchAnything, 315 "parentSpanId": internal.MatchAnything, 316 "priority": internal.MatchAnything, 317 "sampled": internal.MatchAnything, 318 "traceId": internal.MatchAnything, 319 }, 320 UserAttributes: map[string]interface{}{}, 321 AgentAttributes: map[string]interface{}{ 322 "aws.lambda.coldStart": true, 323 "request.uri": "//:4000", 324 }, 325 }}) 326 _, data, err := internal.ParseServerlessPayload(buf.Bytes()) 327 if err != nil { 328 t.Error(err) 329 } 330 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") 331 } 332 333 func TestEventARN(t *testing.T) { 334 originalHandler := func(events.DynamoDBEvent) {} 335 app := testApp(nil, t) 336 wrapped := Wrap(originalHandler, app) 337 w := wrapped.(*wrappedHandler) 338 w.functionName = "functionName" 339 buf := &bytes.Buffer{} 340 w.writer = buf 341 342 req := events.DynamoDBEvent{ 343 Records: []events.DynamoDBEventRecord{{ 344 EventSourceArn: "ARN", 345 }}, 346 } 347 348 reqbytes, err := json.Marshal(req) 349 if err != nil { 350 t.Error("unable to marshal json", err) 351 } 352 353 resp, err := wrapped.Invoke(context.Background(), reqbytes) 354 if err != nil { 355 t.Error(err, string(resp)) 356 } 357 app.(internal.Expect).ExpectMetrics(t, []internal.WantMetric{ 358 {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, 359 {Name: "OtherTransaction/Go/functionName", Scope: "", Forced: true, Data: nil}, 360 {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, 361 {Name: "OtherTransactionTotalTime/Go/functionName", Scope: "", Forced: false, Data: nil}, 362 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, 363 {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, 364 }) 365 app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ 366 Intrinsics: map[string]interface{}{ 367 "name": "OtherTransaction/Go/functionName", 368 "guid": internal.MatchAnything, 369 "priority": internal.MatchAnything, 370 "sampled": internal.MatchAnything, 371 "traceId": internal.MatchAnything, 372 }, 373 UserAttributes: map[string]interface{}{}, 374 AgentAttributes: map[string]interface{}{ 375 "aws.lambda.coldStart": true, 376 "aws.lambda.eventSource.arn": "ARN", 377 }, 378 }}) 379 _, data, err := internal.ParseServerlessPayload(buf.Bytes()) 380 if err != nil { 381 t.Error(err) 382 } 383 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") 384 } 385 386 func TestAPIGatewayProxyResponse(t *testing.T) { 387 originalHandler := func() (events.APIGatewayProxyResponse, error) { 388 return events.APIGatewayProxyResponse{ 389 Body: "Hello World", 390 StatusCode: 200, 391 Headers: map[string]string{ 392 "Content-Type": "text/html", 393 }, 394 }, nil 395 } 396 397 app := testApp(nil, t) 398 wrapped := Wrap(originalHandler, app) 399 w := wrapped.(*wrappedHandler) 400 w.functionName = "functionName" 401 buf := &bytes.Buffer{} 402 w.writer = buf 403 404 resp, err := wrapped.Invoke(context.Background(), nil) 405 if nil != err { 406 t.Error("unexpected err", err) 407 } 408 if !strings.Contains(string(resp), "Hello World") { 409 t.Error("unexpected response", string(resp)) 410 } 411 412 app.(internal.Expect).ExpectTxnEvents(t, []internal.WantEvent{{ 413 Intrinsics: map[string]interface{}{ 414 "name": "OtherTransaction/Go/functionName", 415 "guid": internal.MatchAnything, 416 "priority": internal.MatchAnything, 417 "sampled": internal.MatchAnything, 418 "traceId": internal.MatchAnything, 419 }, 420 UserAttributes: map[string]interface{}{}, 421 AgentAttributes: map[string]interface{}{ 422 "aws.lambda.coldStart": true, 423 "httpResponseCode": "200", 424 "response.headers.contentType": "text/html", 425 }, 426 }}) 427 _, data, err := internal.ParseServerlessPayload(buf.Bytes()) 428 if err != nil { 429 t.Error(err) 430 } 431 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data") 432 } 433 434 func TestCustomEvent(t *testing.T) { 435 originalHandler := func(c context.Context) { 436 if txn := newrelic.FromContext(c); nil != txn { 437 txn.Application().RecordCustomEvent("myEvent", map[string]interface{}{ 438 "zip": "zap", 439 }) 440 } 441 } 442 app := testApp(nil, t) 443 wrapped := Wrap(originalHandler, app) 444 w := wrapped.(*wrappedHandler) 445 w.functionName = "functionName" 446 buf := &bytes.Buffer{} 447 w.writer = buf 448 449 resp, err := wrapped.Invoke(context.Background(), nil) 450 if nil != err || string(resp) != "null" { 451 t.Error("unexpected response", err, string(resp)) 452 } 453 app.(internal.Expect).ExpectCustomEvents(t, []internal.WantEvent{{ 454 Intrinsics: map[string]interface{}{ 455 "type": "myEvent", 456 "timestamp": internal.MatchAnything, 457 }, 458 UserAttributes: map[string]interface{}{ 459 "zip": "zap", 460 }, 461 AgentAttributes: map[string]interface{}{}, 462 }}) 463 _, data, err := internal.ParseServerlessPayload(buf.Bytes()) 464 if err != nil { 465 t.Error(err) 466 } 467 dataShouldContain(t, data, "metric_data", "analytic_event_data", "span_event_data", "custom_event_data") 468 }