Skip to content

feat: Add support for .NET Framework Azure Function Apps #2992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Agent/NewRelic/Home/Home.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
</PropertyGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" Command="del /q &quot;$(TargetDir)*.*&quot;" />
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" WorkingDirectory="$(SolutionDir)Build" Command="powershell.exe -ExecutionPolicy Bypass -NoProfile -NonInteractive -File .\build_home.ps1 -Configuration $(ConfigurationName)" />
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" Command="del /q &quot;$(TargetDir)*.*&quot;"/>
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" WorkingDirectory="$(SolutionDir)Build" Command="powershell.exe -ExecutionPolicy Bypass -NoProfile -NonInteractive -File .\build_home.ps1 -Configuration $(ConfigurationName)"/>
</Target>

<ItemGroup>
<PackageReference Include="NewRelic.Agent.Internal.Profiler" Version="10.34.0.14"/>
<PackageReference Include="NewRelic.Agent.Internal.Profiler" Version="10.36.0.5"/>
</ItemGroup>

</Project>
42 changes: 28 additions & 14 deletions src/Agent/NewRelic/Profiler/Configuration/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,20 @@ namespace NewRelic { namespace Profiler { namespace Configuration {

bool ShouldInstrument(xstring_t const& processPath, xstring_t const& parentProcessPath, xstring_t const& appPoolId, xstring_t const& commandLine, bool isCoreClr)
{
if (IsAzureFunction()) // valid for both .NET Framework and .NET Core
{
if (IsAzureFunctionModeEnabled()) // if not explicitly enabled, fall back to "legacy" behavior
{
auto retVal = ShouldInstrumentAzureFunction(processPath, appPoolId, commandLine);
if (retVal == 0) {
return false;
}
if (retVal == 1) {
return true;
}
}
}

if (isCoreClr)
{
return ShouldInstrumentNetCore(processPath, parentProcessPath, appPoolId, commandLine);
Expand Down Expand Up @@ -610,20 +624,6 @@ namespace NewRelic { namespace Profiler { namespace Configuration {
return false;
}

if (IsAzureFunction())
{
if (IsAzureFunctionModeEnabled()) // if not explicitly enabled, fall back to "legacy" behavior
{
auto retVal = ShouldInstrumentAzureFunction(processPath, appPoolId, commandLine);
if (retVal == 0) {
return false;
}
if (retVal == 1) {
return true;
}
}
}

if (IsW3wpProcess(processPath, parentProcessPath)) {
return ShouldInstrumentApplicationPool(appPoolId);
}
Expand Down Expand Up @@ -680,6 +680,20 @@ namespace NewRelic { namespace Profiler { namespace Configuration {
return 0;
}

// look for -functions-worker-id (or --worker-id, to instrument during local testing) in the command line and instrument if found
bool isFunctionsWorkerId = NewRelic::Profiler::Strings::ContainsCaseInsensitive(commandLine, _X("--functions-worker-id"));
if (isFunctionsWorkerId)
{
LogInfo(L"Command line contains --functions-worker-id. This process will be instrumented.");
return 1;
}
bool isLocalFunctionsWorkerId = NewRelic::Profiler::Strings::ContainsCaseInsensitive(commandLine, _X("--worker-id"));
if (isLocalFunctionsWorkerId)
{
LogInfo(L"Command line contains --worker-id. This process will be instrumented.");
return 1;
}

LogInfo("Couldn't determine whether this Azure Function process should be instrumented based on commandLine. Falling back to checking application pool");
return -1; // indeterminate
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,42 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
Assert::IsTrue(configuration.ShouldInstrument(L"functionsnethost.exe", L"", L"", L"blah blah blah FooBarBaz blah blah blah", true));
}

TEST_METHOD(should_instrument_azure_function_functions_worker_id_in_command_line)
{
std::wstring configurationXml(L"\
<?xml version=\"1.0\"?>\
<configuration>\
<log level=\"deBug\"/>\
</configuration>\
");

auto systemCalls = std::make_shared<NewRelic::Profiler::Logger::Test::SystemCalls>();
systemCalls->environmentVariables[L"FUNCTIONS_WORKER_RUNTIME"] = L"dotnet-isolated";
systemCalls->environmentVariables[L"NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED"] = L"true";

Configuration configuration(configurationXml, _missingConfig, L"", systemCalls);

Assert::IsTrue(configuration.ShouldInstrument(L"SomeFW481FunctionApp.exe", L"", L"", L"blah blah blah --functions-worker-id FooBarBaz blah blah blah", false));
}

TEST_METHOD(should_instrument_azure_function_worker_id_in_command_line)
{
std::wstring configurationXml(L"\
<?xml version=\"1.0\"?>\
<configuration>\
<log level=\"deBug\"/>\
</configuration>\
");

auto systemCalls = std::make_shared<NewRelic::Profiler::Logger::Test::SystemCalls>();
systemCalls->environmentVariables[L"FUNCTIONS_WORKER_RUNTIME"] = L"dotnet-isolated";
systemCalls->environmentVariables[L"NEW_RELIC_AZURE_FUNCTION_MODE_ENABLED"] = L"true";

Configuration configuration(configurationXml, _missingConfig, L"", systemCalls);

Assert::IsTrue(configuration.ShouldInstrument(L"SomeFW481FunctionApp.exe", L"", L"", L"blah blah blah --worker-id FooBarBaz blah blah blah", false));
}

TEST_METHOD(instrument_process)
{
ProcessesPtr processes(new Processes());
Expand Down Expand Up @@ -373,7 +409,8 @@ namespace NewRelic { namespace Profiler { namespace Configuration { namespace Te
TEST_METHOD(Azure_WebSites_background_application_pool_ignored)
{
std::wstring configurationXml(L"<?xml version=\"1.0\"?><configuration/>");
Configuration configuration(configurationXml, _missingAgentEnabledConfigPair);
auto systemCalls = std::make_shared<NewRelic::Profiler::Logger::Test::SystemCalls>();
Configuration configuration(configurationXml, _missingAgentEnabledConfigPair, L"", systemCalls);
Assert::IsFalse(configuration.ShouldInstrument(L"w3wp.exe", L"", L"~Foo", L"", false));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net481;net8.0;net9.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
Expand All @@ -22,7 +22,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<FrameworkReference Include="Microsoft.AspNetCore.App" Condition="'$(TargetFramework)' == 'net9.0'"/>
<None Include="local.settings.json" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#if NET9_0
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -34,3 +35,4 @@ public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ private static async Task Main(string[] args)


var host = new HostBuilder()
// the net8 target uses the "basic" azure function configuration
// the net481 and net8 target uses the "basic" azure function configuration
// the net9 target uses the aspnetcore azure function configuration
#if NET8_0
#if NETFRAMEWORK || NET8_0
.ConfigureFunctionsWorkerDefaults()
#elif NET9_0
.ConfigureFunctionsWebApplication()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,12 @@ public AzureFunctionHttpTriggerTestsCoreLatest(AzureFunctionApplicationFixtureHt
{
}
}

[NetFrameworkTest]
public class AzureFunctionHttpTriggerTestsFWLatest : AzureFunctionHttpTriggerTestsBase<AzureFunctionApplicationFixtureHttpTriggerFWLatest>
{
public AzureFunctionHttpTriggerTestsFWLatest(AzureFunctionApplicationFixtureHttpTriggerFWLatest fixture, ITestOutputHelper output)
: base(fixture, output, AzureFunctionHttpTriggerTestMode.SimpleInvocation)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ public abstract class AzureFunctionApplicationFixture : RemoteApplicationFixture
private const string Priority = "1.23456";
private const string Timestamp = "1518469636025";

protected AzureFunctionApplicationFixture(string functionNames, string targetFramework, bool enableAzureFunctionMode)
: base(new AzureFuncTool(ApplicationDirectoryName, targetFramework, ApplicationType.Bounded, true, true, true, enableAzureFunctionMode))
protected AzureFunctionApplicationFixture(string functionNames, string targetFramework, bool enableAzureFunctionMode, bool isCoreApp = true)
: base(new AzureFuncTool(ApplicationDirectoryName, targetFramework, ApplicationType.Bounded, true, isCoreApp, true, enableAzureFunctionMode))
{
CommandLineArguments = $"start --no-build --language-worker dotnet-isolated --dotnet-isolated --functions {functionNames} ";

Expand Down Expand Up @@ -73,6 +73,13 @@ public AzureFunctionApplicationFixtureHttpTriggerCoreLatest() : base("httpTrigge
}
}

public class AzureFunctionApplicationFixtureHttpTriggerFWLatest : AzureFunctionApplicationFixture
{
public AzureFunctionApplicationFixtureHttpTriggerFWLatest() : base("httpTriggerFunctionUsingSimpleInvocation", "net481", true, false)
{
}
}

public class AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest : AzureFunctionApplicationFixture
{
public AzureFunctionApplicationFixtureInstrumentationDisabledCoreLatest() : base("httpTriggerFunctionUsingAspNetCorePipeline httpTriggerFunctionUsingSimpleInvocation", "net9.0", false)
Expand Down
Loading