|
| 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