Skip to content

feat: Add instrumentation for In-Process Azure Functions #3003

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 33 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
05d4d22
Add profiler support for in-process azure functions
tippmar-nr Feb 13, 2025
d978d3b
Merge profiler changes from POC branch
tippmar-nr Feb 19, 2025
df16319
WIP commit so profiler build will complete
tippmar-nr Feb 19, 2025
199a940
chore: Update Profiler NuGet Package Reference to v10.36.0.10 (#2998)
dotnet-agent-team-bot Feb 19, 2025
6187627
in-process instrumentation initial working commit
tippmar-nr Feb 21, 2025
30b8be5
Oops
tippmar-nr Feb 21, 2025
d52d492
Add DT support for ServiceBus send and receive
tippmar-nr Feb 21, 2025
7722dc6
Add message broker segment for ServiceBus triggers
tippmar-nr Feb 24, 2025
f0b67ac
WIP
tippmar-nr Feb 24, 2025
eec003a
Cleanup, fix support for ServiceBus DT
tippmar-nr Feb 25, 2025
d521407
Integration tests for in-proc; refactored AzureFuncTool
tippmar-nr Feb 26, 2025
c16f7df
cleanup, logging
tippmar-nr Feb 26, 2025
31590b1
Bug fix
tippmar-nr Feb 27, 2025
0f4be7d
Custom output capture for azure func tool
tippmar-nr Feb 27, 2025
524d29f
Integration tests working
tippmar-nr Feb 27, 2025
beba8ed
cleanup
tippmar-nr Feb 27, 2025
a977e54
Added supportability metrics, stubbed out some isolated mode Service …
tippmar-nr Feb 27, 2025
c19f264
Add enabled flag to azure function mode supportability metric
tippmar-nr Feb 27, 2025
49fc18f
Integration test updates for supportability metrics
tippmar-nr Feb 27, 2025
fdf0940
cleanup, added unit tests
tippmar-nr Feb 27, 2025
ef0ac5c
Merge branch 'main' into feature/azure-functions-in-process-support
tippmar-nr Feb 27, 2025
6bf6e47
refactor: remove ServiceBusTriggerDetails handling
tippmar-nr Feb 28, 2025
1228bce
Update trigger list
tippmar-nr Feb 28, 2025
1a0d48f
Merge from main, resolve conflicts
tippmar-nr Mar 3, 2025
9264498
unit test cleanup
tippmar-nr Mar 3, 2025
8ba200e
chore: Update Profiler NuGet Package Reference to v10.37.0.30 (#3012)
dotnet-agent-team-bot Mar 3, 2025
58d2b6d
PR comments
tippmar-nr Mar 5, 2025
c85763b
Merge branch 'main' into feature/azure-functions-in-process-support
tippmar-nr Mar 5, 2025
64a8171
Unit test fix
tippmar-nr Mar 5, 2025
4749710
Merge branch 'feature/azure-functions-in-process-support' of https://…
tippmar-nr Mar 5, 2025
f326066
Merge from main, resolve conflicts
tippmar-nr Mar 6, 2025
35d77a9
chore: Update Profiler NuGet Package Reference to v10.38.0.36 (#3028)
dotnet-agent-team-bot Mar 6, 2025
7fdbf7b
Integration test updates
tippmar-nr Mar 6, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ private void CollectOneTimeMetrics()
ReportIfGCSamplerV2IsEnabled();
ReportIfAwsAccountIdProvided();
ReportIfAgentControlHealthEnabled();
ReportIfAzureFunctionModeIsEnabled();
ReportIfAzureFunctionModeIsDetected();
}

public void CollectMetrics()
Expand Down Expand Up @@ -985,11 +985,11 @@ private void ReportIfAwsAccountIdProvided()
}
}

