diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs
index 20330bc315..404cbac7f4 100644
--- a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs
+++ b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs
@@ -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;
diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
index be528fbb02..9cbe293901 100644
--- a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
+++ b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
@@ -72,6 +72,8 @@ public ISegment CurrentSegment
}
}
+ public bool HasHttpResponseStatusCode => TransactionMetadata.HttpResponseStatusCode.HasValue;
+
public ITracingState TracingState { get; private set; }
public string TraceId
diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs
index fa51268c47..ea0f22e041 100644
--- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs
+++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs
@@ -33,6 +33,8 @@ public interface ITransaction
///
ISegment CurrentSegment { get; }
+ bool HasHttpResponseStatusCode { get; }
+
///
/// End this transaction.
///
diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs
new file mode 100644
index 0000000000..4c32e63e70
--- /dev/null
+++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs
@@ -0,0 +1,56 @@
+// Copyright 2020 New Relic, Inc. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using NewRelic.Agent.Api;
+using NewRelic.Agent.Extensions.Providers.Wrapper;
+
+namespace NewRelic.Providers.Wrapper.AzureFunction;
+
+public class FunctionsHttpProxyingMiddlewareWrapper : IWrapper
+{
+ private const string WrapperName = "FunctionsHttpProxyingMiddlewareWrapper";
+
+ public bool IsTransactionRequired => false;
+
+ public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
+ {
+ return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
+ }
+
+ ///
+ /// Gets request method / path for Azure function HttpTrigger invocations
+ /// in apps that use the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore package
+ ///
+ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
+ {
+ if (agent.Configuration.AzureFunctionModeEnabled)
+ {
+ dynamic httpContext;
+ switch (instrumentedMethodCall.MethodCall.Method.MethodName)
+ {
+ case "AddHttpContextToFunctionContext":
+ httpContext = instrumentedMethodCall.MethodCall.MethodArguments[1];
+
+ agent.CurrentTransaction.SetRequestMethod(httpContext.Request.Method);
+ agent.CurrentTransaction.SetUri(httpContext.Request.Path);
+ break;
+ case "TryHandleHttpResult":
+ if (!agent.CurrentTransaction.HasHttpResponseStatusCode) // these handlers seem to get called more than once; only set the status code one time
+ {
+ httpContext = instrumentedMethodCall.MethodCall.MethodArguments[2];
+ 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 = instrumentedMethodCall.MethodCall.MethodArguments[1];
+ agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode);
+ }
+ break;
+ }
+ }
+
+ return Delegates.NoOp;
+ }
+}
diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml
index 230a0f44b2..821b5cc8d2 100644
--- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml
+++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml
@@ -19,5 +19,18 @@ SPDX-License-Identifier: Apache-2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs
index fc9ae2dcb5..457219d2a8 100644
--- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs
+++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs
@@ -5,198 +5,323 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;
-namespace NewRelic.Providers.Wrapper.AzureFunction
+namespace NewRelic.Providers.Wrapper.AzureFunction;
+
+public class InvokeFunctionAsyncWrapper : IWrapper
{
- public class InvokeFunctionAsyncWrapper : IWrapper
- {
- private static bool _loggedDisabledMessage;
- private const string WrapperName = "AzureFunctionInvokeAsyncWrapper";
+ private static MethodInfo _getInvocationResultMethod;
+ private static bool _loggedDisabledMessage;
+ private const string WrapperName = "AzureFunctionInvokeAsyncWrapper";
- private static bool _coldStart = true;
- private static bool IsColdStart => _coldStart && !(_coldStart = false);
+ private static bool _coldStart = true;
+ private static bool IsColdStart => _coldStart && !(_coldStart = false);
- public bool IsTransactionRequired => false;
+ public bool IsTransactionRequired => false;
- public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
- {
- return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
- }
+ private const string FunctionContextBindingFeatureExtensionsTypeName = "Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions";
- public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent,
- ITransaction transaction)
+ public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
+ {
+ return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
+ }
+
+ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent,
+ ITransaction transaction)
+ {
+ if (!agent.Configuration.AzureFunctionModeEnabled) // bail early if azure function mode isn't enabled
{
- if (!agent.Configuration.AzureFunctionModeEnabled) // bail early if azure function mode isn't enabled
+ if (!_loggedDisabledMessage)
{
- if (!_loggedDisabledMessage)
- {
- agent.Logger.Info("Azure Function mode is not enabled; Azure Functions will not be instrumented.");
- _loggedDisabledMessage = true;
- }
-
- return Delegates.NoOp;
+ agent.Logger.Info("Azure Function mode is not enabled; Azure Functions will not be instrumented.");
+ _loggedDisabledMessage = true;
}
- dynamic functionContext = instrumentedMethodCall.MethodCall.MethodArguments[0];
+ return Delegates.NoOp;
+ }
- if (functionContext == null)
- {
- agent.Logger.Debug($"{WrapperName}: FunctionContext is null, can't instrument this invocation.");
- throw new ArgumentNullException("functionContext");
- }
+ dynamic functionContext = instrumentedMethodCall.MethodCall.MethodArguments[0];
- var functionDetails = new FunctionDetails(functionContext, agent);
- if (!functionDetails.IsValid())
- {
- agent.Logger.Debug($"{WrapperName}: FunctionDetails are invalid, can't instrument this invocation.");
- throw new Exception("FunctionDetails are missing some require information.");
- }
+ if (functionContext == null)
+ {
+ agent.Logger.Debug($"{WrapperName}: FunctionContext is null, can't instrument this invocation.");
+ throw new ArgumentNullException("functionContext");
+ }
- transaction = agent.CreateTransaction(
- isWeb: functionDetails.IsWebTrigger,
- category: "AzureFunction",
- transactionDisplayName: functionDetails.FunctionName,
- doNotTrackAsUnitOfWork: true);
+ var functionDetails = new FunctionDetails(functionContext, agent);
+ if (!functionDetails.IsValid())
+ {
+ agent.Logger.Debug($"{WrapperName}: FunctionDetails are invalid, can't instrument this invocation.");
+ throw new Exception("FunctionDetails are missing some require information.");
+ }
- if (instrumentedMethodCall.IsAsync)
- {
- transaction.AttachToAsync();
- transaction.DetachFromPrimary(); //Remove from thread-local type storage
- }
+ transaction = agent.CreateTransaction(
+ isWeb: functionDetails.IsWebTrigger,
+ category: "AzureFunction",
+ transactionDisplayName: functionDetails.FunctionName,
+ doNotTrackAsUnitOfWork: true);
- if (IsColdStart) // only report this attribute if it's a cold start
- {
- transaction.AddFaasAttribute("faas.coldStart", true);
- }
+ if (instrumentedMethodCall.IsAsync)
+ {
+ transaction.AttachToAsync();
+ transaction.DetachFromPrimary(); //Remove from thread-local type storage
+ }
+
+ if (IsColdStart) // only report this attribute if it's a cold start
+ {
+ transaction.AddFaasAttribute("faas.coldStart", true);
+ }
- transaction.AddFaasAttribute("cloud.resource_id", agent.Configuration.AzureFunctionResourceIdWithFunctionName(functionDetails.FunctionName));
- transaction.AddFaasAttribute("faas.name", functionDetails.FunctionName);
- transaction.AddFaasAttribute("faas.trigger", functionDetails.Trigger);
- transaction.AddFaasAttribute("faas.invocation_id", functionDetails.InvocationId);
+ transaction.AddFaasAttribute("cloud.resource_id", agent.Configuration.AzureFunctionResourceIdWithFunctionName(functionDetails.FunctionName));
+ transaction.AddFaasAttribute("faas.name", functionDetails.FunctionName);
+ transaction.AddFaasAttribute("faas.trigger", functionDetails.Trigger);
+ transaction.AddFaasAttribute("faas.invocation_id", functionDetails.InvocationId);
- var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, functionDetails.FunctionName);
+ if (functionDetails.IsWebTrigger && !string.IsNullOrEmpty(functionDetails.RequestMethod))
+ {
+ transaction.SetRequestMethod(functionDetails.RequestMethod);
+ transaction.SetUri(functionDetails.RequestPath);
+ }
- return Delegates.GetAsyncDelegateFor(
- agent,
- segment,
- false,
- InvokeFunctionAsyncResponse,
- TaskContinuationOptions.ExecuteSynchronously);
+ var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, functionDetails.FunctionName);
- void InvokeFunctionAsyncResponse(Task responseTask)
+ return Delegates.GetAsyncDelegateFor(
+ agent,
+ segment,
+ false,
+ InvokeFunctionAsyncResponse,
+ TaskContinuationOptions.ExecuteSynchronously);
+
+ void InvokeFunctionAsyncResponse(Task responseTask)
+ {
+ try
{
- try
+ if (responseTask.IsFaulted)
{
- if (responseTask.IsFaulted)
- {
- transaction.NoticeError(responseTask.Exception);
- return;
- }
+ transaction.NoticeError(responseTask.Exception);
+ return;
}
- finally
+
+ // 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)
{
- segment.End();
- transaction.End();
+ 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(FunctionContextBindingFeatureExtensionsTypeName);
+ _getInvocationResultMethod = type.GetMethods().Single(m => m.Name == "GetInvocationResult" && !m.ContainsGenericParameters);
+ }
+
+ dynamic invocationResult = _getInvocationResultMethod.Invoke(null, new[] { functionContext });
+ var result = invocationResult?.Value;
+
+ if (result != null && result.StatusCode != null)
+ {
+ var statusCode = result.StatusCode;
+ transaction.SetHttpResponseStatusCode((int)statusCode);
+ }
}
}
+ catch (Exception ex)
+ {
+ agent.Logger.Warn(ex, "Error processing Azure Function response.");
+ throw;
+ }
+ finally
+ {
+ segment.End();
+ transaction.End();
+ }
}
}
+}
- internal class FunctionDetails
- {
- private static ConcurrentDictionary _functionTriggerCache = new();
- private static Func