Skip to content

Commit

Permalink
Final cleanup for http trigger param parsing. Updated integration tes…
Browse files Browse the repository at this point in the history
…ts to exercise more scenarios.
  • Loading branch information
tippmar-nr committed Sep 18, 2024
1 parent 9d770af commit b3a568f
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class NoOpTransaction : ITransaction, ITransactionExperimental
public bool IsValid => false;
public bool IsFinished => false;
public ISegment CurrentSegment => Segment.NoOpSegment;
public bool HasHttpResponseStatusCode => false;

public DateTime StartTime => DateTime.UtcNow;

Expand Down
2 changes: 2 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public ISegment CurrentSegment
}
}

public bool HasHttpResponseStatusCode => TransactionMetadata.HttpResponseStatusCode.HasValue;

public ITracingState TracingState { get; private set; }

public string TraceId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public interface ITransaction
/// </summary>
ISegment CurrentSegment { get; }

bool HasHttpResponseStatusCode { get; }

/// <summary>
/// End this transaction.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,24 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
agent.CurrentTransaction.SetRequestMethod(httpContext.Request.Method);
agent.CurrentTransaction.SetUri(httpContext.Request.Path);
break;
// not needed at present for getting status code, but keep in case we need to get more from httpContext - also update instrumentation.xml
//case "TryHandleHttpResult":
// object result = instrumentedMethodCall.MethodCall.MethodArguments[0];
// httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[2];
// bool isInvocationResult = (bool)instrumentedMethodCall.MethodCall.MethodArguments[3];

// agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode);
// break;
//case "TryHandleOutputBindingsHttpResult":
// httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[1];
// agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode);
// break;
case "TryHandleHttpResult":
if (!agent.CurrentTransaction.HasHttpResponseStatusCode) // these handlers seem to get called more than once; only set the status code one time
{
object result = instrumentedMethodCall.MethodCall.MethodArguments[0];

httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[2];
bool isInvocationResult = (bool)instrumentedMethodCall.MethodCall.MethodArguments[3];

agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode);
}
break;
case "TryHandleOutputBindingsHttpResult":
if (!agent.CurrentTransaction.HasHttpResponseStatusCode) // these handlers seem to get called more than once; only set the status code one time
{
httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[1];
agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode);
}
break;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ SPDX-License-Identifier: Apache-2.0
<match assemblyName="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" className="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware">
<exactMethodMatcher methodName="AddHttpContextToFunctionContext" />
</match>
<!-- save in case we need these later
<match assemblyName="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" className="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware">
<exactMethodMatcher methodName="TryHandleHttpResult" />
</match>
<match assemblyName="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" className="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware">
<exactMethodMatcher methodName="TryHandleOutputBindingsHttpResult" />
</match>
-->

</tracerFactory>
</instrumentation>
</extension>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace NewRelic.Providers.Wrapper.AzureFunction
{
public class InvokeFunctionAsyncWrapper : IWrapper
{
private static MethodInfo _getInvocationResultMethod;
private static bool _loggedDisabledMessage;
private const string WrapperName = "AzureFunctionInvokeAsyncWrapper";

Expand Down Expand Up @@ -105,33 +106,30 @@ void InvokeFunctionAsyncResponse(Task responseTask)
return;
}

if (functionDetails.IsWebTrigger)
// only pull response status code here if it's a web trigger and the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is not loaded.
if (functionDetails.IsWebTrigger && functionDetails.HasAspNetCoreExtensionReference != null && !functionDetails.HasAspNetCoreExtensionReference.Value)
{
// GetInvocationResult is a static extension method
// there are multiple GetInvocationResult methods in this type; we want the one without any generic parameters
Type type = functionContext.GetType().Assembly.GetType("Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions");
var getInvocationResultMethod = type.GetMethods().Single(m => m.Name == "GetInvocationResult" && !m.ContainsGenericParameters);
if (_getInvocationResultMethod == null)
{
// GetInvocationResult is a static extension method
// there are multiple GetInvocationResult methods in this type; we want the one without any generic parameters
Type type = functionContext.GetType().Assembly.GetType("Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions");
_getInvocationResultMethod = type.GetMethods().Single(m => m.Name == "GetInvocationResult" && !m.ContainsGenericParameters);
}

dynamic invocationResult = getInvocationResultMethod.Invoke(null, new[] { functionContext });
dynamic invocationResult = _getInvocationResultMethod.Invoke(null, new[] { functionContext });
var result = invocationResult?.Value;

// the result always seems to be of this type regardless of whether the app
// uses the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore package or not
var resultTypeName = result?.GetType().Name;
if (resultTypeName == "GrpcHttpResponseData")
{
transaction.SetHttpResponseStatusCode((int)result.StatusCode);
}
else
if (result != null && result.StatusCode != null)
{
agent.Logger.Debug($"Unexpected Azure Function invocationResult.Value type '{resultTypeName ?? "(null)"}' - unable to set http response status code.");
var statusCode = result.StatusCode;
transaction.SetHttpResponseStatusCode((int)statusCode);
}
}

}
}
catch (Exception ex)
{
agent.Logger.Error(ex, "Error processing Azure Function response.");
agent.Logger.Warn(ex, "Error processing Azure Function response.");
throw;
}
finally
Expand Down Expand Up @@ -223,7 +221,7 @@ public FunctionDetails(dynamic functionContext, IAgent agent)

