From 05d4d2254ae8ec1f9fc329baa801f6cb204517d0 Mon Sep 17 00:00:00 2001
From: Marty Tippin <120425148+tippmar-nr@users.noreply.github.com>
Date: Thu, 13 Feb 2025 12:26:17 -0600
Subject: [PATCH 1/6] Add profiler support for in-process azure functions
---
.../Profiler/Configuration/Configuration.h | 23 ++++-
.../ConfigurationTest/ConfigurationTest.cpp | 99 ++++++++++++++++++-
2 files changed, 116 insertions(+), 6 deletions(-)
diff --git a/src/Agent/NewRelic/Profiler/Configuration/Configuration.h b/src/Agent/NewRelic/Profiler/Configuration/Configuration.h
index fa4005dfef..fdb092d738 100644
--- a/src/Agent/NewRelic/Profiler/Configuration/Configuration.h
+++ b/src/Agent/NewRelic/Profiler/Configuration/Configuration.h
@@ -644,11 +644,20 @@ namespace NewRelic { namespace Profiler { namespace Configuration {
bool IsAzureFunction() const
{
- // Azure Functions sets the FUNCTIONS_WORKER_RUNTIME environment variable to "dotnet-isolated" when running in the .NET worker.
+ // Azure Functions sets the FUNCTIONS_WORKER_RUNTIME environment variable when running in Azure Functions
auto functionsWorkerRuntime = _systemCalls->TryGetEnvironmentVariable(_X("FUNCTIONS_WORKER_RUNTIME"));
return functionsWorkerRuntime != nullptr && functionsWorkerRuntime->length() > 0;
}
+ bool IsAzureFunctionInProcess() const
+ {
+ auto functionsWorkerRuntime = _systemCalls->TryGetEnvironmentVariable(_X("FUNCTIONS_WORKER_RUNTIME"));
+
+ // "dotnet" when running in in-process mode
+ // "dotnet-isolated" when running in isolated mode
+ return functionsWorkerRuntime != nullptr && Strings::AreEqualCaseInsensitive(*functionsWorkerRuntime, _X("dotnet"));
+ }
+
///
/// Returns 0 if the process should not be instrumented, 1 if it should be instrumented, and -1 if it is indeterminate.
///
@@ -656,6 +665,18 @@ namespace NewRelic { namespace Profiler { namespace Configuration {
{
LogInfo(_X("Azure function detected. Determining whether to instrument ") + commandLine);
+ if (IsAzureFunctionInProcess())
+ {
+ // appPoolName starts with "~" for Azure Functions background tasks (kudu / scm)
+ if (appPoolId.find(_X("~")) == 0) {
+ LogInfo(_X("This application pool (") + appPoolId + _X(") has been identified as an in-process Azure Functions built-in background application and will be ignored."));
+ return 0;
+ }
+
+ LogInfo(L"Function is running in-process. This process will be instrumented.");
+ return 1;
+ }
+
bool isAzureWebJobsScriptWebHost = appPoolId.length() > 0 && NewRelic::Profiler::Strings::ContainsCaseInsensitive(commandLine, appPoolId);
if (isAzureWebJobsScriptWebHost)
{
diff --git a/src/Agent/NewRelic/Profiler/ConfigurationTest/ConfigurationTest.cpp b/src/Agent/NewRelic/Profiler/ConfigurationTest/ConfigurationTest.cpp
index abbfbac2f6..a469e870fc 100644
--- a/src/Agent/NewRelic/Profiler/ConfigurationTest/ConfigurationTest.cpp
+++ b/src/Agent/NewRelic/Profiler/ConfigurationTest/ConfigurationTest.cpp
@@ -96,7 +96,7 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
// tests to verify that "legacy" behavior (before azure function support) is retained.
// If NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED environment variable is not set or is set to false,
// we should behave as if no azure function support has been added.
- TEST_METHOD(azure_function_should_behave_as_legacy_if_azure_function_mode_disabled)
+ TEST_METHOD(isolated_azure_function_should_behave_as_legacy_if_azure_function_mode_disabled)
{
std::wstring configurationXml(L"\
\
@@ -113,8 +113,25 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
Assert::IsTrue(configuration.ShouldInstrument(L"functionsnethost.exe", L"", L"", L"blah blah blah FooBarBaz blah blah blah", true));
}
+ TEST_METHOD(in_proc_azure_function_should_behave_as_legacy_if_azure_function_mode_disabled)
+ {
+ std::wstring configurationXml(L"\
+ \
+ \
+ \
+ \
+ ");
+
+ auto systemCalls = std::make_shared();
+ systemCalls->environmentVariables[L"FUNCTIONS_WORKER_RUNTIME"] = L"dotnet";
+ systemCalls->environmentVariables[L"NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED"] = L"0";
+
+ Configuration configuration(configurationXml, _missingConfig, L"", systemCalls);
+
+ Assert::IsTrue(configuration.ShouldInstrument(L"w3wp.exe", L"", L"someapppoolname", L"blah blah blah FooBarBaz blah blah blah", true));
+ }
- TEST_METHOD(should_not_instrument_azure_function_app_pool_id_in_commandline)
+ TEST_METHOD(should_not_instrument_isolated_azure_function_app_pool_id_in_commandline)
{
std::wstring configurationXml(L"\
\
@@ -132,7 +149,7 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
Assert::IsFalse(configuration.ShouldInstrument(L"w3wp.exe", L"", L"FooBarBaz", L"blah blah blah FooBarBaz blah blah blah", true));
}
- TEST_METHOD(should_instrument_azure_function_fallback_to_app_pool_checking)
+ TEST_METHOD(should_instrument_isolated_azure_function_fallback_to_app_pool_checking)
{
std::wstring configurationXml(L"\
\
@@ -150,7 +167,7 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
Assert::IsTrue(configuration.ShouldInstrument(L"w3wp.exe", L"", L"foo", L"", true));
}
- TEST_METHOD(should_not_instrument_azure_function_func_exe_process_path)
+ TEST_METHOD(should_not_instrument_isolated_azure_function_func_exe_process_path)
{
std::wstring configurationXml(L"\
\
@@ -168,7 +185,7 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
Assert::IsFalse(configuration.ShouldInstrument(L"func.exe", L"", L"", L"blah blah blah FooBarBaz blah blah blah", true));
}
- TEST_METHOD(should_instrument_azure_function_functionsnethost_exe_process_path)
+ TEST_METHOD(should_instrument_isolated_azure_function_functionsnethost_exe_process_path)
{
std::wstring configurationXml(L"\
\
@@ -186,6 +203,78 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
Assert::IsTrue(configuration.ShouldInstrument(L"functionsnethost.exe", L"", L"", L"blah blah blah FooBarBaz blah blah blah", true));
}
+ TEST_METHOD(should_instrument_isolated_azure_function_functions_worker_id_in_command_line)
+ {
+ std::wstring configurationXml(L"\
+ \
+ \
+ \
+ \
+");
+
+ auto systemCalls = std::make_shared();
+ systemCalls->environmentVariables[L"FUNCTIONS_WORKER_RUNTIME"] = L"dotnet-isolated";
+ systemCalls->environmentVariables[L"NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED"] = L"true";
+
+ Configuration configuration(configurationXml, _missingConfig, L"", systemCalls);
+
+ Assert::IsTrue(configuration.ShouldInstrument(L"SomeFW481FunctionApp.exe", L"", L"", L"blah blah blah --functions-worker-id FooBarBaz blah blah blah", false));
+ }
+
+ TEST_METHOD(should_instrument_isolated_azure_function_worker_id_in_command_line)
+ {
+ std::wstring configurationXml(L"\
+ \
+ \
+ \
+ \
+");
+
+ auto systemCalls = std::make_shared();
+ systemCalls->environmentVariables[L"FUNCTIONS_WORKER_RUNTIME"] = L"dotnet-isolated";
+ systemCalls->environmentVariables[L"NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED"] = L"true";
+
+ Configuration configuration(configurationXml, _missingConfig, L"", systemCalls);
+
+ Assert::IsTrue(configuration.ShouldInstrument(L"SomeFW481FunctionApp.exe", L"", L"", L"blah blah blah --worker-id FooBarBaz blah blah blah", false));
+ }
+
+ TEST_METHOD(should_instrument_in_process_azure_function)
+ {
+ std::wstring configurationXml(L"\
+ \
+ \
+ \
+ \
+");
+
+ auto systemCalls = std::make_shared();
+ systemCalls->environmentVariables[L"FUNCTIONS_WORKER_RUNTIME"] = L"dotnet";
+ systemCalls->environmentVariables[L"NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED"] = L"true";
+
+ Configuration configuration(configurationXml, _missingConfig, L"", systemCalls);
+
+ Assert::IsTrue(configuration.ShouldInstrument(L"SomeFW481FunctionApp.exe", L"", L"someapppoolname", L"blah blah blah FooBarBaz blah blah blah", false));
+ }
+
+ TEST_METHOD(should_not_instrument_in_process_azure_function_kudu_app_pool)
+ {
+ std::wstring configurationXml(L"\
+ \
+ \
+ \
+ \
+");
+
+ auto systemCalls = std::make_shared();
+ systemCalls->environmentVariables[L"FUNCTIONS_WORKER_RUNTIME"] = L"dotnet";
+ systemCalls->environmentVariables[L"NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED"] = L"true";
+
+ Configuration configuration(configurationXml, _missingConfig, L"", systemCalls);
+
+ Assert::IsFalse(configuration.ShouldInstrument(L"SomeFW481FunctionApp.exe", L"", L"~somekuduapppool", L"blah blah blah FooBarBaz blah blah blah", false));
+ }
+
TEST_METHOD(instrument_process)
{
ProcessesPtr processes(new Processes());
From df16319af638e5e92c161de5cb7311be6b09e0a4 Mon Sep 17 00:00:00 2001
From: Marty Tippin <120425148+tippmar-nr@users.noreply.github.com>
Date: Wed, 19 Feb 2025 10:03:13 -0600
Subject: [PATCH 2/6] WIP commit so profiler build will complete
---
.../Providers/Wrapper/AzureFunction/Instrumentation.xml | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
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 821b5cc8d2..b265fe0e3a 100644
--- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml
+++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml
@@ -12,6 +12,13 @@ SPDX-License-Identifier: Apache-2.0
+
+
+
@@ -30,7 +37,6 @@ SPDX-License-Identifier: Apache-2.0
-
From 199a94008de20f6f7234fe8af75102914a3336f2 Mon Sep 17 00:00:00 2001
From: dotnet-agent-team-bot
<141066016+dotnet-agent-team-bot@users.noreply.github.com>
Date: Wed, 19 Feb 2025 08:17:21 -0800
Subject: [PATCH 3/6] chore: Update Profiler NuGet Package Reference to
v10.36.0.10 (#2998)
chore: Update Profiler NuGet Package Reference to v10.36.0.10.
Co-authored-by: tippmar-nr <120425148+tippmar-nr@users.noreply.github.com>
---
src/Agent/NewRelic/Home/Home.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Agent/NewRelic/Home/Home.csproj b/src/Agent/NewRelic/Home/Home.csproj
index 2ae7247adc..1ca860e9d5 100644
--- a/src/Agent/NewRelic/Home/Home.csproj
+++ b/src/Agent/NewRelic/Home/Home.csproj
@@ -13,7 +13,7 @@
-
+
From 6187627d6c5a1db24a10b1005c8ab5c6d6757e52 Mon Sep 17 00:00:00 2001
From: Marty Tippin <120425148+tippmar-nr@users.noreply.github.com>
Date: Fri, 21 Feb 2025 09:45:55 -0600
Subject: [PATCH 4/6] in-process instrumentation initial working commit
---
.../Core/Transactions/NoOpTransaction.cs | 5 +
.../Agent/Core/Transactions/Transaction.cs | 6 +
.../Api/ITransaction.cs | 1 +
...zureFunctionInProcessInvokeAsyncWrapper.cs | 172 ++++++++++++++++++
...FunctionInProcessTryExecuteAsyncWrapper.cs | 113 ++++++++++++
...tionIsolatedInvokeFunctionAsyncWrapper.cs} | 11 +-
.../FunctionsHttpProxyingMiddlewareWrapper.cs | 4 +-
.../AzureFunction/InProcessFunctionDetails.cs | 11 ++
.../Wrapper/AzureFunction/Instrumentation.xml | 23 ++-
...nDetails.cs => IsolatedFunctionDetails.cs} | 4 +-
10 files changed, 331 insertions(+), 19 deletions(-)
create mode 100644 src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs
create mode 100644 src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs
rename src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/{InvokeFunctionAsyncWrapper.cs => AzureFunctionIsolatedInvokeFunctionAsyncWrapper.cs} (91%)
create mode 100644 src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InProcessFunctionDetails.cs
rename src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/{FunctionDetails.cs => IsolatedFunctionDetails.cs} (98%)
diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs
index 91c3723e52..42568992e5 100644
--- a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs
+++ b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs
@@ -332,6 +332,11 @@ public void AddFaasAttribute(string name, object value)
return;
}
+ public object GetFaasAttribute(string name)
+ {
+ return null;
+ }
+
public void AddCloudSdkAttribute(string name, object value)
{
return;
diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
index 46f82f827c..e80452b03c 100644
--- a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
+++ b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
@@ -1393,5 +1393,11 @@ public void AddFaasAttribute(string name, object value)
var faasAttrib = _attribDefs.GetFaasAttribute(name);
TransactionMetadata.UserAndRequestAttributes.TrySetValue(faasAttrib, value);
}
+
+ public object GetFaasAttribute(string name)
+ {
+ var faasAttrib = _attribDefs.GetFaasAttribute(name);
+ return TransactionMetadata.UserAndRequestAttributes.GetAttributeValues(AttributeClassification.Intrinsics).FirstOrDefault(v => v.AttributeDefinition == faasAttrib)?.Value;
+ }
}
}
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 c8fd50cfaf..ef241d38fb 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
@@ -315,5 +315,6 @@ ISegment StartMessageBrokerSegment(MethodCall methodCall, MessageBrokerDestinati
void AddLambdaAttribute(string name, object value);
void AddFaasAttribute(string name, object value);
+ object GetFaasAttribute(string name);
}
}
diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs
new file mode 100644
index 0000000000..d28a699121
--- /dev/null
+++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs
@@ -0,0 +1,172 @@
+// Copyright 2020 New Relic, Inc. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using NewRelic.Agent.Api;
+using NewRelic.Agent.Extensions.Providers.Wrapper;
+using NewRelic.Reflection;
+
+namespace NewRelic.Providers.Wrapper.AzureFunction;
+
+public class AzureFunctionInProcessInvokeAsyncWrapper : IWrapper
+{
+ private static readonly ConcurrentDictionary> _getResultFromGenericTask = new();
+
+ public bool IsTransactionRequired => true;
+
+ public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
+ {
+ return new CanWrapResponse(nameof(AzureFunctionInProcessInvokeAsyncWrapper).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
+ {
+ return Delegates.NoOp;
+ }
+
+ // get the transaction and find an attribute called faas.trigger
+ var trigger = transaction.GetFaasAttribute("faas.trigger") as string;
+
+ bool handledHttpArg = false;
+ if (trigger == "http") // this is (currently) the only trigger we care about
+ {
+ object[] args = (object[])instrumentedMethodCall.MethodCall.MethodArguments[1];
+
+ // iterate each argument to find one with a type we can work with
+ foreach (object arg in args)
+ {
+ var argType = arg?.GetType().FullName;
+ handledHttpArg = TryHandleHttpTrigger(arg, argType, transaction);
+ if (handledHttpArg)
+ break;
+ }
+
+ if (!handledHttpArg)
+ {
+ agent.Logger.Info("Unable to set http-specific attributes on this transaction; could not find suitable function argument type.");
+ }
+ }
+
+ return Delegates.GetAsyncDelegateFor(
+ agent,
+ transaction.CurrentSegment,
+ false,
+ InvokeFunctionAsyncResponse,
+ TaskContinuationOptions.ExecuteSynchronously);
+
+ void InvokeFunctionAsyncResponse(Task responseTask)
+ {
+ try
+ {
+ if (responseTask.IsFaulted)
+ {
+ transaction.NoticeError(responseTask.Exception);
+ return;
+ }
+
+ if (trigger == "http" && handledHttpArg) // don't try to set the response code if we didn't handle the http trigger arg
+ {
+ TrySetHttpResponseStatusCode(responseTask, transaction, agent);
+ }
+ }
+ catch (Exception ex)
+ {
+ agent.Logger.Warn(ex, "Error processing Azure Function response.");
+ throw;
+ }
+ }
+ }
+
+ private bool TrySetHttpResponseStatusCode(Task responseTask, ITransaction transaction, IAgent agent)
+ {
+ var result = GetTaskResult(responseTask);
+ // make sure there's a StatusCode property on the result object
+ var statusCodeProperty = result?.GetType().GetProperty("StatusCode");
+ if (statusCodeProperty != null)
+ {
+ var statusCode = statusCodeProperty.GetValue(result);
+ if (statusCode != null)
+ {
+ transaction.SetHttpResponseStatusCode((int)statusCode);
+ return true;
+ }
+ }
+ else
+ {
+ agent.Logger.Debug($"Could not find StatusCode property on response object type {result?.GetType().FullName}");
+ }
+
+ return false;
+ }
+
+ private bool TryHandleHttpTrigger(dynamic arg, string argTypeName, ITransaction transaction)
+ {
+ if (arg is System.Net.Http.HttpRequestMessage httpTriggerArg)
+ {
+ IEnumerable>> headers = httpTriggerArg.Headers; // IEnumerable>>
+
+ Uri requestUri = httpTriggerArg.RequestUri;
+ string requestPath = requestUri.AbsolutePath;
+ transaction.SetUri(requestPath);
+
+ string requestMethod = httpTriggerArg.Method.Method;
+ transaction.SetRequestMethod(requestMethod);
+
+ if (headers != null)
+ {
+ transaction.AcceptDistributedTraceHeaders(headers, GetHeaderValueFromIEnumerable, TransportType.HTTP);
+ }
+
+ return true;
+ }
+
+ if (argTypeName is "Microsoft.AspNetCore.Http.DefaultHttpRequest" or "Microsoft.AspNetCore.Http.HttpRequest")
+ {
+ IDictionary headers = arg.Headers;
+
+ var requestMethod = arg.Method;
+ var requestPath = arg.Path.Value;
+
+ transaction.SetRequestMethod(requestMethod);
+ transaction.SetUri(requestPath);
+
+ if (headers?.Count != 0)
+ {
+ transaction.AcceptDistributedTraceHeaders(headers, GetHeaderValueFromIDictionary, TransportType.HTTP);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private IEnumerable GetHeaderValueFromIEnumerable(IEnumerable>> headers, string key)
+ {
+ var val = headers.FirstOrDefault(kvp => kvp.Key == key).Value;
+ return val;
+ }
+
+ private IEnumerable GetHeaderValueFromIDictionary(IDictionary headers, string key)
+ {
+ var val = headers.FirstOrDefault(kvp => kvp.Key == key).Value;
+ return val;
+ }
+
+
+ private static object GetTaskResult(object task)
+ {
+ if (((Task)task).IsFaulted)
+ {
+ return null;
+ }
+
+ var getResponse = _getResultFromGenericTask.GetOrAdd(task.GetType(), t => VisibilityBypasser.Instance.GeneratePropertyAccessor