This commit is contained in:
macrogreg 2020-03-31 21:32:43 -07:00
Родитель 04226d9c51 7109e3922d
Коммит 4fda15a320
19 изменённых файлов: 1023 добавлений и 504 удалений

Просмотреть файл

@ -1,2 +1,14 @@
# azure-functions-availability-monitoring-extension
Azure Monitor Coded Availability Tests powered by Azure Functions.
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

Просмотреть файл

@ -22,52 +22,56 @@ namespace AvailabilityMonitoringExtensionDemo
_telemetryClient = new TelemetryClient(telemetryConfig);
}
[FunctionName("Availability-Monitoring-Demo-01-A-SimpleIoBinding")]
public async Task SimpleIoBinding(
[TimerTrigger("5 */1 * * * *")] TimerInfo timerInfo,
[AvailabilityTest(TestDisplayName = "Test Display Name",
TestArmResourceName = "Test ARM Resource Name",
LocationDisplayName = "Location Display Name",
LocationId = "Location Id")] AvailabilityTestInvocation testInvocation,
ILogger log)
{
log.LogInformation($"@@@@@@@@@@@@ \"{FunctionName}.{nameof(SimpleIoBinding)}\" started.");
log.LogInformation($"@@@@@@@@@@@@ {ToString(timerInfo)}");
//[FunctionName("Availability-Monitoring-Demo-01-A-SimpleIoBinding")]
//public async Task SimpleIoBinding(
// [TimerTrigger("5 */1 * * * *")] TimerInfo timerInfo,
// //[TimerTrigger("%FooBar%")] TimerInfo timerInfo,
// [AvailabilityTest(TestDisplayName = "Test Display Name",
// TestArmResourceName = "Test ARM Resource Name",
// LocationDisplayName = "Location Display Name",
// LocationId = "Location Id")] AvailabilityTestInfo testInvocation,
// ILogger log)
//{
// log.LogInformation($"@@@@@@@@@@@@ \"{FunctionName}.{nameof(SimpleIoBinding)}\" started.");
// log.LogInformation($"@@@@@@@@@@@@ {ToString(timerInfo)}");
testInvocation.AvailabilityResult.Name += " | Name was modified (A)";
// testInvocation.AvailabilityResult.Name += " | Name was modified (A)";
await Task.Delay(0);
}
// await Task.Delay(0);
//}
[FunctionName("Availability-Monitoring-Demo-01-B-IoBindingWithException")]
public async Task IoBindingWithException(
[TimerTrigger("10 */1 * * * *")] TimerInfo timerInfo,
[AvailabilityTest(TestDisplayName = "Test Display Name",
TestArmResourceName = "Test ARM Resource Name",
LocationDisplayName = "Location Display Name",
LocationId = "Location Id")] AvailabilityTestInvocation testInvocation,
ILogger log)
{
const bool SimulateError = false;
// [FunctionName("Availability-Monitoring-Demo-01-B-IoBindingWithException")]
// public async Task IoBindingWithException(
// [TimerTrigger("10 */1 * * * *")] TimerInfo timerInfo,
// [AvailabilityTest(TestDisplayName = "Test Display Name",
// TestArmResourceName = "Test ARM Resource Name",
// LocationDisplayName = "Location Display Name",
// LocationId = "Location Id")] AvailabilityTestInfo testInvocation,
// ILogger log)
// {
// const bool SimulateError = true;
log.LogInformation($"############ \"{FunctionName}.{nameof(IoBindingWithException)}\" started.");
log.LogInformation($"############ {ToString(timerInfo)}");
// log.LogInformation($"############ \"{FunctionName}.{nameof(IoBindingWithException)}\" started.");
// log.LogInformation($"############ {ToString(timerInfo)}");
testInvocation.AvailabilityResult.Name += " | Name modified before exception (B)";
// testInvocation.AvailabilityResult.Name += " | Name modified before exception (B)";
if (SimulateError)
{
throw new Exception("I AM A TEST EXCEPTION!! (B)");
}
// if (SimulateError)
// {
// throw new Exception("I AM A TEST EXCEPTION!! (B)");
// }
testInvocation.AvailabilityResult.Name += " | Name modified after exception site (B)";
//#pragma warning disable CS0162 // Unreachable code detected
// testInvocation.AvailabilityResult.Name += " | Name modified after exception site (B)";
//#pragma warning restore CS0162 // Unreachable code detected
await Task.Delay(0);
}
// await Task.Delay(0);
// }
[FunctionName("Availability-Monitoring-Demo-01-C-BindingToJObject")]
public async Task BindingToJObject(
[TimerTrigger("15 */1 * * * *")] TimerInfo timerInfo,
//[TimerTrigger("15 */1 * * * *")] TimerInfo timerInfo,
[TimerTrigger(AvailabilityTestInterval.Minute01)] TimerInfo timerInfo,
[AvailabilityTest(TestDisplayName = "Test Display Name",
TestArmResourceName = "Test ARM Resource Name",
LocationDisplayName = "Location Display Name",
@ -94,24 +98,24 @@ namespace AvailabilityMonitoringExtensionDemo
await Task.Delay(0);
}
[FunctionName("Availability-Monitoring-Demo-01-D-BindingToAvailabilityTelemetry")]
public async Task BindingToAvailabilityTelemetry(
[TimerTrigger("20 */1 * * * *")] TimerInfo timerInfo,
[AvailabilityTest(TestDisplayName = "Test Display Name",
TestArmResourceName = "Test ARM Resource Name",
LocationDisplayName = "Location Display Name",
LocationId = "Location Id")] AvailabilityTelemetry availabilityResult,
ILogger log)
{
log.LogInformation($"%%%%%%%%%%%% \"{FunctionName}.{nameof(BindingToAvailabilityTelemetry)}\" started.");
log.LogInformation($"%%%%%%%%%%%% {ToString(timerInfo)}");
//[FunctionName("Availability-Monitoring-Demo-01-D-BindingToAvailabilityTelemetry")]
//public async Task BindingToAvailabilityTelemetry(
// [TimerTrigger("20 */1 * * * *")] TimerInfo timerInfo,
// [AvailabilityTest(TestDisplayName = "Test Display Name",
// TestArmResourceName = "Test ARM Resource Name",
// LocationDisplayName = "Location Display Name",
// LocationId = "Location Id")] AvailabilityTelemetry availabilityResult,
// ILogger log)
//{
// log.LogInformation($"%%%%%%%%%%%% \"{FunctionName}.{nameof(BindingToAvailabilityTelemetry)}\" started.");
// log.LogInformation($"%%%%%%%%%%%% {ToString(timerInfo)}");
availabilityResult.Name += " | AvailabilityResult.Name was modified (D)";
availabilityResult.Message = "This is a test message (D)";
availabilityResult.Properties["Custom Dimension"] = "Custom Dimension Value (D)";
// availabilityResult.Name += " | AvailabilityResult.Name was modified (D)";
// availabilityResult.Message = "This is a test message (D)";
// availabilityResult.Properties["Custom Dimension"] = "Custom Dimension Value (D)";
await Task.Delay(0);
}
// await Task.Delay(0);
//}

Просмотреть файл

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
<RootNamespace>AvailabilityMonitoring_Extension_DemoFunction</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.31" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AvailabilityMonitoring-Extension\Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,20 @@
using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
using Microsoft.Extensions.Logging;
namespace AvailabilityMonitoring_Extension_DemoFunction
{
public static class CatDemoFunctions
{
[FunctionName("CatDemo-SimpleBinding")]
public static void Run(
[TimerTrigger(AvailabilityTestInterval.Minute01)]TimerInfo timerInfo,
[AvailabilityTest] AvailabilityTestInfo testInfo,
ILogger log)
{
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
testInfo.AvailabilityResult.Success = true;
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingExcludedTypes": "Request",
"samplingSettings": {
"isEnabled": true
}
}
}
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
}

Просмотреть файл

@ -1,8 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Azure.WebJobs.Description;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Azure.WebJobs.Host.Config;
@ -13,11 +11,8 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
[Extension("AvailabilityMonitoring")]
internal class AvailabilityMonitoringExtensionConfigProvider : IExtensionConfigProvider
{
private readonly TelemetryClient _telemetryClient;
public AvailabilityMonitoringExtensionConfigProvider(TelemetryConfiguration telemetryConfig)
public AvailabilityMonitoringExtensionConfigProvider()
{
_telemetryClient = new TelemetryClient(telemetryConfig);
}
public void Initialize(ExtensionConfigContext context)
@ -32,45 +27,47 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
FluentBindingRule<AvailabilityTestAttribute> rule = context.AddBindingRule<AvailabilityTestAttribute>();
#pragma warning restore CS0618
rule.BindToValueProvider(CreateAvailabilityTestInvocationBinder);
rule.BindToInput<AvailabilityTestInfo>(CreateAvailabilityTestInvocation);
rule.BindToInput<AvailabilityTelemetry>(CreateAvailabilityTelemetry);
rule.BindToInput<JObject>(CreateJObject);
}
private Task<IValueBinder> CreateAvailabilityTestInvocationBinder(AvailabilityTestAttribute attribute, Type type)
private static Task<AvailabilityTestInfo> CreateAvailabilityTestInvocation(AvailabilityTestAttribute attribute, ValueBindingContext context)
{
Validate.NotNull(attribute, nameof(attribute));
Validate.NotNull(type, nameof(type));
Validate.NotNull(context, nameof(context));
if (AvailabilityTestInvocationBinder.BoundValueType.IsAssignableFrom(type))
{
var binder = new AvailabilityTestInvocationBinder(attribute, _telemetryClient);
return Task.FromResult((IValueBinder) binder);
}
else if (ConverterBinder<AvailabilityTelemetry, AvailabilityTestInvocation>.BoundValueType.IsAssignableFrom(type))
{
var binder = new ConverterBinder<AvailabilityTelemetry, AvailabilityTestInvocation>(
new AvailabilityTestInvocationBinder(attribute, _telemetryClient),
Convert.AvailabilityTestInvocationToAvailabilityTelemetry,
Convert.AvailabilityTelemetryToAvailabilityTestInvocation);
return Task.FromResult((IValueBinder) binder);
}
else if (ConverterBinder<JObject, AvailabilityTestInvocation>.BoundValueType.IsAssignableFrom(type))
{
var binder = new ConverterBinder<JObject, AvailabilityTestInvocation>(
new AvailabilityTestInvocationBinder(attribute, _telemetryClient),
Convert.AvailabilityTestInvocationToJObject,
Convert.JObjectToAvailabilityTestInvocation);
return Task.FromResult((IValueBinder) binder);
}
else
{
// @ToDo Test that IsAssignableFrom stuff!
AvailabilityTestInfo invocationInfo = CreateAndRegisterInvocation(attribute, context.FunctionInstanceId, typeof(AvailabilityTestInfo));
return Task.FromResult(invocationInfo);
}
throw new InvalidOperationException($"Trying to use {nameof(AvailabilityTestAttribute)} to bind a value of type \"{type.FullName}\"."
+ $" This attribute can only bind values of the following types:"
+ $" \"{AvailabilityTestInvocationBinder.BoundValueType.FullName}\","
+ $" \"{ConverterBinder<AvailabilityTelemetry, AvailabilityTestInvocation>.BoundValueType.FullName}\","
+ $" \"{ConverterBinder<JObject, AvailabilityTestInvocation>.BoundValueType.FullName}\".");
}
private static Task<AvailabilityTelemetry> CreateAvailabilityTelemetry(AvailabilityTestAttribute attribute, ValueBindingContext context)
{
Validate.NotNull(attribute, nameof(attribute));
Validate.NotNull(context, nameof(context));
AvailabilityTestInfo invocationInfo = CreateAndRegisterInvocation(attribute, context.FunctionInstanceId, typeof(AvailabilityTelemetry));
return Task.FromResult(Convert.AvailabilityTestInvocationToAvailabilityTelemetry(invocationInfo));
}
private static Task<JObject> CreateJObject(AvailabilityTestAttribute attribute, ValueBindingContext context)
{
Validate.NotNull(attribute, nameof(attribute));
Validate.NotNull(context, nameof(context));
AvailabilityTestInfo invocationInfo = CreateAndRegisterInvocation(attribute, context.FunctionInstanceId, typeof(JObject));
return Task.FromResult(Convert.AvailabilityTestInvocationToJObject(invocationInfo));
}
private static AvailabilityTestInfo CreateAndRegisterInvocation(AvailabilityTestAttribute attribute, Guid functionInstanceId, Type functionParameterType)
{
var availabilityTestInfo = new AvailabilityTestInfo(attribute.TestDisplayName,
attribute.TestArmResourceName,
attribute.LocationDisplayName,
attribute.LocationId);
FunctionInvocationStateCache.SingeltonInstance.RegisterFunctionInvocation(functionInstanceId, availabilityTestInfo, functionParameterType);
return availabilityTestInfo;
}
}
}

