Skip to content

Commit 47d22a1

Browse files
authored
feat: Instrument In-Process Azure Function invocations, with distributed tracing support for Http and Service Bus triggers. (#3003)
1 parent 7ab30d7 commit 47d22a1

File tree

39 files changed

+1364
-170
lines changed

39 files changed

+1364
-170
lines changed

src/Agent/NewRelic/Agent/Core/AgentHealth/AgentHealthReporter.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ private void CollectOneTimeMetrics()
812812
ReportIfGCSamplerV2IsEnabled();
813813
ReportIfAwsAccountIdProvided();
814814
ReportIfAgentControlHealthEnabled();
815-
ReportIfAzureFunctionModeIsEnabled();
815+
ReportIfAzureFunctionModeIsDetected();
816816
}
817817

818818
public void CollectMetrics()
@@ -985,11 +985,11 @@ private void ReportIfAwsAccountIdProvided()
985985
}
986986
}
987987

988-
private void ReportIfAzureFunctionModeIsEnabled()
988+
private void ReportIfAzureFunctionModeIsDetected()
989989
{
990-
if (_configuration.AzureFunctionModeEnabled && _configuration.AzureFunctionModeDetected)
990+
if (_configuration.AzureFunctionModeDetected)
991991
{
992-
ReportSupportabilityCountMetric(MetricNames.SupportabilityAzureFunctionModeEnabled);
992+
ReportSupportabilityCountMetric(MetricNames.SupportabilityAzureFunctionMode(_configuration.AzureFunctionModeEnabled));
993993
}
994994
}
995995

src/Agent/NewRelic/Agent/Core/Metrics/MetricNames.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,11 @@ public static string GetSupportabilityInstallType(string installType)
839839
public const string SupportabilityIgnoredInstrumentation = SupportabilityDotnetPs + "IgnoredInstrumentation";
840840
public const string SupportabilityGCSamplerV2Enabled = SupportabilityDotnetPs + "GCSamplerV2/Enabled";
841841
public const string SupportabilityAwsAccountIdProvided = SupportabilityDotnetPs + "AwsAccountId/Config";
842-
public const string SupportabilityAzureFunctionModeEnabled = SupportabilityDotnetPs + "AzureFunctionMode/Enabled";
842+
843+
public static string SupportabilityAzureFunctionMode(bool enabled)
844+
{
845+
return SupportabilityDotnetPs + "AzureFunctionMode" + PathSeparator + (enabled ? Enabled : Disabled);
846+
}
843847

844848
#endregion Supportability
845849

src/Agent/NewRelic/Agent/Core/Transactions/NoOpTransaction.cs

+5
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ public void AddFaasAttribute(string name, object value)
332332
return;
333333
}
334334

335+
public object GetFaasAttribute(string name)
336+
{
337+
return null;
338+
}
339+
335340
public void AddCloudSdkAttribute(string name, object value)
336341
{
337342
return;

src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs

+6
Original file line numberDiff line numberDiff line change
@@ -1393,5 +1393,11 @@ public void AddFaasAttribute(string name, object value)
13931393
var faasAttrib = _attribDefs.GetFaasAttribute(name);
13941394
TransactionMetadata.UserAndRequestAttributes.TrySetValue(faasAttrib, value);
13951395
}
1396+
1397+
public object GetFaasAttribute(string name)
1398+
{
1399+
var faasAttrib = _attribDefs.GetFaasAttribute(name);
1400+
return TransactionMetadata.UserAndRequestAttributes.GetAttributeValues(AttributeClassification.Intrinsics).FirstOrDefault(v => v.AttributeDefinition == faasAttrib)?.Value;
1401+
}
13961402
}
13971403
}

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/ITransaction.cs

