diff --git a/.github/workflows/all_solutions.yml b/.github/workflows/all_solutions.yml index 2325fa551..9b36246ff 100644 --- a/.github/workflows/all_solutions.yml +++ b/.github/workflows/all_solutions.yml @@ -221,6 +221,7 @@ jobs: Api, AppDomainCaching, AspNetCore, + AwsLambda.AutoInstrumentation, AwsLambda.CloudWatch, AwsLambda.Custom, AwsLambda.DynamoDb, 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 237724e75..54e295904 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs @@ -128,7 +128,8 @@ public bool ValidateWebRequestParameters(InstrumentedMethodCall instrumentedMeth { dynamic requestContext = input.RequestContext; - return !string.IsNullOrEmpty(requestContext.Http.Method) && !string.IsNullOrEmpty(requestContext.Http.Path); + if (requestContext.Http != null) + return !string.IsNullOrEmpty(requestContext.Http.Method) && !string.IsNullOrEmpty(requestContext.Http.Path); } return false; diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/Instrumentation.xml b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/Instrumentation.xml index 436a2878f..90b1759b5 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/Instrumentation.xml +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/Instrumentation.xml @@ -17,5 +17,11 @@ SPDX-License-Identifier: Apache-2.0 + + + + + + diff --git a/tests/Agent/IntegrationTests/ApplicationHelperLibraries/ApplicationLifecycle/AppLifecycleManager.cs b/tests/Agent/IntegrationTests/ApplicationHelperLibraries/ApplicationLifecycle/AppLifecycleManager.cs index a14d3a8e5..743a98b2e 100644 --- a/tests/Agent/IntegrationTests/ApplicationHelperLibraries/ApplicationLifecycle/AppLifecycleManager.cs +++ b/tests/Agent/IntegrationTests/ApplicationHelperLibraries/ApplicationLifecycle/AppLifecycleManager.cs @@ -1,4 +1,4 @@ -// Copyright 2020 New Relic, Inc. All rights reserved. +// Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 using CommandLine; @@ -47,7 +47,8 @@ public static string GetPortFromArgs(string[] args) var commandLine = string.Join(" ", args); Log($"Joined args: {commandLine}"); - Parser.Default.ParseArguments(args) + new Parser(with => { with.IgnoreUnknownArguments = true;}) + .ParseArguments(args) .WithParsed(o => { portToUse = o.Port ?? DefaultPort; diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/AspNetCoreWebApiLambdaApplication.csproj b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/AspNetCoreWebApiLambdaApplication.csproj new file mode 100644 index 000000000..0e4192f88 --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/AspNetCoreWebApiLambdaApplication.csproj @@ -0,0 +1,19 @@ + + + Exe + net8.0 + enable + true + Lambda + + true + + + + + + + + + + \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Controllers/ValuesController.cs b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Controllers/ValuesController.cs new file mode 100644 index 000000000..ce27e08ac --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Controllers/ValuesController.cs @@ -0,0 +1,48 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc; + +namespace AspNetCoreWebApiLambdaApplication.Controllers +{ + [Route("api/[controller]")] + public class ValuesController : ControllerBase + { + public ValuesController() + { + + } + + // GET api/values + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/LambdaEntryPoint.cs b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/LambdaEntryPoint.cs new file mode 100644 index 000000000..c56246943 --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/LambdaEntryPoint.cs @@ -0,0 +1,42 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AspNetCoreWebApiLambdaApplication +{ + public class APIGatewayProxyFunctionEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction + { + protected override void Init(IWebHostBuilder builder) + { + builder + .UseStartup(); + } + + protected override void Init(IHostBuilder builder) + { + } + } + + public class ApplicationLoadBalancerFunctionEntryPoint :Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction + { + protected override void Init(IWebHostBuilder builder) + { + builder.UseStartup(); + } + + protected override void Init(IHostBuilder builder) + { + } + } + + public class APIGatewayHttpApiV2ProxyFunctionEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction + { + protected override void Init(IWebHostBuilder builder) + { + builder.UseStartup(); + } + + protected override void Init(IHostBuilder builder) + { + } + } +} diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Program.cs b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Program.cs new file mode 100644 index 000000000..dfb95079e --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Program.cs @@ -0,0 +1,99 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using ApplicationLifecycle; +using CommandLine; + +namespace AspNetCoreWebApiLambdaApplication +{ + internal class Program + { + private class Options + { + [Option("handler", Required = true, HelpText = "Handler function to use.")] + public string Handler { get; set; } + } + + private static string _port = ""; + private static string _handlerToInvoke = ""; + + private static void Main(string[] args) + { + _port = AppLifecycleManager.GetPortFromArgs(args); + + _handlerToInvoke = GetHandlerFromArgs(args); + + using var cancellationTokenSource = new CancellationTokenSource(); + using var handlerWrapper = GetHandlerWrapper(); + + // Instantiate a LambdaBootstrap and run it. + // It will wait for invocations from AWS Lambda and call the handler function for each one. + using var bootstrap = new LambdaBootstrap(handlerWrapper); + + _ = bootstrap.RunAsync(cancellationTokenSource.Token); + + AppLifecycleManager.CreatePidFile(); + + AppLifecycleManager.WaitForTestCompletion(_port); + + cancellationTokenSource.Cancel(); + } + + private static string GetHandlerFromArgs(string[] args) + { + var handler = string.Empty; + + var commandLine = string.Join(" ", args); + + new Parser(with => { with.IgnoreUnknownArguments = true; }) + .ParseArguments(args) + .WithParsed(o => + { + handler = o.Handler; + }); + + if (string.IsNullOrEmpty(handler)) + throw new Exception("--handler commandline argument could not be parsed."); + + return handler; + } + + private static HandlerWrapper GetHandlerWrapper() + { + var defaultLambdaJsonSerializer = new DefaultLambdaJsonSerializer(); + + switch (_handlerToInvoke) + { + case "APIGatewayProxyFunctionEntryPoint": + { + var entryPoint = new APIGatewayProxyFunctionEntryPoint(); + Func> handlerFunc = entryPoint.FunctionHandlerAsync; + + return HandlerWrapper.GetHandlerWrapper(handlerFunc, defaultLambdaJsonSerializer); + } + case "ApplicationLoadBalancerFunctionEntryPoint": + { + var entryPoint = new ApplicationLoadBalancerFunctionEntryPoint(); + Func> handlerFunc = entryPoint.FunctionHandlerAsync; + + return HandlerWrapper.GetHandlerWrapper(handlerFunc, defaultLambdaJsonSerializer); + } + case "APIGatewayHttpApiV2ProxyFunctionEntryPoint": + { + var entryPoint = new APIGatewayHttpApiV2ProxyFunctionEntryPoint(); + Func> handlerFunc = entryPoint.FunctionHandlerAsync; + + return HandlerWrapper.GetHandlerWrapper(handlerFunc, defaultLambdaJsonSerializer); + } + default: + throw new ArgumentException($"Handler not found: {_handlerToInvoke}"); + } + } + } +} diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Startup.cs b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Startup.cs new file mode 100644 index 000000000..2999eed42 --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/Startup.cs @@ -0,0 +1,45 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AspNetCoreWebApiLambdaApplication +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + //app.UseHttpsRedirection(); + + app.UseRouting(); + + //app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Welcome to running ASP.NET Core on AWS Lambda"); + }); + }); + } + } +} diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/appsettings.Development.json b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/appsettings.Development.json new file mode 100644 index 000000000..905d15a0a --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "AWS": { + "Region": "" + } +} \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/appsettings.json b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/appsettings.json new file mode 100644 index 000000000..8c4d28e04 --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/appsettings.json @@ -0,0 +1,7 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + } +} \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/aws-lambda-tools-defaults.json b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..9858f7277 --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/aws-lambda-tools-defaults.json @@ -0,0 +1,14 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "s3-prefix": "AspNetCoreWebApiLambdaApplication/", + "template": "serverless.template", + "template-parameters": "" +} \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/serverless.template b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/serverless.template new file mode 100644 index 000000000..59ccfdfe6 --- /dev/null +++ b/tests/Agent/IntegrationTests/Applications/AspNetCoreWebApiLambdaApplication/serverless.template @@ -0,0 +1,47 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "An AWS Serverless Application that uses the ASP.NET Core framework running in Amazon Lambda.", + "Parameters": {}, + "Conditions": {}, + "Resources": { + "AspNetCoreFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "AspNetCoreWebApiLambdaApplication::AspNetCoreWebApiLambdaApplication.APIGatewayHttpApiV2ProxyFunctionEntryPoint::FunctionHandlerAsync", + "Runtime": "dotnet8", + "CodeUri": "", + "MemorySize": 512, + "Timeout": 30, + "Role": null, + "Policies": [ + "AWSLambda_FullAccess" + ], + "Events": { + "ProxyResource": { + "Type": "Api", + "Properties": { + "Path": "/{proxy+}", + "Method": "ANY" + } + }, + "RootResource": { + "Type": "Api", + "Properties": { + "Path": "/", + "Method": "ANY" + } + } + } + } + } + }, + "Outputs": { + "ApiURL": { + "Description": "API endpoint URL for Prod environment", + "Value": { + "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" + } + } + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs b/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs index ade1d0a52..7025b93b8 100644 --- a/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs +++ b/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs @@ -348,7 +348,11 @@ public virtual void Initialize() throw new Exception(message); } } - + catch (Exception ex) + { + TestLogger?.WriteLine("Exception occurred in Initialize: " + ex.ToString()); + throw; + } finally { if (AgentLogExpected) diff --git a/tests/Agent/IntegrationTests/IntegrationTests.sln b/tests/Agent/IntegrationTests/IntegrationTests.sln index 8a2b0522b..5e98947bc 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests.sln +++ b/tests/Agent/IntegrationTests/IntegrationTests.sln @@ -51,6 +51,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "Integra {C4F7465E-C9E7-4087-A99B-E5CDD6182FB5} = {C4F7465E-C9E7-4087-A99B-E5CDD6182FB5} {D2013833-D5F9-4C25-A0B3-9D7B2DFF9051} = {D2013833-D5F9-4C25-A0B3-9D7B2DFF9051} {D203693A-F862-4598-92F2-B91C3959EDE6} = {D203693A-F862-4598-92F2-B91C3959EDE6} + {D7E78459-8139-4CB4-B830-E8914454CD60} = {D7E78459-8139-4CB4-B830-E8914454CD60} {E28E36CD-A15F-4438-ADCA-40B26844592A} = {E28E36CD-A15F-4438-ADCA-40B26844592A} {E49FCAB4-88AB-47C3-BCD1-DCE4EFE203D2} = {E49FCAB4-88AB-47C3-BCD1-DCE4EFE203D2} {EDA8AF34-CC6C-47A6-87BF-FFE7D6DC878D} = {EDA8AF34-CC6C-47A6-87BF-FFE7D6DC878D} @@ -183,7 +184,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSerializationHelpers", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSerializationHelpers.Test", "..\Shared\TestSerializationHelpers.Test\TestSerializationHelpers.Test.csproj", "{D33E155E-C7EC-49F1-AA24-B293A74F472D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MFALatestPackages", "SharedApplications\Common\MFALatestPackages\MFALatestPackages.csproj", "{78C0CA57-4E16-4E3E-B0CF-E8C9259BA1F2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MFALatestPackages", "SharedApplications\Common\MFALatestPackages\MFALatestPackages.csproj", "{78C0CA57-4E16-4E3E-B0CF-E8C9259BA1F2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreWebApiLambdaApplication", "Applications\AspNetCoreWebApiLambdaApplication\AspNetCoreWebApiLambdaApplication.csproj", "{D7E78459-8139-4CB4-B830-E8914454CD60}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -423,6 +426,10 @@ Global {78C0CA57-4E16-4E3E-B0CF-E8C9259BA1F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {78C0CA57-4E16-4E3E-B0CF-E8C9259BA1F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {78C0CA57-4E16-4E3E-B0CF-E8C9259BA1F2}.Release|Any CPU.Build.0 = Release|Any CPU + {D7E78459-8139-4CB4-B830-E8914454CD60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7E78459-8139-4CB4-B830-E8914454CD60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7E78459-8139-4CB4-B830-E8914454CD60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7E78459-8139-4CB4-B830-E8914454CD60}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -479,6 +486,7 @@ Global {31DB04AF-2ED3-4379-98D7-7D02F38864F9} = {F0F6F2CE-8AE8-49E1-8EE9-A44B451EFC29} {472F6F9A-97E9-4DC3-9D70-AE79A20FA240} = {F0F6F2CE-8AE8-49E1-8EE9-A44B451EFC29} {78C0CA57-4E16-4E3E-B0CF-E8C9259BA1F2} = {30CF078E-E531-441E-83AB-24AB9B1C179F} + {D7E78459-8139-4CB4-B830-E8914454CD60} = {F0F6F2CE-8AE8-49E1-8EE9-A44B451EFC29} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3830ABDF-4AEA-4D91-83A2-13F091D1DF5F} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTest.cs b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTest.cs new file mode 100644 index 000000000..108cdb209 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTest.cs @@ -0,0 +1,86 @@ +// 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.AutoInstrumentation; + +[NetCoreTest] +public abstract class AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTest : NewRelicIntegrationTest where T : AspNetCoreWebApiLambdaFixtureBase +{ + private readonly T _fixture; + private readonly object _expectedTransactionName; + + protected AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTest(T fixture, ITestOutputHelper output, string expectedTransactionName) : base(fixture) + { + _fixture = fixture; + _expectedTransactionName = expectedTransactionName; + _fixture.TestLogger = output; + _fixture.SetAdditionalEnvironmentVariable("NEW_RELIC_ATTRIBUTES_INCLUDE", "request.headers.*,request.parameters.*"); + _fixture.Actions( + exerciseApplication: () => + { + _fixture.EnqueueAPIGatewayHttpApiV2ProxyRequest(); + _fixture.AgentLog.WaitForLogLines(AgentLogBase.ServerlessPayloadLogLineRegex, TimeSpan.FromMinutes(1), 1); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var serverlessPayloads = _fixture.AgentLog.GetServerlessPayloads().ToList(); + + Assert.Multiple( + () => Assert.Single(serverlessPayloads), + () => ValidateServerlessPayload(serverlessPayloads[0]) + ); + } + + 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", "GET" }, + { "request.uri", "/api/values" }, + { "http.statusCode", 200 }, + { "response.status", "200" }, + }; + + Assert.Equal(_expectedTransactionName, transactionEvent.IntrinsicAttributes["name"]); + + Assertions.TransactionEventHasAttributes(expectedAgentAttributes, TransactionEventAttributeType.Agent, transactionEvent); + Assertions.TransactionEventHasAttributes(expectedAgentAttributeValues, TransactionEventAttributeType.Agent, transactionEvent); + } +} + +public class AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTestTestNet8 : AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTest +{ + public AwsLambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTestTestNet8(LambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTriggerFixtureNet8 fixture, ITestOutputHelper output) + : base(fixture, output, "WebTransaction/MVC/Values/Get") + { + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayRequestAutoInstrumentationTest.cs b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayRequestAutoInstrumentationTest.cs new file mode 100644 index 000000000..5e332ec49 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaAPIGatewayRequestAutoInstrumentationTest.cs @@ -0,0 +1,100 @@ +// 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.AutoInstrumentation; + +[NetCoreTest] +public abstract class AwsLambdaAPIGatewayRequestAutoInstrumentationTest : NewRelicIntegrationTest where T : AspNetCoreWebApiLambdaFixtureBase +{ + private readonly T _fixture; + private readonly object _expectedTransactionName; + + protected AwsLambdaAPIGatewayRequestAutoInstrumentationTest(T fixture, ITestOutputHelper output, string expectedTransactionName) : base(fixture) + { + _fixture = fixture; + _expectedTransactionName = expectedTransactionName; + _fixture.TestLogger = output; + _fixture.SetAdditionalEnvironmentVariable("NEW_RELIC_ATTRIBUTES_INCLUDE", "request.headers.*,request.parameters.*"); + _fixture.Actions( + exerciseApplication: () => + { + _fixture.EnqueueAPIGatewayProxyRequest(); + _fixture.AgentLog.WaitForLogLines(AgentLogBase.ServerlessPayloadLogLineRegex, TimeSpan.FromMinutes(1), 1); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var serverlessPayloads = _fixture.AgentLog.GetServerlessPayloads().ToList(); + + Assert.Multiple( + () => Assert.Single(serverlessPayloads), + () => ValidateServerlessPayload(serverlessPayloads[0]) + ); + } + + 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", "1234567890" }, + { "aws.lambda.eventSource.eventType", "apiGateway" }, + { "aws.lambda.eventSource.resourceId", "123456" }, + { "aws.lambda.eventSource.resourcePath", "/{proxy+}" }, + { "aws.lambda.eventSource.stage", "prod" }, + {"request.headers.accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" }, + {"request.headers.accept-encoding", "gzip, deflate, sdch" }, + {"request.headers.accept-language", "en-US,en;q=0.8" }, + {"request.headers.cache-control", "max-age=0" }, + {"request.headers.cloudfront-forwarded-proto", "https" }, + {"request.headers.cloudfront-is-desktop-viewer", "true" }, + {"request.headers.cloudfront-is-mobile-viewer", "false" }, + {"request.headers.cloudfront-is-smarttv-viewer", "false" }, + {"request.headers.cloudfront-is-tablet-viewer", "false" }, + {"request.headers.cloudfront-viewer-country", "US" }, + {"request.headers.host", "1234567890.execute-api.{dns_suffix}" }, + {"request.headers.upgrade-insecure-requests", "1" }, + {"request.headers.user-agent", "Custom User Agent String" }, + {"request.headers.via", "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" }, + {"request.method", "GET" }, + {"request.uri", "/api/values" }, + { "http.statusCode", 200 }, + { "response.status", "200" }, + }; + + Assert.Equal(_expectedTransactionName, transactionEvent.IntrinsicAttributes["name"]); + + Assertions.TransactionEventHasAttributes(expectedAgentAttributes, TransactionEventAttributeType.Agent, transactionEvent); + Assertions.TransactionEventHasAttributes(expectedAgentAttributeValues, TransactionEventAttributeType.Agent, transactionEvent); + } +} + +public class AwsLambdaAPIGatewayRequestAutoInstrumentationTestTestNet8 : AwsLambdaAPIGatewayRequestAutoInstrumentationTest +{ + public AwsLambdaAPIGatewayRequestAutoInstrumentationTestTestNet8(LambdaAPIGatewayProxyRequestAutoInstrumentationTriggerFixtureNet8 fixture, ITestOutputHelper output) + : base(fixture, output, "WebTransaction/MVC/Values/Get") + { + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTest.cs b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTest.cs new file mode 100644 index 000000000..e7b9764c1 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/AwsLambda/AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTest.cs @@ -0,0 +1,89 @@ +// 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.AutoInstrumentation; + +[NetCoreTest] +public abstract class AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTest : NewRelicIntegrationTest where T : AspNetCoreWebApiLambdaFixtureBase +{ + private readonly T _fixture; + private readonly object _expectedTransactionName; + + protected AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTest(T fixture, ITestOutputHelper output, string expectedTransactionName) : base(fixture) + { + _fixture = fixture; + _expectedTransactionName = expectedTransactionName; + _fixture.TestLogger = output; + _fixture.SetAdditionalEnvironmentVariable("NEW_RELIC_ATTRIBUTES_INCLUDE", "request.headers.*,request.parameters.*"); + _fixture.Actions( + exerciseApplication: () => + { + _fixture.EnqueueApplicationLoadBalancerRequest(); + _fixture.AgentLog.WaitForLogLines(AgentLogBase.ServerlessPayloadLogLineRegex, TimeSpan.FromMinutes(1), 1); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var serverlessPayloads = _fixture.AgentLog.GetServerlessPayloads().ToList(); + + Assert.Multiple( + () => Assert.Single(serverlessPayloads), + () => ValidateServerlessPayload(serverlessPayloads[0]) + ); + } + + 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.eventType", "alb" }, + { "aws.lambda.eventSource.arn", "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"}, + { "request.headers.accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" }, + { "request.headers.accept-encoding", "gzip" }, + { "request.headers.accept-language", "en-US,en;q=0.9" }, + { "request.headers.connection", "keep-alive" }, + { "request.headers.host", "lambda-alb-123578498.us-east-2.elb.amazonaws.com" }, + { "request.headers.upgrade-insecure-requests", "1" }, + { "request.headers.user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" }, + { "request.method", "GET" }, + { "request.uri", "/api/values" }, + { "http.statusCode", 200 }, + { "response.status", "200" }, + }; + + Assert.Equal(_expectedTransactionName, transactionEvent.IntrinsicAttributes["name"]); + + Assertions.TransactionEventHasAttributes(expectedAgentAttributes, TransactionEventAttributeType.Agent, transactionEvent); + Assertions.TransactionEventHasAttributes(expectedAgentAttributeValues, TransactionEventAttributeType.Agent, transactionEvent); + } +} + +public class AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTestTestNet8 : AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTest +{ + public AwsLambdaApplicationLoadBalancerRequestAutoInstrumentationTestTestNet8(LambdaApplicationLoadBalancerRequestAutoInstrumentationTriggerFixtureNet8 fixture, ITestOutputHelper output) + : base(fixture, output, "WebTransaction/MVC/Values/Get") + { + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaAutoInstrumentationTriggerFixtures.cs b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaAutoInstrumentationTriggerFixtures.cs new file mode 100644 index 000000000..00229b70f --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaAutoInstrumentationTriggerFixtures.cs @@ -0,0 +1,169 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using NewRelic.Agent.IntegrationTestHelpers.RemoteServiceFixtures; + +namespace NewRelic.Agent.IntegrationTests.RemoteServiceFixtures.AwsLambda +{ + public abstract class AspNetCoreWebApiLambdaFixtureBase : LambdaTestToolFixture + { + protected AspNetCoreWebApiLambdaFixtureBase(string targetFramework) : + base(new RemoteService("AspNetCoreWebApiLambdaApplication", "AspNetCoreWebApiLambdaApplication.exe", targetFramework, ApplicationType.Bounded, createsPidFile: true, isCoreApp: true, publishApp: true), + "", + "AspNetCoreWebApiLambdaApplication::AspNetCoreWebApiLambdaApplication.LambdaEntryPoint::FunctionHandlerAsync", + "AspNetCoreWebApiLambda", + "latest", + "aspnetcore", + false) + { + + } + + public void EnqueueAPIGatewayProxyRequest() + { + var apiGatewayProxyRequestJson = $$""" + { + "path": "/api/values", + "httpMethod": "GET", + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.{dns_suffix}", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "apiKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "1234567890" + } + } + """; + EnqueueLambdaEvent(apiGatewayProxyRequestJson); + } + + public void EnqueueAPIGatewayHttpApiV2ProxyRequest() + { + var apiGatewayHttpApiV2ProxyRequestJson = $$$""" + { + "Version": "2.0", + "RouteKey": "$default", + "RawPath": "/api/values", + "Headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "RequestContext": { + "AccountId": "123456789012", + "ApiId": "api-id", + "DomainName": "id.execute-api.us-east-1.amazonaws.com", + "DomainPrefix": "id", + "Http": { + "Method": "GET", + "Path": "/api/values", + "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 + }, + "StageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } + } + """; + + EnqueueLambdaEvent(apiGatewayHttpApiV2ProxyRequestJson); + } + + public void EnqueueApplicationLoadBalancerRequest() + { + var ApplicationLoadBalancerRequestJson = $$""" + { + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "GET", + "path": "/api/values", + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "443", + "x-forwarded-proto": "https", + "x-imforwards": "20" + }, + "body": "request_body", + "isBase64Encoded": false + } + """; + EnqueueLambdaEvent(ApplicationLoadBalancerRequestJson); + } + } + + + public class LambdaAPIGatewayProxyRequestAutoInstrumentationTriggerFixtureNet8 : AspNetCoreWebApiLambdaFixtureBase + { + public LambdaAPIGatewayProxyRequestAutoInstrumentationTriggerFixtureNet8() : base("net8.0") + { + CommandLineArguments = "--handler APIGatewayProxyFunctionEntryPoint"; + } + } + public class LambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTriggerFixtureNet8 : AspNetCoreWebApiLambdaFixtureBase + { + public LambdaAPIGatewayHttpApiV2ProxyRequestAutoInstrumentationTriggerFixtureNet8() : base("net8.0") + { + CommandLineArguments = "--handler APIGatewayHttpApiV2ProxyFunctionEntryPoint"; + } + } + public class LambdaApplicationLoadBalancerRequestAutoInstrumentationTriggerFixtureNet8 : AspNetCoreWebApiLambdaFixtureBase + { + public LambdaApplicationLoadBalancerRequestAutoInstrumentationTriggerFixtureNet8() : base("net8.0") + { + CommandLineArguments = "--handler ApplicationLoadBalancerFunctionEntryPoint"; + } + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaSelfExecutingAssemblyFixture.cs b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaSelfExecutingAssemblyFixture.cs index 88dc0b371..010090ecb 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaSelfExecutingAssemblyFixture.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaSelfExecutingAssemblyFixture.cs @@ -8,13 +8,15 @@ namespace NewRelic.Agent.IntegrationTests.RemoteServiceFixtures.AwsLambda { public abstract class LambdaSelfExecutingAssemblyFixture : LambdaTestToolFixture { - public LambdaSelfExecutingAssemblyFixture(string targetFramework, string newRelicLambdaHandler, string lambdaHandler, string lambdaName, string lambdaVersion) : + protected LambdaSelfExecutingAssemblyFixture(string targetFramework, string newRelicLambdaHandler, + string lambdaHandler, string lambdaName, string lambdaVersion) : base(new RemoteService("LambdaSelfExecutingAssembly", "LambdaSelfExecutingAssembly.exe", targetFramework, ApplicationType.Bounded, createsPidFile: true, isCoreApp: true, publishApp: true), newRelicLambdaHandler, lambdaHandler, lambdaName, lambdaVersion, - "self executing assembly") + "self executing assembly", + true) { } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaTestToolFixture.cs b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaTestToolFixture.cs index 4cd13e1ba..1bfa07591 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaTestToolFixture.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AwsLambda/LambdaTestToolFixture.cs @@ -16,7 +16,9 @@ public class LambdaTestToolFixture : RemoteApplicationFixture public DotnetTool LambdaTestTool { get; set; } public Action AdditionalSetupConfiguration { get; set; } - public LambdaTestToolFixture(RemoteApplication remoteApplication, string newRelicLambdaHandler, string lambdaHandler, string lambdaName, string lambdaVersion, string lambdaExecutionEnvironment) : base(remoteApplication) + public LambdaTestToolFixture(RemoteApplication remoteApplication, string newRelicLambdaHandler, + string lambdaHandler, string lambdaName, string lambdaVersion, string lambdaExecutionEnvironment, + bool setNewRelicLambdaHandlerEventVar) : base(remoteApplication) { LambdaTestTool = new DotnetTool("Amazon.Lambda.TestTool-8.0", "lambda-test-tool-8.0", DestinationApplicationDirectoryPath); @@ -33,8 +35,11 @@ public LambdaTestToolFixture(RemoteApplication remoteApplication, string newReli SetAdditionalEnvironmentVariable("NEW_RELIC_ACCOUNT_ID", TestConfiguration.NewRelicAccountId); SetAdditionalEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", $"localhost:{LambdaTestTool.Port}"); - AddAdditionalEnvironmentVariableIfNotNull("NEW_RELIC_LAMBDA_HANDLER", newRelicLambdaHandler); + if (setNewRelicLambdaHandlerEventVar) + AddAdditionalEnvironmentVariableIfNotNull("NEW_RELIC_LAMBDA_HANDLER", newRelicLambdaHandler); + AddAdditionalEnvironmentVariableIfNotNull("_HANDLER", lambdaHandler); + AddAdditionalEnvironmentVariableIfNotNull("AWS_LAMBDA_FUNCTION_NAME", lambdaName); AddAdditionalEnvironmentVariableIfNotNull("AWS_LAMBDA_FUNCTION_VERSION", lambdaVersion); AddAdditionalEnvironmentVariableIfNotNull("AWS_EXECUTION_ENV", lambdaExecutionEnvironment); @@ -82,7 +87,7 @@ private void WarmUpTestTool() GetString(address); return; } - catch(Exception e) + catch (Exception e) { TestLogger?.WriteLine($"Unable to warm up lambda test tool during attempt {attempt}. Exception: {e}"); if (attempt == 3)