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(t, "Result")); + return getResponse(task); + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs new file mode 100644 index 0000000000..de546f64e1 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs @@ -0,0 +1,113 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Concurrent; +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; + +public class AzureFunctionInProcessTryExecuteAsyncWrapper : IWrapper +{ + private static ConcurrentDictionary _functionDetailsCache = new(); + private static Func _fullNameGetter; + private static Func _functionDescriptorGetter; + private static Func _idGetter; + + private static bool _loggedDisabledMessage; + + private static bool _coldStart = true; + private static bool IsColdStart => _coldStart && !(_coldStart = false); + + + public bool IsTransactionRequired => false; + + public CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo) + { + return new CanWrapResponse(nameof(AzureFunctionInProcessTryExecuteAsyncWrapper).Equals(instrumentedMethodInfo.RequestedWrapperName)); + } + + public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction) + { + if (!agent.Configuration.AzureFunctionModeEnabled) // bail early if azure function mode isn't enabled + { + if (!_loggedDisabledMessage) + { + agent.Logger.Info("Azure Function mode is not enabled; Azure Functions will not be instrumented."); + _loggedDisabledMessage = true; + } + + return Delegates.NoOp; + } + + object functionInstance = instrumentedMethodCall.MethodCall.MethodArguments[0]; + + _functionDescriptorGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(functionInstance.GetType(), "FunctionDescriptor"); + var functionDescriptor = _functionDescriptorGetter(functionInstance); + + _fullNameGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(functionDescriptor.GetType(), "FullName"); + string functionClassAndMethodName = _fullNameGetter(functionDescriptor); + + // cache the function details by function name so we only have to reflect on the function once + var inProcessFunctionDetails = _functionDetailsCache.GetOrAdd(functionClassAndMethodName, _ => GetInProcessFunctionDetails(functionClassAndMethodName)); + + _idGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(functionInstance.GetType(), "Id"); + string invocationId = _idGetter(functionInstance).ToString(); + + transaction = agent.CreateTransaction( + isWeb: inProcessFunctionDetails.IsWebTrigger, + category: "AzureFunction", + transactionDisplayName: inProcessFunctionDetails.FunctionName, + doNotTrackAsUnitOfWork: 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(inProcessFunctionDetails.FunctionName)); + transaction.AddFaasAttribute("faas.name", $"{agent.Configuration.AzureFunctionAppName}/{inProcessFunctionDetails.FunctionName}"); + transaction.AddFaasAttribute("faas.trigger", inProcessFunctionDetails.TriggerType); + transaction.AddFaasAttribute("faas.invocation_id", invocationId); + + var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, inProcessFunctionDetails.FunctionName); + + return Delegates.GetAsyncDelegateFor(agent, segment); + } + + private InProcessFunctionDetails GetInProcessFunctionDetails(string functionClassAndMethodName) + { + string functionClassName = functionClassAndMethodName.Substring(0, functionClassAndMethodName.LastIndexOf('.')); + string functionMethodName = functionClassAndMethodName.Substring(functionClassAndMethodName.LastIndexOf('.') + 1); + + // get the type for functionClassName from any loaded assembly, since it's not in the current assembly + // TODO: is there a better way to do this? + Type functionClassType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName == functionClassName); + + MethodInfo functionMethod = functionClassType?.GetMethod(functionMethodName); + var functionNameAttribute = functionMethod?.GetCustomAttributes().FirstOrDefault(a => a.GetType().Name == "FunctionNameAttribute"); + string functionName = functionNameAttribute?.GetType().GetProperty("Name")?.GetValue(functionNameAttribute) as string; + + var triggerAttributeParameter = functionMethod?.GetParameters().FirstOrDefault(p => p.GetCustomAttributes().Any(a => a.GetType().Name.Contains("TriggerAttribute"))); + var triggerAttribute = triggerAttributeParameter?.GetCustomAttributes().FirstOrDefault(); + string triggerAttributeName = triggerAttribute?.GetType().Name; + string triggerType = triggerAttributeName?.ResolveTriggerType(); + + return new InProcessFunctionDetails + { + TriggerType = triggerType, + FunctionName = functionName, + }; + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionIsolatedInvokeFunctionAsyncWrapper.cs similarity index 91% rename from src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs rename to src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionIsolatedInvokeFunctionAsyncWrapper.cs index 16f9ee5150..90b2f456d6 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionIsolatedInvokeFunctionAsyncWrapper.cs @@ -11,11 +11,10 @@ namespace NewRelic.Providers.Wrapper.AzureFunction; -public class InvokeFunctionAsyncWrapper : IWrapper +public class AzureFunctionIsolatedInvokeAsyncWrapper : IWrapper { 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); @@ -26,7 +25,7 @@ public class InvokeFunctionAsyncWrapper : IWrapper public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) { - return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName)); + return new CanWrapResponse(nameof(AzureFunctionIsolatedInvokeAsyncWrapper).Equals(methodInfo.RequestedWrapperName)); } public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, @@ -47,14 +46,14 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins if (functionContext == null) { - agent.Logger.Debug($"{WrapperName}: FunctionContext is null, can't instrument this invocation."); + agent.Logger.Debug($"{nameof(AzureFunctionIsolatedInvokeAsyncWrapper)}: FunctionContext is null, can't instrument this invocation."); throw new ArgumentNullException("functionContext"); } - var functionDetails = new FunctionDetails(functionContext, agent); + var functionDetails = new IsolatedFunctionDetails(functionContext, agent); if (!functionDetails.IsValid()) { - agent.Logger.Debug($"{WrapperName}: FunctionDetails are invalid, can't instrument this invocation."); + agent.Logger.Debug($"{nameof(AzureFunctionIsolatedInvokeAsyncWrapper)}: FunctionDetails are invalid, can't instrument this invocation."); throw new Exception("FunctionDetails are missing some require information."); } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs index 5d9e8fe857..b6923b362f 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs @@ -10,13 +10,11 @@ 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)); + return new CanWrapResponse(nameof(FunctionsHttpProxyingMiddlewareWrapper).Equals(methodInfo.RequestedWrapperName)); } /// diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InProcessFunctionDetails.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InProcessFunctionDetails.cs new file mode 100644 index 0000000000..fb422600d5 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InProcessFunctionDetails.cs @@ -0,0 +1,11 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace NewRelic.Providers.Wrapper.AzureFunction; + +public class InProcessFunctionDetails +{ + public string TriggerType { get; set; } + public bool IsWebTrigger => TriggerType == "http"; + public string FunctionName { get; set; } +} 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 b265fe0e3a..bbc3291df9 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml @@ -7,25 +7,32 @@ SPDX-License-Identifier: Apache-2.0 - + - - - - + + + + + + + + + - + + + diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionDetails.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/IsolatedFunctionDetails.cs similarity index 98% rename from src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionDetails.cs rename to src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/IsolatedFunctionDetails.cs index d4d3eabb24..fc71c0ce57 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionDetails.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/IsolatedFunctionDetails.cs @@ -13,7 +13,7 @@ namespace NewRelic.Providers.Wrapper.AzureFunction; -internal class FunctionDetails +internal class IsolatedFunctionDetails { private static MethodInfo _bindFunctionInputAsync; private static MethodInfo _genericFunctionInputBindingFeatureGetter; @@ -27,7 +27,7 @@ internal class FunctionDetails private const string AspNetCoreExtensionsAssemblyName = "Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"; private const string IFunctionInputBindingFeatureTypeName = "Microsoft.Azure.Functions.Worker.Context.Features.IFunctionInputBindingFeature"; - public FunctionDetails(dynamic functionContext, IAgent agent) + public IsolatedFunctionDetails(dynamic functionContext, IAgent agent) { try { From 30b8be5fdc075971d10b52f83614839d5c86eea6 Mon Sep 17 00:00:00 2001 From: Marty Tippin <120425148+tippmar-nr@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:27:11 -0600 Subject: [PATCH 5/6] Oops --- ...FunctionInProcessTryExecuteAsyncWrapper.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs index de546f64e1..141cc8b784 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs @@ -83,7 +83,28 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, inProcessFunctionDetails.FunctionName); - return Delegates.GetAsyncDelegateFor(agent, segment); + return Delegates.GetAsyncDelegateFor( + agent, + segment, + false, + InvokeFunctionAsyncResponse, + TaskContinuationOptions.ExecuteSynchronously); + + void InvokeFunctionAsyncResponse(Task responseTask) + { + try + { + if (responseTask.IsFaulted) + { + transaction.NoticeError(responseTask.Exception); + } + } + finally + { + segment.End(); + transaction.End(); + } + } } private InProcessFunctionDetails GetInProcessFunctionDetails(string functionClassAndMethodName) From d52d49244c4c1213ff8d6cb944b6e51f060b58ce Mon Sep 17 00:00:00 2001 From: Marty Tippin <120425148+tippmar-nr@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:34:55 -0600 Subject: [PATCH 6/6] Add DT support for ServiceBus send and receive --- ...zureFunctionInProcessInvokeAsyncWrapper.cs | 91 +++++++++++++++++-- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs index d28a699121..991394491a 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using NewRelic.Agent.Api; @@ -33,10 +34,11 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins // get the transaction and find an attribute called faas.trigger var trigger = transaction.GetFaasAttribute("faas.trigger") as string; + object[] args = (object[])instrumentedMethodCall.MethodCall.MethodArguments[1]; + bool handledHttpArg = false; - if (trigger == "http") // this is (currently) the only trigger we care about + if (trigger == "http") { - 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) @@ -52,6 +54,15 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins agent.Logger.Info("Unable to set http-specific attributes on this transaction; could not find suitable function argument type."); } } + else if (trigger == "pubsub") + { + foreach (object arg in args) + { + var argType = arg?.GetType().FullName; + if (TryExtractServiceBusDTHeaders(arg, argType, transaction)) // TODO: remove this if/when AzureServiceBus instrumentation is released + break; + } + } return Delegates.GetAsyncDelegateFor( agent, @@ -70,9 +81,25 @@ void InvokeFunctionAsyncResponse(Task responseTask) return; } + var result = GetTaskResult(responseTask); + if (result == null) + { + return; + } + + var resultType = result.GetType(); + agent.Logger.Debug($"Azure Function response type: {resultType.FullName}"); + + if (trigger == "pubsub" && resultType.FullName == "Azure.Messaging.ServiceBus.ServiceBusMessage") + { + TryInsertServiceBusDTHeaders(transaction, result); + 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); + TrySetHttpResponseStatusCode(result, resultType, transaction, agent); + return; } } catch (Exception ex) @@ -83,11 +110,10 @@ void InvokeFunctionAsyncResponse(Task responseTask) } } - private bool TrySetHttpResponseStatusCode(Task responseTask, ITransaction transaction, IAgent agent) + private bool TrySetHttpResponseStatusCode(dynamic result, Type resultType, 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"); + var statusCodeProperty = resultType.GetProperty("StatusCode"); if (statusCodeProperty != null) { var statusCode = statusCodeProperty.GetValue(result); @@ -146,6 +172,59 @@ private bool TryHandleHttpTrigger(dynamic arg, string argTypeName, ITransaction return false; } + /// + /// Extract ServiceBus DT headers from the ApplicationProperties collection, if available + /// + private bool TryExtractServiceBusDTHeaders(dynamic arg, string argTypeName, ITransaction transaction) + { + if (argTypeName == "Azure.Messaging.ServiceBus.ServiceBusReceivedMessage") + { + if (arg.ApplicationProperties is ReadOnlyDictionary applicationProperties) + { + transaction.AcceptDistributedTraceHeaders(applicationProperties, GetServiceBusDTHeaders, TransportType.Queue); + return true; + } + } + + return false; + + IEnumerable GetServiceBusDTHeaders(ReadOnlyDictionary applicationProperties, string key) + { + var headerValues = new List(); + foreach (var item in applicationProperties) + { + if (item.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + headerValues.Add(item.Value as string); + } + } + + return headerValues; + } + } + + /// + /// Insert ServiceBus DT headers into the ApplicationProperties collection + /// + private bool TryInsertServiceBusDTHeaders(ITransaction transaction, object result) + { + // inject distributed tracing headers into the response message + dynamic serviceBusMessage = result; + if (serviceBusMessage.ApplicationProperties is IDictionary applicationProperties) + { + transaction.InsertDistributedTraceHeaders(applicationProperties, SetServiceBusDTHeaders); + return true; + } + + return false; + + void SetServiceBusDTHeaders(IDictionary applicationProperties, string key, string value) + { + applicationProperties.Add(key, value); + } + + } + private IEnumerable GetHeaderValueFromIEnumerable(IEnumerable>> headers, string key) { var val = headers.FirstOrDefault(kvp => kvp.Key == key).Value;