Merging.
This commit is contained in:
Коммит
4fda15a320
16
README.md
16
README.md
|
@ -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);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче