private IAvailabilityTestConfiguration GetOrRegister(string functionName,
                                                             IAvailabilityTestConfiguration testConfig,
                                                             bool isAvailabilityTest,
                                                             ILogger log,
                                                             string causeDescriptionMsg)
        {
            Validate.NotNullOrWhitespace(functionName, nameof(functionName));
            Validate.NotNull(testConfig, nameof(testConfig));
            causeDescriptionMsg = causeDescriptionMsg ?? "unknown reason";

            // The test will be already registered in all cases, except the first invocation.
            // Optimize for that and pay a small perf premium during the very first invocation.

            if (_registeredAvailabilityTests.TryGetValue(functionName, out AvailabilityTestRegistration registration))
            {
                if (registration.IsAvailabilityTest != isAvailabilityTest)
                {
                    throw new InvalidOperationException($"Registering Funtion \"{functionName}\"as {(isAvailabilityTest ? "" : "NOT")} "
                                                        + $"a Coded Availability Test ({causeDescriptionMsg}),"
                                                        + " but a Function with the same name is already registered as with the opposite"
                                                        + " IsAvailabilityTest-setting. Are you mixing .Net-based (in-proc) and"
                                                        + " non-.Net (out-of-proc) Functions in the same App and share the same Function name?"
                                                        + " That scenario that is not supported.");
                }

                return(registration.Config);
            }

            // We did not have a registration. Let's try to insert one:
            return(GetOrRegisterSlow(functionName, testConfig, isAvailabilityTest, log, causeDescriptionMsg));
        }
Beispiel #2
0
 internal static AvailabilityTestScope StartNew(IAvailabilityTestConfiguration testConfig,
                                                TelemetryConfiguration telemetryConfig,
                                                bool flushOnDispose,
                                                ILogger log)
 {
     return(StartNew(testConfig, telemetryConfig, flushOnDispose, log, logScope: null));
 }
            public AvailabilityTestRegistration(string functionName, IAvailabilityTestConfiguration config, bool isAvailabilityTest)
            {
                Validate.NotNullOrWhitespace(functionName, nameof(functionName));
                Validate.NotNull(config, nameof(config));

                this.FunctionName       = functionName;
                this.Config             = config;
                this.IsAvailabilityTest = isAvailabilityTest;
            }
Beispiel #4
0
 internal static AvailabilityTestScope StartNew(IAvailabilityTestConfiguration testConfig,
                                                TelemetryConfiguration telemetryConfig,
                                                bool flushOnDispose,
                                                ILogger log,
                                                object logScope)
 {
     Validate.NotNull(testConfig, nameof(testConfig));
     return(StartNew(testConfig.TestDisplayName, testConfig.LocationDisplayName, testConfig.LocationId, telemetryConfig, flushOnDispose, log, logScope));
 }
        private IAvailabilityTestConfiguration GetOrRegisterSlow(string functionName,
                                                                 IAvailabilityTestConfiguration testConfig,
                                                                 bool isAvailabilityTest,
                                                                 ILogger log,
                                                                 string causeDescriptionMsg)
        {
            AvailabilityTestRegistration newRegistration  = null;
            AvailabilityTestRegistration usedRegistration = _registeredAvailabilityTests.GetOrAdd(
                functionName,
                (fn) =>
            {
                newRegistration = new AvailabilityTestRegistration(functionName, testConfig, isAvailabilityTest);
                return(newRegistration);
            });

            if (usedRegistration == newRegistration)
            {
                log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);

                if (isAvailabilityTest)
                {
                    log?.LogInformation($"A new Coded Availability Test was discovered ({causeDescriptionMsg}):"
                                        + " {{ FunctionName=\"{FunctionName}\" }}",
                                        functionName);
                }
                else
                {
                    log?.LogInformation($"A Function was registered as NOT a Coded Availability Test ({causeDescriptionMsg}):"
                                        + " {{ FunctionName=\"{FunctionName}\" }}",
                                        functionName);
                }
            }
            else
            {
                if (usedRegistration.IsAvailabilityTest != isAvailabilityTest)
                {
                    throw new InvalidOperationException($"Registering Funtion \"{functionName}\"as {(isAvailabilityTest ? "" : "NOT")} "
                                                        + $"a Coded Availability Test ({causeDescriptionMsg}),"
                                                        + " but a Function with the same name is already registered as with the opposite"
                                                        + " IsAvailabilityTest-setting. Are you mixing .Net-based (in-proc) and"
                                                        + " non-.Net (out-of-proc) Functions in the same App and share the same Function name?"
                                                        + " That scenario that is not supported.");
                }
            }

            return(usedRegistration.Config);
        }
        public IAvailabilityTestInternalConfiguration Resolve(IAvailabilityTestConfiguration testConfig, string functionName)
        {
            // Test Display Name:
            string testDisplayName = testConfig?.TestDisplayName;

            if (String.IsNullOrWhiteSpace(testDisplayName))
            {
                testDisplayName = functionName;
            }

            if (String.IsNullOrWhiteSpace(testDisplayName))
            {
                throw new ArgumentException("The Availability Test Display Name must be set, but it was not."
                                            + " To set that value, explicitly set the property \"{nameof(AvailabilityTestResultAttribute.TestDisplayName)}\""
                                            + $" Otherwise the name of the Azure Function will be used as a fallback.");
            }


            // Location Display Name:
            string locationDisplayName = TryFillValueFromEnvironment(null, EnvironmentVariableNames.LocationDisplayName1);

            locationDisplayName = TryFillValueFromEnvironment(locationDisplayName, EnvironmentVariableNames.LocationDisplayName2);

            if (String.IsNullOrWhiteSpace(locationDisplayName))
            {
                throw new ArgumentException("The Location Display Name of the Availability Test must be set, but it was not."
                                            + " Check that one of the following environment variables are set:"
                                            + $" (a) \"{EnvironmentVariableNames.LocationDisplayName1}\";"
                                            + $" (b) \"{EnvironmentVariableNames.LocationDisplayName2}\".");
            }

            // We did our best to get the config.

            var resolvedConfig = new AvailabilityTestScopeSettingsResolver.AvailabilityTestConfiguration(testDisplayName, locationDisplayName);

            return(resolvedConfig);
        }