Просмотреть файл

@ -1,4 +1,7 @@
using System;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
@ -9,7 +12,17 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
Validate.NotNull(builder, nameof(builder));
builder.AddExtension<AvailabilityMonitoringExtensionConfigProvider>();
IServiceCollection serviceCollection = builder.Services;
#pragma warning disable CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
serviceCollection.AddSingleton<IFunctionFilter, FunctionInvocationManagementFilter>();
#pragma warning restore CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
serviceCollection.AddSingleton<INameResolver, AvailabilityTimerTriggerScheduleNameResolver>();
return builder;
}
}
}

Просмотреть файл

@ -5,24 +5,34 @@ using Microsoft.ApplicationInsights.DataContracts;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
public class AvailabilityTestInvocation
public class AvailabilityTestInfo
{
[JsonProperty]
public string TestDisplayName { get; }
[JsonProperty]
public string TestArmResourceName { get; }
[JsonProperty]
public string LocationDisplayName { get; }
[JsonProperty]
public string LocationId { get; }
public DateTimeOffset StartTime { get; }
[JsonProperty]
public DateTimeOffset StartTime { get; private set; }
[JsonProperty]
public AvailabilityTelemetry AvailabilityResult { get; }
public AvailabilityTestInvocation(
[JsonProperty]
internal Guid Identity { get; }
public AvailabilityTestInfo(
string testDisplayName,
string testArmResourceName,
string locationDisplayName,
string locationId,
DateTimeOffset startTime)
string locationId)
{
Validate.NotNullOrWhitespace(testDisplayName, nameof(testDisplayName));
Validate.NotNullOrWhitespace(testArmResourceName, nameof(testArmResourceName));
@ -33,32 +43,36 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
this.TestArmResourceName = testArmResourceName;
this.LocationDisplayName = locationDisplayName;
this.LocationId = locationId;
this.StartTime = startTime;
this.StartTime = default(DateTimeOffset);
this.AvailabilityResult = CreateNewAvailabilityResult();
this.Identity = Guid.NewGuid();
}
public AvailabilityTestInvocation(AvailabilityTelemetry availabilityResult)
public AvailabilityTestInfo(AvailabilityTelemetry availabilityResult)
: this(Convert.NotNullOrWord(availabilityResult?.Name),
Convert.GetPropertyOrNullWord(availabilityResult, "WebtestArmResourceName"),
Convert.NotNullOrWord(availabilityResult?.RunLocation),
Convert.GetPropertyOrNullWord(availabilityResult, "WebtestLocationId"),
availabilityResult?.Timestamp ?? DateTimeOffset.Now)
Convert.GetPropertyOrNullWord(availabilityResult, "WebtestLocationId"))
{
Validate.NotNull(availabilityResult, nameof(availabilityResult));
this.StartTime = availabilityResult.Timestamp;
this.AvailabilityResult = availabilityResult;
this.Identity = OutputTelemetryFormat.GetAvailabilityTestInfoIdentity(availabilityResult);
}
/// <summary>
/// This is called by Newtonsoft.Json when converting from JObject.
/// </summary>
[JsonConstructor]
private AvailabilityTestInvocation(
private AvailabilityTestInfo(
string testDisplayName,
string testArmResourceName,
string locationDisplayName,
string locationId,
Guid identity,
DateTimeOffset startTime,
AvailabilityTelemetry availabilityResult)
{
@ -72,21 +86,16 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
this.TestArmResourceName = testArmResourceName;
this.LocationDisplayName = locationDisplayName;
this.LocationId = locationId;
this.Identity = identity;
this.StartTime = startTime;
this.AvailabilityResult = availabilityResult;
}
//private void InitSampleValues()
//{
// this.TestDisplayName = "User-specified Test name";
// this.TestArmResourceName = "user-specified-test-name-appinsights-component-name";
// this.LocationDisplayName = "Southeast Asia";
// this.LocationId = "apac-sg-sin-azr";
// this.StartTime = DateTimeOffset.Now;
//}
internal void SetStartTime (DateTimeOffset startTime)
{
this.StartTime = startTime;
this.AvailabilityResult.Timestamp = startTime.ToUniversalTime();
}
private AvailabilityTelemetry CreateNewAvailabilityResult()
{
@ -102,7 +111,6 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
availabilityResult.Name = this.TestDisplayName;
availabilityResult.RunLocation = this.LocationDisplayName;
// availabilityResult.Properties["FullTestResultAvailable"] = "to what do we set this?";
availabilityResult.Properties["SyntheticMonitorId"] = $"default_{this.TestArmResourceName}_{this.LocationId}";
availabilityResult.Properties["WebtestArmResourceName"] = this.TestArmResourceName;
availabilityResult.Properties["WebtestLocationId"] = this.LocationId;
@ -110,6 +118,9 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
+ $"/applications/{mockApplicationInsightsArmResourceName}"
+ $"/features/{this.TestArmResourceName}"
+ $"/locations/{this.LocationId}";
OutputTelemetryFormat.AddAvailabilityTestInfoIdentity(availabilityResult, Identity);
return availabilityResult;
}
}

