diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventType.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventType.cs index 5e1835bc56..62ab6bfb0d 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventType.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventType.cs @@ -7,6 +7,7 @@ public enum AwsLambdaEventType { Unknown, APIGatewayProxyRequest, + APIGatewayHttpApiV2ProxyRequest, ApplicationLoadBalancerRequest, CloudWatchScheduledEvent, KinesisStreamingEvent, diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventTypeExtensions.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventTypeExtensions.cs index 201442e491..a04571dec7 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventTypeExtensions.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/AwsLambdaEventTypeExtensions.cs @@ -12,6 +12,7 @@ public static AwsLambdaEventType ToEventType(this string typeFullName) return typeFullName switch { "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest" => AwsLambdaEventType.APIGatewayProxyRequest, + "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest" => AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest, "Amazon.Lambda.ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest" => AwsLambdaEventType.ApplicationLoadBalancerRequest, "Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent" => AwsLambdaEventType.CloudWatchScheduledEvent, "Amazon.Lambda.KinesisEvents.KinesisEvent" => AwsLambdaEventType.KinesisStreamingEvent, @@ -32,6 +33,7 @@ public static string ToEventTypeString(this AwsLambdaEventType eventType) return eventType switch { AwsLambdaEventType.APIGatewayProxyRequest => "apiGateway", + AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest => "apiGateway", AwsLambdaEventType.ApplicationLoadBalancerRequest => "alb", AwsLambdaEventType.CloudWatchScheduledEvent => "cloudWatch_scheduled", AwsLambdaEventType.KinesisStreamingEvent => "kinesis", @@ -48,6 +50,6 @@ public static string ToEventTypeString(this AwsLambdaEventType eventType) public static bool IsWebEvent(this AwsLambdaEventType eventType) { - return eventType == AwsLambdaEventType.APIGatewayProxyRequest || eventType == AwsLambdaEventType.ApplicationLoadBalancerRequest; + return eventType == AwsLambdaEventType.APIGatewayProxyRequest || eventType == AwsLambdaEventType.ApplicationLoadBalancerRequest || eventType == AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest; } } diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/LambdaEventHelpers.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/LambdaEventHelpers.cs index ca1ecd0042..a0551a2a36 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/LambdaEventHelpers.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Lambda/LambdaEventHelpers.cs @@ -23,7 +23,7 @@ public static void AddEventTypeAttributes(IAgent agent, ITransaction transaction { case AwsLambdaEventType.APIGatewayProxyRequest: dynamic apiReqEvent = inputObject; // Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest - SetWebRequestProperties(agent, transaction, apiReqEvent); + SetWebRequestProperties(agent, transaction, apiReqEvent, eventType); if (apiReqEvent.RequestContext != null) { @@ -34,14 +34,29 @@ public static void AddEventTypeAttributes(IAgent agent, ITransaction transaction transaction.AddEventSourceAttribute("resourceId", (string)requestContext.ResourceId); transaction.AddEventSourceAttribute("resourcePath", (string)requestContext.ResourcePath); transaction.AddEventSourceAttribute("stage", (string)requestContext.Stage); + } + break; + + case AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest: + dynamic apiReqv2Event = inputObject; // Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest + SetWebRequestProperties(agent, transaction, apiReqv2Event, eventType); + if (apiReqv2Event.RequestContext != null) + { + dynamic requestContext = apiReqv2Event.RequestContext; + // arn is not available + transaction.AddEventSourceAttribute("accountId", (string)requestContext.AccountId); + transaction.AddEventSourceAttribute("apiId", (string)requestContext.ApiId); + // resourceId is not available for v2 + // resourcePath is not available for v2 + transaction.AddEventSourceAttribute("stage", (string)requestContext.Stage); } break; case AwsLambdaEventType.ApplicationLoadBalancerRequest: dynamic albReqEvent = inputObject; //Amazon.Lambda.ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest - SetWebRequestProperties(agent, transaction, albReqEvent); + SetWebRequestProperties(agent, transaction, albReqEvent, eventType); transaction.AddEventSourceAttribute("arn", (string)albReqEvent.RequestContext.Elb.TargetGroupArn); break; @@ -231,26 +246,30 @@ private static void TryParseSNSDistributedTraceHeaders(dynamic snsEvent, ITransa transaction.AcceptDistributedTraceHeaders(snsHeaders, GetHeaderValue, TransportType.Other); } - private static void SetWebRequestProperties(IAgent agent, ITransaction transaction, dynamic webReqEvent) + private static void SetWebRequestProperties(IAgent agent, ITransaction transaction, dynamic webReqEvent, AwsLambdaEventType eventType) { //HTTP headers IDictionary headers = webReqEvent.Headers; Func, string, string> headersGetter = (h, k) => h[k]; - IDictionary> multiValueHeaders = webReqEvent.MultiValueHeaders; - Func>, string, string> multiValueHeadersGetter = (h, k) => string.Join(",", h[k]); - - if (multiValueHeaders != null) + if (eventType != AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest) // v2 doesn't have MultiValueHeaders { - transaction.SetRequestHeaders(multiValueHeaders, agent.Configuration.AllowAllRequestHeaders ? multiValueHeaders.Keys : Statics.DefaultCaptureHeaders, multiValueHeadersGetter); + IDictionary> multiValueHeaders = webReqEvent.MultiValueHeaders; + Func>, string, string> multiValueHeadersGetter = (h, k) => string.Join(",", h[k]); - // DT transport comes from the X-Forwarded-Proto header, if present - var forwardedProto = GetMultiHeaderValue(multiValueHeaders, xForwardedProtoHeader).FirstOrDefault(); - var dtTransport = GetDistributedTransportType(forwardedProto); + if (multiValueHeaders != null) + { + transaction.SetRequestHeaders(multiValueHeaders, agent.Configuration.AllowAllRequestHeaders ? multiValueHeaders.Keys : Statics.DefaultCaptureHeaders, multiValueHeadersGetter); + + // DT transport comes from the X-Forwarded-Proto header, if present + var forwardedProto = GetMultiHeaderValue(multiValueHeaders, xForwardedProtoHeader).FirstOrDefault(); + var dtTransport = GetDistributedTransportType(forwardedProto); - transaction.AcceptDistributedTraceHeaders(multiValueHeaders, GetMultiHeaderValue, dtTransport); + transaction.AcceptDistributedTraceHeaders(multiValueHeaders, GetMultiHeaderValue, dtTransport); + } } - else if (headers != null) + + if (headers != null) { transaction.SetRequestHeaders(headers, agent.Configuration.AllowAllRequestHeaders ? webReqEvent.Headers?.Keys : Statics.DefaultCaptureHeaders, headersGetter); @@ -261,8 +280,18 @@ private static void SetWebRequestProperties(IAgent agent, ITransaction transacti transaction.AcceptDistributedTraceHeaders(headers, GetHeaderValue, dtTransport); } - transaction.SetRequestMethod(webReqEvent.HttpMethod); - transaction.SetUri(webReqEvent.Path); + if (eventType == AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest) // v2 buries method and path + { + var reqContext = webReqEvent.RequestContext; + transaction.SetRequestMethod(reqContext.Http.Method); + transaction.SetUri(reqContext.Http.Path); + } + else + { + transaction.SetRequestMethod(webReqEvent.HttpMethod); + transaction.SetUri(webReqEvent.Path); + } + transaction.SetRequestParameters(webReqEvent.QueryStringParameters); } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs index 581a1a154d..7a0553489f 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs @@ -110,6 +110,8 @@ public object GetInputObject(InstrumentedMethodCall instrumentedMethodCall) } return null; } + + public bool IsWebRequest => EventType is AwsLambdaEventType.APIGatewayProxyRequest or AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest or AwsLambdaEventType.ApplicationLoadBalancerRequest; } private List _webResponseHeaders = ["content-type", "content-length"]; @@ -249,7 +251,7 @@ void InvokeTryProcessResponse(Task responseTask) } // capture response data for specific request / response types - if (_functionDetails.EventType is AwsLambdaEventType.APIGatewayProxyRequest or AwsLambdaEventType.ApplicationLoadBalancerRequest) + if (_functionDetails.IsWebRequest) { var responseGetter = _getRequestResponseFromGeneric ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(responseTask.GetType(), "Result"); var response = responseGetter(responseTask); @@ -268,7 +270,7 @@ void InvokeTryProcessResponse(Task responseTask) return Delegates.GetDelegateFor( onSuccess: response => { - if (_functionDetails.EventType is AwsLambdaEventType.APIGatewayProxyRequest or AwsLambdaEventType.ApplicationLoadBalancerRequest) + if (_functionDetails.IsWebRequest) CaptureResponseData(transaction, response, agent); segment.End(); @@ -290,6 +292,8 @@ private void CaptureResponseData(ITransaction transaction, object response, IAge // check response type based on request type to be sure it has the properties we're looking for var responseType = response.GetType().FullName; if ((_functionDetails.EventType == AwsLambdaEventType.APIGatewayProxyRequest && responseType != "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse") + || + (_functionDetails.EventType == AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest && responseType != "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse") || (_functionDetails.EventType == AwsLambdaEventType.ApplicationLoadBalancerRequest && responseType != "Amazon.Lambda.ApplicationLoadBalancerEvents.ApplicationLoadBalancerResponse")) { diff --git a/tests/Agent/IntegrationTests/Applications/LambdaSelfExecutingAssembly/Program.cs b/tests/Agent/IntegrationTests/Applications/LambdaSelfExecutingAssembly/Program.cs index e2bc921ec2..ff5f3fd87a 100644 --- a/tests/Agent/IntegrationTests/Applications/LambdaSelfExecutingAssembly/Program.cs +++ b/tests/Agent/IntegrationTests/Applications/LambdaSelfExecutingAssembly/Program.cs @@ -85,6 +85,10 @@ private static void Main(string[] args) return HandlerWrapper.GetHandlerWrapper(ApiGatewayProxyRequestHandlerReturnsStream, serializer); case nameof (ApiGatewayProxyRequestHandlerReturnsStreamAsync): return HandlerWrapper.GetHandlerWrapper(ApiGatewayProxyRequestHandlerReturnsStreamAsync, serializer); + case nameof(ApiGatewayHttpApiV2ProxyRequestHandler): + return HandlerWrapper.GetHandlerWrapper(ApiGatewayHttpApiV2ProxyRequestHandler, serializer); + case nameof(ApiGatewayHttpApiV2ProxyRequestHandlerAsync): + return HandlerWrapper.GetHandlerWrapper(ApiGatewayHttpApiV2ProxyRequestHandlerAsync, serializer); case nameof(ApplicationLoadBalancerRequestHandler): return HandlerWrapper.GetHandlerWrapper(ApplicationLoadBalancerRequestHandler, serializer); case nameof(ApplicationLoadBalancerRequestHandlerAsync): @@ -263,6 +267,14 @@ public static APIGatewayProxyResponse ApiGatewayProxyRequestHandler(APIGatewayPr return new APIGatewayProxyResponse() { Body = apiGatewayProxyRequest.Body, StatusCode = 200, Headers = new Dictionary { { "Content-Type", "application/json" }, { "Content-Length", "12345" } } }; } + public static async Task ApiGatewayProxyRequestHandlerAsync(APIGatewayProxyRequest apiGatewayProxyRequest, ILambdaContext __) + { + Console.WriteLine("Executing lambda {0}", nameof(ApiGatewayProxyRequestHandlerAsync)); + await Task.Delay(100); + + return new APIGatewayProxyResponse() { Body = apiGatewayProxyRequest.Body, StatusCode = 200, Headers = new Dictionary { { "Content-Type", "application/json" }, { "Content-Length", "12345" } } }; + } + public static Stream ApiGatewayProxyRequestHandlerReturnsStream(APIGatewayProxyRequest apiGatewayProxyRequest, ILambdaContext __) { Console.WriteLine("Executing lambda {0}", nameof(ApiGatewayProxyRequestHandlerReturnsStream)); @@ -290,12 +302,19 @@ public static async Task ApiGatewayProxyRequestHandlerReturnsStreamAsync return stream; } - public static async Task ApiGatewayProxyRequestHandlerAsync(APIGatewayProxyRequest apiGatewayProxyRequest, ILambdaContext __) + public static APIGatewayHttpApiV2ProxyResponse ApiGatewayHttpApiV2ProxyRequestHandler(APIGatewayHttpApiV2ProxyRequest apiGatewayProxyRequest, ILambdaContext __) { - Console.WriteLine("Executing lambda {0}", nameof(ApiGatewayProxyRequestHandlerAsync)); + Console.WriteLine("Executing lambda {0}", nameof(ApiGatewayHttpApiV2ProxyRequestHandler)); + + return new APIGatewayHttpApiV2ProxyResponse() { Body = apiGatewayProxyRequest.Body, StatusCode = 200, Headers = new Dictionary { { "Content-Type", "application/json" }, { "Content-Length", "12345" } } }; + } + + public static async Task ApiGatewayHttpApiV2ProxyRequestHandlerAsync(APIGatewayHttpApiV2ProxyRequest apiGatewayProxyRequest, ILambdaContext __) + { + Console.WriteLine("Executing lambda {0}", nameof(ApiGatewayHttpApiV2ProxyRequestHandlerAsync)); await Task.Delay(100); - return new APIGatewayProxyResponse() { Body = apiGatewayProxyRequest.Body, StatusCode = 200, Headers = new Dictionary { { "Content-Type", "application/json" }, { "Content-Length", "12345" } } }; + return new APIGatewayHttpApiV2ProxyResponse() { Body = apiGatewayProxyRequest.Body, StatusCode = 200, Headers = new Dictionary { { "Content-Type", "application/json" }, { "Content-Length", "12345" } } }; } public static ApplicationLoadBalancerResponse ApplicationLoadBalancerRequestHandler(ApplicationLoadBalancerRequest applicationLoadBalancerRequest, ILambdaContext __) diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest.cs b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest.cs new file mode 100644 index 0000000000..68a622e86f --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest.cs @@ -0,0 +1,142 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTests.RemoteServiceFixtures.AwsLambda; +using NewRelic.Agent.Tests.TestSerializationHelpers.Models; +using Xunit; +using Xunit.Abstractions; + +namespace NewRelic.Agent.IntegrationTests.AwsLambda.WebRequest +{ + [NetCoreTest] + public abstract class AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest : NewRelicIntegrationTest where T : LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase + { + private readonly LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase _fixture; + private readonly string _expectedTransactionName; + private readonly bool _returnsStream; + private const string TestTraceId = "74be672b84ddc4e4b28be285632bbc0a"; + private const string TestParentSpanId = "27ddd2d8890283b4"; + + protected AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest(T fixture, ITestOutputHelper output, string expectedTransactionName, bool returnsStream) + : base(fixture) + { + _fixture = fixture; + _expectedTransactionName = expectedTransactionName; + _returnsStream = returnsStream; + _fixture.TestLogger = output; + _fixture.SetAdditionalEnvironmentVariable("NEW_RELIC_ATTRIBUTES_INCLUDE", "request.headers.*,request.parameters.*"); + _fixture.Actions( + exerciseApplication: () => + { + _fixture.EnqueueAPIGatewayHttpApiV2ProxyRequest(); + _fixture.EnqueueAPIGatewayHttpApiV2ProxyRequestWithDTHeaders(TestTraceId, TestParentSpanId); + _fixture.AgentLog.WaitForLogLines(AgentLogBase.ServerlessPayloadLogLineRegex, TimeSpan.FromMinutes(1), 2); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var serverlessPayloads = _fixture.AgentLog.GetServerlessPayloads().ToList(); + + Assert.Multiple( + () => Assert.Equal(2, serverlessPayloads.Count), + () => Assert.All(serverlessPayloads, ValidateServerlessPayload), + () => ValidateTraceHasNoParent(serverlessPayloads[0]), + () => ValidateTraceHasParent(serverlessPayloads[1]) + ); + } + + private void ValidateServerlessPayload(ServerlessPayload serverlessPayload) + { + var transactionEvent = serverlessPayload.Telemetry.TransactionEventsPayload.TransactionEvents.Single(); + + var expectedAgentAttributes = new[] + { + "aws.lambda.arn", + "aws.requestId", + "host.displayName" + }; + + var expectedAgentAttributeValues = new Dictionary + { + { "aws.lambda.eventSource.accountId", "123456789012" }, + { "aws.lambda.eventSource.apiId", "api-id" }, + { "aws.lambda.eventSource.eventType", "apiGateway" }, + { "aws.lambda.eventSource.stage", "$default" }, + { "request.headers.header1", "value1" }, + { "request.headers.header2", "value1,value2" }, + { "request.method", "POST" }, + { "request.uri", "/path/to/resource" }, + { "request.parameters.parameter1", "value1,value2" }, + { "request.parameters.parameter2", "value" }, + { "http.statusCode", 200 }, + { "response.status", "200" }, + { "response.headers.content-type", "application/json" }, + { "response.headers.content-length", "12345" } + }; + + Assert.Equal(_expectedTransactionName, transactionEvent.IntrinsicAttributes["name"]); + + Assertions.TransactionEventHasAttributes(expectedAgentAttributes, TransactionEventAttributeType.Agent, transactionEvent); + Assertions.TransactionEventHasAttributes(expectedAgentAttributeValues, TransactionEventAttributeType.Agent, transactionEvent); + } + + private void ValidateTraceHasNoParent(ServerlessPayload serverlessPayload) + { + var entrySpan = serverlessPayload.Telemetry.SpanEventsPayload.SpanEvents.Single(s => (string)s.IntrinsicAttributes["name"] == _expectedTransactionName); + + Assertions.SpanEventDoesNotHaveAttributes(["parentId"], SpanEventAttributeType.Intrinsic, entrySpan); + } + + private void ValidateTraceHasParent(ServerlessPayload serverlessPayload) + { + var entrySpan = serverlessPayload.Telemetry.SpanEventsPayload.SpanEvents.Single(s => (string)s.IntrinsicAttributes["name"] == _expectedTransactionName); + + var expectedAttributeValues = new Dictionary + { + { "traceId", TestTraceId }, + { "parentId", TestParentSpanId } + }; + + Assertions.SpanEventHasAttributes(expectedAttributeValues, SpanEventAttributeType.Intrinsic, entrySpan); + } + } + + public class AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestNet6 : AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest + { + public AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestNet6(LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet6 fixture, ITestOutputHelper output) + : base(fixture, output, "WebTransaction/Lambda/ApiGatewayHttpApiV2ProxyRequestHandler", false) + { + } + } + + public class AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestNet8 : AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest + { + public AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestNet8(LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet8 fixture, ITestOutputHelper output) + : base(fixture, output, "WebTransaction/Lambda/ApiGatewayHttpApiV2ProxyRequestHandler", false) + { + } + } + public class AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestAsyncNet6 : AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest + { + public AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestAsyncNet6(AsyncLambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet6 fixture, ITestOutputHelper output) + : base(fixture, output, "WebTransaction/Lambda/ApiGatewayHttpApiV2ProxyRequestHandlerAsync", false) + { + } + } + + public class AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestAsyncNet8 : AwsLambdaAPIGatewayHttpApiV2ProxyRequestTest + { + public AwsLambdaAPIGatewayHttpApiV2ProxyRequestTestAsyncNet8(AsyncLambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet8 fixture, ITestOutputHelper output) + : base(fixture, output, "WebTransaction/Lambda/ApiGatewayHttpApiV2ProxyRequestHandlerAsync", false) + { + } + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixture.cs b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixture.cs new file mode 100644 index 0000000000..1e55f9c305 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixture.cs @@ -0,0 +1,205 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + + +using NewRelic.Agent.IntegrationTestHelpers; + +namespace NewRelic.Agent.IntegrationTests.RemoteServiceFixtures.AwsLambda +{ + public abstract class LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase : LambdaSelfExecutingAssemblyFixture + { + private static string GetHandlerString(bool isAsync) + { + return "LambdaSelfExecutingAssembly::LambdaSelfExecutingAssembly.Program::ApiGatewayHttpApiV2ProxyRequestHandler"+ (isAsync ? "Async" : ""); + } + + protected LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase(string targetFramework, bool isAsync) : + base(targetFramework, + null, + GetHandlerString(isAsync), + "ApiGatewayHttpApiV2ProxyRequestHandler" + (isAsync ? "Async" : ""), + null) + { + } + + public void EnqueueAPIGatewayHttpApiV2ProxyRequest() + { + var apiGatewayProxyRequestJson = $$""" + { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/path/to/resource", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/path/to/resource", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1/32", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": true, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } + } + """; + EnqueueLambdaEvent(apiGatewayProxyRequestJson); + } + + public void EnqueueAPIGatewayHttpApiV2ProxyRequestWithDTHeaders(string traceId, string spanId) + { + var apiGatewayProxyRequestJson = $$""" + { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/path/to/resource", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https", + "traceparent": "{{GetTestTraceParentHeaderValue(traceId, spanId)}}", + "tracestate": "{{GetTestTraceStateHeaderValue(spanId)}}" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/path/to/resource", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1/32", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": true, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } + } + """; + EnqueueLambdaEvent(apiGatewayProxyRequestJson); + } + } + + public class LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet6 : LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase + { + public LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet6() : base("net6.0", false) { } + } + + public class AsyncLambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet6 : LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase + { + public AsyncLambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet6() : base("net6.0", true) { } + } + + public class LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet8 : LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase + { + public LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet8() : base("net8.0", false) { } + } + + public class AsyncLambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet8 : LambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureBase + { + public AsyncLambdaAPIGatewayHttpApiV2ProxyRequestTriggerFixtureNet8() : base("net8.0", true) { } + } +} diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/AwsLambdaEventTypeExtensionsTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/AwsLambdaEventTypeExtensionsTests.cs index 08d3eed55f..f9f517285c 100644 --- a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/AwsLambdaEventTypeExtensionsTests.cs +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/AwsLambdaEventTypeExtensionsTests.cs @@ -10,6 +10,7 @@ internal class AwsLambdaEventTypeExtensionsTests { [Test] [TestCase("Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest", AwsLambdaEventType.APIGatewayProxyRequest)] + [TestCase("Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest", AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest)] [TestCase("Amazon.Lambda.ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest", AwsLambdaEventType.ApplicationLoadBalancerRequest)] [TestCase("Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent", AwsLambdaEventType.CloudWatchScheduledEvent)] [TestCase("Amazon.Lambda.KinesisEvents.KinesisEvent", AwsLambdaEventType.KinesisStreamingEvent)] @@ -30,6 +31,7 @@ public void ToEventType_ReturnsCorrectEventType(string inputType, AwsLambdaEvent } [Test] [TestCase(AwsLambdaEventType.APIGatewayProxyRequest, "apiGateway")] + [TestCase(AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest, "apiGateway")] [TestCase(AwsLambdaEventType.ApplicationLoadBalancerRequest, "alb")] [TestCase(AwsLambdaEventType.CloudWatchScheduledEvent, "cloudWatch_scheduled")] [TestCase(AwsLambdaEventType.KinesisStreamingEvent, "kinesis")] diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/LambdaEventHelpersTests.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/LambdaEventHelpersTests.cs index 06b4c33755..1dbd052d2f 100644 --- a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/LambdaEventHelpersTests.cs +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/LambdaEventHelpersTests.cs @@ -74,7 +74,7 @@ public void SetUp() .DoInstead((string key, object value) => _attributes.Add(key, value)); Mock.Arrange(() => _transaction.AcceptDistributedTraceHeaders(Arg.IsAny>(), Arg.IsAny, string, IEnumerable>>(), Arg.IsAny())) - .DoInstead((IDictionary headers, Func, string, IEnumerable> getter, TransportType transportType) => { _parsedHeaders = headers; _transportType = transportType;}); + .DoInstead((IDictionary headers, Func, string, IEnumerable> getter, TransportType transportType) => { _parsedHeaders = headers; _transportType = transportType; }); Mock.Arrange(() => _transaction.AcceptDistributedTraceHeaders(Arg.IsAny>>(), Arg.IsAny>, string, IEnumerable>>(), Arg.IsAny())) .DoInstead((IDictionary> headers, Func>, string, IEnumerable> getter, TransportType transportType) => { _parsedMultiValueHeaders = headers; _transportType = transportType; }); @@ -292,6 +292,66 @@ public void AddEventTypeAttributes_APIGatewayProxyRequest_SetsDistributedTranspo Assert.That(_transportType, Is.EqualTo(expecteTransportType)); } + // APIGatewayHttpApiV2ProxyRequest + [Test] + public void AddEventTypeAttributes_APIGatewayHttpApiV2ProxyRequest_AddsCorrectAttributes() + { + // Arrange + var eventType = AwsLambdaEventType.APIGatewayHttpApiV2ProxyRequest; + var inputObject = new NewRelic.Mock.Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest + { + RequestContext = new() + { + AccountId = "testAccountId", + ApiId = "testApiId", + RouteId = "testRouteId", + RouteKey = "testRouteKey", + Stage = "testStage", + Http = new() + { + Path = "testPath", + Method = "testMethod", + } + }, + Headers = new Dictionary() + { + { "header1", "value1,value1a" }, + { "header2", "value2" }, + { NewRelicDistributedTraceKey, $"{NewRelicDistributedTracePayload}, {NewRelicDistributedTracePayload2}" } + }, + QueryStringParameters = new Dictionary() + { + { "param1", "value1" }, + { "param2", "value2" } + } + }; + + // Mock the SetRequestHeaders, SetRequestMethod, SetUri, SetRequestParameters, and AcceptDistributedTraceHeaders methods + Mock.Arrange(() => _transaction.SetRequestHeaders(Arg.IsAny>>(), Arg.IsAny>(), Arg.IsAny>, string, string>>())).DoNothing(); + Mock.Arrange(() => _transaction.SetRequestMethod(Arg.IsAny())).DoNothing(); + Mock.Arrange(() => _transaction.SetUri(Arg.IsAny())).DoNothing(); + Mock.Arrange(() => _transaction.SetRequestParameters(Arg.IsAny>())).DoNothing(); + + // Act + LambdaEventHelpers.AddEventTypeAttributes(_agent, _transaction, eventType, (dynamic)inputObject); + + // Assert + Assert.Multiple(() => + { + Assert.That(_attributes["aws.lambda.eventSource.accountId"], Is.EqualTo("testAccountId")); + Assert.That(_attributes["aws.lambda.eventSource.apiId"], Is.EqualTo("testApiId")); + Assert.That(_attributes["aws.lambda.eventSource.stage"], Is.EqualTo("testStage")); + + // Assert that the SetRequestHeaders, SetRequestMethod, SetUri, SetRequestParameters, and AcceptDistributedTraceHeaders methods were called with the correct arguments + Mock.Assert(() => _transaction.SetRequestHeaders(inputObject.Headers, Arg.IsAny>(), Arg.IsAny, string, string>>())); + Mock.Assert(() => _transaction.SetRequestMethod(inputObject.RequestContext.Http.Method)); + Mock.Assert(() => _transaction.SetUri(inputObject.RequestContext.Http.Path)); + Mock.Assert(() => _transaction.SetRequestParameters(inputObject.QueryStringParameters)); + + Assert.That(_parsedHeaders[NewRelicDistributedTraceKey], Is.EqualTo($"{NewRelicDistributedTracePayload}, {NewRelicDistributedTracePayload2}")); + Assert.That(_transportType, Is.EqualTo(TransportType.Unknown)); + }); + } // ApplicationLoadBalancerRequest [Test] diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/Models/AmazonAPIGatewayEventsModel.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/Models/AmazonAPIGatewayEventsModel.cs index 77524599c2..c31cd0fbbc 100644 --- a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/Models/AmazonAPIGatewayEventsModel.cs +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/Models/AmazonAPIGatewayEventsModel.cs @@ -10,7 +10,7 @@ public class APIGatewayProxyRequest { public ProxyRequestContext RequestContext { get; set; } public Dictionary Headers { get; set; } - public Dictionary> MultiValueHeaders {get; set;} + public Dictionary> MultiValueHeaders { get; set; } public string HttpMethod { get; set; } public string Path { get; set; } public Dictionary QueryStringParameters { get; set; } diff --git a/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/Models/AmazonAPIGatewayV2EventsModel.cs b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/Models/AmazonAPIGatewayV2EventsModel.cs new file mode 100644 index 0000000000..bfe678f68f --- /dev/null +++ b/tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Lambda/Models/AmazonAPIGatewayV2EventsModel.cs @@ -0,0 +1,342 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace NewRelic.Mock.Amazon.Lambda.APIGatewayEvents +{ + // https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayHttpApiV2ProxyRequest.cs + public class APIGatewayHttpApiV2ProxyRequest + { + /// + /// The payload format version + /// + public string Version { get; set; } + + /// + /// The route key + /// + public string RouteKey { get; set; } + + /// + /// The raw path + /// + public string RawPath { get; set; } + + /// + /// The raw query string + /// + public string RawQueryString { get; set; } + + /// + /// Cookies sent along with the request + /// + public string[] Cookies { get; set; } + + /// + /// Headers sent with the request. Multiple values for the same header will be separated by a comma. + /// + public IDictionary Headers { get; set; } + + /// + /// Query string parameters sent with the request. Multiple values for the same parameter will be separated by a comma. + /// + public IDictionary QueryStringParameters { get; set; } + + /// + /// The request context for the request + /// + public ProxyRequestContext RequestContext { get; set; } + + /// + /// The HTTP request body. + /// + public string Body { get; set; } + + /// + /// Path parameters sent with the request. + /// + public IDictionary PathParameters { get; set; } + + /// + /// True if the body of the request is base 64 encoded. + /// + public bool IsBase64Encoded { get; set; } + + /// + /// The stage variables defined for the stage in API Gateway + /// + public IDictionary StageVariables { get; set; } + + /// + /// The ProxyRequestContext contains the information to identify the AWS account and resources invoking the + /// Lambda function. + /// + public class ProxyRequestContext + { + /// + /// The account id that owns the executing Lambda function + /// + public string AccountId { get; set; } + + /// + /// The API Gateway rest API Id. + /// + public string ApiId { get; set; } + + /// + /// Information about the current requesters authorization including claims and scopes. + /// + public AuthorizerDescription Authorizer { get; set; } + + /// + /// The domin name. + /// + public string DomainName { get; set; } + + /// + /// The domain prefix + /// + public string DomainPrefix { get; set; } + + /// + /// Information about the HTTP request like the method and path. + /// + public HttpDescription Http { get; set; } + + /// + /// The unique request id + /// + public string RequestId { get; set; } + + /// + /// The route id + /// + public string RouteId { get; set; } + + /// + /// The selected route key. + /// + public string RouteKey { get; set; } + + /// + /// The API Gateway stage name + /// + public string Stage { get; set; } + + /// + /// Gets and sets the request time. + /// + public string Time { get; set; } + + /// + /// Gets and sets the request time as an epoch. + /// + public long TimeEpoch { get; set; } + + /// + /// Properties for authentication. + /// + public ProxyRequestAuthentication Authentication { get; set; } + } + + + /// + /// Container for authentication properties. + /// + public class ProxyRequestAuthentication + { + /// + /// Properties for a client certificate. + /// + public ProxyRequestClientCert ClientCert { get; set; } + } + + /// + /// Container for the properties of the client certificate. + /// + public class ProxyRequestClientCert + { + /// + /// The PEM-encoded client certificate that the client presented during mutual TLS authentication. + /// Present when a client accesses an API by using a custom domain name that has mutual + /// TLS enabled. Present only in access logs if mutual TLS authentication fails. + /// + public string ClientCertPem { get; set; } + + /// + /// The distinguished name of the subject of the certificate that a client presents. + /// Present when a client accesses an API by using a custom domain name that has + /// mutual TLS enabled. Present only in access logs if mutual TLS authentication fails. + /// + public string SubjectDN { get; set; } + + /// + /// The distinguished name of the issuer of the certificate that a client presents. + /// Present when a client accesses an API by using a custom domain name that has + /// mutual TLS enabled. Present only in access logs if mutual TLS authentication fails. + /// + public string IssuerDN { get; set; } + + /// + /// The serial number of the certificate. Present when a client accesses an API by + /// using a custom domain name that has mutual TLS enabled. + /// Present only in access logs if mutual TLS authentication fails. + /// + public string SerialNumber { get; set; } + + /// + /// The rules for when the client cert is valid. + /// + public ClientCertValidity Validity { get; set; } + } + + /// + /// Container for the validation properties of a client cert. + /// + public class ClientCertValidity + { + /// + /// The date before which the certificate is invalid. Present when a client accesses an API by using a custom domain name + /// that has mutual TLS enabled. Present only in access logs if mutual TLS authentication fails. + /// + public string NotBefore { get; set; } + + /// + /// The date after which the certificate is invalid. Present when a client accesses an API by using a custom domain name that + /// has mutual TLS enabled. Present only in access logs if mutual TLS authentication fails. + /// + public string NotAfter { get; set; } + } + + /// + /// Information about the HTTP elements for the request. + /// + public class HttpDescription + { + /// + /// The HTTP method like POST or GET. + /// + public string Method { get; set; } + + /// + /// The path of the request. + /// + public string Path { get; set; } + + /// + /// The protocal used to make the rquest + /// + public string Protocol { get; set; } + + /// + /// The source ip for the request. + /// + public string SourceIp { get; set; } + + /// + /// The user agent for the request. + /// + public string UserAgent { get; set; } + } + + /// + /// Information about the current requesters authorization. + /// + public class AuthorizerDescription + { + /// + /// The JWT description including claims and scopes. + /// + public JwtDescription Jwt { get; set; } + + /// + /// The Lambda authorizer description + /// + public IDictionary Lambda { get; set; } + + /// + /// The IAM authorizer description + /// + public IAMDescription IAM { get; set; } + + + /// + /// Describes the information from an IAM authorizer + /// + public class IAMDescription + { + /// + /// The Access Key of the IAM Authorizer + /// + public string AccessKey { get; set; } + + /// + /// The Account Id of the IAM Authorizer + /// + public string AccountId { get; set; } + + /// + /// The Caller Id of the IAM Authorizer + /// + public string CallerId { get; set; } + + /// + /// The Cognito Identity of the IAM Authorizer + /// + public CognitoIdentityDescription CognitoIdentity { get; set; } + + /// + /// The Principal Org Id of the IAM Authorizer + /// + public string PrincipalOrgId { get; set; } + + /// + /// The User ARN of the IAM Authorizer + /// + public string UserARN { get; set; } + + /// + /// The User Id of the IAM Authorizer + /// + public string UserId { get; set; } + } + + /// + /// The Cognito identity description for an IAM authorizer + /// + public class CognitoIdentityDescription + { + /// + /// The AMR of the IAM Authorizer + /// + public IList AMR { get; set; } + + /// + /// The Identity Id of the IAM Authorizer + /// + public string IdentityId { get; set; } + + /// + /// The Identity Pool Id of the IAM Authorizer + /// + public string IdentityPoolId { get; set; } + } + + /// + /// Describes the information in the JWT token + /// + public class JwtDescription + { + /// + /// Map of the claims for the requester. + /// + public IDictionary Claims { get; set; } + /// + /// List of the scopes for the requester. + /// + public string[] Scopes { get; set; } + } + } + } +}