Skip to content

Commit ceaefc5

Browse files
authored
feat: Auto-instrument ASP.NET Core Lambda functions (#2662) (#2674)
1 parent c3faea2 commit ceaefc5

21 files changed

+808
-10
lines changed

.github/workflows/all_solutions.yml

+1
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ jobs:
221221
Api,
222222
AppDomainCaching,
223223
AspNetCore,
224+
AwsLambda.AutoInstrumentation,
224225
AwsLambda.CloudWatch,
225226
AwsLambda.Custom,
226227
AwsLambda.DynamoDb,

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/HandlerMethodWrapper.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ public bool ValidateWebRequestParameters(InstrumentedMethodCall instrumentedMeth
128128
{
129129
dynamic requestContext = input.RequestContext;
130130

131-
return !string.IsNullOrEmpty(requestContext.Http.Method) && !string.IsNullOrEmpty(requestContext.Http.Path);
131+
if (requestContext.Http != null)
132+
return !string.IsNullOrEmpty(requestContext.Http.Method) && !string.IsNullOrEmpty(requestContext.Http.Path);
132133
}
133134

134135
return false;

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AwsLambda/Instrumentation.xml

+6
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,11 @@ SPDX-License-Identifier: Apache-2.0
1717
<exactMethodMatcher methodName=".ctor" />
1818
</match>
1919
</tracerFactory>
20+
<!-- Instrument the function handler for AspNetCore lambdas -->
21+
<tracerFactory name="NewRelic.Providers.Wrapper.AwsLambda.HandlerMethod">
22+
<match assemblyName="Amazon.Lambda.AspNetCoreServer" className="Amazon.Lambda.AspNetCoreServer.AbstractAspNetCoreFunction`2">
23+
<exactMethodMatcher methodName="FunctionHandlerAsync" />
24+
</match>
25+
</tracerFactory>
2026
</instrumentation>
2127
</extension>

tests/Agent/IntegrationTests/ApplicationHelperLibraries/ApplicationLifecycle/AppLifecycleManager.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2020 New Relic, Inc. All rights reserved.
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
using CommandLine;
@@ -47,7 +47,8 @@ public static string GetPortFromArgs(string[] args)
4747
var commandLine = string.Join(" ", args);
4848
Log($"Joined args: {commandLine}");
4949

50-
Parser.Default.ParseArguments<Options>(args)
50+
new Parser(with => { with.IgnoreUnknownArguments = true;})
51+
.ParseArguments<Options>(args)
5152
.WithParsed(o =>
5253
{
5354
portToUse = o.Port ?? DefaultPort;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
7+
<AWSProjectType>Lambda</AWSProjectType>
8+
<!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
9+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
10+
</PropertyGroup>
11+
<ItemGroup>
12+
<PackageReference Include="Amazon.Lambda.AspNetCoreServer" Version="9.0.0" />
13+
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.10.0" />
14+
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.3" />
15+
</ItemGroup>
16+
<ItemGroup>
17+
<ProjectReference Include="..\..\ApplicationHelperLibraries\ApplicationLifecycle\ApplicationLifecycle.csproj" />
18+
</ItemGroup>
19+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using Microsoft.AspNetCore.Mvc;
5+
6+
namespace AspNetCoreWebApiLambdaApplication.Controllers
7+
{
8+
[Route("api/[controller]")]
9+
public class ValuesController : ControllerBase
10+
{
11+
public ValuesController()
12+
{
13+
14+
}
15+
16+
// GET api/values
17+
[HttpGet]
18+
public IEnumerable<string> Get()
19+
{
20+
return new string[] { "value1", "value2" };
21+
}
22+
23+
// GET api/values/5
24+
[HttpGet("{id}")]
25+
public string Get(int id)
26+
{
27+
return "value";
28+
}
29+
30+
// POST api/values
31+
[HttpPost]
32+
public void Post([FromBody] string value)
33+
{
34+
}
35+
36+
// PUT api/values/5
37+
[HttpPut("{id}")]
38+
public void Put(int id, [FromBody] string value)
39+
{
40+
}
41+
42+
// DELETE api/values/5
43+
[HttpDelete("{id}")]
44+
public void Delete(int id)
45+
{
46+
}
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
namespace AspNetCoreWebApiLambdaApplication
5+
{
6+
public class APIGatewayProxyFunctionEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
7+
{
8+
protected override void Init(IWebHostBuilder builder)
9+
{
10+
builder
11+
.UseStartup<Startup>();
12+
}
13+
14+
protected override void Init(IHostBuilder builder)
15+
{
16+
}
17+
}
18+
19+
public class ApplicationLoadBalancerFunctionEntryPoint :Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction
20+
{
21+
protected override void Init(IWebHostBuilder builder)
22+
{
23+
builder.UseStartup<Startup>();
24+
}
25+
26+
protected override void Init(IHostBuilder builder)
27+
{
28+
}
29+
}
30+
31+
public class APIGatewayHttpApiV2ProxyFunctionEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction
32+
{
33+
protected override void Init(IWebHostBuilder builder)
34+
{
35+
builder.UseStartup<Startup>();
36+
}
37+
38+
protected override void Init(IHostBuilder builder)
39+
{
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Diagnostics;
5+
using Amazon.Lambda.APIGatewayEvents;
6+
using Amazon.Lambda.ApplicationLoadBalancerEvents;
7+
using Amazon.Lambda.Core;
8+
using Amazon.Lambda.RuntimeSupport;
9+
using Amazon.Lambda.Serialization.SystemTextJson;
10+
using ApplicationLifecycle;
11+
using CommandLine;
12+
13+
namespace AspNetCoreWebApiLambdaApplication
14+
{
15+
internal class Program
16+
{
17+
private class Options
18+
{
19+
[Option("handler", Required = true, HelpText = "Handler function to use.")]
20+
public string Handler { get; set; }
21+
}
22+
23+
private static string _port = "";
24+
private static string _handlerToInvoke = "";
25+
26+
private static void Main(string[] args)
27+
{
28+
_port = AppLifecycleManager.GetPortFromArgs(args);
29+
30+
_handlerToInvoke = GetHandlerFromArgs(args);
31+
32+
using var cancellationTokenSource = new CancellationTokenSource();
33+
using var handlerWrapper = GetHandlerWrapper();
34+
35+
// Instantiate a LambdaBootstrap and run it.
36+
// It will wait for invocations from AWS Lambda and call the handler function for each one.
37+
using var bootstrap = new LambdaBootstrap(handlerWrapper);
38+
39+
_ = bootstrap.RunAsync(cancellationTokenSource.Token);
40+
41+
AppLifecycleManager.CreatePidFile();
42+
43+
AppLifecycleManager.WaitForTestCompletion(_port);
44+
45+
cancellationTokenSource.Cancel();
46+
}
47+
48+
private static string GetHandlerFromArgs(string[] args)
49+
{
50+
var handler = string.Empty;
51+
52+
var commandLine = string.Join(" ", args);
53+
54+
new Parser(with => { with.IgnoreUnknownArguments = true; })
55+
.ParseArguments<Options>(args)
56+
.WithParsed(o =>
57+
{
58+
handler = o.Handler;
59+
});
60+
61+
if (string.IsNullOrEmpty(handler))
62+
throw new Exception("--handler commandline argument could not be parsed.");
63+
64+
return handler;
65+
}
66+
67+
private static HandlerWrapper GetHandlerWrapper()
68+
{
69+
var defaultLambdaJsonSerializer = new DefaultLambdaJsonSerializer();
70+
71+
switch (_handlerToInvoke)
72+
{
73+
case "APIGatewayProxyFunctionEntryPoint":
74+
{
75+
var entryPoint = new APIGatewayProxyFunctionEntryPoint();
76+
Func<APIGatewayProxyRequest, ILambdaContext, Task<APIGatewayProxyResponse>> handlerFunc = entryPoint.FunctionHandlerAsync;
77+
78+
return HandlerWrapper.GetHandlerWrapper(handlerFunc, defaultLambdaJsonSerializer);
79+
}
80+
case "ApplicationLoadBalancerFunctionEntryPoint":
81+
{
82+
var entryPoint = new ApplicationLoadBalancerFunctionEntryPoint();
83+
Func<ApplicationLoadBalancerRequest, ILambdaContext, Task<ApplicationLoadBalancerResponse>> handlerFunc = entryPoint.FunctionHandlerAsync;
84+
85+
return HandlerWrapper.GetHandlerWrapper(handlerFunc, defaultLambdaJsonSerializer);
86+
}
87+
case "APIGatewayHttpApiV2ProxyFunctionEntryPoint":
88+
{
89+
var entryPoint = new APIGatewayHttpApiV2ProxyFunctionEntryPoint();
90+
Func<APIGatewayHttpApiV2ProxyRequest, ILambdaContext, Task<APIGatewayHttpApiV2ProxyResponse>> handlerFunc = entryPoint.FunctionHandlerAsync;
91+
92+
return HandlerWrapper.GetHandlerWrapper(handlerFunc, defaultLambdaJsonSerializer);
93+
}
94+
default:
95+
throw new ArgumentException($"Handler not found: {_handlerToInvoke}");
96+
}
97+
}
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
namespace AspNetCoreWebApiLambdaApplication
5+
{
6+
public class Startup
7+
{
8+
public Startup(IConfiguration configuration)
9+
{
10+
Configuration = configuration;
11+
}
12+
13+
public IConfiguration Configuration { get; }
14+
15+
// This method gets called by the runtime. Use this method to add services to the container
16+
public void ConfigureServices(IServiceCollection services)
17+
{
18+
services.AddControllers();
19+
}
20+
21+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
22+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
23+
{
24+
if (env.IsDevelopment())
25+
{
26+
app.UseDeveloperExceptionPage();
27+
}
28+
29+
//app.UseHttpsRedirection();
30+
31+
app.UseRouting();
32+
33+
//app.UseAuthorization();
34+
35+
app.UseEndpoints(endpoints =>
36+
{
37+
endpoints.MapControllers();
38+
endpoints.MapGet("/", async context =>
39+
{
40+
await context.Response.WriteAsync("Welcome to running ASP.NET Core on AWS Lambda");
41+
});
42+
});
43+
}
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"AWS": {
3+
"Region": ""
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information"
5+
}
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"Information": [
3+
"This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
4+
"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.",
5+
"dotnet lambda help",
6+
"All the command line options for the Lambda command can be specified in this file."
7+
],
8+
"profile": "",
9+
"region": "",
10+
"configuration": "Release",
11+
"s3-prefix": "AspNetCoreWebApiLambdaApplication/",
12+
"template": "serverless.template",
13+
"template-parameters": ""
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"AWSTemplateFormatVersion": "2010-09-09",
3+
"Transform": "AWS::Serverless-2016-10-31",
4+
"Description": "An AWS Serverless Application that uses the ASP.NET Core framework running in Amazon Lambda.",
5+
"Parameters": {},
6+
"Conditions": {},
7+
"Resources": {
8+
"AspNetCoreFunction": {
9+
"Type": "AWS::Serverless::Function",
10+
"Properties": {
11+
"Handler": "AspNetCoreWebApiLambdaApplication::AspNetCoreWebApiLambdaApplication.APIGatewayHttpApiV2ProxyFunctionEntryPoint::FunctionHandlerAsync",
12+
"Runtime": "dotnet8",
13+
"CodeUri": "",
14+
"MemorySize": 512,
15+
"Timeout": 30,
16+
"Role": null,
17+
"Policies": [
18+
"AWSLambda_FullAccess"
19+
],
20+
"Events": {
21+
"ProxyResource": {
22+
"Type": "Api",
23+
"Properties": {
24+
"Path": "/{proxy+}",
25+
"Method": "ANY"
26+
}
27+
},
28+
"RootResource": {
29+
"Type": "Api",
30+
"Properties": {
31+
"Path": "/",
32+
"Method": "ANY"
33+
}
34+
}
35+
}
36+
}
37+
}
38+
},
39+
"Outputs": {
40+
"ApiURL": {
41+
"Description": "API endpoint URL for Prod environment",
42+
"Value": {
43+
"Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
44+
}
45+
}
46+
}
47+
}

tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteApplicationFixture.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,11 @@ public virtual void Initialize()
348348
throw new Exception(message);
349349
}
350350
}
351-
351+
catch (Exception ex)
352+
{
353+
TestLogger?.WriteLine("Exception occurred in Initialize: " + ex.ToString());
354+
throw;
355+
}
352356
finally
353357
{
354358
if (AgentLogExpected)

0 commit comments

Comments
 (0)