From b3a568f8cc86c3b4c70691a7de0836c07d131eac Mon Sep 17 00:00:00 2001 From: Marty Tippin <120425148+tippmar-nr@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:30:41 -0500 Subject: [PATCH] Final cleanup for http trigger param parsing. Updated integration tests to exercise more scenarios. --- .../Core/Transactions/NoOpTransaction.cs | 1 + .../Agent/Core/Transactions/Transaction.cs | 2 + .../Api/ITransaction.cs | 2 + .../FunctionsHttpProxyingMiddlewareWrapper.cs | 30 ++-- .../Wrapper/AzureFunction/Instrumentation.xml | 3 +- .../InvokeFunctionAsyncWrapper.cs | 67 +++++---- .../AzureFunctionApplication.csproj | 4 +- ...pTriggerFunctionUsingAspNetCorePipeline.cs | 4 +- ...ttpTriggerFunctionUsingSimpleInvocation.cs | 7 + .../AzureFunctionApplication/Program.cs | 6 + .../AzureFunctionHttpTriggerTests.cs | 132 +++++++++++++++--- ...ureFunctionInstrumentationDisabledTests.cs | 2 +- .../IntegrationTests/IntegrationTests.csproj | 1 + 13 files changed, 195 insertions(+), 66 deletions(-) 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 index c6e44e7dc2..e26016a5c2 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/FunctionsHttpProxyingMiddlewareWrapper.cs @@ -32,18 +32,24 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins agent.CurrentTransaction.SetRequestMethod(httpContext.Request.Method); agent.CurrentTransaction.SetUri(httpContext.Request.Path); break; - // not needed at present for getting status code, but keep in case we need to get more from httpContext - also update instrumentation.xml - //case "TryHandleHttpResult": - // object result = instrumentedMethodCall.MethodCall.MethodArguments[0]; - // httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[2]; - // bool isInvocationResult = (bool)instrumentedMethodCall.MethodCall.MethodArguments[3]; - - // agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode); - // break; - //case "TryHandleOutputBindingsHttpResult": - // httpContext = (HttpContext)instrumentedMethodCall.MethodCall.MethodArguments[1]; - // agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode); - // break; + 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]; + + 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]; + agent.CurrentTransaction.SetHttpResponseStatusCode(httpContext.Response.StatusCode); + } + break; } } 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 81560a5090..821b5cc8d2 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/Instrumentation.xml @@ -24,14 +24,13 @@ 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 b56232ca27..3ba11d76cb 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AzureFunction/InvokeFunctionAsyncWrapper.cs @@ -16,6 +16,7 @@ namespace NewRelic.Providers.Wrapper.AzureFunction { public class InvokeFunctionAsyncWrapper : IWrapper { + private static MethodInfo _getInvocationResultMethod; private static bool _loggedDisabledMessage; private const string WrapperName = "AzureFunctionInvokeAsyncWrapper"; @@ -105,33 +106,30 @@ void InvokeFunctionAsyncResponse(Task responseTask) return; } - if (functionDetails.IsWebTrigger) + // 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) { - // 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("Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions"); - var getInvocationResultMethod = type.GetMethods().Single(m => m.Name == "GetInvocationResult" && !m.ContainsGenericParameters); + 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("Microsoft.Azure.Functions.Worker.FunctionContextBindingFeatureExtensions"); + _getInvocationResultMethod = type.GetMethods().Single(m => m.Name == "GetInvocationResult" && !m.ContainsGenericParameters); + } - dynamic invocationResult = getInvocationResultMethod.Invoke(null, new[] { functionContext }); + dynamic invocationResult = _getInvocationResultMethod.Invoke(null, new[] { functionContext }); var result = invocationResult?.Value; - // the result always seems to be of this type regardless of whether the app - // uses the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore package or not - var resultTypeName = result?.GetType().Name; - if (resultTypeName == "GrpcHttpResponseData") - { - transaction.SetHttpResponseStatusCode((int)result.StatusCode); - } - else + if (result != null && result.StatusCode != null) { - agent.Logger.Debug($"Unexpected Azure Function invocationResult.Value type '{resultTypeName ?? "(null)"}' - unable to set http response status code."); + var statusCode = result.StatusCode; + transaction.SetHttpResponseStatusCode((int)statusCode); } - } - + } } catch (Exception ex) { - agent.Logger.Error(ex, "Error processing Azure Function response."); + agent.Logger.Warn(ex, "Error processing Azure Function response."); throw; } finally @@ -223,7 +221,7 @@ public FunctionDetails(dynamic functionContext, IAgent agent) if (IsWebTrigger) { - ParseRequestParameters(agent, functionContext); + ParseHttpTriggerParameters(agent, functionContext); } } catch (Exception ex) @@ -233,17 +231,18 @@ public FunctionDetails(dynamic functionContext, IAgent agent) } } - private void ParseRequestParameters(IAgent agent, dynamic functionContext) + 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 == "Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"); + _hasAspNetCoreExtensionsReference = assembly != null; if (_hasAspNetCoreExtensionsReference.Value) - agent.Logger.Debug("Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is loaded; not parsing request parameters in InvokeFunctionAsyncWrapper."); + agent.Logger.Debug("Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore 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. @@ -285,17 +284,23 @@ private void ParseRequestParameters(IAgent agent, dynamic functionContext) if (_genericFunctionInputBindingFeatureGetter != null) { // Get the input binding feature and bind the input from the function context - var inputBindingFeature = _genericFunctionInputBindingFeatureGetter.Invoke(features, new object[] { }); - dynamic valueTask = _bindFunctionInputAsync.Invoke(inputBindingFeature, new object[] { functionContext }); - valueTask.AsTask().Wait(); - var inputArguments = valueTask.Result.Values; - var reqData = inputArguments[0]; + 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 - if (reqData != null && reqData.GetType().Name == "GrpcHttpRequestData" && !string.IsNullOrEmpty(reqData.Method)) + object[] inputArguments = valueTask.Result.Values; + + if (inputArguments is { Length: > 0 }) { - RequestMethod = reqData.Method; - Uri uri = reqData.Url; - RequestPath = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped); + 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 + } } } } @@ -312,6 +317,8 @@ public bool IsValid() 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 2fa2c392f4..ce6ae40981 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionHttpTriggerTests.cs @@ -11,14 +11,21 @@ namespace NewRelic.Agent.IntegrationTests.AzureFunction { + public enum AzureFunctionHttpTriggerTestMode + { + 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) + protected AzureFunctionHttpTriggerTestsBase(TFixture fixture, ITestOutputHelper output, AzureFunctionHttpTriggerTestMode testMode) : base(fixture) { _fixture = fixture; + _testMode = testMode; _fixture.TestLogger = output; _fixture.AddActions( @@ -33,9 +40,17 @@ protected AzureFunctionHttpTriggerTestsBase(TFixture fixture, ITestOutputHelper }, exerciseApplication: () => { - _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 + 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)); } ); @@ -43,9 +58,11 @@ protected AzureFunctionHttpTriggerTestsBase(TFixture fixture, ITestOutputHelper _fixture.Initialize(); } - [Fact] - public void Test() + [SkippableFact] + public void Test_SimpleInvocationMode() { + Skip.IfNot(_testMode == AzureFunctionHttpTriggerTestMode.SimpleInvocation); + var firstTransactionExpectedTransactionEventIntrinsicAttributes = new List { "faas.coldStart", @@ -60,26 +77,105 @@ public void Test() "faas.coldStart" }; - var simpleTransactionExpectedTransactionEventIntrinsicAttributes = new List + 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); + + 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); + + 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); + } + } + + + [SkippableFact] + public void Test_PipelineMode() + { + Skip.IfNot(_testMode == AzureFunctionHttpTriggerTestMode.AspNetCorePipeline); + + var firstTransactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.coldStart", "faas.invocation_id", "faas.name", "faas.trigger", "cloud.resource_id" }; - var expectedAgentAttributes = new Dictionary + var secondTransactionUnexpectedTransactionEventIntrinsicAttributes = new List { - { "request.uri", "/api/httpTriggerFunctionUsingAspNetCorePipeline"} + "faas.coldStart" + }; + + var simpleTransactionExpectedTransactionEventIntrinsicAttributes = new List + { + "faas.invocation_id", + "faas.name", + "faas.trigger", + "cloud.resource_id" }; - var transactionTraceExpectedAttributes = new Dictionary() + var expectedAgentAttributes = 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" } + { "request.uri", "/api/httpTriggerFunctionUsingAspNetCorePipeline"}, + { "request.method", "GET" }, + { "http.statusCode", 200 } }; var pipelineTransactionName = "WebTransaction/AzureFunction/HttpTriggerFunctionUsingAspNetCorePipeline"; @@ -157,20 +253,22 @@ public void Test() } } + // 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) + : 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) + : base(fixture, output, AzureFunctionHttpTriggerTestMode.AspNetCorePipeline) { } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs index de915c0584..81925af22b 100644 --- a/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs +++ b/tests/Agent/IntegrationTests/IntegrationTests/AzureFunction/AzureFunctionInstrumentationDisabledTests.cs @@ -11,7 +11,7 @@ namespace NewRelic.Agent.IntegrationTests.AzureFunction public class AzureFunctionInstrumentationDisabledTestsCoreLatest : AzureFunctionHttpTriggerTestsBase { public AzureFunctionInstrumentationDisabledTestsCoreLatest(AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest fixture, ITestOutputHelper output) - : base(fixture, output) + : base(fixture, output, AzureFunctionHttpTriggerTestMode.AspNetCorePipeline) // test mode doesn't really matter here { } } 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 +