Просмотреть файл

@ -0,0 +1,117 @@
using System;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
public static class AvailabilityTestInterval
{
private const string Moniker = "AvailabilityTestInterval";
public const string Minute01 = "% AvailabilityTestInterval.Minute01 %";
public const string Minutes05 = "% AvailabilityTestInterval.Minutes05 %";
public const string Minutes10 = "% AvailabilityTestInterval.Minutes10 %";
public const string Minutes15 = "% AvailabilityTestInterval.Minutes15 %";
private static class ValidSpecifiers
{
public static readonly string Minutes01 = RemoveEnclosingNameResolverTags("% AvailabilityTestInterval.Minutes01 %");
public static readonly string Minute01 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minute01);
public static readonly string Minutes05 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minutes05);
public static readonly string Minutes10 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minutes10);
public static readonly string Minutes15 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minutes15);
}
private static readonly Random Rnd = new Random();
internal static bool IsSpecification(string testIntervalSpec)
{
// Ignore nulls and empty strings:
if (String.IsNullOrWhiteSpace(testIntervalSpec))
{
return false;
}
// If starts AND ends with '%', throw those away (this also trims):
testIntervalSpec = RemoveEnclosingNameResolverTags(testIntervalSpec);
// Check that the specified 'name' starts with the right prefix:
return testIntervalSpec.StartsWith(Moniker, StringComparison.OrdinalIgnoreCase);
}
internal static int Parse(string testIntervalSpec)
{
// Ensure not null:
testIntervalSpec = Convert.NotNullOrWord(testIntervalSpec);
// Remove '%' (if any) and trim:
testIntervalSpec = RemoveEnclosingNameResolverTags(testIntervalSpec);
if (AvailabilityTestInterval.ValidSpecifiers.Minute01.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
{
return 1;
}
if (AvailabilityTestInterval.ValidSpecifiers.Minutes01.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
{
return 1;
}
if (AvailabilityTestInterval.ValidSpecifiers.Minutes05.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
{
return 5;
}
if (AvailabilityTestInterval.ValidSpecifiers.Minutes10.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
{
return 10;
}
if (AvailabilityTestInterval.ValidSpecifiers.Minutes15.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
{
return 15;
}
throw new FormatException($"Invalid availability test interval specification: \"{testIntervalSpec}\""
+ $" (Expected format is \"{Moniker}.MinSpec\", where \'MinSpec\' is one of"
+ $" {{\"{nameof(ValidSpecifiers.Minute01)}\", \"{nameof(ValidSpecifiers.Minutes05)}\","
+ $" \"{nameof(ValidSpecifiers.Minutes10)}\", \"{nameof(ValidSpecifiers.Minutes15)}\"}}).");
}
internal static string CreateCronIntervalSpecWithRandomOffset(int intervalMins)
{
// The basic format of the CRON expressions is:
// {second} {minute} {hour} {day} {month} {day of the week}
// E.g.
// TimerTrigger("15 2/5 * * * *")
// means every 5 minutes starting at 2, on 15 secs past the minute, i.e., 02:15, 07:15, 12:15, 17:15, ...
if (intervalMins != 1 && intervalMins != 5 && intervalMins != 10 && intervalMins != 15)
{
throw new ArgumentException($"Invalid number of minutes in the interval: valid values are M = 1, 5, 10 or 15; specified value is \'{intervalMins}\'.");
}
int intervalTotalSecs = intervalMins * 60;
int rndOffsTotalSecs = Rnd.Next(0, intervalTotalSecs);
int rndOffsWholeMins = rndOffsTotalSecs / 60;
int rndOffsSubminSecs = rndOffsTotalSecs % 60;
string cronSpec = $"{rndOffsSubminSecs} {rndOffsWholeMins}/{intervalMins} * * * *";
return cronSpec;
}
private static string RemoveEnclosingNameResolverTags(string s)
{
// Trim:
s = s.Trim();
// If starts AND ends with '%', remove those:
while (s.Length > 2 && s.StartsWith("%") && s.EndsWith("%"))
{
s = s.Substring(1, s.Length - 2);
s = s.Trim();
}
return s;
}
}
}

Просмотреть файл

@ -1,287 +0,0 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using System.Collections.Generic;
using Microsoft.ApplicationInsights.Channel;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
internal class AvailabilityTestInvocationBinder : IValueBinder, IErrorAwareValueBinder
{
public static Type BoundValueType { get; } = typeof(AvailabilityTestInvocation);
private static readonly Task CompletedTask = Task.FromResult(result: true);
private const string DefaultResultMessage_Pass = "Passed: Coded Availability Test completed normally and reported Success.";
private const string DefaultResultMessage_Fail = "Failed: Coded Availability Test completed normally and reported Failure.";
private const string DefaultResultMessage_Error = "Error: An exception escaped from the Coded Availability Test.";
private const string DefaultResultMessage_Timeout = "Error: The Coded Availability Test timed out.";
private const string Moniker_AssociatedAvailabilityResultWithException = "AssociatedAvailabilityResult";
private const string Moniker_AssociatedExceptionWithAvailabilityResult = "AssociatedException";
private const string ErrorSetButNotSpecified = "Coded Availability Test completed abnormally, but no error information is available.";
private static string FormatActivityName(string testDisplayName, string locationDisplayName)
{
return String.Format("{0} / {1}", testDisplayName, locationDisplayName);
}
private static string FormatActivitySpanId(Activity activity)
{
if (activity == null)
{
return null;
}
return activity.SpanId.ToHexString();
}
private static IDictionary<string, string> CreateExceptionCustomPropertiesForError(AvailabilityTelemetry availabilityResult)
{
var exceptionCustomProperties = new Dictionary<string, string>()
{
[$"{Moniker_AssociatedAvailabilityResultWithException}.Name"] = availabilityResult.Name,
[$"{Moniker_AssociatedAvailabilityResultWithException}.RunLocation"] = availabilityResult.RunLocation,
[$"{Moniker_AssociatedAvailabilityResultWithException}.Id"] = availabilityResult.Id,
};
return exceptionCustomProperties;
}
private static IDictionary<string, string> CreateAvailabilityResultCustomPropertiesForError(ExceptionTelemetry exception)
{
var availabilityResultCustomProperties = new Dictionary<string, string>()
{
[$"{Moniker_AssociatedExceptionWithAvailabilityResult}.ProblemId"] = Convert.NotNullOrWord(exception?.ProblemId),
};
return availabilityResultCustomProperties;
}
// @ToDo: Vaidate that an instance of an IValueBinder is created for each invocation of the function and instances are not reused.
// If that ws nt the case, we could not keep state in here.
private readonly AvailabilityTestAttribute _attribute;
private readonly TelemetryClient _telemetryClient;
private Activity _userActivity = null;
private DateTimeOffset _startTime = default(DateTimeOffset);
public AvailabilityTestInvocationBinder(AvailabilityTestAttribute attribute, TelemetryClient telemetryClient)
{
Validate.NotNull(attribute, nameof(attribute));
_attribute = attribute;
_telemetryClient = telemetryClient;
}
Type IValueProvider.Type
{
get
{
return BoundValueType;
}
}
private AvailabilityTestInvocation CreateInvocationInfo()
{
var invocationInfo = new AvailabilityTestInvocation(
_attribute.TestDisplayName,
_attribute.TestArmResourceName,
_attribute.LocationDisplayName,
_attribute.LocationId,
_startTime);
return invocationInfo;
}
public Task<object> GetValueAsync()
{
_startTime = DateTimeOffset.Now;
AvailabilityTestInvocation invocationInfo = CreateInvocationInfo();
Task<object> wrappedInvocationInfo = Task.FromResult((object) invocationInfo);
string activityName = FormatActivityName(invocationInfo.TestDisplayName, invocationInfo.LocationDisplayName);
Activity userActivity = new Activity(activityName).Start();
Activity prevActivity = Interlocked.CompareExchange(ref _userActivity, userActivity, null);
if (prevActivity != null)
{
throw new InvalidOperationException($"Error initializing Coded Availability Test: {nameof(GetValueAsync)}(..) should"
+ $" be called exactly once, but it was called at least twice. Activity span id"
+ $" associated with the first invocation is: {FormatActivitySpanId(prevActivity)}.");
}
invocationInfo.AvailabilityResult.Id = FormatActivitySpanId(userActivity);
return wrappedInvocationInfo;
}
public string ToInvokeString()
{
// @ToDo: What is the purpose of this method??
throw new NotImplementedException();
}
public Task SetValueAsync(object valueToSet, CancellationToken cancelControl)
{
SetValueOrError(valueToSet, error: null, errorOcurred: false, cancelControl);
// @ToDo Remove later:
Console.WriteLine($"**** {nameof(AvailabilityTestInvocationBinder)}.{nameof(IValueBinder.SetValueAsync)}({valueToSet?.GetType()?.FullName ?? "null" })");
Console.WriteLine($"**** valueToSet={(valueToSet == null ? "null" : JObject.FromObject(valueToSet).ToString())}");
// This method completes synchronously:
return CompletedTask;
}
public Task SetErrorAsync(object valueToSet, Exception error, CancellationToken cancelControl)
{
SetValueOrError(valueToSet, error, errorOcurred: true, cancelControl);
// @ToDo Remove later:
Console.WriteLine($"**** {nameof(AvailabilityTestInvocationBinder)}.{nameof(SetErrorAsync)}("
+ $"{Convert.NotNullOrWord(valueToSet?.GetType()?.FullName)}, {Convert.NotNullOrWord(error?.GetType().Name)})");
Console.WriteLine($"**** valueToSet={(valueToSet == null ? "null" : JObject.FromObject(valueToSet).ToString())}");
Console.WriteLine($"**** error={Convert.NotNullOrWord(error?.ToString())}");
// This method completes synchronously:
return CompletedTask;
}
private void SetValueOrError(object valueToSet, Exception error, bool errorOcurred, CancellationToken cancelControl)
{
// Measure user time (plus the minimal runtime overhead within the bracket of this binding:
DateTimeOffset endTime = DateTimeOffset.Now;
// Fetch state:
Activity userActivity = Interlocked.Exchange(ref _userActivity, null);
string userActivitySpadId;
// Stop activity & handle related errors (this also ensures that userActivity != null):
try
{
userActivitySpadId = FormatActivitySpanId(userActivity);
userActivity.Stop();
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error while stopping {nameof(_userActivity)}. Possible reasons include:"
+ $" {nameof(GetValueAsync)}(..) was not called correctly,"
+ $" one of {nameof(SetValueAsync)}(..)/{nameof(SetErrorAsync)}(..) was not called exactly once,"
+ $" other reasons (see original exception).",
ex);
}
// Validate the specified valueToSet and convert it to AvailabilityTestInvocation:
AvailabilityTestInvocation invocationInfo = ValidateValueToSet(valueToSet, errorOcurred, userActivitySpadId);
if (errorOcurred)
{
// If the user code completed with an error or a timeout, then the Test resuls is always "fail":
invocationInfo.AvailabilityResult.Success = false;
// Track the exception:
IDictionary<string, string> exProps = CreateExceptionCustomPropertiesForError(invocationInfo.AvailabilityResult);
ITelemetry errorTelemetry = (error == null)
? (ITelemetry) new TraceTelemetry(ErrorSetButNotSpecified, SeverityLevel.Error)
: (ITelemetry) new ExceptionTelemetry(error);
foreach(KeyValuePair<string, string> prop in exProps)
{
((ISupportProperties) errorTelemetry).Properties[prop.Key] = prop.Value;
}
// @ToDo: How do we make sure that we do not double-track this exception?
_telemetryClient.Track(errorTelemetry);
// Add references about the exception we just tracked to the availability result:
IDictionary<string, string> avResProps = CreateAvailabilityResultCustomPropertiesForError(errorTelemetry as ExceptionTelemetry);
foreach (KeyValuePair<string, string> prop in avResProps)
{
invocationInfo.AvailabilityResult.Properties[prop.Key] = prop.Value;
}
}
// If user did not initialize Message, initialize it to default value according to the result:
if (String.IsNullOrEmpty(invocationInfo.AvailabilityResult.Message))
{
invocationInfo.AvailabilityResult.Message = errorOcurred
? IsUserCodeTimeout(error, cancelControl)
? DefaultResultMessage_Timeout
: DefaultResultMessage_Error
: invocationInfo.AvailabilityResult.Success
? DefaultResultMessage_Pass
: DefaultResultMessage_Fail;
}
// If user did not initialize Duration, initialize it to default value according to the measurement:
if (invocationInfo.AvailabilityResult.Duration == TimeSpan.Zero)
{
invocationInfo.AvailabilityResult.Duration = endTime - invocationInfo.AvailabilityResult.Timestamp;
}
// Send the availability result to the backend:
_telemetryClient.TrackAvailability(invocationInfo.AvailabilityResult);
// Make sure everyting we trracked is put on the wire, if case the Function runtime shuts down:
_telemetryClient.Flush();
}
private AvailabilityTestInvocation ValidateValueToSet(object valueToSet, bool errorOcurred, string userActivitySpadId)
{
try
{
// If (and only if) we are processing a user code error, we can handle an uninitialized value.
// In that case, we re-create it based on the binding attribute:
if (errorOcurred && valueToSet == null)
{
AvailabilityTestInvocation recreatedValue = CreateInvocationInfo();
recreatedValue.AvailabilityResult.Id = userActivitySpadId;
valueToSet = recreatedValue;
}
// Now valueToSet must not be null:
Validate.NotNull(valueToSet, nameof(valueToSet));
// valueToSet must be of type AvailabilityTestInvocation:
AvailabilityTestInvocation invocationInfo = valueToSet as AvailabilityTestInvocation;
if (invocationInfo == null)
{
throw new InvalidCastException($"The expected type of {nameof(valueToSet)} is \"{typeof(AvailabilityTestInvocation).FullName}\","
+ $" but the actual type was \"{valueToSet.GetType().FullName}\".");
}
// valueToSet is a AvailabilityTestInvocation, so it contains an AvailabilityResult. Its id must match the userActivitySpadId:
if (! userActivitySpadId.Equals(invocationInfo.AvailabilityResult.Id, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException($"The {nameof(invocationInfo.AvailabilityResult.Id)} of"
+ $" the {nameof(invocationInfo.AvailabilityResult)} specified"
+ $" in {nameof(valueToSet)} does not match the span Id of {nameof(_userActivity)}:"
+ $" {nameof(invocationInfo.AvailabilityResult)}.{nameof(invocationInfo.AvailabilityResult.Id)}=\"{invocationInfo.AvailabilityResult.Id}\";"
+ $" {nameof(userActivitySpadId)}=\"{invocationInfo.AvailabilityResult.Id}\".");
}
return invocationInfo;
}
catch (Exception ex)
{
throw new InvalidOperationException($"The {nameof(valueToSet)} passed to {(errorOcurred ? nameof(SetErrorAsync) : nameof(SetValueAsync))}(..) is invalid.", ex);
}
}
private static bool IsUserCodeTimeout(Exception error, CancellationToken cancelControl)
{
bool IsUserCodeTimeout = (cancelControl == (error as TaskCanceledException)?.CancellationToken);
return IsUserCodeTimeout;
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using System;
using Microsoft.Azure.WebJobs;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
/// <summary>
/// The format we are looking for:
/// AvailabilityTestInterval.Minute01
/// AvailabilityTestInterval.Minutes05
/// AvailabilityTestInterval.Minutes10
/// AvailabilityTestInterval.Minutes15
/// We are NON-case-sensitive and ONLY the values 1, 5, 10 and 15 minutes are allowed.
/// </summary>
internal class AvailabilityTimerTriggerScheduleNameResolver : INameResolver
{
public string Resolve(string testIntervalSpec)
{
// Unless we have the right prefix, ignore the name - someone else will resolve it:
if (false == AvailabilityTestInterval.IsSpecification(testIntervalSpec))
{
return testIntervalSpec;
}
int minuteInterval = AvailabilityTestInterval.Parse(testIntervalSpec);
string cronSpec = AvailabilityTestInterval.CreateCronIntervalSpecWithRandomOffset(minuteInterval);
return cronSpec;
}
}
}

Просмотреть файл

@ -16,6 +16,12 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
return s ?? NullWord;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object NotNullOrWord(object s)
{
return s ?? NullWord;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetPropertyOrNullWord(AvailabilityTelemetry availabilityResult, string propertyName)
{
@ -25,7 +31,6 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
public static string GetPropertyOrNull(AvailabilityTelemetry availabilityResult, string propertyName)
{
IDictionary<string, string> properties = availabilityResult?.Properties;
if (properties == null || propertyName == null)
{
return null;
@ -39,30 +44,53 @@ namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
return propertyValue;
}
public static JObject AvailabilityTestInvocationToJObject(AvailabilityTestInvocation availabilityTestInvocation)
public static JObject AvailabilityTestInvocationToJObject(AvailabilityTestInfo availabilityTestInvocation)
{
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
JObject jObject = JObject.FromObject(availabilityTestInvocation);
return jObject;
}
public static AvailabilityTestInvocation JObjectToAvailabilityTestInvocation(JObject availabilityTestInvocation)
public static AvailabilityTestInfo JObjectToAvailabilityTestInvocation(JObject availabilityTestInvocation)
{
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
AvailabilityTestInvocation stronglyTypedTestInvocation = availabilityTestInvocation.ToObject<AvailabilityTestInvocation>();
return stronglyTypedTestInvocation;
try
{
AvailabilityTestInfo stronglyTypedTestInvocation = availabilityTestInvocation.ToObject<AvailabilityTestInfo>();
return stronglyTypedTestInvocation;
}
catch(Exception)
{
return null;
}
}
public static AvailabilityTelemetry AvailabilityTestInvocationToAvailabilityTelemetry(AvailabilityTestInvocation availabilityTestInvocation)
public static AvailabilityTelemetry JObjectToAvailabilityTelemetry(JObject availabilityResult)
{
Validate.NotNull(availabilityResult, nameof(availabilityResult));
try
{
AvailabilityTelemetry stronglyTypedAvailabilityTelemetry = availabilityResult.ToObject<AvailabilityTelemetry>();
return stronglyTypedAvailabilityTelemetry;
}
catch (Exception)
{
return null;
}
}
public static AvailabilityTelemetry AvailabilityTestInvocationToAvailabilityTelemetry(AvailabilityTestInfo availabilityTestInvocation)
{
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
return availabilityTestInvocation.AvailabilityResult;
}
public static AvailabilityTestInvocation AvailabilityTelemetryToAvailabilityTestInvocation(AvailabilityTelemetry availabilityResult)
public static AvailabilityTestInfo AvailabilityTelemetryToAvailabilityTestInvocation(AvailabilityTelemetry availabilityResult)
{
Validate.NotNull(availabilityResult, nameof(availabilityResult));
return new AvailabilityTestInvocation(availabilityResult);
return new AvailabilityTestInfo(availabilityResult);
}
}
}

Просмотреть файл

@ -1,64 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Host.Bindings;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
internal class ConverterBinder<TBoundValueType, TInnerBinderValueType> : IValueBinder
{
public static Type BoundValueType { get { return typeof(TBoundValueType); } }
private readonly IValueBinder _innerBinder;
private readonly Func<TInnerBinderValueType, TBoundValueType> _inputConverter;
private readonly Func<TBoundValueType, TInnerBinderValueType> _outputConverter;
public ConverterBinder(IValueBinder innerBinder, Func<TInnerBinderValueType, TBoundValueType> inputConverter, Func<TBoundValueType, TInnerBinderValueType> outputConverter)
{
Validate.NotNull(innerBinder, nameof(innerBinder));
Validate.NotNull(inputConverter, nameof(inputConverter));
Validate.NotNull(outputConverter, nameof(outputConverter));
if (false == innerBinder.Type.IsAssignableFrom(typeof(TInnerBinderValueType)))
{
// @ToDo: Test this IsAssignableFrom business!
throw new ArgumentException($"The {nameof(innerBinder)} specified to this"
+ $" {nameof(ConverterBinder<TBoundValueType, TInnerBinderValueType>)}<{typeof(TBoundValueType).Name}, {typeof(TInnerBinderValueType).Name}>"
+ $" ctor specifies the Type property with the value \"{innerBinder.Type?.FullName ?? "null"}\", which is not assignabe"
+ $" to \"{typeof(TInnerBinderValueType).FullName}\".");
}
_innerBinder = innerBinder;
_inputConverter = inputConverter;
_outputConverter = outputConverter;
}
Type IValueProvider.Type
{
get
{
return typeof(TBoundValueType);
}
}
async Task<object> IValueProvider.GetValueAsync()
{
object objectValue = await _innerBinder.GetValueAsync();
TInnerBinderValueType innerBinderValue = (TInnerBinderValueType) objectValue;
TBoundValueType outerValue = _inputConverter(innerBinderValue);
return outerValue;
}
string IValueProvider.ToInvokeString()
{
return _innerBinder.ToInvokeString();
}
Task IValueBinder.SetValueAsync(object value, CancellationToken cancellationToken)
{
TBoundValueType outerValue = (TBoundValueType) value;
TInnerBinderValueType innerBinderValue = _outputConverter(outerValue);
return _innerBinder.SetValueAsync(innerBinderValue, cancellationToken);
}
}
}

Просмотреть файл

@ -0,0 +1,354 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
#pragma warning disable CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
internal class FunctionInvocationManagementFilter : IFunctionInvocationFilter
#pragma warning restore CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
{
private static bool IsUserCodeTimeout(Exception error, CancellationToken cancelControl)
{
bool IsUserCodeTimeout = (cancelControl == (error as TaskCanceledException)?.CancellationToken);
return IsUserCodeTimeout;
}
private readonly TelemetryClient _telemetryClient;
public FunctionInvocationManagementFilter(TelemetryClient telemetryClient)
{
Validate.NotNull(telemetryClient, nameof(telemetryClient));
_telemetryClient = telemetryClient;
}
#pragma warning disable CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancelControl)
#pragma warning restore CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
{
Validate.NotNull(executingContext, nameof(executingContext));
// Check that the functionInstanceId is registered.
// If not, this function does not have a parameter marked with AvailabilityTestAttribute. In that case there is nothing to do:
bool isAvailabilityTest = FunctionInvocationStateCache.SingeltonInstance.TryStartFunctionInvocation(
executingContext.FunctionInstanceId,
out FunctionInvocationState invocationState);
if (! isAvailabilityTest)
{
return Task.CompletedTask;
}
IdentifyManagedParameters(invocationState, executingContext.Arguments);
// Start activity:
string activityName = Convert.NotNullOrWord(invocationState.ActivitySpanName);
invocationState.ActivitySpan = new Activity(activityName).Start();
string activitySpanId = invocationState.ActivitySpan.SpanId.ToHexString();
// Start the timer:
DateTimeOffset startTime = DateTimeOffset.Now;
// Look at every paramater and update it with the activity ID and the start time:
foreach (FunctionInvocationState.Parameter regRaram in invocationState.Parameters.Values)
{
regRaram.AvailabilityTestInfo.AvailabilityResult.Id = activitySpanId;
if (regRaram.Type.Equals(typeof(AvailabilityTestInfo)))
{
AvailabilityTestInfo actParam = (AvailabilityTestInfo) executingContext.Arguments[regRaram.Name];
actParam.AvailabilityResult.Id = activitySpanId;
actParam.SetStartTime(startTime);
}
else if (regRaram.Type.Equals(typeof(AvailabilityTelemetry)))
{
AvailabilityTelemetry actParam = (AvailabilityTelemetry) executingContext.Arguments[regRaram.Name];
actParam.Id = activitySpanId;
actParam.Timestamp = startTime.ToUniversalTime();
}
else if (regRaram.Type.Equals(typeof(JObject)))
{
JObject actParam = (JObject) executingContext.Arguments[regRaram.Name];
actParam["AvailabilityResult"]["Id"].Replace(JToken.FromObject(activitySpanId));
actParam["StartTime"].Replace(JToken.FromObject(startTime));
actParam["AvailabilityResult"]["Timestamp"].Replace(JToken.FromObject(startTime.ToUniversalTime()));
}
else
{
throw new InvalidOperationException($"Unexpected managed parameter type: \"{regRaram.Type.FullName}\".");
}
}
// Done:
return Task.CompletedTask;
}
#pragma warning disable CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
public Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancelControl)
#pragma warning restore CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
{
Validate.NotNull(executedContext, nameof(executedContext));
// Check that the functionInstanceId is registered.
// If not, this function does not have a parameter marked with AvailabilityTestAttribute. In that case there is nothing to do:
bool isAvailabilityTest = FunctionInvocationStateCache.SingeltonInstance.TryCompleteFunctionInvocation(
executedContext.FunctionInstanceId,
out FunctionInvocationState invocationState);
if (!isAvailabilityTest)
{
return Task.CompletedTask;
}
// Measure user time (plus the minimal runtime overhead within the bracket of this binding):
DateTimeOffset endTime = DateTimeOffset.Now;
// Stop activity:
string activitySpadId = invocationState.ActivitySpan.SpanId.ToHexString();
invocationState.ActivitySpan.Stop();
// Get Function result (failed or not):
bool errorOcurred = ! executedContext.FunctionResult.Succeeded;
Exception error = errorOcurred
? executedContext.FunctionResult.Exception
: null;
// Look at every paramater that was originally tagged with the attribute:
foreach (FunctionInvocationState.Parameter registeredRaram in invocationState.Parameters.Values)
{
// Find the actual parameter value in the function arguments (named lookup):
if (false == executedContext.Arguments.TryGetValue(registeredRaram.Name, out object functionOutputParam))
{
throw new InvalidOperationException($"A parameter with name \"{Convert.NotNullOrWord(registeredRaram?.Name)}\" and"
+ $" type \"{Convert.NotNullOrWord(registeredRaram?.Type)}\" was registered for"
+ $" the function \"{Convert.NotNullOrWord(executedContext?.FunctionName)}\", but it was not found in the"
+ $" actual argument list after the function invocation.");
}
if (functionOutputParam == null)
{
throw new InvalidOperationException($"A parameter with name \"{Convert.NotNullOrWord(registeredRaram?.Name)}\" and"
+ $" type \"{Convert.NotNullOrWord(registeredRaram?.Type)}\" was registered for"
+ $" the function \"{Convert.NotNullOrWord(executedContext?.FunctionName)}\", and the corresponding value in the"
+ $" actual argument list after the function invocation was null.");
}
// Based on parameter type, convert it to AvailabilityTestInfo and then process:
bool functionOutputParamProcessed = false;
{
// If this argument is a AvailabilityTestInfo:
var testInfoParameter = functionOutputParam as AvailabilityTestInfo;
if (testInfoParameter != null)
{
ProcessOutputParameter(endTime, errorOcurred, error, testInfoParameter, activitySpadId, cancelControl);
functionOutputParamProcessed = true;
}
}
{
// If this argument is a AvailabilityTelemetry:
var availabilityResultParameter = functionOutputParam as AvailabilityTelemetry;
if (availabilityResultParameter != null)
{
AvailabilityTestInfo testInfoParameter = Convert.AvailabilityTelemetryToAvailabilityTestInvocation(availabilityResultParameter);
ProcessOutputParameter(endTime, errorOcurred, error, testInfoParameter, activitySpadId, cancelControl);
functionOutputParamProcessed = true;
}
}
{
// If this argument is a JObject:
var jObjectParameter = functionOutputParam as JObject;
if (jObjectParameter != null)
{
// Can jObjectParameter be cnverted to a AvailabilityTestInfo (null if not):
AvailabilityTestInfo testInfoParameter = Convert.JObjectToAvailabilityTestInvocation(jObjectParameter);
if (testInfoParameter != null)
{
ProcessOutputParameter(endTime, errorOcurred, error, testInfoParameter, activitySpadId, cancelControl);
functionOutputParamProcessed = true;
}
}
}
if (false == functionOutputParamProcessed)
{
throw new InvalidOperationException($"A parameter with name \"{Convert.NotNullOrWord(registeredRaram?.Name)}\" and"
+ $" type \"{Convert.NotNullOrWord(registeredRaram?.Type)}\" was registered for"
+ $" the function \"{Convert.NotNullOrWord(executedContext?.FunctionName)}\", and the corresponding value in the"
+ $" actual argument list after the function invocation cannot be processed"
+ $" ({Convert.NotNullOrWord(functionOutputParam?.GetType()?.Name)}).");
}
}
// Make sure everyting we trracked is put on the wire, in case the Function runtime shuts down:
_telemetryClient.Flush();
return Task.CompletedTask;
}
private void ProcessOutputParameter(
DateTimeOffset endTime,
bool errorOcurred,
Exception error,
AvailabilityTestInfo functionOutputParam,
string activitySpadId,
CancellationToken cancelControl)
{
if (errorOcurred)
{
// If the user code completed with an error or a timeout, then the Test resuls is always "fail":
functionOutputParam.AvailabilityResult.Success = false;
// Annotate exception and the availability result:
OutputTelemetryFormat.AnnotateFunctionError(error, functionOutputParam);
OutputTelemetryFormat.AnnotateAvailabilityResultWithErrorInfo(functionOutputParam, error);
}
// If user did not initialize Message, initialize it to default value according to the result:
if (String.IsNullOrEmpty(functionOutputParam.AvailabilityResult.Message))
{
functionOutputParam.AvailabilityResult.Message = errorOcurred
? IsUserCodeTimeout(error, cancelControl)
? OutputTelemetryFormat.DefaultResultMessage_Timeout
: OutputTelemetryFormat.DefaultResultMessage_Error
: functionOutputParam.AvailabilityResult.Success
? OutputTelemetryFormat.DefaultResultMessage_Pass
: OutputTelemetryFormat.DefaultResultMessage_Fail;
}
// If user did not initialize Duration, initialize it to default value according to the measurement:
if (functionOutputParam.AvailabilityResult.Duration == TimeSpan.Zero)
{
functionOutputParam.AvailabilityResult.Duration = endTime - functionOutputParam.AvailabilityResult.Timestamp;
}
// Send the availability result to the backend:
OutputTelemetryFormat.RemoveAvailabilityTestInfoIdentity(functionOutputParam.AvailabilityResult);
functionOutputParam.AvailabilityResult.Id = activitySpadId;
_telemetryClient.TrackAvailability(functionOutputParam.AvailabilityResult);
}
private void IdentifyManagedParameters(FunctionInvocationState invocationState, IReadOnlyDictionary<string, object> actualFunctionParameters)
{
int identifiedParameterCount = 0;
// Look at each argument:
if (actualFunctionParameters != null)
{
foreach (KeyValuePair<string, object> actualFunctionParameter in actualFunctionParameters)
{
// Skip null value:
if (actualFunctionParameter.Value == null)
{
continue;
}
{
// If this argument is a AvailabilityTestInfo:
var testInfoParameter = actualFunctionParameter.Value as AvailabilityTestInfo;
if (testInfoParameter != null)
{
// Find registered parameter with the right ID, validate, and store its name:
Guid actualFunctionParameterId = testInfoParameter.Identity;
if (TryIdentifyAndValidateManagedParameter(invocationState, actualFunctionParameter.Key, actualFunctionParameter.Value, actualFunctionParameterId))
{
identifiedParameterCount++;
}
}
}
{
// If this argument is a AvailabilityTelemetry:
var availabilityResultParameter = actualFunctionParameter.Value as AvailabilityTelemetry;
if (availabilityResultParameter != null)
{
// Find registered parameter with the right ID, validate, and store its name:
Guid actualFunctionParameterId = OutputTelemetryFormat.GetAvailabilityTestInfoIdentity(availabilityResultParameter);
if (TryIdentifyAndValidateManagedParameter(invocationState, actualFunctionParameter.Key, actualFunctionParameter.Value, actualFunctionParameterId))
{
identifiedParameterCount++;
}
}
}
{
// If this argument is a JObject:
var jObjectParameter = actualFunctionParameter.Value as JObject;
if (jObjectParameter != null)
{
// Can jObjectParameter be cnverted to a AvailabilityTestInfo (null if not):
AvailabilityTestInfo testInfoParameter = Convert.JObjectToAvailabilityTestInvocation(jObjectParameter);
if (testInfoParameter != null)
{
// Find registered parameter with the right ID, validate, and store its name:
Guid actualFunctionParameterId = testInfoParameter.Identity;
if (TryIdentifyAndValidateManagedParameter(invocationState, actualFunctionParameter.Key, actualFunctionParameter.Value, actualFunctionParameterId))
{
identifiedParameterCount++;
}
}
}
}
}
}
if (identifiedParameterCount != invocationState.Parameters.Count)
{
throw new InvalidOperationException($"{invocationState.Parameters.Count} parameters were marked with the {nameof(AvailabilityTestAttribute)},"
+ $" but {identifiedParameterCount} parameters were identified during the actual function invocation.");
}
}
private bool TryIdentifyAndValidateManagedParameter(FunctionInvocationState invocationState, string actualParamName, object actualParamValue, Guid actualParamId)
{
// Check if the actual param matches a registered managed param:
if (false == invocationState.Parameters.TryGetValue(actualParamId, out FunctionInvocationState.Parameter registeredParam))
{
return false;
}
// Validate type match:
if (false == actualParamValue.GetType().Equals(registeredParam.Type))
{
throw new InvalidProgramException($"The parameter with the identity \'{OutputTelemetryFormat.FormatGuid(actualParamId)}\'"
+ $" is expected to be of type {registeredParam.Type},"
+ $" but in reality it is of type {actualParamValue.GetType().Name}.");
}
// Validate ID uniqueness:
if (registeredParam.Name != null)
{
throw new InvalidProgramException($"The parameter with the identity \'{OutputTelemetryFormat.FormatGuid(actualParamId)}\'"
+ $" has the name \'{actualParamName}\', but a parameter with that"
+ $" identify has already been encountered under the name \'{registeredParam.Name}\'.");
}
registeredParam.Name = actualParamName;
return true;
}
private void ValidateactivitySpanId(string activitySpanId, AvailabilityTestInfo availabilityTestInfo)
{
if (! activitySpanId.Equals(availabilityTestInfo.AvailabilityResult.Id, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"The {nameof(availabilityTestInfo.AvailabilityResult.Id)} of"
+ $" the {nameof(availabilityTestInfo.AvailabilityResult)} does not match the"
+ $" span Id of the activity span associated with this function:"
+ $" {nameof(availabilityTestInfo.AvailabilityResult)}.{nameof(availabilityTestInfo.AvailabilityResult.Id)}"
+ $"=\"{availabilityTestInfo.AvailabilityResult.Id}\";"
+ $" {nameof(activitySpanId)}=\"{activitySpanId}\".");
}
}
}
}

Просмотреть файл

@ -0,0 +1,91 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
internal class FunctionInvocationState
{
public enum Stage : Int32
{
New = 10,
//Registered = 20,
Started = 30,
Completed = 40,
Removed = 50
}
public class Parameter
{
public AvailabilityTestInfo AvailabilityTestInfo { get; }
public Type Type { get; }
public string Name { get; set; }
public Parameter(AvailabilityTestInfo availabilityTestInfo, Type type)
{
AvailabilityTestInfo = availabilityTestInfo;
Type = type;
Name = null;
}
}
private readonly ConcurrentDictionary<Guid, Parameter> _paremeters = new ConcurrentDictionary<Guid, Parameter>();
private int _currentStage = (int) Stage.New;
private string _activitySpanName = null;
public Guid FunctionInstanceId { get; }
public string FormattedFunctionInstanceId { get { return OutputTelemetryFormat.FormatGuid (FunctionInstanceId); } }
public IReadOnlyDictionary<Guid, Parameter> Parameters { get { return _paremeters; } }
public FunctionInvocationState.Stage CurrentStage { get { return (Stage)_currentStage; } }
public DateTimeOffset StartTime { get; set; }
public Activity ActivitySpan { get; set; }
public string ActivitySpanName { get { return _activitySpanName; } }
public FunctionInvocationState(Guid functionInstanceId)
{
this.FunctionInstanceId = functionInstanceId;
}
public void AddManagedParameter(AvailabilityTestInfo availabilityTestInfo, Type functionParameterType)
{
Validate.NotNull(availabilityTestInfo, nameof(availabilityTestInfo));
Validate.NotNull(functionParameterType, nameof(functionParameterType));
if (CurrentStage != Stage.New)
{
throw new InvalidOperationException($"{nameof(AddManagedParameter)}(..) should only be called when {nameof(CurrentStage)} is"
+ $" {Stage.New}; however, {nameof(CurrentStage)} is {CurrentStage}.");
}
string activitySpanName = OutputTelemetryFormat.FormatActivityName(availabilityTestInfo.TestDisplayName, availabilityTestInfo.LocationDisplayName);
Interlocked.CompareExchange(ref _activitySpanName, activitySpanName, null);
_paremeters.TryAdd(availabilityTestInfo.Identity, new Parameter(availabilityTestInfo, functionParameterType));
}
public void Transition(FunctionInvocationState.Stage from, FunctionInvocationState.Stage to)
{
int fromStage = (int) from, toStage = (int) to;
int prevStage = Interlocked.CompareExchange(ref _currentStage, toStage, fromStage);
if (prevStage != fromStage)
{
throw new InvalidOperationException($"Error transitioning {nameof(CurrentStage)} of the {nameof(FunctionInvocationState)}"
+ $" for {nameof(FunctionInstanceId)} = {FormattedFunctionInstanceId}"
+ $" from \'{from}\' (={fromStage}) to \'{to}\' (={toStage}):"
+ $" Original {nameof(CurrentStage)} value was {((Stage)prevStage)} (={prevStage}).");
}
}
}
}

Просмотреть файл

@ -0,0 +1,95 @@
using System;
using System.Collections.Concurrent;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
internal class FunctionInvocationStateCache
{
public static readonly FunctionInvocationStateCache SingeltonInstance = new FunctionInvocationStateCache();
private readonly ConcurrentDictionary<Guid, FunctionInvocationState> _invocationStates = new ConcurrentDictionary<Guid, FunctionInvocationState>();
public void RegisterFunctionInvocation(Guid functionInstanceId, AvailabilityTestInfo availabilityTestInfo, Type functionParameterType)
{
Validate.NotNull(availabilityTestInfo, nameof(availabilityTestInfo));
Validate.NotNull(functionParameterType, nameof(functionParameterType));
FunctionInvocationState invocationState = _invocationStates.GetOrAdd(functionInstanceId, (_) => new FunctionInvocationState(functionInstanceId));
invocationState.AddManagedParameter(availabilityTestInfo, functionParameterType);
}
public bool TryStartFunctionInvocation(Guid functionInstanceId, out FunctionInvocationState invocationState)
{
bool isRegistered = _invocationStates.TryGetValue(functionInstanceId, out invocationState);
if (! isRegistered)
{
return false;
}
try
{
invocationState.Transition(from: FunctionInvocationState.Stage.New, to: FunctionInvocationState.Stage.Started);
return true;
}
catch (InvalidOperationException invOpEx)
{
throw new InvalidOperationException($"Cannot transition {nameof(FunctionInvocationState)}.{nameof(invocationState.CurrentStage)}"
+ $" to \'{nameof(FunctionInvocationState.Stage.Started)}\'."
+ $" This indicates that a {nameof(invocationState.FunctionInstanceId)} was not unique,"
+ $" or that the assumption that the {nameof(FunctionInvocationManagementFilter)}"
+ $" is invoked exactly once before and after the function invocation may be violated.",
invOpEx);
}
}
public bool TryCompleteFunctionInvocation(Guid functionInstanceId, out FunctionInvocationState invocationState)
{
bool isRegistered = _invocationStates.TryGetValue(functionInstanceId, out invocationState);
if (!isRegistered)
{
return false;
}
try
{
invocationState.Transition(from: FunctionInvocationState.Stage.Started, to: FunctionInvocationState.Stage.Completed);
return true;
}
catch (InvalidOperationException invOpEx)
{
throw new InvalidOperationException($"Cannot transition {nameof(FunctionInvocationState)}.{nameof(invocationState.CurrentStage)}"
+ $" to \'{nameof(FunctionInvocationState.Stage.Completed)}\'."
+ $" This indicates that a {nameof(invocationState.FunctionInstanceId)} was not unique,"
+ $" or that the assumption that the {nameof(FunctionInvocationManagementFilter)}"
+ $" is invoked exactly once before and after the function invocation may be violated.",
invOpEx);
}
}
public void RemoveFunctionInvocationRegistration(FunctionInvocationState invocationState)
{
Validate.NotNull(invocationState, nameof(invocationState));
bool wasRegistered = _invocationStates.TryRemove(invocationState.FunctionInstanceId, out FunctionInvocationState _);
if (! wasRegistered)
{
throw new InvalidOperationException($"Error removing the registration of a {nameof(FunctionInvocationState)}"
+ $" for {nameof(invocationState.FunctionInstanceId)} = {invocationState.FormattedFunctionInstanceId}"
+ $" from a {nameof(FunctionInvocationStateCache)}: A {nameof(FunctionInvocationState)} with"
+ $" this {nameof(invocationState.FunctionInstanceId)} is not registered.");
}
try
{
invocationState.Transition(from: FunctionInvocationState.Stage.Completed, to: FunctionInvocationState.Stage.Removed);
}
catch (InvalidOperationException invOpEx)
{
throw new InvalidOperationException($"Cannot transition {nameof(FunctionInvocationState)}.{nameof(invocationState.CurrentStage)}"
+ $" to \'{nameof(FunctionInvocationState.Stage.Removed)}\'.",
invOpEx);
}
}
}
}