private void ReportIfAzureFunctionModeIsEnabled()
private void ReportIfAzureFunctionModeIsDetected()
{
if (_configuration.AzureFunctionModeEnabled && _configuration.AzureFunctionModeDetected)
if (_configuration.AzureFunctionModeDetected)
{
ReportSupportabilityCountMetric(MetricNames.SupportabilityAzureFunctionModeEnabled);
ReportSupportabilityCountMetric(MetricNames.SupportabilityAzureFunctionMode(_configuration.AzureFunctionModeEnabled));
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,11 @@ public static string GetSupportabilityInstallType(string installType)
public const string SupportabilityIgnoredInstrumentation = SupportabilityDotnetPs + "IgnoredInstrumentation";
public const string SupportabilityGCSamplerV2Enabled = SupportabilityDotnetPs + "GCSamplerV2/Enabled";
public const string SupportabilityAwsAccountIdProvided = SupportabilityDotnetPs + "AwsAccountId/Config";
public const string SupportabilityAzureFunctionModeEnabled = SupportabilityDotnetPs + "AzureFunctionMode/Enabled";

public static string SupportabilityAzureFunctionMode(bool enabled)
{
return SupportabilityDotnetPs + "AzureFunctionMode" + PathSeparator + (enabled ? Enabled : Disabled);
}

#endregion Supportability

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ public void AddFaasAttribute(string name, object value)
return;
}

public object GetFaasAttribute(string name)
{
return null;
}

public void AddCloudSdkAttribute(string name, object value)
{
return;
Expand Down
6 changes: 6 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1393,5 +1393,11 @@ public void AddFaasAttribute(string name, object value)
var faasAttrib = _attribDefs.GetFaasAttribute(name);
TransactionMetadata.UserAndRequestAttributes.TrySetValue(faasAttrib, value);
}

public object GetFaasAttribute(string name)
{
var faasAttrib = _attribDefs.GetFaasAttribute(name);
return TransactionMetadata.UserAndRequestAttributes.GetAttributeValues(AttributeClassification.Intrinsics).FirstOrDefault(v => v.AttributeDefinition == faasAttrib)?.Value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -315,5 +315,6 @@ ISegment StartMessageBrokerSegment(MethodCall methodCall, MessageBrokerDestinati
void AddLambdaAttribute(string name, object value);

void AddFaasAttribute(string name, object value);
object GetFaasAttribute(string name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;

namespace NewRelic.Providers.Wrapper.AzureFunction;

public class AzureFunctionInProcessExecuteWithWatchersAsyncWrapper : IWrapper
{
private static ConcurrentDictionary<string, InProcessFunctionDetails> _functionDetailsCache = new();
private static Func<object, string> _fullNameGetter;
private static Func<object, object> _functionDescriptorGetter;
private static Func<object, Guid> _idGetter;

private static bool _loggedDisabledMessage;

private static bool _coldStart = true;
private static bool IsColdStart => _coldStart && !(_coldStart = false);


public bool IsTransactionRequired => false;

public CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo)
{
return new CanWrapResponse(nameof(AzureFunctionInProcessExecuteWithWatchersAsyncWrapper).Equals(instrumentedMethodInfo.RequestedWrapperName));
}

public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
{
if (!agent.Configuration.AzureFunctionModeEnabled) // bail early if azure function mode isn't enabled
{
if (!_loggedDisabledMessage)
{
agent.Logger.Info("Azure Function mode is not enabled; Azure Functions will not be instrumented.");
_loggedDisabledMessage = true;
}

return Delegates.NoOp;
}

object functionInstance = instrumentedMethodCall.MethodCall.MethodArguments[0];

_functionDescriptorGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(functionInstance.GetType(), "FunctionDescriptor");
var functionDescriptor = _functionDescriptorGetter(functionInstance);

_fullNameGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<string>(functionDescriptor.GetType(), "FullName");
string functionClassAndMethodName = _fullNameGetter(functionDescriptor);

// cache the function details by function name so we only have to reflect on the function once
var inProcessFunctionDetails = _functionDetailsCache.GetOrAdd(functionClassAndMethodName, _ => GetInProcessFunctionDetails(functionClassAndMethodName));

_idGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<Guid>(functionInstance.GetType(), "Id");
string invocationId = _idGetter(functionInstance).ToString();

agent.Logger.Finest("Instrumenting in-process Azure Function: {FunctionName} / invocation ID {invocationId} / Trigger {Trigger}.", inProcessFunctionDetails.FunctionName, invocationId, inProcessFunctionDetails.Trigger);

agent.RecordSupportabilityMetric($"DotNet/AzureFunction/Worker/InProcess");
agent.RecordSupportabilityMetric($"DotNet/AzureFunction/Trigger/{inProcessFunctionDetails.TriggerTypeName ?? "unknown"}");

transaction = agent.CreateTransaction(
isWeb: inProcessFunctionDetails.IsWebTrigger,
category: "AzureFunction",
transactionDisplayName: inProcessFunctionDetails.FunctionName,
doNotTrackAsUnitOfWork: true);

if (instrumentedMethodCall.IsAsync)
{
transaction.AttachToAsync();
transaction.DetachFromPrimary(); //Remove from thread-local type storage
}

if (IsColdStart) // only report this attribute if it's a cold start
{
transaction.AddFaasAttribute("faas.coldStart", true);
}

transaction.AddFaasAttribute("cloud.resource_id", agent.Configuration.AzureFunctionResourceIdWithFunctionName(inProcessFunctionDetails.FunctionName));
transaction.AddFaasAttribute("faas.name", $"{agent.Configuration.AzureFunctionAppName}/{inProcessFunctionDetails.FunctionName}");
transaction.AddFaasAttribute("faas.trigger", inProcessFunctionDetails.Trigger);
transaction.AddFaasAttribute("faas.invocation_id", invocationId);

var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, "Azure In-Proc Pipeline");

return Delegates.GetAsyncDelegateFor<Task>(
agent,
segment,
false,
InvokeFunctionAsyncResponse,
TaskContinuationOptions.ExecuteSynchronously);

void InvokeFunctionAsyncResponse(Task responseTask)
{
try
{
if (responseTask.IsFaulted)
{
transaction.NoticeError(responseTask.Exception);
}
}
finally
{
segment.End();
transaction.End();
}
}
}

private InProcessFunctionDetails GetInProcessFunctionDetails(string functionClassAndMethodName)
{
string functionClassName = functionClassAndMethodName.Substring(0, functionClassAndMethodName.LastIndexOf('.'));
string functionMethodName = functionClassAndMethodName.Substring(functionClassAndMethodName.LastIndexOf('.') + 1);

// get the type for functionClassName from any loaded assembly, since it's not in the current assembly
Type functionClassType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName == functionClassName);

MethodInfo functionMethod = functionClassType?.GetMethod(functionMethodName);
var functionNameAttribute = functionMethod?.GetCustomAttributes().FirstOrDefault(a => a.GetType().Name == "FunctionNameAttribute");
string functionName = functionNameAttribute?.GetType().GetProperty("Name")?.GetValue(functionNameAttribute) as string;

var triggerAttributeParameter = functionMethod?.GetParameters().FirstOrDefault(p => p.GetCustomAttributes().Any(a => a.GetType().Name.Contains("TriggerAttribute")));
var triggerAttribute = triggerAttributeParameter?.GetCustomAttributes().FirstOrDefault();
string triggerAttributeName = triggerAttribute?.GetType().Name;
string triggerType = triggerAttributeName?.ResolveTriggerType();

var inProcessFunctionDetails = new InProcessFunctionDetails
{
Trigger = triggerType,
TriggerTypeName = triggerAttributeName?.Replace("TriggerAttribute", string.Empty),
FunctionName = functionName,
};

return inProcessFunctionDetails;
}
}
Loading
Loading