diff --git a/v3/integrations/nrawssdk-v2/nrawssdk.go b/v3/integrations/nrawssdk-v2/nrawssdk.go index 2a844bcf5..8ff3a8ab6 100644 --- a/v3/integrations/nrawssdk-v2/nrawssdk.go +++ b/v3/integrations/nrawssdk-v2/nrawssdk.go @@ -50,6 +50,11 @@ func (m nrMiddleware) deserializeMiddleware(stack *smithymiddle.Stack) error { ctx context.Context, in smithymiddle.DeserializeInput, next smithymiddle.DeserializeHandler) ( out smithymiddle.DeserializeOutput, metadata smithymiddle.Metadata, err error) { + txn := m.txn + if txn == nil { + txn = newrelic.FromContext(ctx) + } + smithyRequest := in.Request.(*smithyhttp.Request) // The actual http.Request is inside the smithyhttp.Request @@ -70,10 +75,10 @@ func (m nrMiddleware) deserializeMiddleware(stack *smithymiddle.Stack) error { Host: httpRequest.URL.Host, PortPathOrID: httpRequest.URL.Port(), DatabaseName: "", - StartTime: m.txn.StartSegmentNow(), + StartTime: txn.StartSegmentNow(), } } else { - segment = newrelic.StartExternalSegment(m.txn, httpRequest) + segment = newrelic.StartExternalSegment(txn, httpRequest) } // Hand off execution to other middlewares and then perform the request @@ -84,15 +89,15 @@ func (m nrMiddleware) deserializeMiddleware(stack *smithymiddle.Stack) error { if ok { // Set additional span attributes - integrationsupport.AddAgentSpanAttribute(m.txn, + integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeResponseCode, strconv.Itoa(response.StatusCode)) - integrationsupport.AddAgentSpanAttribute(m.txn, + integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSOperation, operation) - integrationsupport.AddAgentSpanAttribute(m.txn, + integrationsupport.AddAgentSpanAttribute(txn, newrelic.SpanAttributeAWSRegion, region) requestID, ok := awsmiddle.GetRequestIDMetadata(metadata) if ok { - integrationsupport.AddAgentSpanAttribute(m.txn, + integrationsupport.AddAgentSpanAttribute(txn, newrelic.AttributeAWSRequestID, requestID) } } @@ -105,19 +110,34 @@ func (m nrMiddleware) deserializeMiddleware(stack *smithymiddle.Stack) error { // AppendMiddlewares inserts New Relic middleware in the given `apiOptions` for // the AWS SDK V2 for Go. It must be called only once per AWS configuration. // +// If `txn` is provided as nil, the New Relic transaction will be retrieved +// using `newrelic.FromContext`. +// // Additional attributes will be added to transaction trace segments and span // events: aws.region, aws.requestId, and aws.operation. In addition, // http.statusCode will be added to span events. // -// To see segments and spans for each AWS invocation, call AppendMiddlewares -// with the AWS Config `apiOptions` and pass in your current New Relic -// transaction. For example: +// To see segments and spans for all AWS invocations, call AppendMiddlewares +// with the AWS Config `apiOptions` and provide nil for `txn`. For example: +// +// awsConfig, err := config.LoadDefaultConfig(ctx) +// if err != nil { +// log.Fatal(err) +// } +// nraws.AppendMiddlewares(&awsConfig.APIOptions, nil) +// +// If do not want the transaction to be retrived from the context, you can +// explicitly set `txn`. For example: +// +// awsConfig, err := config.LoadDefaultConfig(ctx) +// if err != nil { +// log.Fatal(err) +// } +// +// ... // -// awsConfig, err := config.LoadDefaultConfig(ctx) -// if err != nil { -// log.Fatal(err) -// } -// nraws.AppendMiddlewares(ctx, &awsConfig.APIOptions, txn) +// txn := loadNewRelicTransaction() +// nraws.AppendMiddlewares(&awsConfig.APIOptions, txn) func AppendMiddlewares(apiOptions *[]func(*smithymiddle.Stack) error, txn *newrelic.Transaction) { m := nrMiddleware{txn: txn} *apiOptions = append(*apiOptions, m.deserializeMiddleware) diff --git a/v3/integrations/nrawssdk-v2/nrawssdk_test.go b/v3/integrations/nrawssdk-v2/nrawssdk_test.go index 521d8c741..c57bba514 100644 --- a/v3/integrations/nrawssdk-v2/nrawssdk_test.go +++ b/v3/integrations/nrawssdk-v2/nrawssdk_test.go @@ -188,54 +188,121 @@ var ( }...) ) -func TestInstrumentRequestExternal(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName) - ctx := context.TODO() - - client := lambda.NewFromConfig(newConfig(ctx, txn)) - - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: types.InvocationTypeRequestResponse, - LogType: types.LogTypeTail, - Payload: []byte("{}"), - } +type testTableEntry struct { + Name string + + BuildContext func(txn *newrelic.Transaction) context.Context + BuildConfig func(ctx context.Context, txn *newrelic.Transaction) aws.Config +} - _, err := client.Invoke(ctx, input) - if err != nil { - t.Error(err) +func runTestTable(t *testing.T, table []*testTableEntry, executeEntry func(t *testing.T, entry *testTableEntry)) { + for _, entry := range table { + entry := entry // Pin range variable + + t.Run(entry.Name, func(t *testing.T) { + executeEntry(t, entry) + }) } +} + +func TestInstrumentRequestExternal(t *testing.T) { + runTestTable(t, + []*testTableEntry{ + { + Name: "with manually set transaction", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return context.Background() + }, + BuildConfig: newConfig, + }, + { + Name: "with transaction set in context", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return newrelic.NewContext(context.Background(), txn) + }, + BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { + return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context + }, + }, + }, + + func(t *testing.T, entry *testTableEntry) { + app := testApp() + txn := app.StartTransaction(txnName) + ctx := entry.BuildContext(txn) - txn.End() + client := lambda.NewFromConfig(entry.BuildConfig(ctx, txn)) - app.ExpectMetrics(t, externalMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - externalSpan, genericSpan}) + input := &lambda.InvokeInput{ + ClientContext: aws.String("MyApp"), + FunctionName: aws.String("non-existent-function"), + InvocationType: types.InvocationTypeRequestResponse, + LogType: types.LogTypeTail, + Payload: []byte("{}"), + } + + _, err := client.Invoke(ctx, input) + if err != nil { + t.Error(err) + } + + txn.End() + + app.ExpectMetrics(t, externalMetrics) + app.ExpectSpanEvents(t, []internal.WantEvent{ + externalSpan, genericSpan}) + }, + ) } func TestInstrumentRequestDatastore(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName) - ctx := context.TODO() + runTestTable(t, + []*testTableEntry{ + { + Name: "with manually set transaction", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return context.Background() + }, + BuildConfig: newConfig, + }, + { + Name: "with transaction set in context", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return newrelic.NewContext(context.Background(), txn) + }, + BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { + return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context + }, + }, + }, - client := dynamodb.NewFromConfig(newConfig(ctx, txn)) + func(t *testing.T, entry *testTableEntry) { + app := testApp() + txn := app.StartTransaction(txnName) + ctx := entry.BuildContext(txn) - input := &dynamodb.DescribeTableInput{ - TableName: aws.String("thebesttable"), - } + client := dynamodb.NewFromConfig(entry.BuildConfig(ctx, txn)) - _, err := client.DescribeTable(ctx, input) - if err != nil { - t.Error(err) - } + input := &dynamodb.DescribeTableInput{ + TableName: aws.String("thebesttable"), + } + + _, err := client.DescribeTable(ctx, input) + if err != nil { + t.Error(err) + } - txn.End() + txn.End() - app.ExpectMetrics(t, datastoreMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - datastoreSpan, genericSpan}) + app.ExpectMetrics(t, datastoreMetrics) + app.ExpectSpanEvents(t, []internal.WantEvent{ + datastoreSpan, genericSpan}) + }, + ) } type firstFailingTransport struct { @@ -259,142 +326,192 @@ func (t *firstFailingTransport) RoundTrip(r *http.Request) (*http.Response, erro } func TestRetrySend(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName) - ctx := context.TODO() - - cfg := newConfig(ctx, txn) - - cfg.HTTPClient = &http.Client{ - Transport: &firstFailingTransport{failing: true}, - } - - customRetry := retry.NewStandard(func(o *retry.StandardOptions) { - o.MaxAttempts = 2 - }) - client := lambda.NewFromConfig(cfg, func(o *lambda.Options) { - o.Retryer = customRetry - }) - - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: types.InvocationTypeRequestResponse, - LogType: types.LogTypeTail, - Payload: []byte("{}"), - } - - _, err := client.Invoke(ctx, input) - if err != nil { - t.Error(err) - } - - txn.End() - - app.ExpectMetrics(t, externalMetrics) - - app.ExpectSpanEvents(t, []internal.WantEvent{ - { - Intrinsics: map[string]interface{}{ - "name": "External/lambda.us-west-2.amazonaws.com/http/POST", - "sampled": true, - "category": "http", - "priority": internal.MatchAnything, - "guid": internal.MatchAnything, - "transactionId": internal.MatchAnything, - "traceId": internal.MatchAnything, - "parentId": internal.MatchAnything, - "component": "http", - "span.kind": "client", + runTestTable(t, + []*testTableEntry{ + { + Name: "with manually set transaction", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return context.Background() + }, + BuildConfig: newConfig, }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{ - "aws.operation": "Invoke", - "aws.region": awsRegion, - "http.method": "POST", - "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", - "http.statusCode": "0", + { + Name: "with transaction set in context", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return newrelic.NewContext(context.Background(), txn) + }, + BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { + return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context + }, }, - }, { - Intrinsics: map[string]interface{}{ - "name": "External/lambda.us-west-2.amazonaws.com/http/POST", - "sampled": true, - "category": "http", - "priority": internal.MatchAnything, - "guid": internal.MatchAnything, - "transactionId": internal.MatchAnything, - "traceId": internal.MatchAnything, - "parentId": internal.MatchAnything, - "component": "http", - "span.kind": "client", - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{ - "aws.operation": "Invoke", - "aws.region": awsRegion, - "aws.requestId": requestID, - "http.method": "POST", - "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", - "http.statusCode": "200", - }, - }, { - Intrinsics: map[string]interface{}{ - "name": "OtherTransaction/Go/" + txnName, - "transaction.name": "OtherTransaction/Go/" + txnName, - "sampled": true, - "category": "generic", - "priority": internal.MatchAnything, - "guid": internal.MatchAnything, - "transactionId": internal.MatchAnything, - "nr.entryPoint": true, - "traceId": internal.MatchAnything, - }, - UserAttributes: map[string]interface{}{}, - AgentAttributes: map[string]interface{}{}, - }}) + }, + + func(t *testing.T, entry *testTableEntry) { + app := testApp() + txn := app.StartTransaction(txnName) + ctx := entry.BuildContext(txn) + + cfg := entry.BuildConfig(ctx, txn) + + cfg.HTTPClient = &http.Client{ + Transport: &firstFailingTransport{failing: true}, + } + + customRetry := retry.NewStandard(func(o *retry.StandardOptions) { + o.MaxAttempts = 2 + }) + client := lambda.NewFromConfig(cfg, func(o *lambda.Options) { + o.Retryer = customRetry + }) + + input := &lambda.InvokeInput{ + ClientContext: aws.String("MyApp"), + FunctionName: aws.String("non-existent-function"), + InvocationType: types.InvocationTypeRequestResponse, + LogType: types.LogTypeTail, + Payload: []byte("{}"), + } + + _, err := client.Invoke(ctx, input) + if err != nil { + t.Error(err) + } + + txn.End() + + app.ExpectMetrics(t, externalMetrics) + + app.ExpectSpanEvents(t, []internal.WantEvent{ + { + Intrinsics: map[string]interface{}{ + "name": "External/lambda.us-west-2.amazonaws.com/http/POST", + "sampled": true, + "category": "http", + "priority": internal.MatchAnything, + "guid": internal.MatchAnything, + "transactionId": internal.MatchAnything, + "traceId": internal.MatchAnything, + "parentId": internal.MatchAnything, + "component": "http", + "span.kind": "client", + }, + UserAttributes: map[string]interface{}{}, + AgentAttributes: map[string]interface{}{ + "aws.operation": "Invoke", + "aws.region": awsRegion, + "http.method": "POST", + "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", + "http.statusCode": "0", + }, + }, { + Intrinsics: map[string]interface{}{ + "name": "External/lambda.us-west-2.amazonaws.com/http/POST", + "sampled": true, + "category": "http", + "priority": internal.MatchAnything, + "guid": internal.MatchAnything, + "transactionId": internal.MatchAnything, + "traceId": internal.MatchAnything, + "parentId": internal.MatchAnything, + "component": "http", + "span.kind": "client", + }, + UserAttributes: map[string]interface{}{}, + AgentAttributes: map[string]interface{}{ + "aws.operation": "Invoke", + "aws.region": awsRegion, + "aws.requestId": requestID, + "http.method": "POST", + "http.url": "https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/non-existent-function/invocations", + "http.statusCode": "200", + }, + }, { + Intrinsics: map[string]interface{}{ + "name": "OtherTransaction/Go/" + txnName, + "transaction.name": "OtherTransaction/Go/" + txnName, + "sampled": true, + "category": "generic", + "priority": internal.MatchAnything, + "guid": internal.MatchAnything, + "transactionId": internal.MatchAnything, + "nr.entryPoint": true, + "traceId": internal.MatchAnything, + }, + UserAttributes: map[string]interface{}{}, + AgentAttributes: map[string]interface{}{}, + }}) + }, + ) } func TestRequestSentTwice(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName) - ctx := context.TODO() - - client := lambda.NewFromConfig(newConfig(ctx, txn)) - - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: types.InvocationTypeRequestResponse, - LogType: types.LogTypeTail, - Payload: []byte("{}"), - } - - _, firstErr := client.Invoke(ctx, input) - if firstErr != nil { - t.Error(firstErr) - } - - _, secondErr := client.Invoke(ctx, input) - if secondErr != nil { - t.Error(secondErr) - } - - txn.End() + runTestTable(t, + []*testTableEntry{ + { + Name: "with manually set transaction", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return context.Background() + }, + BuildConfig: newConfig, + }, + { + Name: "with transaction set in context", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return newrelic.NewContext(context.Background(), txn) + }, + BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { + return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context + }, + }, + }, - app.ExpectMetrics(t, []internal.WantMetric{ - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, - {Name: "External/all", Scope: "", Forced: true, Data: []float64{2}}, - {Name: "External/allOther", Scope: "", Forced: true, Data: []float64{2}}, - {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: []float64{2}}, - {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: []float64{2}}, - {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, - {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, - {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, - {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, - }) - app.ExpectSpanEvents(t, []internal.WantEvent{ - externalSpan, externalSpan, genericSpan}) + func(t *testing.T, entry *testTableEntry) { + app := testApp() + txn := app.StartTransaction(txnName) + ctx := entry.BuildContext(txn) + + client := lambda.NewFromConfig(entry.BuildConfig(ctx, txn)) + + input := &lambda.InvokeInput{ + ClientContext: aws.String("MyApp"), + FunctionName: aws.String("non-existent-function"), + InvocationType: types.InvocationTypeRequestResponse, + LogType: types.LogTypeTail, + Payload: []byte("{}"), + } + + _, firstErr := client.Invoke(ctx, input) + if firstErr != nil { + t.Error(firstErr) + } + + _, secondErr := client.Invoke(ctx, input) + if secondErr != nil { + t.Error(secondErr) + } + + txn.End() + + app.ExpectMetrics(t, []internal.WantMetric{ + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: nil}, + {Name: "External/all", Scope: "", Forced: true, Data: []float64{2}}, + {Name: "External/allOther", Scope: "", Forced: true, Data: []float64{2}}, + {Name: "External/lambda.us-west-2.amazonaws.com/all", Scope: "", Forced: false, Data: []float64{2}}, + {Name: "External/lambda.us-west-2.amazonaws.com/http/POST", Scope: "OtherTransaction/Go/" + txnName, Forced: false, Data: []float64{2}}, + {Name: "OtherTransaction/Go/" + txnName, Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransactionTotalTime/Go/" + txnName, Scope: "", Forced: false, Data: nil}, + {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, + }) + app.ExpectSpanEvents(t, []internal.WantEvent{ + externalSpan, externalSpan, genericSpan}) + }, + ) } type noRequestIDTransport struct{} @@ -408,31 +525,56 @@ func (t *noRequestIDTransport) RoundTrip(r *http.Request) (*http.Response, error } func TestNoRequestIDFound(t *testing.T) { - app := testApp() - txn := app.StartTransaction(txnName) - ctx := context.TODO() - - cfg := newConfig(ctx, txn) - cfg.HTTPClient = &http.Client{ - Transport: &noRequestIDTransport{}, - } - client := lambda.NewFromConfig(cfg) - - input := &lambda.InvokeInput{ - ClientContext: aws.String("MyApp"), - FunctionName: aws.String("non-existent-function"), - InvocationType: types.InvocationTypeRequestResponse, - LogType: types.LogTypeTail, - Payload: []byte("{}"), - } - _, err := client.Invoke(ctx, input) - if err != nil { - t.Error(err) - } - - txn.End() + runTestTable(t, + []*testTableEntry{ + { + Name: "with manually set transaction", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return context.Background() + }, + BuildConfig: newConfig, + }, + { + Name: "with transaction set in context", + + BuildContext: func(txn *newrelic.Transaction) context.Context { + return newrelic.NewContext(context.Background(), txn) + }, + BuildConfig: func(ctx context.Context, txn *newrelic.Transaction) aws.Config { + return newConfig(ctx, nil) // Set txn to nil to ensure transaction is retrieved from the context + }, + }, + }, - app.ExpectMetrics(t, externalMetrics) - app.ExpectSpanEvents(t, []internal.WantEvent{ - externalSpanNoRequestID, genericSpan}) + func(t *testing.T, entry *testTableEntry) { + app := testApp() + txn := app.StartTransaction(txnName) + ctx := entry.BuildContext(txn) + + cfg := entry.BuildConfig(ctx, txn) + cfg.HTTPClient = &http.Client{ + Transport: &noRequestIDTransport{}, + } + client := lambda.NewFromConfig(cfg) + + input := &lambda.InvokeInput{ + ClientContext: aws.String("MyApp"), + FunctionName: aws.String("non-existent-function"), + InvocationType: types.InvocationTypeRequestResponse, + LogType: types.LogTypeTail, + Payload: []byte("{}"), + } + _, err := client.Invoke(ctx, input) + if err != nil { + t.Error(err) + } + + txn.End() + + app.ExpectMetrics(t, externalMetrics) + app.ExpectSpanEvents(t, []internal.WantEvent{ + externalSpanNoRequestID, genericSpan}) + }, + ) }