// Type 'FunctionInvocationContext' (and other Filter-related types) is marked as preview/obsolete,
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
#pragma warning disable CS0618
        public bool IsAvailabilityTest(FunctionInvocationContext functionInvocationContext, out string functionName, out IAvailabilityTestConfiguration testConfig)
#pragma warning restore CS0618
        {
            Validate.NotNull(functionInvocationContext, nameof(functionInvocationContext));

            functionName = functionInvocationContext.FunctionName;
            Validate.NotNullOrWhitespace(functionName, "functionInvocationContext.FunctionName");

            // In most cases we have already registered the Function:
            // either by callign this method from the filter during an earlier execution (out-of-proc languages)
            // or by calling Register(..) from the binding (.Net (in-proc) functions).

            if (_registeredAvailabilityTests.TryGetValue(functionName, out AvailabilityTestRegistration registration))
            {
                testConfig = registration.Config;
                return(registration.IsAvailabilityTest);
            }

            ILogger log = functionInvocationContext.Logger;

            log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);

            // Getting here means that we are executing out-of-proc language function for the first time.
            // In such cases, bindings happen late and dynamically, AFTER filters. Thus, NO binding has yet occurred.
            // We will read the function metadata to see if the return value of the function is tagged with the right attribute.

            try
            {
                // Attempt to parse the function metadata file. This will throw if something goes wrong.
                // We will catch immediately, but this is rare if it happens at all) and helps attaching debuggers.
                GetTestConfigFromMetadata(functionName, functionInvocationContext, log, out bool isAvailabilityTest, out testConfig);

                // We got here becasue the function was not registered, so take the insertion path right away:
                GetOrRegisterSlow(functionName, testConfig, isAvailabilityTest, log, "based on the function metadata file");
                return(isAvailabilityTest);
            }
            catch (Exception ex)
            {
                log.LogError(ex,
                             $"Error while processing function metadata file to determine whether this function is a Coded Availability Test:"
                             + " FunctionName=\"{FunctionName}\", {{ErrorType=\"{ErrorType}\", {{ErrorMessage=\"{ErrorMessage}\"}}",
                             functionName,
                             ex.GetType().Name,
                             ex.Message);

                // We could not conclusively determine the aswer from metadata.
                // We assume "NOT an Availability Test", but we do not cache this, so we will keep checking in case this was some transient IO error.
                // We are not worried about the resulting perf impace, bacause this should not happen anyway.

                testConfig = null;
                return(false);
            }
        }
 public void Register(string functionName, IAvailabilityTestConfiguration testConfig, ILogger log)
 {
     GetOrRegister(functionName, testConfig, isAvailabilityTest: true, log, "based on a code attribute annotation");
 }
