diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs index 91c3723e5..42568992e 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 46f82f827..e80452b03 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 c8fd50cfa..ef241d38f 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 000000000..991394491 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessInvokeAsyncWrapper.cs @@ -0,0 +1,251 @@ +// 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.Collections.ObjectModel; +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; + + object[] args = (object[])instrumentedMethodCall.MethodCall.MethodArguments[1]; + + bool handledHttpArg = false; + if (trigger == "http") + { + + // 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."); + } + } + 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, + transaction.CurrentSegment, + false, + InvokeFunctionAsyncResponse, + TaskContinuationOptions.ExecuteSynchronously); + + void InvokeFunctionAsyncResponse(Task responseTask) + { + try + { + if (responseTask.IsFaulted) + { + transaction.NoticeError(responseTask.Exception); + 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(result, resultType, transaction, agent); + return; + } + } + catch (Exception ex) + { + agent.Logger.Warn(ex, "Error processing Azure Function response."); + throw; + } + } + } + + private bool TrySetHttpResponseStatusCode(dynamic result, Type resultType, ITransaction transaction, IAgent agent) + { + // make sure there's a StatusCode property on the result object + var statusCodeProperty = resultType.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; + } + + /// + /// 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; + 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 000000000..141cc8b78 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunctionInProcessTryExecuteAsyncWrapper.cs @@ -0,0 +1,134 @@ +// 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, + 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) + { + 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 16f9ee515..90b2f456d 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 5d9e8fe85..b6923b362 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 000000000..fb422600d --- /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 821b5cc8d..bbc3291df 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml @@ -7,18 +7,32 @@ SPDX-License-Identifier: Apache-2.0 - + - - + + + + + + + + + + + + + + - + + + @@ -30,7 +44,6 @@ 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 d4d3eabb2..fc71c0ce5 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 { diff --git a/src/Agent/NewRelic/Home/Home.csproj b/src/Agent/NewRelic/Home/Home.csproj index 2ae7247ad..1ca860e9d 100644 --- a/src/Agent/NewRelic/Home/Home.csproj +++ b/src/Agent/NewRelic/Home/Home.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Agent/NewRelic/Profiler/Configuration/Configuration.h b/src/Agent/NewRelic/Profiler/Configuration/Configuration.h index 59f72be28..386ada546 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 a6648b758..f7614bc94 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,7 +203,7 @@ 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_azure_function_functions_worker_id_in_command_line) + TEST_METHOD(should_instrument_isolated_azure_function_functions_worker_id_in_command_line) { std::wstring configurationXml(L"\ \ @@ -204,7 +221,7 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te 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_azure_function_worker_id_in_command_line) + TEST_METHOD(should_instrument_isolated_azure_function_worker_id_in_command_line) { std::wstring configurationXml(L"\ \ @@ -222,6 +239,42 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te 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());