Просмотреть файл

@ -0,0 +1,89 @@
using Microsoft.ApplicationInsights.DataContracts;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
static internal class OutputTelemetryFormat
{
public const string DefaultResultMessage_Pass = "Passed: Coded Availability Test completed normally and reported Success.";
public const string DefaultResultMessage_Fail = "Failed: Coded Availability Test completed normally and reported Failure.";
public const string DefaultResultMessage_Error = "Error: An exception escaped from the Coded Availability Test.";
public const string DefaultResultMessage_Timeout = "Error: The Coded Availability Test timed out.";
public const string ErrorSetButNotSpecified = "Coded Availability Test completed abnormally, but no error information is available.";
private const string Moniker_AssociatedAvailabilityResultWithException = "AssociatedAvailabilityResult";
private const string Moniker_AssociatedExceptionWithAvailabilityResult = "AssociatedException";
private const string Moniker_AvailabilityTestInfoIdentity = "_MS.AvailabilityTestInfo.Identity";
public static void AnnotateFunctionError(Exception error, AvailabilityTestInfo functionOutputParam)
{
if (error == null)
{
return;
}
error.Data[$"{Moniker_AssociatedAvailabilityResultWithException}.Name"] = functionOutputParam.AvailabilityResult.Name;
error.Data[$"{Moniker_AssociatedAvailabilityResultWithException}.RunLocation"] = functionOutputParam.AvailabilityResult.RunLocation;
error.Data[$"{Moniker_AssociatedAvailabilityResultWithException}.Id"] = functionOutputParam.AvailabilityResult.RunLocation;
}
public static void AnnotateAvailabilityResultWithErrorInfo(AvailabilityTestInfo functionOutputParam, Exception error)
{
string errorInfo = (error == null)
? ErrorSetButNotSpecified
: $"{error.GetType().Name}: {error.Message}";
functionOutputParam.AvailabilityResult.Properties[$"{Moniker_AssociatedExceptionWithAvailabilityResult}.Info"] = errorInfo;
}
public static void AddAvailabilityTestInfoIdentity(AvailabilityTelemetry availabilityResult, Guid functionInstanceId)
{
Validate.NotNull(availabilityResult, nameof(availabilityResult));
availabilityResult.Properties[Moniker_AvailabilityTestInfoIdentity] = FormatGuid(functionInstanceId);
}
public static void RemoveAvailabilityTestInfoIdentity(AvailabilityTelemetry availabilityResult)
{
Validate.NotNull(availabilityResult, nameof(availabilityResult));
availabilityResult.Properties.Remove(Moniker_AvailabilityTestInfoIdentity);
}
public static Guid GetAvailabilityTestInfoIdentity(AvailabilityTelemetry availabilityResult)
{
string functionInstanceIdStr = null;
bool? hasId = availabilityResult?.Properties?.TryGetValue(Moniker_AvailabilityTestInfoIdentity, out functionInstanceIdStr);
if (true == hasId)
{
if (functionInstanceIdStr != null && Guid.TryParse(functionInstanceIdStr, out Guid functionInstanceId))
{
return functionInstanceId;
}
}
return default(Guid);
}
public static string FormatGuid(Guid functionInstanceId)
{
return functionInstanceId.ToString("D").ToUpper();
}
public static string FormatActivityName(string testDisplayName, string locationDisplayName)
{
return String.Format("{0} / {1}", testDisplayName, locationDisplayName);
}
public static string FormatTimestamp(DateTimeOffset timestamp)
{
return JsonConvert.SerializeObject(timestamp);
}
}
}

Просмотреть файл

@ -1,31 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Azure.WebJobs.Host.Bindings
{
/// <summary>
/// @ToDo
///
/// We need to remove this later. This iface will be included into the WebJobs SDK.
/// Its semantics are described here:
/// https://github.com/Azure/azure-webjobs-sdk/issues/2450
///
/// When an WebJobs SDK version with this iface is available, we will remove it frm here.
/// For now, this ensures that we can code against this iface.
/// </summary>
public interface IErrorAwareValueBinder : IValueBinder
{
/// <summary>
/// Sets the error.
/// </summary>
/// <param name="value">The value / state.</param>
/// <param name="error">The error thrown.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
/// <returns>A <see cref="Task"/> for the operation.</returns>
Task SetErrorAsync(object value, Exception error, CancellationToken cancellationToken);
}
}