diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs index 20330bc315..404cbac7f4 100644 --- a/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs +++ b/src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs @@ -23,6 +23,7 @@ public class NoOpTransaction : ITransaction, ITransactionExperimental public bool IsValid => false; public bool IsFinished => false; public ISegment CurrentSegment => Segment.NoOpSegment; + public bool HasHttpResponseStatusCode => false; public DateTime StartTime => DateTime.UtcNow; diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs index be528fbb02..9cbe293901 100644 --- a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs +++ b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs @@ -72,6 +72,8 @@ public ISegment CurrentSegment } } + public bool HasHttpResponseStatusCode => TransactionMetadata.HttpResponseStatusCode.HasValue; + public ITracingState TracingState { get; private set; } public string TraceId diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs index fa51268c47..ea0f22e041 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs @@ -33,6 +33,8 @@ public interface ITransaction /// ISegment CurrentSegment { get; } + bool HasHttpResponseStatusCode { get; } + /// /// End this transaction. /// diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs new file mode 100644 index 0000000000..4c32e63e70 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs @@ -0,0 +1,56 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using NewRelic.Agent.Api; +using NewRelic.Agent.Extensions.Providers.Wrapper; + +namespace NewRelic.Providers.Wrapper.AzureFunction; + +public class FunctionsHttpProxyingMiddlewareWrapper : IWrapper +{ + private const string WrapperName = "FunctionsHttpProxyingMiddlewareWrapper"; + + public bool IsTransactionRequired => false; + + public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) + { + return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName)); + } + + /// + /// Gets request method / path for Azure function HttpTrigger invocations + /// in apps that use the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore package + /// + public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction) + { + if (agent.Configuration.AzureFunctionModeEnabled) + { + dynamic httpContext; + switch (instrumentedMethodCall.MethodCall.Method.MethodName) + { + case "AddHttpContextToFunctionContext": + httpContext = instrumentedMethodCall.MethodCall.MethodArguments[1]; + + agent.CurrentTransaction.SetRequestMethod(httpContext.Request.Method); + agent.CurrentTransaction.SetUri(httpContext.Request.Path); + break; + case "TryHandleHttpResult": + if (!agent.CurrentTransaction.HasHttpResponseStatusCode) // these handlers seem to get called more than once; only set the status code one time + { + httpContext = instrumentedMethodCall.MethodCall.MethodArguments[2]; + agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode); + } + break; + case "TryHandleOutputBindingsHttpResult": + if (!agent.CurrentTransaction.HasHttpResponseStatusCode) // these handlers seem to get called more than once; only set the status code one time + { + httpContext = instrumentedMethodCall.MethodCall.MethodArguments[1]; + agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode); + } + break; + } + } + + return Delegates.NoOp; + } +} diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml index 230a0f44b2..821b5cc8d2 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml @@ -19,5 +19,18 @@ SPDX-License-Identifier: Apache-2.0 + + + + + + + + + + + + + diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs index fc9ae2dcb5..457219d2a8 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs @@ -5,198 +5,323 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Threading.Tasks; using NewRelic.Agent.Api; using NewRelic.Agent.Extensions.Providers.Wrapper; using NewRelic.Reflection; -namespace NewRelic.Providers.Wrapper.AzureFunction +namespace NewRelic.Providers.Wrapper.AzureFunction; + +public class InvokeFunctionAsyncWrapper : IWrapper { - public class InvokeFunctionAsyncWrapper : IWrapper - { - private static bool _loggedDisabledMessage; - private const string WrapperName = "AzureFunctionInvokeAsyncWrapper"; + private static MethodInfo _getInvocationResultMethod; + private static bool _loggedDisabledMessage; + private const string WrapperName = "AzureFunctionInvokeAsyncWrapper"; - private static bool _coldStart = true; - private static bool IsColdStart => _coldStart && !(_coldStart = false); + private static bool _coldStart = true; + private static bool IsColdStart => _coldStart && !(_coldStart = false); - public bool IsTransactionRequired => false; + public bool IsTransactionRequired => false; - public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) - { - return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName)); - } + private const string FunctionContextBindingFeatureExtensionsTypeName = "Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions"; - public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, - ITransaction transaction) + public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) + { + return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName)); + } + + public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, + ITransaction transaction) + { + if (!agent.Configuration.AzureFunctionModeEnabled) // bail early if azure function mode isn't enabled { - if (!agent.Configuration.AzureFunctionModeEnabled) // bail early if azure function mode isn't enabled + if (!_loggedDisabledMessage) { - if (!_loggedDisabledMessage) - { - agent.Logger.Info("Azure Function mode is not enabled; Azure Functions will not be instrumented."); - _loggedDisabledMessage = true; - } - - return Delegates.NoOp; + agent.Logger.Info("Azure Function mode is not enabled; Azure Functions will not be instrumented."); + _loggedDisabledMessage = true; } - dynamic functionContext = instrumentedMethodCall.MethodCall.MethodArguments[0]; + return Delegates.NoOp; + } - if (functionContext == null) - { - agent.Logger.Debug($"{WrapperName}: FunctionContext is null, can't instrument this invocation."); - throw new ArgumentNullException("functionContext"); - } + dynamic functionContext = instrumentedMethodCall.MethodCall.MethodArguments[0]; - var functionDetails = new FunctionDetails(functionContext, agent); - if (!functionDetails.IsValid()) - { - agent.Logger.Debug($"{WrapperName}: FunctionDetails are invalid, can't instrument this invocation."); - throw new Exception("FunctionDetails are missing some require information."); - } + if (functionContext == null) + { + agent.Logger.Debug($"{WrapperName}: FunctionContext is null, can't instrument this invocation."); + throw new ArgumentNullException("functionContext"); + } - transaction = agent.CreateTransaction( - isWeb: functionDetails.IsWebTrigger, - category: "AzureFunction", - transactionDisplayName: functionDetails.FunctionName, - doNotTrackAsUnitOfWork: true); + var functionDetails = new FunctionDetails(functionContext, agent); + if (!functionDetails.IsValid()) + { + agent.Logger.Debug($"{WrapperName}: FunctionDetails are invalid, can't instrument this invocation."); + throw new Exception("FunctionDetails are missing some require information."); + } - if (instrumentedMethodCall.IsAsync) - { - transaction.AttachToAsync(); - transaction.DetachFromPrimary(); //Remove from thread-local type storage - } + transaction = agent.CreateTransaction( + isWeb: functionDetails.IsWebTrigger, + category: "AzureFunction", + transactionDisplayName: functionDetails.FunctionName, + doNotTrackAsUnitOfWork: true); - if (IsColdStart) // only report this attribute if it's a cold start - { - transaction.AddFaasAttribute("faas.coldStart", true); - } + if (instrumentedMethodCall.IsAsync) + { + transaction.AttachToAsync(); + transaction.DetachFromPrimary(); //Remove from thread-local type storage + } + + if (IsColdStart) // only report this attribute if it's a cold start + { + transaction.AddFaasAttribute("faas.coldStart", true); + } - transaction.AddFaasAttribute("cloud.resource_id", agent.Configuration.AzureFunctionResourceIdWithFunctionName(functionDetails.FunctionName)); - transaction.AddFaasAttribute("faas.name", functionDetails.FunctionName); - transaction.AddFaasAttribute("faas.trigger", functionDetails.Trigger); - transaction.AddFaasAttribute("faas.invocation_id", functionDetails.InvocationId); + transaction.AddFaasAttribute("cloud.resource_id", agent.Configuration.AzureFunctionResourceIdWithFunctionName(functionDetails.FunctionName)); + transaction.AddFaasAttribute("faas.name", functionDetails.FunctionName); + transaction.AddFaasAttribute("faas.trigger", functionDetails.Trigger); + transaction.AddFaasAttribute("faas.invocation_id", functionDetails.InvocationId); - var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, functionDetails.FunctionName); + if (functionDetails.IsWebTrigger && !string.IsNullOrEmpty(functionDetails.RequestMethod)) + { + transaction.SetRequestMethod(functionDetails.RequestMethod); + transaction.SetUri(functionDetails.RequestPath); + } - return Delegates.GetAsyncDelegateFor( - agent, - segment, - false, - InvokeFunctionAsyncResponse, - TaskContinuationOptions.ExecuteSynchronously); + var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, functionDetails.FunctionName); - void InvokeFunctionAsyncResponse(Task responseTask) + return Delegates.GetAsyncDelegateFor( + agent, + segment, + false, + InvokeFunctionAsyncResponse, + TaskContinuationOptions.ExecuteSynchronously); + + void InvokeFunctionAsyncResponse(Task responseTask) + { + try { - try + if (responseTask.IsFaulted) { - if (responseTask.IsFaulted) - { - transaction.NoticeError(responseTask.Exception); - return; - } + transaction.NoticeError(responseTask.Exception); + return; } - finally + + // only pull response status code here if it's a web trigger and the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is not loaded. + if (functionDetails.IsWebTrigger && functionDetails.HasAspNetCoreExtensionReference != null && !functionDetails.HasAspNetCoreExtensionReference.Value) { - segment.End(); - transaction.End(); + if (_getInvocationResultMethod == null) + { + // GetInvocationResult is a static extension method + // there are multiple GetInvocationResult methods in this type; we want the one without any generic parameters + Type type = functionContext.GetType().Assembly.GetType(FunctionContextBindingFeatureExtensionsTypeName); + _getInvocationResultMethod = type.GetMethods().Single(m => m.Name == "GetInvocationResult" && !m.ContainsGenericParameters); + } + + dynamic invocationResult = _getInvocationResultMethod.Invoke(null, new[] { functionContext }); + var result = invocationResult?.Value; + + if (result != null && result.StatusCode != null) + { + var statusCode = result.StatusCode; + transaction.SetHttpResponseStatusCode((int)statusCode); + } } } + catch (Exception ex) + { + agent.Logger.Warn(ex, "Error processing Azure Function response."); + throw; + } + finally + { + segment.End(); + transaction.End(); + } } } +} - internal class FunctionDetails - { - private static ConcurrentDictionary _functionTriggerCache = new(); - private static Func _functionDefinitionGetter; - private static Func _parametersGetter; - private static Func> _propertiesGetter; +internal class FunctionDetails +{ + private static MethodInfo _bindFunctionInputAsync; + private static MethodInfo _genericFunctionInputBindingFeatureGetter; + private static bool? _hasAspNetCoreExtensionsReference; - public FunctionDetails(dynamic functionContext, IAgent agent) + 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"; + + 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")) { - if (propVal.GetType().Name.Contains("Trigger")) - { - triggerAttribute = propVal; - break; - } - } - - if (triggerAttribute == null) - { - 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; + var triggerTypeName = triggerAttribute.GetType().Name; + Trigger = triggerTypeName.ResolveTriggerType(); + foundTrigger = true; + break; } - else + + // shouldn't happen, as all functions are required to have a trigger + if (!foundTrigger) { - Trigger = trigger; + 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; } + + if (IsWebTrigger) + { + ParseHttpTriggerParameters(agent, functionContext); + } + } + catch (Exception ex) + { + agent.Logger.Error(ex, "Error getting Azure Function details."); + throw; } + } - public bool IsValid() + private void ParseHttpTriggerParameters(IAgent agent, dynamic functionContext) + { + if (!_hasAspNetCoreExtensionsReference.HasValue) { - return !string.IsNullOrEmpty(FunctionName) && !string.IsNullOrEmpty(Trigger) && !string.IsNullOrEmpty(InvocationId); + // 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); + + _hasAspNetCoreExtensionsReference = assembly != null; + + if (_hasAspNetCoreExtensionsReference.Value) + agent.Logger.Debug($"{AspNetCoreExtensionsAssemblyName} assembly is loaded; InvokeFunctionAsyncWrapper will defer HttpTrigger parameter parsing to FunctionsHttpProxyingMiddlewareWrapper."); } - public string FunctionName { get; private set; } + // 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; + } - public string Trigger { get; private set; } - public string InvocationId { get; private set; } - public bool IsWebTrigger => Trigger == "http"; + 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; + } + + 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; + } + } + + 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]); + + valueTask.AsTask().Wait(); // BindFunctionInputAsync returns a ValueTask, so we need to convert it to a Task to wait on it + + object[] inputArguments = valueTask.Result.Values; + + 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 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/Applications/AzureFunctionApplication/AzureFunctionApplication.csproj b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/AzureFunctionApplication.csproj index d958307a4a..01a31a08ca 100644 --- a/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/AzureFunctionApplication.csproj +++ b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/AzureFunctionApplication.csproj @@ -1,4 +1,4 @@ - + net6.0;net8.0 v4 @@ -26,7 +26,7 @@ - + diff --git a/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingAspNetCorePipeline.cs b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingAspNetCorePipeline.cs index 94513157af..81fb495bdf 100644 --- a/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingAspNetCorePipeline.cs +++ b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingAspNetCorePipeline.cs @@ -1,6 +1,7 @@ // Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker; @@ -19,7 +20,7 @@ public HttpTriggerFunctionUsingAspNetCorePipeline(ILogger Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req) + public async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] [FromQuery, Required] string someParam) { _logger.LogInformation("HttpTriggerFunctionUsingAspNetCorePipeline processed a request."); @@ -29,7 +30,6 @@ public async Task Run([HttpTrigger(AuthorizationLevel.Function, " _firstTime = false; } - return new OkObjectResult("Welcome to Azure Functions!"); } } diff --git a/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingSimpleInvocation.cs b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingSimpleInvocation.cs index 655eb94026..108ce91c41 100644 --- a/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingSimpleInvocation.cs +++ b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/HttpTriggerFunctionUsingSimpleInvocation.cs @@ -13,6 +13,7 @@ namespace AzureFunctionApplication /// public class HttpTriggerFunctionUsingSimpleInvocation { + private static bool _firstTime = true; private readonly ILogger _logger; public HttpTriggerFunctionUsingSimpleInvocation(ILogger logger) @@ -25,6 +26,12 @@ public async Task Run([HttpTrigger(AuthorizationLevel.Function { _logger.LogInformation("HttpTriggerFunctionUsingSimpleInvocation processed a request."); + if (_firstTime) + { + await Task.Delay(250); // to ensure that the first invocation gets sampled + _firstTime = false; + } + var response = reqData.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "text/plain; charset=utf-8"); diff --git a/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/Program.cs b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/Program.cs index be35451855..5f8c3c6f37 100644 --- a/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/Program.cs +++ b/tests/Agent/IntegrationTests/Applications/AzureFunctionApplication/Program.cs @@ -20,7 +20,13 @@ private static async Task Main(string[] args) var host = new HostBuilder() +// the net6 target uses the "basic" azure function configuration +// the net8 target uses the aspnetcore azure function configuration +#if NET6_0 + .ConfigureFunctionsWorkerDefaults() +#else .ConfigureFunctionsWebApplication() +#endif .Build(); var task = host.RunAsync(cts.Token); diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs index 0ef72db77f..32f4788d02 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs @@ -9,169 +9,269 @@ using Xunit; using Xunit.Abstractions; -namespace NewRelic.Agent.IntegrationTests.AzureFunction +namespace NewRelic.Agent.IntegrationTests.AzureFunction; + +public enum AzureFunctionHttpTriggerTestMode { - public abstract class AzureFunctionHttpTriggerTestsBase : NewRelicIntegrationTest - where TFixture : AzureFunctionApplicationFixture - { - private readonly TFixture _fixture; + AspNetCorePipeline, + SimpleInvocation +} +public abstract class AzureFunctionHttpTriggerTestsBase : NewRelicIntegrationTest + where TFixture : AzureFunctionApplicationFixture +{ + private readonly TFixture _fixture; + private readonly AzureFunctionHttpTriggerTestMode _testMode; - protected AzureFunctionHttpTriggerTestsBase(TFixture fixture, ITestOutputHelper output) : base(fixture) - { - _fixture = fixture; - _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 { - _fixture.Get("api/httpTriggerFunctionUsingAspNetCorePipeline"); - _fixture.Get("api/httpTriggerFunctionUsingAspNetCorePipeline"); // 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 - _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(); + } + + [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 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 transactionSample = _fixture.AgentLog.TryGetTransactionSample(simpleTransactionName); + + 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 firstTransaction = simpleTransactionEvents.FirstOrDefault(); + var secondTransaction = simpleTransactionEvents.Skip(1).FirstOrDefault(); + + if (_fixture.AzureFunctionModeEnabled) + { + Assertions.MetricsExist(simpleExpectedMetrics, metrics); + + Assert.NotNull(transactionSample); + Assert.NotNull(firstTransaction); + Assert.NotNull(secondTransaction); - _fixture.Initialize(); + 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.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); - [Fact] - public void Test() + 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 firstTransactionExpectedTransactionEventIntrinsicAttributes = new List - { - "faas.coldStart", - "faas.invocation_id", - "faas.name", - "faas.trigger", - "cloud.resource_id" - }; - - var secondTransactionUnexpectedTransactionEventIntrinsicAttributes = new List - { - "faas.coldStart" - }; + var disabledLogLine = _fixture.AgentLog.TryGetLogLine(AgentLogBase.AzureFunctionModeDisabledLogLineRegex); + Assert.NotNull(disabledLogLine); + } + } - var simpleTransactionExpectedTransactionEventIntrinsicAttributes = new List - { - "faas.invocation_id", - "faas.name", - "faas.trigger", - "cloud.resource_id" - }; - var expectedAgentAttributes = new Dictionary - { - { "request.uri", "/Unknown"} - }; + [SkippableFact] + public void Test_PipelineMode() + { + Skip.IfNot(IsPipelineTest, "This test is for the Pipeline mode only."); - 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", "HttpTriggerFunctionUsingAspNetCorePipeline" }, - { "faas.trigger", "http" }, - { "cloud.resource_id", "/subscriptions/subscription_id/resourceGroups/my_resource_group/providers/Microsoft.Web/sites/IntegrationTestAppName/functions/HttpTriggerFunctionUsingAspNetCorePipeline" } - }; - - 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 firstTransactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart", + "faas.invocation_id", + "faas.name", + "faas.trigger", + "cloud.resource_id" + }; - 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 secondTransactionUnexpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart" + }; - var transactionSample = _fixture.AgentLog.TryGetTransactionSample(pipelineTransactionName); + var simpleTransactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.invocation_id", + "faas.name", + "faas.trigger", + "cloud.resource_id" + }; - var metrics = _fixture.AgentLog.GetMetrics().ToList(); + var expectedAgentAttributes = new Dictionary + { + { "request.uri", "/api/httpTriggerFunctionUsingAspNetCorePipeline"}, + { "request.method", "GET" }, + { "http.statusCode", 200 } + }; - var pipelineTransactionEvents = _fixture.AgentLog.GetTransactionEvents() - .Where(@event => @event?.IntrinsicAttributes?["name"]?.ToString() == pipelineTransactionName) - .OrderBy(x => x.IntrinsicAttributes?["timestamp"]) - .ToList(); + 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 firstTransaction = pipelineTransactionEvents.FirstOrDefault(); - var secondTransaction = pipelineTransactionEvents.Skip(1).FirstOrDefault(); + 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 simpleTransaction = _fixture.AgentLog.TryGetTransactionEvent(simpleTransactionName); + var transactionSample = _fixture.AgentLog.TryGetTransactionSample(pipelineTransactionName); - if (_fixture.AzureFunctionModeEnabled) - { - Assertions.MetricsExist(pipelineExpectedMetrics, metrics); - Assertions.MetricsExist(simpleExpectedMetrics, metrics); + var metrics = _fixture.AgentLog.GetMetrics().ToList(); - Assert.NotNull(transactionSample); - Assert.NotNull(firstTransaction); - Assert.NotNull(secondTransaction); - Assert.NotNull(simpleTransaction); + var pipelineTransactionEvents = _fixture.AgentLog.GetTransactionEvents() + .Where(@event => @event?.IntrinsicAttributes?["name"]?.ToString() == pipelineTransactionName) + .OrderBy(x => x.IntrinsicAttributes?["timestamp"]) + .ToList(); - Assertions.TransactionTraceHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); - Assertions.TransactionTraceHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Agent, transactionSample); + var firstTransaction = pipelineTransactionEvents.FirstOrDefault(); + var secondTransaction = pipelineTransactionEvents.Skip(1).FirstOrDefault(); - Assertions.TransactionEventHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, firstTransaction); - Assertions.TransactionEventHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Agent, firstTransaction); + var simpleTransaction = _fixture.AgentLog.TryGetTransactionEvent(simpleTransactionName); - Assertions.TransactionEventDoesNotHaveAttributes(secondTransactionUnexpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, secondTransaction); + if (_fixture.AzureFunctionModeEnabled) + { + Assertions.MetricsExist(pipelineExpectedMetrics, metrics); + Assertions.MetricsExist(simpleExpectedMetrics, metrics); - Assertions.TransactionEventHasAttributes(simpleTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, simpleTransaction); + Assert.NotNull(transactionSample); + Assert.NotNull(firstTransaction); + Assert.NotNull(secondTransaction); + Assert.NotNull(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); + Assertions.TransactionTraceHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Intrinsic, transactionSample); + Assertions.TransactionTraceHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionTraceAttributeType.Agent, transactionSample); - Assert.Empty(pipelineTransactionEvents); // there should be no transactions when azure function mode is disabled - Assert.Null(simpleTransaction); - } + Assertions.TransactionEventHasAttributes(firstTransactionExpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, firstTransaction); + Assertions.TransactionEventHasAttributes(expectedAgentAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Agent, firstTransaction); - 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.TransactionEventDoesNotHaveAttributes(secondTransactionUnexpectedTransactionEventIntrinsicAttributes, Tests.TestSerializationHelpers.Models.TransactionEventAttributeType.Intrinsic, secondTransaction); + + 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); - [NetCoreTest] - public class AzureFunctionHttpTriggerTestsCoreOldest : AzureFunctionHttpTriggerTestsBase - { - public AzureFunctionHttpTriggerTestsCoreOldest(AzureFunctionApplicationFixtureHttpTriggerCoreOldest fixture, ITestOutputHelper output) - : base(fixture, output) + 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); } } - [NetCoreTest] - public class AzureFunctionHttpTriggerTestsCoreLatest : 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) + { + } +} + +// 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) - { - } } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs index de915c0584..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) - { - } } } 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/IntegrationTests.csproj b/tests/Agent/IntegrationTests/IntegrationTests/IntegrationTests.csproj index 00850a046d..21a6d82f89 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/IntegrationTests.csproj +++ b/tests/Agent/IntegrationTests/IntegrationTests/IntegrationTests.csproj @@ -54,6 +54,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + 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); + } }