// Type 'FunctionInvocationContext' (and other Filter-related types) is marked as preview/obsolete,
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
#pragma warning disable CS0618
        private static void GetTestConfigFromMetadata(string functionName,
                                                      FunctionInvocationContext functionInvocationContext,
                                                      ILogger log,
                                                      out bool isAvailabilityTest,
                                                      out IAvailabilityTestConfiguration testConfig)
#pragma warning restore CS0618
        {
            // We will do very verbose error checking and logging via exception here to aid supportability
            // in case out assumptions about Function Runtime behaviur get violated.

            const string BeginAnalysisLogMessage = "Analysis of function metadata file to determine whether this function"
                                                   + " is a Coded Availability Test beginning:"
                                                   + " {{FunctionName=\"{FunctionName}\"}}";

            const string FinishAnalysisLogMessage = "Analysis of function metadata file to determine whether this function"
                                                    + " is a Coded Availability Test finished:"
                                                    + " {{FunctionName=\"{FunctionName}\", IsAvailabilityTest=\"{IsAvailabilityTest}\"}}";

            log?.LogDebug(BeginAnalysisLogMessage, functionName);

            string metadataFileContent = ReadFunctionMetadataFile(functionInvocationContext);

            FunctionMetadata functionMetadata = JsonConvert.DeserializeObject <FunctionMetadata>(metadataFileContent);

            if (functionMetadata == null)
            {
                throw new InvalidOperationException($"Could not parse the function metadata for function \"{functionName}\".");
            }

            if (functionMetadata.Bindings == null)
            {
                throw new InvalidOperationException($"The function metadata for function \"{functionName}\" was parsed,"
                                                    + " but it did not contain a list of bindings.");
            }

            if (functionMetadata.Bindings.Count == 0)
            {
                throw new InvalidOperationException($"The function metadata for function \"{functionName}\" was parsed;"
                                                    + " it contained a list of bindings, but the list had no entries.");
            }

            foreach (BindingMetadata bindingMetadata in functionMetadata.Bindings)
            {
                if (bindingMetadata == null || bindingMetadata.Type == null)
                {
                    continue;
                }

                if (bindingMetadata.Type.Equals(AvailabilityTestResultAttribute.BindingTypeName, StringComparison.OrdinalIgnoreCase) ||
                    bindingMetadata.Type.Equals(nameof(AvailabilityTestResultAttribute), StringComparison.OrdinalIgnoreCase))
                {
                    isAvailabilityTest = true;
                    testConfig         = bindingMetadata;

                    log?.LogDebug(FinishAnalysisLogMessage, functionName, isAvailabilityTest);
                    return;
                }
            }

            isAvailabilityTest = false;
            testConfig         = null;

            log?.LogDebug(FinishAnalysisLogMessage, functionName, isAvailabilityTest);
            return;
        }