+1
Original file line numberDiff line numberDiff line change
@@ -315,5 +315,6 @@ ISegment StartMessageBrokerSegment(MethodCall methodCall, MessageBrokerDestinati
315315
void AddLambdaAttribute(string name, object value);
316316

317317
void AddFaasAttribute(string name, object value);
318+
object GetFaasAttribute(string name);
318319
}
319320
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Linq;
7+
using System.Reflection;
8+
using System.Threading.Tasks;
9+
using NewRelic.Agent.Api;
10+
using NewRelic.Agent.Extensions.Providers.Wrapper;
11+
using NewRelic.Reflection;
12+
13+
namespace NewRelic.Providers.Wrapper.AzureFunction;
14+
15+
public class AzureFunctionInProcessExecuteWithWatchersAsyncWrapper : IWrapper
16+
{
17+
private static ConcurrentDictionary<string, InProcessFunctionDetails> _functionDetailsCache = new();
18+
private static Func<object, string> _fullNameGetter;
19+
private static Func<object, object> _functionDescriptorGetter;
20+
private static Func<object, Guid> _idGetter;
21+
22+
private static bool _loggedDisabledMessage;
23+
24+
private static bool _coldStart = true;
25+
private static bool IsColdStart => _coldStart && !(_coldStart = false);
26+
27+
28+
public bool IsTransactionRequired => false;
29+
30+
public CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo)
31+
{
32+
return new CanWrapResponse(nameof(AzureFunctionInProcessExecuteWithWatchersAsyncWrapper).Equals(instrumentedMethodInfo.RequestedWrapperName));
33+
}
34+
35+
public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
36+
{
37+
if (!agent.Configuration.AzureFunctionModeEnabled) // bail early if azure function mode isn't enabled
38+
{
39+
if (!_loggedDisabledMessage)
40+
{
41+
agent.Logger.Info("Azure Function mode is not enabled; Azure Functions will not be instrumented.");
42+
_loggedDisabledMessage = true;
43+
}
44+
45+
return Delegates.NoOp;
46+
}
47+
48+
object functionInstance = instrumentedMethodCall.MethodCall.MethodArguments[0];
49+
50+
_functionDescriptorGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(functionInstance.GetType(), "FunctionDescriptor");
51+
var functionDescriptor = _functionDescriptorGetter(functionInstance);
52+
53+
_fullNameGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<string>(functionDescriptor.GetType(), "FullName");
54+
string functionClassAndMethodName = _fullNameGetter(functionDescriptor);
55+
56+
// cache the function details by function name so we only have to reflect on the function once
57+
var inProcessFunctionDetails = _functionDetailsCache.GetOrAdd(functionClassAndMethodName, _ => GetInProcessFunctionDetails(functionClassAndMethodName));
58+
59+
_idGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<Guid>(functionInstance.GetType(), "Id");
60+
string invocationId = _idGetter(functionInstance).ToString();
61+
62+
agent.Logger.Finest("Instrumenting in-process Azure Function: {FunctionName} / invocation ID {invocationId} / Trigger {Trigger}.", inProcessFunctionDetails.FunctionName, invocationId, inProcessFunctionDetails.Trigger);
63+
64+
agent.RecordSupportabilityMetric($"DotNet/AzureFunction/Worker/InProcess");
65+
agent.RecordSupportabilityMetric($"DotNet/AzureFunction/Trigger/{inProcessFunctionDetails.TriggerTypeName ?? "unknown"}");
66+
67+
transaction = agent.CreateTransaction(
68+
isWeb: inProcessFunctionDetails.IsWebTrigger,
69+
category: "AzureFunction",
70+
transactionDisplayName: inProcessFunctionDetails.FunctionName,
71+
doNotTrackAsUnitOfWork: true);
72+
73+
if (instrumentedMethodCall.IsAsync)
74+
{
75+
transaction.AttachToAsync();
76+
transaction.DetachFromPrimary(); //Remove from thread-local type storage
77+
}
78+
79+
if (IsColdStart) // only report this attribute if it's a cold start
80+
{
81+
transaction.AddFaasAttribute("faas.coldStart", true);
82+
}
83+
84+
transaction.AddFaasAttribute("cloud.resource_id", agent.Configuration.AzureFunctionResourceIdWithFunctionName(inProcessFunctionDetails.FunctionName));
85+
transaction.AddFaasAttribute("faas.name", $"{agent.Configuration.AzureFunctionAppName}/{inProcessFunctionDetails.FunctionName}");
86+
transaction.AddFaasAttribute("faas.trigger", inProcessFunctionDetails.Trigger);
87+
transaction.AddFaasAttribute("faas.invocation_id", invocationId);
88+
89+
var segment = transaction.StartTransactionSegment(instrumentedMethodCall.MethodCall, "Azure In-Proc Pipeline");
90+
91+
return Delegates.GetAsyncDelegateFor<Task>(
92+
agent,
93+
segment,
94+
false,
95+
InvokeFunctionAsyncResponse,
96+
TaskContinuationOptions.ExecuteSynchronously);
97+
98+
void InvokeFunctionAsyncResponse(Task responseTask)
99+
{
100+
try
101+
{
102+
if (responseTask.IsFaulted)
103+
{
104+
transaction.NoticeError(responseTask.Exception);
105+
}
106+
}
107+
finally
108+
{
109+
segment.End();
110+
transaction.End();
111+
}
112+
}
113+
}
114+
115+
private InProcessFunctionDetails GetInProcessFunctionDetails(string functionClassAndMethodName)
116+
{
117+
string functionClassName = functionClassAndMethodName.Substring(0, functionClassAndMethodName.LastIndexOf('.'));
118+
string functionMethodName = functionClassAndMethodName.Substring(functionClassAndMethodName.LastIndexOf('.') + 1);
119+
120+
// get the type for functionClassName from any loaded assembly, since it's not in the current assembly
121+
Type functionClassType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName == functionClassName);
122+
123+
MethodInfo functionMethod = functionClassType?.GetMethod(functionMethodName);
124+
var functionNameAttribute = functionMethod?.GetCustomAttributes().FirstOrDefault(a => a.GetType().Name == "FunctionNameAttribute");
125+
string functionName = functionNameAttribute?.GetType().GetProperty("Name")?.GetValue(functionNameAttribute) as string;
126+
127+
var triggerAttributeParameter = functionMethod?.GetParameters().FirstOrDefault(p => p.GetCustomAttributes().Any(a => a.GetType().Name.Contains("TriggerAttribute")));
128+
var triggerAttribute = triggerAttributeParameter?.GetCustomAttributes().FirstOrDefault();
129+
string triggerAttributeName = triggerAttribute?.GetType().Name;
130+
string triggerType = triggerAttributeName?.ResolveTriggerType();
131+
132+
var inProcessFunctionDetails = new InProcessFunctionDetails
133+
{
134+
Trigger = triggerType,
135+
TriggerTypeName = triggerAttributeName?.Replace("TriggerAttribute", string.Empty),
136+
FunctionName = functionName,
137+
};
138+
139+
return inProcessFunctionDetails;
140+
}
141+
}

0 commit comments

Comments
 (0)