Initial prototype for an Availability-Test-binding with error handling and telemetry tracking.

This commit is contained in:
macrogreg 2020-03-17 00:07:11 +01:00
Родитель 26e1b6e7e7
Коммит 297bc18470
18 изменённых файлов: 1224 добавлений и 1 удалений

192
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,192 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
x64/
#build/
bld/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
#NUNIT
*.VisualState.xml
TestResult.xml
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
*.ncrunch*
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/*
## TODO: If the tool you use requires repositories.config, also uncomment the next line
#!packages/repositories.config
# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
# This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented)
!packages/build/
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
!.StyleCop/*
!.StyleCop/*/**
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.publishsettings
node_modules/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# =========================
# Windows detritus
# =========================
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
.vs
# Files with resolved secrets
*.out.*
/packages
/.tools

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

@ -1,2 +1,2 @@
# azure-functions-availability-monitoring-extension
This is a new Repo. Description is ToDo.
Azure Monitor Coded Availability Tests powered by Azure Functions.

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

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>AvailabilityMonitoring-Extension-Demo</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.14" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.6" />
<PackageReference Include="Microsoft.Azure.WebJobs.Logging.ApplicationInsights" Version="3.0.14" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AvailabilityMonitoring-Extension\Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

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

@ -0,0 +1,129 @@
using System;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace AvailabilityMonitoringExtensionDemo
{
public class AvailabilityMonitoringDemo01
{
public const string Version = "1";
public static readonly string FunctionName = typeof(AvailabilityMonitoringDemo01).FullName + $".Version.{AvailabilityMonitoringDemo01.Version}";
private readonly TelemetryClient _telemetryClient;
public AvailabilityMonitoringDemo01(TelemetryConfiguration telemetryConfig)
{
_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)}");
testInvocation.AvailabilityResult.Name += " | Name was modified (A)";
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;
log.LogInformation($"############ \"{FunctionName}.{nameof(IoBindingWithException)}\" started.");
log.LogInformation($"############ {ToString(timerInfo)}");
testInvocation.AvailabilityResult.Name += " | Name modified before exception (B)";
if (SimulateError)
{
throw new Exception("I AM A TEST EXCEPTION!! (B)");
}
testInvocation.AvailabilityResult.Name += " | Name modified after exception site (B)";
await Task.Delay(0);
}
[FunctionName("Availability-Monitoring-Demo-01-C-BindingToJObject")]
public async Task BindingToJObject(
[TimerTrigger("15 */1 * * * *")] TimerInfo timerInfo,
[AvailabilityTest(TestDisplayName = "Test Display Name",
TestArmResourceName = "Test ARM Resource Name",
LocationDisplayName = "Location Display Name",
LocationId = "Location Id")] JObject testInvocation,
ILogger log)
{
log.LogInformation($"$$$$$$$$$$$$ \"{FunctionName}.{nameof(BindingToJObject)}\" started.");
log.LogInformation($"$$$$$$$$$$$$ {ToString(timerInfo)}");
log.LogInformation($"$$$$$$$$$$$$ testInvocation.GetType()={testInvocation?.GetType()?.FullName ?? "null"}");
dynamic testInvocationInfo = testInvocation;
log.LogDebug($"$$$$$$$$$$$$ testInvocationInfo.GetType()={testInvocationInfo?.GetType()?.FullName ?? "null"}");
log.LogDebug($"$$$$$$$$$$$$ testInvocationInfo.AvailabilityResult.GetType()={testInvocationInfo?.AvailabilityResult?.GetType()?.FullName ?? "null"}");
log.LogDebug($"$$$$$$$$$$$$ testInvocationInfo.AvailabilityResult.Name.GetType()={testInvocationInfo?.AvailabilityResult?.Name?.GetType()?.FullName ?? "null"}");
testInvocationInfo.TestDisplayName += " | TestDisplayName was modified (C)";
testInvocationInfo.AvailabilityResult.Name += " | AvailabilityResult.Name was modified (C)";
testInvocationInfo.AvailabilityResult.Message = "This is a test message (C)";
testInvocationInfo.AvailabilityResult.NonExistentMessage = "This is a test non-existent message (C)";
testInvocationInfo.AvailabilityResult.Properties["Custom Dimension"] = "Custom Dimension Value (C)";
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)}");
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);
}
private static string ToString(TimerInfo timerInfo)
{
string timerInfoString = String.Format("TimerInfo: Last = '{0}', Next = '{1}', LastUpdated = '{2}', IsPastDue = '{3}', NextOccurrences(5) = '{4}'.",
timerInfo.ScheduleStatus.Last,
timerInfo.ScheduleStatus.Next,
timerInfo.ScheduleStatus.LastUpdated,
timerInfo.IsPastDue,
timerInfo.FormatNextOccurrences(5));
return timerInfoString;
}
}
}

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

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
namespace AvailabilityMonitoringExtensionDemo
{
public static class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine($"Starting {typeof(Program).FullName}");
Console.WriteLine($"Initializing HostBuilder.");
IHostBuilder builder = new HostBuilder()
.UseEnvironment("Development")
.ConfigureAppConfiguration((configBuilder) =>
{
configBuilder.AddJsonFile("local.settings.json");
})
.ConfigureWebJobs((webJobsBuilder) =>
{
webJobsBuilder
.AddAzureStorageCoreServices()
.AddExecutionContextBinding()
.AddTimers()
.AddAvailabilityMonitoring();
})
.ConfigureLogging((context, loggingBuilder) =>
{
loggingBuilder.SetMinimumLevel(LogLevel.Debug);
loggingBuilder.AddConsole();
string appInsightsInstrumentationKey = context.Configuration["Values:APPINSIGHTS_INSTRUMENTATIONKEY"];
if (!String.IsNullOrEmpty(appInsightsInstrumentationKey))
{
loggingBuilder.AddApplicationInsightsWebJobs((opts) => { opts.InstrumentationKey = appInsightsInstrumentationKey; });
}
})
.ConfigureServices((serviceCollection) =>
{
//serviceCollection.AddSingleton(typeof(AvailabilityMonitoringDemo01));
})
.UseConsoleLifetime();
Console.WriteLine($"Building host.");
IHost host = builder.Build();
Console.WriteLine($"Starting host.");
using (host)
{
await host.RunAsync();
}
Console.WriteLine($"Host finished.");
Console.WriteLine($"Press enter to end.");
Console.ReadLine();
}
}
}

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

