From 94d4855b86c5b61d9334b95015de831a89ec966a Mon Sep 17 00:00:00 2001 From: Marty Tippin <120425148+tippmar-nr@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:59:59 -0500 Subject: [PATCH] PR feedback, unit test update, code cleanup/formatting --- .../AzureFunction/AzureFunction.csproj | 1 - .../FunctionsHttpProxyingMiddlewareWrapper.cs | 14 +- .../InvokeFunctionAsyncWrapper.cs | 468 +++++++++--------- .../AzureFunctionHttpTriggerTests.cs | 420 ++++++++-------- ...ureFunctionInstrumentationDisabledTests.cs | 13 +- .../AzureFunctionQueueTriggerTests.cs | 191 ++++--- .../AzureFunctionApplicationFixture.cs | 89 ++-- .../Transactions/TransactionTests.cs | 22 + 8 files changed, 617 insertions(+), 601 deletions(-) diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunction.csproj b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunction.csproj index e6de41b2ba..070dd10f1a 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunction.csproj +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/AzureFunction.csproj @@ -13,7 +13,6 @@ - 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 e26016a5c2..4c32e63e70 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs @@ -1,10 +1,11 @@ // Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -using Microsoft.AspNetCore.Http; using NewRelic.Agent.Api; using NewRelic.Agent.Extensions.Providers.Wrapper; +namespace NewRelic.Providers.Wrapper.AzureFunction; + public class FunctionsHttpProxyingMiddlewareWrapper : IWrapper { private const string WrapperName = "FunctionsHttpProxyingMiddlewareWrapper"; @@ -24,10 +25,11 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins { if (agent.Configuration.AzureFunctionModeEnabled) { + dynamic httpContext; switch (instrumentedMethodCall.MethodCall.Method.MethodName) { case "AddHttpContextToFunctionContext": - var httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[1]; + httpContext = instrumentedMethodCall.MethodCall.MethodArguments[1]; agent.CurrentTransaction.SetRequestMethod(httpContext.Request.Method); agent.CurrentTransaction.SetUri(httpContext.Request.Path); @@ -35,18 +37,14 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins case "TryHandleHttpResult": if (!agent.CurrentTransaction.HasHttpResponseStatusCode) // these handlers seem to get called more than once; only set the status code one time { - object result = instrumentedMethodCall.MethodCall.MethodArguments[0]; - - httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[2]; - bool isInvocationResult = (bool)instrumentedMethodCall.MethodCall.MethodArguments[3]; - + 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 = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[1]; + httpContext = instrumentedMethodCall.MethodCall.MethodArguments[1]; agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode); } break; 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 47ac643df9..457219d2a8 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs @@ -12,318 +12,316 @@ 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 MethodInfo _getInvocationResultMethod; - 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; - private const string FunctionContextBindingFeatureExtensionsTypeName = "Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions"; + private const string FunctionContextBindingFeatureExtensionsTypeName = "Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions"; - public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) - { - return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName)); - } + public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) + { + return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName)); + } - public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, - ITransaction transaction) + 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 + } - 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); + if (IsColdStart) // only report this attribute if it's a cold start + { + transaction.AddFaasAttribute("faas.coldStart", true); + } - if (functionDetails.IsWebTrigger && !string.IsNullOrEmpty(functionDetails.RequestMethod)) - { - transaction.SetRequestMethod(functionDetails.RequestMethod); - transaction.SetUri(functionDetails.RequestPath); - } + 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); + + if (functionDetails.IsWebTrigger && !string.IsNullOrEmpty(functionDetails.RequestMethod)) + { + transaction.SetRequestMethod(functionDetails.RequestMethod); + transaction.SetUri(functionDetails.RequestPath); + } - var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, functionDetails.FunctionName); + var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, functionDetails.FunctionName); - return Delegates.GetAsyncDelegateFor( - agent, - segment, - false, - InvokeFunctionAsyncResponse, - TaskContinuationOptions.ExecuteSynchronously); + return Delegates.GetAsyncDelegateFor( + agent, + segment, + false, + InvokeFunctionAsyncResponse, + TaskContinuationOptions.ExecuteSynchronously); - void InvokeFunctionAsyncResponse(Task responseTask) + void InvokeFunctionAsyncResponse(Task responseTask) + { + try { - try + if (responseTask.IsFaulted) { - if (responseTask.IsFaulted) - { - transaction.NoticeError(responseTask.Exception); - return; - } + transaction.NoticeError(responseTask.Exception); + return; + } - // 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) + // 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) + { + if (_getInvocationResultMethod == null) { - 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); - } + // 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; + 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); - } + 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(); - } + } + catch (Exception ex) + { + agent.Logger.Warn(ex, "Error processing Azure Function response."); + throw; + } + finally + { + segment.End(); + transaction.End(); } } } +} - internal class FunctionDetails - { - private static MethodInfo _bindFunctionInputAsync; - private static MethodInfo _genericFunctionInputBindingFeatureGetter; - private static bool? _hasAspNetCoreExtensionsReference; +internal class FunctionDetails +{ + private static MethodInfo _bindFunctionInputAsync; + private static MethodInfo _genericFunctionInputBindingFeatureGetter; + private static bool? _hasAspNetCoreExtensionsReference; - private static readonly ConcurrentDictionary _functionTriggerCache = new(); - private static Func _functionDefinitionGetter; - private static Func _parametersGetter; - private static Func> _propertiesGetter; + private static readonly ConcurrentDictionary _functionTriggerCache = new(); + private static Func _functionDefinitionGetter; + private static Func _parametersGetter; + private static Func> _propertiesGetter; - private const string AspNetCoreExtensionsAssemblyName = "Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"; - private const string IFunctionInputBindingFeatureTypeName = "Microsoft.Azure.Functions.Worker.Context.Features.IFunctionInputBindingFeature"; + 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 FunctionDetails(dynamic functionContext, IAgent agent) + { + try { - try - { - FunctionName = functionContext.FunctionDefinition.Name; - InvocationId = functionContext.InvocationId; + FunctionName = functionContext.FunctionDefinition.Name; + InvocationId = functionContext.InvocationId; - // cache the trigger by function name - if (!_functionTriggerCache.TryGetValue(FunctionName, out string trigger)) - { - // functionContext.FunctionDefinition.Parameters is an ImmutableArray - var funcAsObj = (object)functionContext; - _functionDefinitionGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(funcAsObj.GetType(), "FunctionDefinition"); - var functionDefinition = _functionDefinitionGetter(funcAsObj); + // cache the trigger by function name + if (!_functionTriggerCache.TryGetValue(FunctionName, out string trigger)) + { + // functionContext.FunctionDefinition.Parameters is an ImmutableArray + var funcAsObj = (object)functionContext; + _functionDefinitionGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(funcAsObj.GetType(), "FunctionDefinition"); + var functionDefinition = _functionDefinitionGetter(funcAsObj); - _parametersGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(functionDefinition.GetType(), "Parameters"); - var parameters = _parametersGetter(functionDefinition) as IEnumerable; + _parametersGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor(functionDefinition.GetType(), "Parameters"); + var parameters = _parametersGetter(functionDefinition) as IEnumerable; - // Trigger is normally the first parameter, but we'll check all parameters to be sure. - var foundTrigger = false; - foreach (var parameter in parameters) + // Trigger is normally the first parameter, but we'll check all parameters to be sure. + var foundTrigger = false; + foreach (var parameter in parameters) + { + // Properties is an IReadOnlyDictionary + _propertiesGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor>(parameter.GetType(), "Properties"); + var properties = _propertiesGetter(parameter); + if (properties == null || properties.Count == 0) { - // Properties is an IReadOnlyDictionary - _propertiesGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor>(parameter.GetType(), "Properties"); - var properties = _propertiesGetter(parameter); - if (properties == null || properties.Count == 0) - { - continue; - } + continue; + } - if (!properties.TryGetValue("bindingAttribute", out var triggerAttribute)) + if (!properties.TryGetValue("bindingAttribute", out var triggerAttribute)) + { + foreach (var propVal in properties.Values) { - foreach (var propVal in properties.Values) - { - if (propVal.GetType().Name.Contains("Trigger")) - { - triggerAttribute = propVal; - break; - } - } - - if (triggerAttribute == null) + if (propVal.GetType().Name.Contains("Trigger")) { - continue; + triggerAttribute = propVal; + break; } } - var triggerTypeName = triggerAttribute.GetType().Name; - Trigger = triggerTypeName.ResolveTriggerType(); - foundTrigger = true; - break; - } - - // shouldn't happen, as all functions are required to have a trigger - if (!foundTrigger) - { - agent.Logger.Debug($"Function {FunctionName} does not have a trigger, defaulting to 'other'"); - Trigger = "other"; + if (triggerAttribute == null) + { + continue; + } } - _functionTriggerCache[FunctionName] = Trigger; - } - else - { - Trigger = trigger; + var triggerTypeName = triggerAttribute.GetType().Name; + Trigger = triggerTypeName.ResolveTriggerType(); + foundTrigger = true; + break; } - if (IsWebTrigger) + // shouldn't happen, as all functions are required to have a trigger + if (!foundTrigger) { - ParseHttpTriggerParameters(agent, functionContext); + agent.Logger.Debug($"Function {FunctionName} does not have a trigger, defaulting to 'other'"); + Trigger = "other"; } + + _functionTriggerCache[FunctionName] = Trigger; } - catch (Exception ex) + else { - agent.Logger.Error(ex, "Error getting Azure Function details."); - throw; + Trigger = trigger; } - } - private void ParseHttpTriggerParameters(IAgent agent, dynamic functionContext) - { - if (!_hasAspNetCoreExtensionsReference.HasValue) + if (IsWebTrigger) { - // see if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is in the list of loaded assemblies - var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - var assembly = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == AspNetCoreExtensionsAssemblyName); + ParseHttpTriggerParameters(agent, functionContext); + } + } + catch (Exception ex) + { + agent.Logger.Error(ex, "Error getting Azure Function details."); + throw; + } + } - _hasAspNetCoreExtensionsReference = assembly != null; + private void ParseHttpTriggerParameters(IAgent agent, dynamic functionContext) + { + if (!_hasAspNetCoreExtensionsReference.HasValue) + { + // see if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is in the list of loaded assemblies + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var assembly = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == AspNetCoreExtensionsAssemblyName); - if (_hasAspNetCoreExtensionsReference.Value) - agent.Logger.Debug($"{AspNetCoreExtensionsAssemblyName} assembly is loaded; InvokeFunctionAsyncWrapper will defer HttpTrigger parameter parsing to FunctionsHttpProxyingMiddlewareWrapper."); - } + _hasAspNetCoreExtensionsReference = assembly != null; - // don't parse request parameters here if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is loaded. - // If it is loaded, parsing occurs over in FunctionsHttpProxyingMiddlewareWrapper if (_hasAspNetCoreExtensionsReference.Value) + agent.Logger.Debug($"{AspNetCoreExtensionsAssemblyName} assembly is loaded; InvokeFunctionAsyncWrapper will defer HttpTrigger parameter parsing to FunctionsHttpProxyingMiddlewareWrapper."); + } + + // don't parse request parameters here if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is loaded. + // If it is loaded, parsing occurs over in FunctionsHttpProxyingMiddlewareWrapper + if (_hasAspNetCoreExtensionsReference.Value) + { + return; + } + + object features = functionContext.Features; + + if (_genericFunctionInputBindingFeatureGetter == null) // cache the methodinfo lookups for performance + { + var get = features.GetType().GetMethod("Get"); + if (get != null) { + _genericFunctionInputBindingFeatureGetter = get.MakeGenericMethod(features.GetType().Assembly.GetType(IFunctionInputBindingFeatureTypeName)); + } + else + { + agent.Logger.Debug("Unable to find FunctionContext.Features.Get method; unable to parse request parameters."); return; } - object features = functionContext.Features; - - if (_genericFunctionInputBindingFeatureGetter == null) // cache the methodinfo lookups for performance + var bindFunctionInputType = features.GetType().Assembly.GetType(IFunctionInputBindingFeatureTypeName); + if (bindFunctionInputType == null) { - var get = features.GetType().GetMethod("Get"); - if (get != null) - { - _genericFunctionInputBindingFeatureGetter = get.MakeGenericMethod(features.GetType().Assembly.GetType(IFunctionInputBindingFeatureTypeName)); - } - else - { - agent.Logger.Debug("Unable to find FunctionContext.Features.Get method; unable to parse request parameters."); - return; - } - - var bindFunctionInputType = features.GetType().Assembly.GetType(IFunctionInputBindingFeatureTypeName); - if (bindFunctionInputType == null) - { - agent.Logger.Debug("Unable to find IFunctionInputBindingFeature type; unable to parse request parameters."); - return; - } - _bindFunctionInputAsync = bindFunctionInputType.GetMethod("BindFunctionInputAsync"); - if (_bindFunctionInputAsync == null) - { - agent.Logger.Debug("Unable to find BindFunctionInputAsync method; unable to parse request parameters."); - return; - } + agent.Logger.Debug("Unable to find IFunctionInputBindingFeature type; unable to parse request parameters."); + return; } - - if (_genericFunctionInputBindingFeatureGetter != null) + _bindFunctionInputAsync = bindFunctionInputType.GetMethod("BindFunctionInputAsync"); + if (_bindFunctionInputAsync == null) { - // Get the input binding feature and bind the input from the function context - var inputBindingFeature = _genericFunctionInputBindingFeatureGetter.Invoke(features, []); - dynamic valueTask = _bindFunctionInputAsync.Invoke(inputBindingFeature, [functionContext]); + agent.Logger.Debug("Unable to find BindFunctionInputAsync method; unable to parse request parameters."); + return; + } + } - valueTask.AsTask().Wait(); // BindFunctionInputAsync returns a ValueTask, so we need to convert it to a Task to wait on it + if (_genericFunctionInputBindingFeatureGetter != null) + { + // Get the input binding feature and bind the input from the function context + var inputBindingFeature = _genericFunctionInputBindingFeatureGetter.Invoke(features, []); + dynamic valueTask = _bindFunctionInputAsync.Invoke(inputBindingFeature, [functionContext]); - object[] inputArguments = valueTask.Result.Values; + valueTask.AsTask().Wait(); // BindFunctionInputAsync returns a ValueTask, so we need to convert it to a Task to wait on it - if (inputArguments is { Length: > 0 }) - { - var reqData = (dynamic)inputArguments[0]; + object[] inputArguments = valueTask.Result.Values; - if (reqData != null && reqData.GetType().Name == "GrpcHttpRequestData" && !string.IsNullOrEmpty(reqData.Method)) - { - RequestMethod = reqData.Method; - Uri uri = reqData.Url; - RequestPath = $"/{uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)}"; // has to start with a slash - } + if (inputArguments is { Length: > 0 }) + { + var reqData = (dynamic)inputArguments[0]; + + if (reqData != null && reqData.GetType().Name == "GrpcHttpRequestData" && !string.IsNullOrEmpty(reqData.Method)) + { + RequestMethod = reqData.Method; + Uri uri = reqData.Url; + RequestPath = $"/{uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)}"; // has to start with a slash } } } + } - public bool IsValid() - { - return !string.IsNullOrEmpty(FunctionName) && !string.IsNullOrEmpty(Trigger) && !string.IsNullOrEmpty(InvocationId); - } - - public string FunctionName { get; } + public bool IsValid() + { + return !string.IsNullOrEmpty(FunctionName) && !string.IsNullOrEmpty(Trigger) && !string.IsNullOrEmpty(InvocationId); + } - public string Trigger { get; } - public string InvocationId { get; } - public bool IsWebTrigger => Trigger == "http"; - public string RequestMethod { get; private set; } - public string RequestPath { get; private set; } + public string FunctionName { get; } - public bool? HasAspNetCoreExtensionReference => _hasAspNetCoreExtensionsReference; - } + public string Trigger { get; } + public string InvocationId { get; } + public bool IsWebTrigger => Trigger == "http"; + public string RequestMethod { get; private set; } + public string RequestPath { get; private set; } + public bool? HasAspNetCoreExtensionReference => _hasAspNetCoreExtensionsReference; } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs index ce6ae40981..32f4788d02 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs @@ -9,267 +9,269 @@ using Xunit; using Xunit.Abstractions; -namespace NewRelic.Agent.IntegrationTests.AzureFunction +namespace NewRelic.Agent.IntegrationTests.AzureFunction; + +public enum AzureFunctionHttpTriggerTestMode { - public enum AzureFunctionHttpTriggerTestMode - { - AspNetCorePipeline, - SimpleInvocation - } - public abstract class AzureFunctionHttpTriggerTestsBase : NewRelicIntegrationTest - where TFixture : AzureFunctionApplicationFixture - { - private readonly TFixture _fixture; - private readonly AzureFunctionHttpTriggerTestMode _testMode; + AspNetCorePipeline, + SimpleInvocation +} +public abstract class AzureFunctionHttpTriggerTestsBase : NewRelicIntegrationTest + where TFixture : AzureFunctionApplicationFixture +{ + private readonly TFixture _fixture; + private readonly AzureFunctionHttpTriggerTestMode _testMode; - protected AzureFunctionHttpTriggerTestsBase(TFixture fixture, ITestOutputHelper output, AzureFunctionHttpTriggerTestMode testMode) : base(fixture) - { - _fixture = fixture; - _testMode = testMode; - _fixture.TestLogger = output; + protected AzureFunctionHttpTriggerTestsBase(TFixture fixture, ITestOutputHelper output, AzureFunctionHttpTriggerTestMode testMode) : base(fixture) + { + _fixture = fixture; + _testMode = testMode; + _fixture.TestLogger = output; - _fixture.AddActions( - setupConfiguration: () => + _fixture.AddActions( + setupConfiguration: () => + { + new NewRelicConfigModifier(fixture.DestinationNewRelicConfigFilePath) + .ForceTransactionTraces() + .ConfigureFasterTransactionTracesHarvestCycle(20) + .ConfigureFasterMetricsHarvestCycle(15) + .ConfigureFasterSpanEventsHarvestCycle(15) + .SetLogLevel("finest"); + }, + exerciseApplication: () => + { + if (IsPipelineTest) { - new NewRelicConfigModifier(fixture.DestinationNewRelicConfigFilePath) - .ForceTransactionTraces() - .ConfigureFasterTransactionTracesHarvestCycle(20) - .ConfigureFasterMetricsHarvestCycle(15) - .ConfigureFasterSpanEventsHarvestCycle(15) - .SetLogLevel("finest"); - }, - exerciseApplication: () => + _fixture.Get("api/httpTriggerFunctionUsingAspNetCorePipeline?someParameter=foo"); + _fixture.Get("api/httpTriggerFunctionUsingAspNetCorePipeline?someParameter=bar"); // make a second call to verify coldStart is not sent + _fixture.Get("api/httpTriggerFunctionUsingSimpleInvocation"); // invoke an http trigger function that does not use the aspnet core pipeline, even in pipeline test mode + } + else { - if (_testMode == AzureFunctionHttpTriggerTestMode.AspNetCorePipeline) - { - _fixture.Get("api/httpTriggerFunctionUsingAspNetCorePipeline?someParameter=foo"); - _fixture.Get("api/httpTriggerFunctionUsingAspNetCorePipeline?someParameter=bar"); // make a second call to verify coldStart is not sent - _fixture.Get("api/httpTriggerFunctionUsingSimpleInvocation"); // invoke an http trigger function that does not use the aspnet core pipeline, even in pipeline test mode - } - else - { - _fixture.Get("api/httpTriggerFunctionUsingSimpleInvocation"); - _fixture.Get("api/httpTriggerFunctionUsingSimpleInvocation"); // make a second call to verify coldStart is not sent - } - _fixture.AgentLog.WaitForLogLines(AgentLogBase.TransactionSampleLogLineRegex, TimeSpan.FromMinutes(2)); + _fixture.Get("api/httpTriggerFunctionUsingSimpleInvocation"); + _fixture.Get("api/httpTriggerFunctionUsingSimpleInvocation"); // make a second call to verify coldStart is not sent } - ); + _fixture.AgentLog.WaitForLogLines(AgentLogBase.TransactionSampleLogLineRegex, TimeSpan.FromMinutes(2)); + } + ); - _fixture.Initialize(); - } + _fixture.Initialize(); + } - [SkippableFact] - public void Test_SimpleInvocationMode() - { - Skip.IfNot(_testMode == AzureFunctionHttpTriggerTestMode.SimpleInvocation); + [SkippableFact()] + public void Test_SimpleInvocationMode() + { + Skip.IfNot(IsSimpleInvocationTest, "This test is for the Simple Invocation mode only."); - var firstTransactionExpectedTransactionEventIntrinsicAttributes = new List - { - "faas.coldStart", - "faas.invocation_id", - "faas.name", - "faas.trigger", - "cloud.resource_id" - }; - - var secondTransactionUnexpectedTransactionEventIntrinsicAttributes = new List - { - "faas.coldStart" - }; + var firstTransactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart", + "faas.invocation_id", + "faas.name", + "faas.trigger", + "cloud.resource_id" + }; + + var secondTransactionUnexpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart" + }; - var expectedAgentAttributes = new Dictionary - { - { "request.uri", "/api/httpTriggerFunctionUsingSimpleInvocation"}, - { "request.method", "GET" }, - { "http.statusCode", 200 } - }; + var expectedAgentAttributes = new Dictionary + { + { "request.uri", "/api/httpTriggerFunctionUsingSimpleInvocation"}, + { "request.method", "GET" }, + { "http.statusCode", 200 } + }; - var simpleTransactionName = "WebTransaction/AzureFunction/HttpTriggerFunctionUsingSimpleInvocation"; - var simpleExpectedMetrics = new List() - { - new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", callCount = 2}, - new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", metricScope = simpleTransactionName, callCount = 2}, - new() {metricName = simpleTransactionName, callCount = 2}, - }; + var simpleTransactionName = "WebTransaction/AzureFunction/HttpTriggerFunctionUsingSimpleInvocation"; + var simpleExpectedMetrics = new List() + { + new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", callCount = 2}, + new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", metricScope = simpleTransactionName, callCount = 2}, + new() {metricName = simpleTransactionName, callCount = 2}, + }; - var transactionSample = _fixture.AgentLog.TryGetTransactionSample(simpleTransactionName); + var transactionSample = _fixture.AgentLog.TryGetTransactionSample(simpleTransactionName); - var metrics = _fixture.AgentLog.GetMetrics().ToList(); + var metrics = _fixture.AgentLog.GetMetrics().ToList(); - var simpleTransactionEvents = _fixture.AgentLog.GetTransactionEvents() - .Where(@event => @event?.IntrinsicAttributes?["name"]?.ToString() == simpleTransactionName) - .OrderBy(x => x.IntrinsicAttributes?["timestamp"]) - .ToList(); + var simpleTransactionEvents = _fixture.AgentLog.GetTransactionEvents() + .Where(@event => @event?.IntrinsicAttributes?["name"]?.ToString() == simpleTransactionName) + .OrderBy(x => x.IntrinsicAttributes?["timestamp"]) + .ToList(); - var firstTransaction = simpleTransactionEvents.FirstOrDefault(); - var secondTransaction = simpleTransactionEvents.Skip(1).FirstOrDefault(); + var firstTransaction = simpleTransactionEvents.FirstOrDefault(); + var secondTransaction = simpleTransactionEvents.Skip(1).FirstOrDefault(); - if (_fixture.AzureFunctionModeEnabled) - { - Assertions.MetricsExist(simpleExpectedMetrics, metrics); + if (_fixture.AzureFunctionModeEnabled) + { + Assertions.MetricsExist(simpleExpectedMetrics, metrics); - Assert.NotNull(transactionSample); - Assert.NotNull(firstTransaction); - Assert.NotNull(secondTransaction); + Assert.NotNull(transactionSample); + Assert.NotNull(firstTransaction); + Assert.NotNull(secondTransaction); - Assertions.TransactionTraceHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); - Assertions.TransactionTraceHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Agent, transactionSample); + Assertions.TransactionTraceHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); + Assertions.TransactionTraceHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Agent, transactionSample); - Assertions.TransactionEventHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, firstTransaction); - Assertions.TransactionEventHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Agent, firstTransaction); + Assertions.TransactionEventHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, firstTransaction); + Assertions.TransactionEventHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Agent, firstTransaction); - Assertions.TransactionEventDoesNotHaveAttributes(secondTransactionUnexpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, secondTransaction); + Assertions.TransactionEventDoesNotHaveAttributes(secondTransactionUnexpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, secondTransaction); - Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("cloud.resource_id", out var cloudResourceIdValue)); - Assert.Equal("/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/HttpTriggerFunctionUsingSimpleInvocation", cloudResourceIdValue); - Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.name", out var faasNameValue)); - Assert.Equal("HttpTriggerFunctionUsingSimpleInvocation", faasNameValue); - Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.trigger", out var faasTriggerValue)); - Assert.Equal("http", faasTriggerValue); - } - else - { - Assertions.MetricsDoNotExist(simpleExpectedMetrics, metrics); - Assert.Null(transactionSample); + Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("cloud.resource_id", out var cloudResourceIdValue)); + Assert.Equal("/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/HttpTriggerFunctionUsingSimpleInvocation", cloudResourceIdValue); + Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.name", out var faasNameValue)); + Assert.Equal("HttpTriggerFunctionUsingSimpleInvocation", faasNameValue); + Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.trigger", out var faasTriggerValue)); + Assert.Equal("http", faasTriggerValue); + } + else + { + Assertions.MetricsDoNotExist(simpleExpectedMetrics, metrics); + Assert.Null(transactionSample); - Assert.Empty(simpleTransactionEvents); // there should be no transactions when azure function mode is disabled - } + Assert.Empty(simpleTransactionEvents); // there should be no transactions when azure function mode is disabled + } - if (!_fixture.AzureFunctionModeEnabled) // look for a specific log line that indicates azure function mode is disabled - { - var disabledLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.AzureFunctionModeDisabledLogLineRegex); - Assert.NotNull(disabledLogLine); - } + if (!_fixture.AzureFunctionModeEnabled) // look for a specific log line that indicates azure function mode is disabled + { + var disabledLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.AzureFunctionModeDisabledLogLineRegex); + Assert.NotNull(disabledLogLine); } + } - [SkippableFact] - public void Test_PipelineMode() - { - Skip.IfNot(_testMode == AzureFunctionHttpTriggerTestMode.AspNetCorePipeline); + [SkippableFact] + public void Test_PipelineMode() + { + Skip.IfNot(IsPipelineTest, "This test is for the Pipeline mode only."); - var firstTransactionExpectedTransactionEventIntrinsicAttributes = new List - { - "faas.coldStart", - "faas.invocation_id", - "faas.name", - "faas.trigger", - "cloud.resource_id" - }; - - var secondTransactionUnexpectedTransactionEventIntrinsicAttributes = new List - { - "faas.coldStart" - }; + var firstTransactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart", + "faas.invocation_id", + "faas.name", + "faas.trigger", + "cloud.resource_id" + }; + + var secondTransactionUnexpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart" + }; - var simpleTransactionExpectedTransactionEventIntrinsicAttributes = new List - { - "faas.invocation_id", - "faas.name", - "faas.trigger", - "cloud.resource_id" - }; + var simpleTransactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.invocation_id", + "faas.name", + "faas.trigger", + "cloud.resource_id" + }; - var expectedAgentAttributes = new Dictionary - { - { "request.uri", "/api/httpTriggerFunctionUsingAspNetCorePipeline"}, - { "request.method", "GET" }, - { "http.statusCode", 200 } - }; + var expectedAgentAttributes = new Dictionary + { + { "request.uri", "/api/httpTriggerFunctionUsingAspNetCorePipeline"}, + { "request.method", "GET" }, + { "http.statusCode", 200 } + }; - var pipelineTransactionName = "WebTransaction/AzureFunction/HttpTriggerFunctionUsingAspNetCorePipeline"; - var pipelineExpectedMetrics = new List() - { - new() {metricName = "DotNet/HttpTriggerFunctionUsingAspNetCorePipeline", callCount = 2}, - new() {metricName = "DotNet/HttpTriggerFunctionUsingAspNetCorePipeline", metricScope = pipelineTransactionName, callCount = 2}, - new() {metricName = pipelineTransactionName, callCount = 2}, - }; + var pipelineTransactionName = "WebTransaction/AzureFunction/HttpTriggerFunctionUsingAspNetCorePipeline"; + var pipelineExpectedMetrics = new List() + { + new() {metricName = "DotNet/HttpTriggerFunctionUsingAspNetCorePipeline", callCount = 2}, + new() {metricName = "DotNet/HttpTriggerFunctionUsingAspNetCorePipeline", metricScope = pipelineTransactionName, callCount = 2}, + new() {metricName = pipelineTransactionName, callCount = 2}, + }; - var simpleTransactionName = "WebTransaction/AzureFunction/HttpTriggerFunctionUsingSimpleInvocation"; - var simpleExpectedMetrics = new List() - { - new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", callCount = 1}, - new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", metricScope = simpleTransactionName, callCount = 1}, - new() {metricName = simpleTransactionName, callCount = 1}, - }; + var simpleTransactionName = "WebTransaction/AzureFunction/HttpTriggerFunctionUsingSimpleInvocation"; + var simpleExpectedMetrics = new List() + { + new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", callCount = 1}, + new() {metricName = "DotNet/HttpTriggerFunctionUsingSimpleInvocation", metricScope = simpleTransactionName, callCount = 1}, + new() {metricName = simpleTransactionName, callCount = 1}, + }; - var transactionSample = _fixture.AgentLog.TryGetTransactionSample(pipelineTransactionName); + var transactionSample = _fixture.AgentLog.TryGetTransactionSample(pipelineTransactionName); - var metrics = _fixture.AgentLog.GetMetrics().ToList(); + var metrics = _fixture.AgentLog.GetMetrics().ToList(); - var pipelineTransactionEvents = _fixture.AgentLog.GetTransactionEvents() - .Where(@event => @event?.IntrinsicAttributes?["name"]?.ToString() == pipelineTransactionName) - .OrderBy(x => x.IntrinsicAttributes?["timestamp"]) - .ToList(); + var pipelineTransactionEvents = _fixture.AgentLog.GetTransactionEvents() + .Where(@event => @event?.IntrinsicAttributes?["name"]?.ToString() == pipelineTransactionName) + .OrderBy(x => x.IntrinsicAttributes?["timestamp"]) + .ToList(); - var firstTransaction = pipelineTransactionEvents.FirstOrDefault(); - var secondTransaction = pipelineTransactionEvents.Skip(1).FirstOrDefault(); + var firstTransaction = pipelineTransactionEvents.FirstOrDefault(); + var secondTransaction = pipelineTransactionEvents.Skip(1).FirstOrDefault(); - var simpleTransaction = _fixture.AgentLog.TryGetTransactionEvent(simpleTransactionName); + var simpleTransaction = _fixture.AgentLog.TryGetTransactionEvent(simpleTransactionName); - if (_fixture.AzureFunctionModeEnabled) - { - Assertions.MetricsExist(pipelineExpectedMetrics, metrics); - Assertions.MetricsExist(simpleExpectedMetrics, metrics); + if (_fixture.AzureFunctionModeEnabled) + { + Assertions.MetricsExist(pipelineExpectedMetrics, metrics); + Assertions.MetricsExist(simpleExpectedMetrics, metrics); - Assert.NotNull(transactionSample); - Assert.NotNull(firstTransaction); - Assert.NotNull(secondTransaction); - Assert.NotNull(simpleTransaction); + Assert.NotNull(transactionSample); + Assert.NotNull(firstTransaction); + Assert.NotNull(secondTransaction); + Assert.NotNull(simpleTransaction); - Assertions.TransactionTraceHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); - Assertions.TransactionTraceHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Agent, transactionSample); + Assertions.TransactionTraceHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); + Assertions.TransactionTraceHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Agent, transactionSample); - Assertions.TransactionEventHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, firstTransaction); - Assertions.TransactionEventHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Agent, firstTransaction); + Assertions.TransactionEventHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, firstTransaction); + Assertions.TransactionEventHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Agent, firstTransaction); - Assertions.TransactionEventDoesNotHaveAttributes(secondTransactionUnexpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, secondTransaction); + Assertions.TransactionEventDoesNotHaveAttributes(secondTransactionUnexpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, secondTransaction); - Assertions.TransactionEventHasAttributes(simpleTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, simpleTransaction); + Assertions.TransactionEventHasAttributes(simpleTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, simpleTransaction); - Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("cloud.resource_id", out var cloudResourceIdValue)); - Assert.Equal("/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/HttpTriggerFunctionUsingAspNetCorePipeline", cloudResourceIdValue); - Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.name", out var faasNameValue)); - Assert.Equal("HttpTriggerFunctionUsingAspNetCorePipeline", faasNameValue); - Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.trigger", out var faasTriggerValue)); - Assert.Equal("http", faasTriggerValue); - } - else - { - Assertions.MetricsDoNotExist(pipelineExpectedMetrics, metrics); - Assertions.MetricsDoNotExist(simpleExpectedMetrics, metrics); - Assert.Null(transactionSample); + Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("cloud.resource_id", out var cloudResourceIdValue)); + Assert.Equal("/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/HttpTriggerFunctionUsingAspNetCorePipeline", cloudResourceIdValue); + Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.name", out var faasNameValue)); + Assert.Equal("HttpTriggerFunctionUsingAspNetCorePipeline", faasNameValue); + Assert.True(firstTransaction.IntrinsicAttributes.TryGetValue("faas.trigger", out var faasTriggerValue)); + Assert.Equal("http", faasTriggerValue); + } + else + { + Assertions.MetricsDoNotExist(pipelineExpectedMetrics, metrics); + Assertions.MetricsDoNotExist(simpleExpectedMetrics, metrics); + Assert.Null(transactionSample); - Assert.Empty(pipelineTransactionEvents); // there should be no transactions when azure function mode is disabled - Assert.Null(simpleTransaction); - } + Assert.Empty(pipelineTransactionEvents); // there should be no transactions when azure function mode is disabled + Assert.Null(simpleTransaction); + } - if (!_fixture.AzureFunctionModeEnabled) // look for a specific log line that indicates azure function mode is disabled - { - var disabledLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.AzureFunctionModeDisabledLogLineRegex); - Assert.NotNull(disabledLogLine); - } + if (!_fixture.AzureFunctionModeEnabled) // look for a specific log line that indicates azure function mode is disabled + { + var disabledLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.AzureFunctionModeDisabledLogLineRegex); + Assert.NotNull(disabledLogLine); } } - // The net6 target builds the function app without the aspnetcore pipeline package included - [NetCoreTest] - public class AzureFunctionHttpTriggerTestsCoreOldest : AzureFunctionHttpTriggerTestsBase + private bool IsSimpleInvocationTest => _testMode == AzureFunctionHttpTriggerTestMode.SimpleInvocation; + private bool IsPipelineTest => _testMode == AzureFunctionHttpTriggerTestMode.AspNetCorePipeline; +} + +// The net6 target builds the function app without the aspnetcore pipeline package included +[NetCoreTest] +public class AzureFunctionHttpTriggerTestsCoreOldest : AzureFunctionHttpTriggerTestsBase +{ + public AzureFunctionHttpTriggerTestsCoreOldest(AzureFunctionApplicationFixtureHttpTriggerCoreOldest fixture, ITestOutputHelper output) + : base(fixture, output, AzureFunctionHttpTriggerTestMode.SimpleInvocation) { - public AzureFunctionHttpTriggerTestsCoreOldest(AzureFunctionApplicationFixtureHttpTriggerCoreOldest fixture, ITestOutputHelper output) - : base(fixture, output, AzureFunctionHttpTriggerTestMode.SimpleInvocation) - { - } } +} - // the net8 target builds the function app with the aspnetcore pipeline package - [NetCoreTest] - public class AzureFunctionHttpTriggerTestsCoreLatest : AzureFunctionHttpTriggerTestsBase +// the net8 target builds the function app with the aspnetcore pipeline package +[NetCoreTest] +public class AzureFunctionHttpTriggerTestsCoreLatest : AzureFunctionHttpTriggerTestsBase +{ + public AzureFunctionHttpTriggerTestsCoreLatest(AzureFunctionApplicationFixtureHttpTriggerCoreLatest fixture, ITestOutputHelper output) + : base(fixture, output, AzureFunctionHttpTriggerTestMode.AspNetCorePipeline) { - public AzureFunctionHttpTriggerTestsCoreLatest(AzureFunctionApplicationFixtureHttpTriggerCoreLatest fixture, ITestOutputHelper output) - : base(fixture, output, AzureFunctionHttpTriggerTestMode.AspNetCorePipeline) - { - } } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs index 81925af22b..625b344dfa 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs @@ -5,14 +5,13 @@ using NewRelic.Agent.IntegrationTests.RemoteServiceFixtures; using Xunit.Abstractions; -namespace NewRelic.Agent.IntegrationTests.AzureFunction +namespace NewRelic.Agent.IntegrationTests.AzureFunction; + +[NetCoreTest] +public class AzureFunctionInstrumentationDisabledTestsCoreLatest : AzureFunctionHttpTriggerTestsBase { - [NetCoreTest] - public class AzureFunctionInstrumentationDisabledTestsCoreLatest : AzureFunctionHttpTriggerTestsBase + public AzureFunctionInstrumentationDisabledTestsCoreLatest(AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest fixture, ITestOutputHelper output) + : base(fixture, output, AzureFunctionHttpTriggerTestMode.AspNetCorePipeline) // test mode doesn't really matter here { - public AzureFunctionInstrumentationDisabledTestsCoreLatest(AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest fixture, ITestOutputHelper output) - : base(fixture, output, AzureFunctionHttpTriggerTestMode.AspNetCorePipeline) // test mode doesn't really matter here - { - } } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionQueueTriggerTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionQueueTriggerTests.cs index ea3582ee2c..965b85437f 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionQueueTriggerTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionQueueTriggerTests.cs @@ -9,124 +9,123 @@ using Xunit; using Xunit.Abstractions; -namespace NewRelic.Agent.IntegrationTests.AzureFunction +namespace NewRelic.Agent.IntegrationTests.AzureFunction; + +public abstract class AzureFunctionQueueTriggerTestsBase : NewRelicIntegrationTest + where TFixture : AzureFunctionApplicationFixture { - public abstract class AzureFunctionQueueTriggerTestsBase : NewRelicIntegrationTest - where TFixture : AzureFunctionApplicationFixture - { - private readonly TFixture _fixture; + private readonly TFixture _fixture; - protected AzureFunctionQueueTriggerTestsBase(TFixture fixture, ITestOutputHelper output) : base(fixture) - { - _fixture = fixture; - _fixture.TestLogger = output; - - _fixture.AddActions( - setupConfiguration: () => - { - new NewRelicConfigModifier(fixture.DestinationNewRelicConfigFilePath) - .ForceTransactionTraces() - .ConfigureFasterTransactionTracesHarvestCycle(20) - .ConfigureFasterMetricsHarvestCycle(15) - .ConfigureFasterSpanEventsHarvestCycle(15) - .SetLogLevel("finest"); - }, - exerciseApplication: () => - { - _fixture.PostToAzureFuncTool("QueueTriggerFunction", "test message"); - - _fixture.AgentLog.WaitForLogLines(AgentLogBase.TransactionSampleLogLineRegex, TimeSpan.FromMinutes(2)); - } - ); - - _fixture.Initialize(); - } + protected AzureFunctionQueueTriggerTestsBase(TFixture fixture, ITestOutputHelper output) : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; - [Fact] - public void Test() - { - var transactionExpectedTransactionEventIntrinsicAttributes = new List - { - "faas.coldStart", - "faas.invocation_id", - "faas.name", - "faas.trigger", - "cloud.resource_id" - }; - - var transactionTraceExpectedAttributes = new Dictionary() + _fixture.AddActions( + setupConfiguration: () => { - { "faas.coldStart", true}, - //new("faas.invocation_id", "test_invocation_id"), This one is a random guid, not something we can specifically look for - { "faas.name", "QueueTriggerFunction" }, - { "faas.trigger", "datasource" }, - { "cloud.resource_id", "/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/QueueTriggerFunction" } - }; - - var transactionName = "OtherTransaction/AzureFunction/QueueTriggerFunction"; - var expectedMetrics = new List() + new NewRelicConfigModifier(fixture.DestinationNewRelicConfigFilePath) + .ForceTransactionTraces() + .ConfigureFasterTransactionTracesHarvestCycle(20) + .ConfigureFasterMetricsHarvestCycle(15) + .ConfigureFasterSpanEventsHarvestCycle(15) + .SetLogLevel("finest"); + }, + exerciseApplication: () => { - new() {metricName = "DotNet/QueueTriggerFunction", callCount = 1}, - new() {metricName = "DotNet/QueueTriggerFunction", metricScope = transactionName, callCount = 1}, - new() {metricName = transactionName, callCount = 1}, - }; + _fixture.PostToAzureFuncTool("QueueTriggerFunction", "test message"); + _fixture.AgentLog.WaitForLogLines(AgentLogBase.TransactionSampleLogLineRegex, TimeSpan.FromMinutes(2)); + } + ); - var transactionSample = _fixture.AgentLog.TryGetTransactionSample(transactionName); + _fixture.Initialize(); + } - var metrics = _fixture.AgentLog.GetMetrics().ToList(); + [Fact] + public void Test() + { + var transactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart", + "faas.invocation_id", + "faas.name", + "faas.trigger", + "cloud.resource_id" + }; + + var transactionTraceExpectedAttributes = new Dictionary() + { + { "faas.coldStart", true}, + //new("faas.invocation_id", "test_invocation_id"), This one is a random guid, not something we can specifically look for + { "faas.name", "QueueTriggerFunction" }, + { "faas.trigger", "datasource" }, + { "cloud.resource_id", "/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/QueueTriggerFunction" } + }; + + var transactionName = "OtherTransaction/AzureFunction/QueueTriggerFunction"; + var expectedMetrics = new List() + { + new() {metricName = "DotNet/QueueTriggerFunction", callCount = 1}, + new() {metricName = "DotNet/QueueTriggerFunction", metricScope = transactionName, callCount = 1}, + new() {metricName = transactionName, callCount = 1}, + }; - var transaction = _fixture.AgentLog.TryGetTransactionEvent(transactionName); - if (_fixture.AzureFunctionModeEnabled) - { - Assertions.MetricsExist(expectedMetrics, metrics); + var transactionSample = _fixture.AgentLog.TryGetTransactionSample(transactionName); - Assert.NotNull(transactionSample); - Assert.NotNull(transaction); + var metrics = _fixture.AgentLog.GetMetrics().ToList(); - Assertions.TransactionTraceHasAttributes(transactionTraceExpectedAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); + var transaction = _fixture.AgentLog.TryGetTransactionEvent(transactionName); - Assertions.TransactionEventHasAttributes(transactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, transaction); + if (_fixture.AzureFunctionModeEnabled) + { + Assertions.MetricsExist(expectedMetrics, metrics); - Assert.True(transaction.IntrinsicAttributes.TryGetValue("cloud.resource_id", out var cloudResourceIdValue)); - Assert.Equal("/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/QueueTriggerFunction", cloudResourceIdValue); - Assert.True(transaction.IntrinsicAttributes.TryGetValue("faas.name", out var faasNameValue)); - Assert.Equal("QueueTriggerFunction", faasNameValue); - Assert.True(transaction.IntrinsicAttributes.TryGetValue("faas.trigger", out var faasTriggerValue)); - Assert.Equal("datasource", faasTriggerValue); - } - else - { - Assertions.MetricsDoNotExist(expectedMetrics, metrics); - Assert.Null(transactionSample); + Assert.NotNull(transactionSample); + Assert.NotNull(transaction); - Assert.Null(transaction); - } + Assertions.TransactionTraceHasAttributes(transactionTraceExpectedAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); - if (!_fixture.AzureFunctionModeEnabled) // look for a specific log line that indicates azure function mode is disabled - { - var disabledLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.AzureFunctionModeDisabledLogLineRegex); - Assert.NotNull(disabledLogLine); - } + Assertions.TransactionEventHasAttributes(transactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, transaction); + + Assert.True(transaction.IntrinsicAttributes.TryGetValue("cloud.resource_id", out var cloudResourceIdValue)); + Assert.Equal("/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/QueueTriggerFunction", cloudResourceIdValue); + Assert.True(transaction.IntrinsicAttributes.TryGetValue("faas.name", out var faasNameValue)); + Assert.Equal("QueueTriggerFunction", faasNameValue); + Assert.True(transaction.IntrinsicAttributes.TryGetValue("faas.trigger", out var faasTriggerValue)); + Assert.Equal("datasource", faasTriggerValue); } - } + else + { + Assertions.MetricsDoNotExist(expectedMetrics, metrics); + Assert.Null(transactionSample); - [NetCoreTest] - public class AzureFunctionQueueTriggerTestsCoreOldest : AzureFunctionQueueTriggerTestsBase - { - public AzureFunctionQueueTriggerTestsCoreOldest(AzureFunctionApplicationFixtureQueueTriggerCoreOldest fixture, ITestOutputHelper output) - : base(fixture, output) + Assert.Null(transaction); + } + + if (!_fixture.AzureFunctionModeEnabled) // look for a specific log line that indicates azure function mode is disabled { + var disabledLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.AzureFunctionModeDisabledLogLineRegex); + Assert.NotNull(disabledLogLine); } } +} - [NetCoreTest] - public class AzureFunctionQueueTriggerTestsCoreLatest : AzureFunctionQueueTriggerTestsBase +[NetCoreTest] +public class AzureFunctionQueueTriggerTestsCoreOldest : AzureFunctionQueueTriggerTestsBase +{ + public AzureFunctionQueueTriggerTestsCoreOldest(AzureFunctionApplicationFixtureQueueTriggerCoreOldest fixture, ITestOutputHelper output) + : base(fixture, output) + { + } +} + +[NetCoreTest] +public class AzureFunctionQueueTriggerTestsCoreLatest : AzureFunctionQueueTriggerTestsBase +{ + public AzureFunctionQueueTriggerTestsCoreLatest(AzureFunctionApplicationFixtureQueueTriggerCoreLatest fixture, ITestOutputHelper output) + : base(fixture, output) { - public AzureFunctionQueueTriggerTestsCoreLatest(AzureFunctionApplicationFixtureQueueTriggerCoreLatest fixture, ITestOutputHelper output) - : base(fixture, output) - { - } } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AzureFunctionApplicationFixture.cs b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AzureFunctionApplicationFixture.cs index 59a6667950..5596656374 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AzureFunctionApplicationFixture.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/RemoteServiceFixtures/AzureFunctionApplicationFixture.cs @@ -3,73 +3,72 @@ using NewRelic.Agent.IntegrationTestHelpers.RemoteServiceFixtures; -namespace NewRelic.Agent.IntegrationTests.RemoteServiceFixtures +namespace NewRelic.Agent.IntegrationTests.RemoteServiceFixtures; + +public abstract class AzureFunctionApplicationFixture : RemoteApplicationFixture { - public abstract class AzureFunctionApplicationFixture : RemoteApplicationFixture - { - private const string ApplicationDirectoryName = @"AzureFunctionApplication"; + private const string ApplicationDirectoryName = @"AzureFunctionApplication"; - protected AzureFunctionApplicationFixture(string functionNames, string targetFramework, bool enableAzureFunctionMode) - : base(new AzureFuncTool(ApplicationDirectoryName, targetFramework, ApplicationType.Bounded, true, true, true, enableAzureFunctionMode)) - { - CommandLineArguments = $"start --no-build --language-worker dotnet-isolated --dotnet-isolated --functions {functionNames} "; + protected AzureFunctionApplicationFixture(string functionNames, string targetFramework, bool enableAzureFunctionMode) + : base(new AzureFuncTool(ApplicationDirectoryName, targetFramework, ApplicationType.Bounded, true, true, true, enableAzureFunctionMode)) + { + CommandLineArguments = $"start --no-build --language-worker dotnet-isolated --dotnet-isolated --functions {functionNames} "; #if DEBUG - // set a long timeout if you're going to debug into the function - CommandLineArguments += "--timeout 600 "; + // set a long timeout if you're going to debug into the function + CommandLineArguments += "--timeout 600 "; #endif - AzureFunctionModeEnabled = enableAzureFunctionMode; - } - + AzureFunctionModeEnabled = enableAzureFunctionMode; + } - public string Get(string endpoint) - { - var address = $"http://{DestinationServerName}:{Port}/{endpoint}"; - return GetString(address); - } + public string Get(string endpoint) + { + var address = $"http://{DestinationServerName}:{Port}/{endpoint}"; - public void PostToAzureFuncTool(string triggerName, string payload) - { - var address = $"http://{DestinationServerName}:{Port}/admin/functions/{triggerName}"; + return GetString(address); + } - var inputPayload = $$"""{"input":"{{payload}}"}"""; - PostJson(address, inputPayload); - } + public void PostToAzureFuncTool(string triggerName, string payload) + { + var address = $"http://{DestinationServerName}:{Port}/admin/functions/{triggerName}"; - public bool AzureFunctionModeEnabled { get; } + var inputPayload = $$"""{"input":"{{payload}}"}"""; + PostJson(address, inputPayload); } - public class AzureFunctionApplicationFixtureHttpTriggerCoreOldest : AzureFunctionApplicationFixture + public bool AzureFunctionModeEnabled { get; } +} + +public class AzureFunctionApplicationFixtureHttpTriggerCoreOldest : AzureFunctionApplicationFixture +{ + public AzureFunctionApplicationFixtureHttpTriggerCoreOldest() : base("httpTriggerFunctionUsingAspNetCorePipeline httpTriggerFunctionUsingSimpleInvocation", "net6.0", true) { - public AzureFunctionApplicationFixtureHttpTriggerCoreOldest() : base("httpTriggerFunctionUsingAspNetCorePipeline httpTriggerFunctionUsingSimpleInvocation", "net6.0", true) - { - } } - public class AzureFunctionApplicationFixtureHttpTriggerCoreLatest : AzureFunctionApplicationFixture +} +public class AzureFunctionApplicationFixtureHttpTriggerCoreLatest : AzureFunctionApplicationFixture +{ + public AzureFunctionApplicationFixtureHttpTriggerCoreLatest() : base("httpTriggerFunctionUsingAspNetCorePipeline httpTriggerFunctionUsingSimpleInvocation", "net8.0", true) { - public AzureFunctionApplicationFixtureHttpTriggerCoreLatest() : base("httpTriggerFunctionUsingAspNetCorePipeline httpTriggerFunctionUsingSimpleInvocation", "net8.0", true) - { - } } - public class AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest : AzureFunctionApplicationFixture +} +public class AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest : AzureFunctionApplicationFixture +{ + public AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest() : base("httpTriggerFunctionUsingAspNetCorePipeline httpTriggerFunctionUsingSimpleInvocation", "net8.0", false) { - public AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest() : base("httpTriggerFunctionUsingAspNetCorePipeline httpTriggerFunctionUsingSimpleInvocation", "net8.0", false) - { - } } +} - public class AzureFunctionApplicationFixtureQueueTriggerCoreOldest : AzureFunctionApplicationFixture +public class AzureFunctionApplicationFixtureQueueTriggerCoreOldest : AzureFunctionApplicationFixture +{ + public AzureFunctionApplicationFixtureQueueTriggerCoreOldest() : base("queueTriggerFunction", "net6.0", true) { - public AzureFunctionApplicationFixtureQueueTriggerCoreOldest() : base("queueTriggerFunction", "net6.0", true) - { - } } - public class AzureFunctionApplicationFixtureQueueTriggerCoreLatest : AzureFunctionApplicationFixture +} +public class AzureFunctionApplicationFixtureQueueTriggerCoreLatest : AzureFunctionApplicationFixture +{ + public AzureFunctionApplicationFixtureQueueTriggerCoreLatest() : base("queueTriggerFunction", "net8.0", true) { - public AzureFunctionApplicationFixtureQueueTriggerCoreLatest() : base("queueTriggerFunction", "net8.0", true) - { - } } } diff --git a/tests/Agent/UnitTests/Core.UnitTest/Transactions/TransactionTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Transactions/TransactionTests.cs index fb449b307e..929a38be34 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Transactions/TransactionTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Transactions/TransactionTests.cs @@ -569,4 +569,26 @@ public void AddFaasAttribute_DoesNotSetAttribute_WhenKeyIsBad(string key) // Assert Assert.That(_transaction.TransactionMetadata.UserAndRequestAttributes.Count, Is.EqualTo(0)); } + + [Test] + public void HasHttpResponseStatusCode_ReturnsTrue_WhenStatusCodeIsSet() + { + // Arrange + _transaction.SetHttpResponseStatusCode(200); + + // Act + var result = _transaction.HasHttpResponseStatusCode; + + // Assert + Assert.That(result, Is.True); + } + [Test] + public void HasHttpResponseStatusCode_ReturnsFalse_WhenStatusCodeIsNotSet() + { + // Act + var result = _transaction.HasHttpResponseStatusCode; + + // Assert + Assert.That(result, Is.False); + } }