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  }