@ -0,0 +1,9 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
},
"ConnectionStrings": {
"AzureWebJobsStorage": "UseDevelopmentStorage=trueX"
}
}

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

@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"APPINSIGHTS_INSTRUMENTATIONKEY": "a418f0b6-c28b-44c7-b396-a1f8d2207aed"
},
"ConnectionStrings": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
}
}

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

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.6" />
<PackageReference Include="Microsoft.Azure.WebJobs.Logging.ApplicationInsights" Version="3.0.14" />
</ItemGroup>
</Project>

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

@ -0,0 +1,76 @@
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;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
[Extension("AvailabilityMonitoring")]
internal class AvailabilityMonitoringExtensionConfigProvider : IExtensionConfigProvider
{
private readonly TelemetryClient _telemetryClient;
public AvailabilityMonitoringExtensionConfigProvider(TelemetryConfiguration telemetryConfig)
{
_telemetryClient = new TelemetryClient(telemetryConfig);
}
public void Initialize(ExtensionConfigContext context)
{
Validate.NotNull(context, nameof(context));
// FluentBindingRule<ApiAvailabilityTest> is marked as Obsolete, yet it is the type returned from AddBindingRule(..)
// We could use "var", but one should NEVER use "var" except in Lync expressions
// or when the type is clear from the *same* line to an unfamiliar reader.
// Neither is the case, so we use the type explicitly and work around the obsolete-warning.
#pragma warning disable CS0618
FluentBindingRule<AvailabilityTestAttribute> rule = context.AddBindingRule<AvailabilityTestAttribute>();
#pragma warning restore CS0618
rule.BindToValueProvider(CreateAvailabilityTestInvocationBinder);
}
private Task<IValueBinder> CreateAvailabilityTestInvocationBinder(AvailabilityTestAttribute attribute, Type type)
{
Validate.NotNull(attribute, nameof(attribute));
Validate.NotNull(type, nameof(type));
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!
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}\".");
}
}
}
}

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