Beispiel #10
0
        public IAvailabilityTestConfiguration Resolve(IAvailabilityTestConfiguration testConfig, string functionName)
        {
            // Whenever a setting is missing, attempt to fill it from the config ir the environment:

            // Test Display Name:
            string testDisplayName = testConfig?.TestDisplayName;

            testDisplayName = TryFillValueFromConfig(
                testDisplayName,
                ConfigurationKeys.SectionNames.AvailabilityTestResults,
                ConfigurationKeys.KeyNames.TestDisplayName);

            testDisplayName = TryFillValueFromEnvironment(
                testDisplayName,
                ConfigurationKeys.EnvironmentVariableNames.TestDisplayName);

            if (String.IsNullOrWhiteSpace(testDisplayName))
            {
                testDisplayName = functionName;
            }

            if (String.IsNullOrWhiteSpace(testDisplayName))
            {
                throw new ArgumentException("The Availability Test Display Name must be set, but it was not."
                                            + " To set that value, use one of the following (in order of precedence):"
                                            + $" (a) Explicitly set the property \"{nameof(AvailabilityTestResultAttribute.TestDisplayName)}\""
                                            + $" on the '{AvailabilityTestResultAttribute.BindingTypeName}'-binding"
                                            + " (via the attribute or via function.json) (%%-tags are supported);"
                                            + $" (b) Use the App Setting \"{ConfigurationKeys.KeyNames.TestDisplayName}\" in"
                                            + $" configuration section \"{ConfigurationKeys.SectionNames.AvailabilityTestResults}\";"
                                            + $" (c) Use an environment variable \"{ConfigurationKeys.EnvironmentVariableNames.TestDisplayName}\";"
                                            + " (d) The name of the Azure Function will be used as a fallback.");
            }


            // Location Display Name:
            string locationDisplayName = testConfig?.LocationDisplayName;

            locationDisplayName = TryFillValueFromConfig(
                locationDisplayName,
                ConfigurationKeys.SectionNames.AvailabilityTestResults,
                ConfigurationKeys.KeyNames.LocationDisplayName);

            locationDisplayName = TryFillValueFromEnvironment(
                locationDisplayName,
                ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName);

            locationDisplayName = TryFillValueFromEnvironment(
                locationDisplayName,
                ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback1);

            locationDisplayName = TryFillValueFromEnvironment(
                locationDisplayName,
                ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback2);

            if (String.IsNullOrWhiteSpace(locationDisplayName))
            {
                throw new ArgumentException("The Location Display Name of the Availability Test must be set, but it was not."
                                            + " To set that value, use one of the following (in order of precedence):"
                                            + $" (a) Explicitly set the property \"{nameof(AvailabilityTestResultAttribute.LocationDisplayName)}\""
                                            + $" on the '{AvailabilityTestResultAttribute.BindingTypeName}'-binding"
                                            + " (via the attribute or via function.json) (%%-tags are supported);"
                                            + $" (b) Use the App Setting \"{ConfigurationKeys.KeyNames.LocationDisplayName}\" in"
                                            + $" configuration section \"{ConfigurationKeys.SectionNames.AvailabilityTestResults}\";"
                                            + $" (c) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName}\";"
                                            + $" (d) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback1}\";"
                                            + $" (e) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback2}\".");
            }

            // Location Id:
            string locationId = testConfig?.LocationId;

            locationId = TryFillValueFromConfig(
                locationId,
                ConfigurationKeys.SectionNames.AvailabilityTestResults,
                ConfigurationKeys.KeyNames.LocationId);

            locationId = TryFillValueFromEnvironment(
                locationId,
                ConfigurationKeys.EnvironmentVariableNames.LocationId);

            if (locationId == null)
            {
                locationId = Format.AvailabilityTest.LocationNameAsId(locationDisplayName);
            }

            if (String.IsNullOrWhiteSpace(locationId))
            {
                throw new ArgumentException($"The Location Id of the Availability Test must be set, but it was not."
                                            + $" To set that value, use one of the following (in order of precedence):"
                                            + $" (a) Explicitly set the property \"{nameof(AvailabilityTestResultAttribute.LocationId)}\""
                                            + $" on the '{AvailabilityTestResultAttribute.BindingTypeName}'-binding"
                                            + " (via the attribute or via function.json) (%%-tags are supported);"
                                            + $" (b) Use the App Setting \"{ConfigurationKeys.KeyNames.LocationId}\" in"
                                            + $" configuration section \"{ConfigurationKeys.SectionNames.AvailabilityTestResults}\";"
                                            + $" (c) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationId}\";"
                                            + $" (d) As a fallback, a Location Id will be derived from the Location Display Name (only if that is set).");
            }

            // We did our best to get the config.

            var resolvedConfig = new AvailabilityTestScopeSettingsResolver.AvailabilityTestConfiguration(testDisplayName, locationDisplayName, locationId);

            return(resolvedConfig);
        }
Beispiel #11
0
// Types 'FunctionExecutingContext' and 'IFunctionFilter' (and other Filter-related types) are marked as preview/obsolete,
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
#pragma warning disable CS0618
        public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancelControl)