if (IsWebTrigger)
{
ParseRequestParameters(agent, functionContext);
ParseHttpTriggerParameters(agent, functionContext);
}
}
catch (Exception ex)
Expand All @@ -233,17 +231,18 @@ public FunctionDetails(dynamic functionContext, IAgent agent)
}
}

private void ParseRequestParameters(IAgent agent, dynamic functionContext)
private void ParseHttpTriggerParameters(IAgent agent, dynamic functionContext)
{
if (!_hasAspNetCoreExtensionsReference.HasValue)
{
// see if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is in the list of loaded assemblies
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var assembly = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == "Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore");

_hasAspNetCoreExtensionsReference = assembly != null;

if (_hasAspNetCoreExtensionsReference.Value)
agent.Logger.Debug("Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is loaded; not parsing request parameters in InvokeFunctionAsyncWrapper.");
agent.Logger.Debug("Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is loaded; InvokeFunctionAsyncWrapper will defer HttpTrigger parameter parsing to FunctionsHttpProxyingMiddlewareWrapper.");
}

// don't parse request parameters here if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is loaded.
Expand Down Expand Up @@ -285,17 +284,23 @@ private void ParseRequestParameters(IAgent agent, dynamic functionContext)
if (_genericFunctionInputBindingFeatureGetter != null)
{
// Get the input binding feature and bind the input from the function context
var inputBindingFeature = _genericFunctionInputBindingFeatureGetter.Invoke(features, new object[] { });
dynamic valueTask = _bindFunctionInputAsync.Invoke(inputBindingFeature, new object[] { functionContext });
valueTask.AsTask().Wait();
var inputArguments = valueTask.Result.Values;
var reqData = inputArguments[0];
var inputBindingFeature = _genericFunctionInputBindingFeatureGetter.Invoke(features, []);
dynamic valueTask = _bindFunctionInputAsync.Invoke(inputBindingFeature, [functionContext]);

valueTask.AsTask().Wait(); // BindFunctionInputAsync returns a ValueTask, so we need to convert it to a Task to wait on it

if (reqData != null && reqData.GetType().Name == "GrpcHttpRequestData" && !string.IsNullOrEmpty(reqData.Method))
object[] inputArguments = valueTask.Result.Values;

if (inputArguments is { Length: > 0 })
{
RequestMethod = reqData.Method;
Uri uri = reqData.Url;
RequestPath = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
var reqData = (dynamic)inputArguments[0];

if (reqData != null && reqData.GetType().Name == "GrpcHttpRequestData" && !string.IsNullOrEmpty(reqData.Method))
{
RequestMethod = reqData.Method;
Uri uri = reqData.Url;
RequestPath = $"/{uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)}"; // has to start with a slash
}
}
}
}
Expand All @@ -312,6 +317,8 @@ public bool IsValid()
public bool IsWebTrigger => Trigger == "http";
public string RequestMethod { get; private set; }
public string RequestPath { get; private set; }

public bool? HasAspNetCoreExtensionReference => _hasAspNetCoreExtensionsReference;
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
Expand Down Expand Up @@ -26,7 +26,7 @@
<None Include="local.settings.json" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.23.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.3.2" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.3.2" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.5.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.4" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
Expand All @@ -19,7 +20,7 @@ public HttpTriggerFunctionUsingAspNetCorePipeline(ILogger<HttpTriggerFunctionUsi
}

[Function("HttpTriggerFunctionUsingAspNetCorePipeline")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] [FromQuery, Required] string someParam)
{
_logger.LogInformation("HttpTriggerFunctionUsingAspNetCorePipeline processed a request.");

Expand All @@ -29,7 +30,6 @@ public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "
_firstTime = false;
}


return new OkObjectResult("Welcome to Azure Functions!");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace AzureFunctionApplication
/// </summary>
public class HttpTriggerFunctionUsingSimpleInvocation
{
private static bool _firstTime = true;
private readonly ILogger<HttpTriggerFunctionUsingSimpleInvocation> _logger;

public HttpTriggerFunctionUsingSimpleInvocation(ILogger<HttpTriggerFunctionUsingSimpleInvocation> logger)
Expand All @@ -25,6 +26,12 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function
{
_logger.LogInformation("HttpTriggerFunctionUsingSimpleInvocation processed a request.");

if (_firstTime)
{
await Task.Delay(250); // to ensure that the first invocation gets sampled
_firstTime = false;
}

var response = reqData.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ private static async Task Main(string[] args)


var host = new HostBuilder()
// the net6 target uses the "basic" azure function configuration
// the net8 target uses the aspnetcore azure function configuration
#if NET6_0
.ConfigureFunctionsWorkerDefaults()
#else
.ConfigureFunctionsWebApplication()
#endif
.Build();

var task = host.RunAsync(cts.Token);
Expand Down
Loading

0 comments on commit b3a568f

Please sign in to comment.