@ -0,0 +1,15 @@
using System;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
public static class AvailabilityMonitoringWebJobsBuilderExtensions
{
public static IWebJobsBuilder AddAvailabilityMonitoring(this IWebJobsBuilder builder)
{
Validate.NotNull(builder, nameof(builder));
builder.AddExtension<AvailabilityMonitoringExtensionConfigProvider>();
return builder;
}
}
}

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

@ -0,0 +1,17 @@
using System;
using Microsoft.Azure.WebJobs.Description;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
[AttributeUsage(AttributeTargets.Parameter)]
[Binding]
public class AvailabilityTestAttribute : Attribute
{
public string TestDisplayName { get; set; }
public string TestArmResourceName { get; set; }
public string LocationDisplayName { get; set; }
public string LocationId { get; set; }
}
}

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

@ -0,0 +1,116 @@
using Newtonsoft.Json;
using System;
using Microsoft.ApplicationInsights.DataContracts;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
public class AvailabilityTestInvocation
{
public string TestDisplayName { get; }
public string TestArmResourceName { get; }
public string LocationDisplayName { get; }
public string LocationId { get; }
public DateTimeOffset StartTime { get; }
public AvailabilityTelemetry AvailabilityResult { get; }
public AvailabilityTestInvocation(
string testDisplayName,
string testArmResourceName,
string locationDisplayName,
string locationId,
DateTimeOffset startTime)
{
Validate.NotNullOrWhitespace(testDisplayName, nameof(testDisplayName));
Validate.NotNullOrWhitespace(testArmResourceName, nameof(testArmResourceName));
Validate.NotNullOrWhitespace(locationDisplayName, nameof(locationDisplayName));
Validate.NotNullOrWhitespace(locationId, nameof(locationId));
this.TestDisplayName = testDisplayName;
this.TestArmResourceName = testArmResourceName;
this.LocationDisplayName = locationDisplayName;
this.LocationId = locationId;
this.StartTime = startTime;
this.AvailabilityResult = CreateNewAvailabilityResult();
}
public AvailabilityTestInvocation(AvailabilityTelemetry availabilityResult)
: this(Convert.NotNullOrWord(availabilityResult?.Name),
Convert.GetPropertyOrNullWord(availabilityResult, "WebtestArmResourceName"),
Convert.NotNullOrWord(availabilityResult?.RunLocation),
Convert.GetPropertyOrNullWord(availabilityResult, "WebtestLocationId"),
availabilityResult?.Timestamp ?? DateTimeOffset.Now)
{
Validate.NotNull(availabilityResult, nameof(availabilityResult));
this.AvailabilityResult = availabilityResult;
}
/// <summary>
/// This is called by Newtonsoft.Json when converting from JObject.
/// </summary>
[JsonConstructor]
private AvailabilityTestInvocation(
string testDisplayName,
string testArmResourceName,
string locationDisplayName,
string locationId,
DateTimeOffset startTime,
AvailabilityTelemetry availabilityResult)
{
Validate.NotNullOrWhitespace(testDisplayName, nameof(testDisplayName));
Validate.NotNullOrWhitespace(testArmResourceName, nameof(testArmResourceName));
Validate.NotNullOrWhitespace(locationDisplayName, nameof(locationDisplayName));
Validate.NotNullOrWhitespace(locationId, nameof(locationId));
Validate.NotNull(availabilityResult, nameof(availabilityResult));
this.TestDisplayName = testDisplayName;
this.TestArmResourceName = testArmResourceName;
this.LocationDisplayName = locationDisplayName;
this.LocationId = locationId;
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;
//}
private AvailabilityTelemetry CreateNewAvailabilityResult()
{
const string mockApplicationInsightsAppId = "00000000-0000-0000-0000-000000000000";
const string mockApplicationInsightsArmResourceName = "Application-Insights-Component";
var availabilityResult = new AvailabilityTelemetry();
availabilityResult.Timestamp = this.StartTime.ToUniversalTime();
availabilityResult.Duration = TimeSpan.Zero;
availabilityResult.Success = false;
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;
availabilityResult.Properties["SourceId"] = $"sid://{mockApplicationInsightsAppId}.visualstudio.com"
+ $"/applications/{mockApplicationInsightsArmResourceName}"
+ $"/features/{this.TestArmResourceName}"
+ $"/locations/{this.LocationId}";
return availabilityResult;
}
}
}

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

