|
| 1 | +// Copyright 2020 New Relic, Inc. All rights reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Collections; |
| 6 | +using System.Collections.Concurrent; |
| 7 | +using System.Collections.Generic; |
| 8 | +using System.Linq; |
| 9 | +using System.Reflection; |
| 10 | +using NewRelic.Agent.Api; |
| 11 | +using NewRelic.Agent.Extensions.Helpers; |
| 12 | +using NewRelic.Reflection; |
| 13 | + |
| 14 | +namespace NewRelic.Providers.Wrapper.AzureFunction; |
| 15 | + |
| 16 | +internal class FunctionDetails |
| 17 | +{ |
| 18 | + private static MethodInfo _bindFunctionInputAsync; |
| 19 | + private static MethodInfo _genericFunctionInputBindingFeatureGetter; |
| 20 | + private static bool? _hasAspNetCoreExtensionsReference; |
| 21 | + |
| 22 | + private static readonly ConcurrentDictionary<string, string> _functionTriggerCache = new(); |
| 23 | + private static Func<object, object> _functionDefinitionGetter; |
| 24 | + private static Func<object, object> _parametersGetter; |
| 25 | + private static Func<object, IReadOnlyDictionary<string, object>> _propertiesGetter; |
| 26 | + |
| 27 | + private const string AspNetCoreExtensionsAssemblyName = "Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"; |
| 28 | + private const string IFunctionInputBindingFeatureTypeName = "Microsoft.Azure.Functions.Worker.Context.Features.IFunctionInputBindingFeature"; |
| 29 | + |
| 30 | + public FunctionDetails(dynamic functionContext, IAgent agent) |
| 31 | + { |
| 32 | + try |
| 33 | + { |
| 34 | + FunctionName = functionContext.FunctionDefinition.Name; |
| 35 | + InvocationId = functionContext.InvocationId; |
| 36 | + |
| 37 | + // cache the trigger by function name |
| 38 | + if (!_functionTriggerCache.TryGetValue(FunctionName, out string trigger)) |
| 39 | + { |
| 40 | + // functionContext.FunctionDefinition.Parameters is an ImmutableArray<FunctionParameter> |
| 41 | + var funcAsObj = (object)functionContext; |
| 42 | + _functionDefinitionGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(funcAsObj.GetType(), "FunctionDefinition"); |
| 43 | + var functionDefinition = _functionDefinitionGetter(funcAsObj); |
| 44 | + |
| 45 | + _parametersGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(functionDefinition.GetType(), "Parameters"); |
| 46 | + var parameters = _parametersGetter(functionDefinition) as IEnumerable; |
| 47 | + |
| 48 | + // Trigger is normally the first parameter, but we'll check all parameters to be sure. |
| 49 | + var foundTrigger = false; |
| 50 | + foreach (var parameter in parameters) |
| 51 | + { |
| 52 | + // Properties is an IReadOnlyDictionary<string, object> |
| 53 | + _propertiesGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<IReadOnlyDictionary<string, object>>(parameter.GetType(), "Properties"); |
| 54 | + var properties = _propertiesGetter(parameter); |
| 55 | + if (properties == null || properties.Count == 0) |
| 56 | + { |
| 57 | + continue; |
| 58 | + } |
| 59 | + |
| 60 | + if (!properties.TryGetValue("bindingAttribute", out var triggerAttribute)) |
| 61 | + { |
| 62 | + foreach (var propVal in properties.Values) |
| 63 | + { |
| 64 | + if (propVal.GetType().Name.Contains("Trigger")) |
| 65 | + { |
| 66 | + triggerAttribute = propVal; |
| 67 | + break; |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + if (triggerAttribute == null) |
| 72 | + { |
| 73 | + continue; |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + var triggerTypeName = triggerAttribute.GetType().Name; |
| 78 | + Trigger = triggerTypeName.ResolveTriggerType(); |
| 79 | + foundTrigger = true; |
| 80 | + break; |
| 81 | + } |
| 82 | + |
| 83 | + // shouldn't happen, as all functions are required to have a trigger |
| 84 | + if (!foundTrigger) |
| 85 | + { |
| 86 | + agent.Logger.Debug($"Function {FunctionName} does not have a trigger, defaulting to 'other'"); |
| 87 | + Trigger = "other"; |
| 88 | + } |
| 89 | + |
| 90 | + _functionTriggerCache[FunctionName] = Trigger; |
| 91 | + } |
| 92 | + else |
| 93 | + { |
| 94 | + Trigger = trigger; |
| 95 | + } |
| 96 | + |
| 97 | + if (IsWebTrigger) |
| 98 | + { |
| 99 | + ParseHttpTriggerParameters(agent, functionContext); |
| 100 | + } |
| 101 | + } |
| 102 | + catch (Exception ex) |
| 103 | + { |
| 104 | + agent.Logger.Error(ex, "Error getting Azure Function details."); |
| 105 | + throw; |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + private void ParseHttpTriggerParameters(IAgent agent, dynamic functionContext) |
| 110 | + { |
| 111 | + if (!_hasAspNetCoreExtensionsReference.HasValue) |
| 112 | + { |
| 113 | + // see if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is in the list of loaded assemblies |
| 114 | + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); |
| 115 | + var assembly = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == AspNetCoreExtensionsAssemblyName); |
| 116 | + |
| 117 | + _hasAspNetCoreExtensionsReference = assembly != null; |
| 118 | + |
| 119 | + if (_hasAspNetCoreExtensionsReference.Value) |
| 120 | + agent.Logger.Debug($"{AspNetCoreExtensionsAssemblyName} assembly is loaded; InvokeFunctionAsyncWrapper will defer HttpTrigger parameter parsing to FunctionsHttpProxyingMiddlewareWrapper."); |
| 121 | + } |
| 122 | + |
| 123 | + // don't parse request parameters here if the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore assembly is loaded. |
| 124 | + // If it is loaded, parsing occurs over in FunctionsHttpProxyingMiddlewareWrapper |
| 125 | + if (_hasAspNetCoreExtensionsReference.Value) |
| 126 | + { |
| 127 | + return; |
| 128 | + } |
| 129 | + |
| 130 | + object features = functionContext.Features; |
| 131 | + |
| 132 | + if (_genericFunctionInputBindingFeatureGetter == null) // cache the methodinfo lookups for performance |
| 133 | + { |
| 134 | + var get = features.GetType().GetMethod("Get"); |
| 135 | + if (get != null) |
| 136 | + { |
| 137 | + _genericFunctionInputBindingFeatureGetter = get.MakeGenericMethod(features.GetType().Assembly.GetType(IFunctionInputBindingFeatureTypeName)); |
| 138 | + } |
| 139 | + else |
| 140 | + { |
| 141 | + agent.Logger.Debug("Unable to find FunctionContext.Features.Get method; unable to parse request parameters."); |
| 142 | + return; |
| 143 | + } |
| 144 | + |
| 145 | + var bindFunctionInputType = features.GetType().Assembly.GetType(IFunctionInputBindingFeatureTypeName); |
| 146 | + if (bindFunctionInputType == null) |
| 147 | + { |
| 148 | + agent.Logger.Debug("Unable to find IFunctionInputBindingFeature type; unable to parse request parameters."); |
| 149 | + return; |
| 150 | + } |
| 151 | + _bindFunctionInputAsync = bindFunctionInputType.GetMethod("BindFunctionInputAsync"); |
| 152 | + if (_bindFunctionInputAsync == null) |
| 153 | + { |
| 154 | + agent.Logger.Debug("Unable to find BindFunctionInputAsync method; unable to parse request parameters."); |
| 155 | + return; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + if (_genericFunctionInputBindingFeatureGetter != null) |
| 160 | + { |
| 161 | + // Get the input binding feature and bind the input from the function context |
| 162 | + var inputBindingFeature = _genericFunctionInputBindingFeatureGetter.Invoke(features, []); |
| 163 | + dynamic valueTask = _bindFunctionInputAsync.Invoke(inputBindingFeature, [functionContext]); |
| 164 | + |
| 165 | + valueTask.AsTask().Wait(); // BindFunctionInputAsync returns a ValueTask, so we need to convert it to a Task to wait on it |
| 166 | + |
| 167 | + object[] inputArguments = valueTask.Result.Values; |
| 168 | + |
| 169 | + if (inputArguments is { Length: > 0 }) |
| 170 | + { |
| 171 | + var reqData = (dynamic)inputArguments[0]; |
| 172 | + |
| 173 | + if (reqData != null && reqData.GetType().Name == "GrpcHttpRequestData" && !string.IsNullOrEmpty(reqData.Method)) |
| 174 | + { |
| 175 | + RequestMethod = reqData.Method; |
| 176 | + Uri uri = reqData.Url; |
| 177 | + RequestPath = $"/{uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)}"; // has to start with a slash |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + if (functionContext?.BindingContext?.BindingData is IReadOnlyDictionary<string, object> bindingData && bindingData.ContainsKey("Headers")) |
| 183 | + { |
| 184 | + // The headers are stored as a JSON blob. |
| 185 | + var headersJson = bindingData["Headers"].ToString(); |
| 186 | + Headers = DictionaryHelpers.FromJson(headersJson); |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + public bool IsValid() |
| 191 | + { |
| 192 | + return !string.IsNullOrEmpty(FunctionName) && !string.IsNullOrEmpty(Trigger) && !string.IsNullOrEmpty(InvocationId); |
| 193 | + } |
| 194 | + |
| 195 | + public string FunctionName { get; } |
| 196 | + |
| 197 | + public string Trigger { get; } |
| 198 | + public string InvocationId { get; } |
| 199 | + public bool IsWebTrigger => Trigger == "http"; |
| 200 | + public string RequestMethod { get; private set; } |
| 201 | + public string RequestPath { get; private set; } |
| 202 | + public IReadOnlyDictionary<string, object> Headers { get; private set; } |
| 203 | + public bool? HasAspNetCoreExtensionReference => _hasAspNetCoreExtensionsReference; |
| 204 | +} |
0 commit comments