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)); }
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; }
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; }
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); }
// 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); }