@ -0,0 +1,287 @@
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,68 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.ApplicationInsights.DataContracts;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
internal static class Convert
{
private static readonly string NullWord = "null";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string NotNullOrWord(string s)
{
return s ?? NullWord;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetPropertyOrNullWord(AvailabilityTelemetry availabilityResult, string propertyName)
{
return NotNullOrWord(GetPropertyOrNull(availabilityResult, propertyName));
}
public static string GetPropertyOrNull(AvailabilityTelemetry availabilityResult, string propertyName)
{
IDictionary<string, string> properties = availabilityResult?.Properties;
if (properties == null || propertyName == null)
{
return null;
}
if (false == properties.TryGetValue(propertyName, out string propertyValue))
{
return null;
}
return propertyValue;
}
public static JObject AvailabilityTestInvocationToJObject(AvailabilityTestInvocation availabilityTestInvocation)
{
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
JObject jObject = JObject.FromObject(availabilityTestInvocation);
return jObject;
}
public static AvailabilityTestInvocation JObjectToAvailabilityTestInvocation(JObject availabilityTestInvocation)
{
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
AvailabilityTestInvocation stronglyTypedTestInvocation = availabilityTestInvocation.ToObject<AvailabilityTestInvocation>();
return stronglyTypedTestInvocation;
}
public static AvailabilityTelemetry AvailabilityTestInvocationToAvailabilityTelemetry(AvailabilityTestInvocation availabilityTestInvocation)
{
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
return availabilityTestInvocation.AvailabilityResult;
}
public static AvailabilityTestInvocation AvailabilityTelemetryToAvailabilityTestInvocation(AvailabilityTelemetry availabilityResult)
{
Validate.NotNull(availabilityResult, nameof(availabilityResult));
return new AvailabilityTestInvocation(availabilityResult);
}
}
}

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

@ -0,0 +1,64 @@
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,69 @@
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
{
internal static class Validate
{
private const string FallbackParameterName = "specified parameter";
/// <summary>
/// Parameter check for Null.
/// </summary>
/// <param name="value">Value to be checked.</param>
/// <param name="name">Name of the parameter being checked.</param>
/// <exception cref="ArgumentNullException">If the value is null.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotNull(object value, string name)
{
if (value == null)
{
throw new ArgumentNullException(name ?? Validate.FallbackParameterName);
}
}
/// <summary>
/// String parameter check with a more informative exception that specifies whether
/// the problem was that the string was null or empty.
/// </summary>
/// <param name="value">Value to be checked.</param>
/// <param name="name">Name of the parameter being checked.</param>
/// <exception cref="ArgumentNullException">If the value is null.</exception>
/// <exception cref="ArgumentException">If the value is an empty string.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotNullOrEmpty(string value, string name)
{
if (value == null)
{
throw new ArgumentNullException(name ?? Validate.FallbackParameterName);
}
if (value.Length == 0)
{
throw new ArgumentException((name ?? Validate.FallbackParameterName) + " may not be empty.");
}
}
/// <summary>
/// String parameter check with a more informative exception that specifies whether
/// the problem was that the string was null, empty or whitespace only.
/// </summary>
/// <param name="value">Value to be checked.</param>
/// <param name="name">Name of the parameter being checked.</param>
/// <exception cref="ArgumentNullException">If the value is null.</exception>
/// <exception cref="ArgumentException">If the value is an empty string or a string containing whitespaces only;
/// the message describes which of these two applies.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotNullOrWhitespace(string value, string name)
{
NotNullOrEmpty(value, name);
if (String.IsNullOrWhiteSpace(value))
{
throw new ArgumentException((name ?? Validate.FallbackParameterName) + " may not be whitespace only.");
}
}
}
}

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

@ -0,0 +1,31 @@
// 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);
}
}

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

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29519.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring", "AvailabilityMonitoring-Extension\Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.csproj", "{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvailabilityMonitoring-Extension-Demo", "AvailabilityMonitoring-Extension-Demo\AvailabilityMonitoring-Extension-Demo.csproj", "{0AA499F5-F903-4FAC-BF3B-EA75D6735167}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Release|Any CPU.Build.0 = Release|Any CPU
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B62196E4-D124-4879-B726-71519C486A1A}
EndGlobalSection
EndGlobal