#pragma warning restore CS0618
        {
            // A few lines which we need for attaching a debugger during development.
            // @ToDo: Remove before shipping.
            Console.WriteLine($"Filter Entry Point: {nameof(FunctionInvocationManagementFilter)}.{nameof(OnExecutingAsync)}(..).");
            Console.WriteLine($"FunctionInstanceId: {Format.SpellIfNull(executingContext?.FunctionInstanceId)}.");
            Process proc = Process.GetCurrentProcess();

            Console.WriteLine($"Process name: \"{proc.ProcessName}\", Process Id: \"{proc.Id}\".");
            // --

            Validate.NotNull(executingContext, nameof(executingContext));

            // Grab the invocation id and the logger:
            Guid    functionInstanceId = executingContext.FunctionInstanceId;
            ILogger log = executingContext.Logger;

            // Check if this is an Availability Test.
            // There are 3 cases:
            //  1) This IS an Availability Test and this is an in-proc/.Net functuion:
            //     This filter runs AFTER the bindings.
            //     The current function was already registered, becasue the attribute binding was already executed.
            //  2) This IS an Availability Test and this is an out-of-proc/non-.Net function:
            //     This filter runs BEFORE the bindings.
            //      a) If this is the first time the filter runs for the current function, TryGetTestConfig(..) will
            //         read the metadata file, extract the config and return True.
            //      b) If this is not the first time, the function is already registered as described in (a).
            //  3) This is NOT an Availability Test:
            //     We will get False here and do nothing.

            bool isAvailabilityTest = _availabilityTestRegistry.Functions.IsAvailabilityTest(executingContext, out string functionName, out IAvailabilityTestConfiguration testConfig);

            if (!isAvailabilityTest)
            {
                if (log != null)
                {
                    using (log.BeginScope(LogMonikers.Scopes.CreateForTestInvocation(functionName)))
                    {
                        log.LogDebug($"Availability Test Pre-Function routine was invoked and determned that this function is NOT an Availability Test:"
                                     + " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\"}}",
                                     functionName, functionInstanceId);
                    }
                }

                return(Task.CompletedTask);
            }

            // If configured, use a fall-back logger:
            log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);

            IReadOnlyDictionary <string, object> logScopeInfo = LogMonikers.Scopes.CreateForTestInvocation(functionName);

            using (log.BeginScopeSafe(logScopeInfo))
            {
                log?.LogDebug($"Availability Test Pre-Function routine was invoked:"
                              + " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\","
                              + " TestConfiguration={{TestDisplayNameTemplate=\"{TestDisplayNameTemplate}\","
                              + " LocationDisplayNameTemplate=\"{LocationDisplayNameTemplate}\","
                              + " LocationIdTemplate=\"{LocationIdTemplate}\"}} }}",
                              functionName, functionInstanceId, testConfig.TestDisplayName, testConfig.LocationDisplayName, testConfig.LocationId);

                // - In case (1) described above, we have already registered this invocation:
                //   The function parameters have been instantiated, and attached to the invocationState.
                //   However, the parameters are NOT yet initialized, as we did not have a AvailabilityTestScope instance yet.
                //   We will set up an AvailabilityTestScope and attach it to the invocationState.
                //   Then we will initialize the parameters using data from that scope.
                // - In case (2) described above, we have not yet registered the invocation:
                //   A new invocationState will end up being created now.
                //   We will set up an AvailabilityTestScope and attach it to the invocationState.
                //   Subsequently, when the binings eventually get invoked by the Functions tuntime,
                //   they will instantiate and initialize the parameters using data from that scope.

                // Get the invocation state bag:

                AvailabilityTestInvocationState invocationState = _availabilityTestRegistry.Invocations.GetOrRegister(functionInstanceId, log);

                // If test configuration makes reference to configuration, resolve the settings
                IAvailabilityTestConfiguration resolvedTestConfig = _availabilityTestScopeSettingsResolver.Resolve(testConfig, functionName);

                // Start the availability test scope (this will start timers and set up the activity span):
                AvailabilityTestScope testScope = AvailabilityTest.StartNew(resolvedTestConfig, _telemetryConfiguration, flushOnDispose: true, log, logScopeInfo);
                invocationState.AttachTestScope(testScope);

                // If we have previously instantiated a result collector, initialize it now:
                if (invocationState.TryGetResultCollector(out AvailabilityResultAsyncCollector resultCollector))
                {
                    resultCollector.Initialize(testScope);
                }

                // If we have previously instantiated a test info, initialize it now:
                if (invocationState.TryGetTestInfos(out IEnumerable <AvailabilityTestInfo> testInfos))
                {
                    AvailabilityTestInfo model = testScope.CreateAvailabilityTestInfo();
                    foreach (AvailabilityTestInfo testInfo in testInfos)
                    {
                        testInfo.CopyFrom(model);
                    }
                }
            }

            return(Task.CompletedTask);
        }