Merge branch 'dev'
This commit is contained in:
Коммит
b6abe1fc8d
|
@ -1,6 +1,10 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# Publish profiles
|
||||
*.pubxml
|
||||
PublishProfiles/
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
|
|
|
@ -22,23 +22,23 @@ namespace AvailabilityMonitoringExtensionDemo
|
|||
_telemetryClient = new TelemetryClient(telemetryConfig);
|
||||
}
|
||||
|
||||
//[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)}");
|
||||
[FunctionName("Availability-Monitoring-Demo-01-A-SimpleIoBinding")]
|
||||
public async Task SimpleIoBinding(
|
||||
[TimerTrigger("0,15,30,45 */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(
|
||||
|
@ -68,35 +68,35 @@ namespace AvailabilityMonitoringExtensionDemo
|
|||
// await Task.Delay(0);
|
||||
// }
|
||||
|
||||
[FunctionName("Availability-Monitoring-Demo-01-C-BindingToJObject")]
|
||||
public async Task BindingToJObject(
|
||||
//[TimerTrigger("15 */1 * * * *")] TimerInfo timerInfo,
|
||||
[TimerTrigger(AvailabilityTestInterval.Minute01)] 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"}");
|
||||
//[FunctionName("Availability-Monitoring-Demo-01-C-BindingToJObject")]
|
||||
//public async Task BindingToJObject(
|
||||
// //[TimerTrigger("15 */1 * * * *")] TimerInfo timerInfo,
|
||||
// [TimerTrigger(AvailabilityTestInterval.Minute01)] 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;
|
||||
// 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"}");
|
||||
// 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.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)";
|
||||
// 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);
|
||||
}
|
||||
// await Task.Delay(0);
|
||||
//}
|
||||
|
||||
//[FunctionName("Availability-Monitoring-Demo-01-D-BindingToAvailabilityTelemetry")]
|
||||
//public async Task BindingToAvailabilityTelemetry(
|
|
@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.Extensions;
|
||||
|
||||
namespace AvailabilityMonitoringExtensionDemo
|
||||
{
|
||||
|
@ -44,7 +44,6 @@ namespace AvailabilityMonitoringExtensionDemo
|
|||
})
|
||||
.ConfigureServices((serviceCollection) =>
|
||||
{
|
||||
//serviceCollection.AddSingleton(typeof(AvailabilityMonitoringDemo01));
|
||||
})
|
||||
.UseConsoleLifetime();
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
|
||||
"APPINSIGHTS_INSTRUMENTATIONKEY": "a418f0b6-c28b-44c7-b396-a1f8d2207aed"
|
||||
"APPINSIGHTS_INSTRUMENTATIONKEY": "18765e1c-90ea-4efd-a776-55b942954582"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
|
|
@ -1,11 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<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" />
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.29" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AvailabilityMonitoring-Extension\Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.csproj" />
|
||||
|
@ -19,4 +19,7 @@
|
|||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\PublishProfiles\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -1,4 +1,9 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -7,14 +12,119 @@ namespace AvailabilityMonitoring_Extension_DemoFunction
|
|||
{
|
||||
public static class CatDemoFunctions
|
||||
{
|
||||
[FunctionName("CatDemo-SimpleBinding")]
|
||||
public static void Run(
|
||||
[TimerTrigger(AvailabilityTestInterval.Minute01)]TimerInfo timerInfo,
|
||||
[AvailabilityTest] AvailabilityTestInfo testInfo,
|
||||
//[FunctionName("CatDemo-SimpleBinding")]
|
||||
//[return: AvailabilityTestResult(TestDisplayName = "Validation Test 1", LocationDisplayName = "Validation Location 1", LocationId = "val-loc-1")]
|
||||
//public static async Task<AvailabilityTelemetry> Run(
|
||||
// [TimerTrigger(AvailabilityTestInterval.Minute01)] TimerInfo timerInfo,
|
||||
// //[TimerTrigger("*/5 * * * * *")] TimerInfo timerInfo,
|
||||
// [AvailabilityTestInfo] AvailabilityTestInfo testInfo,
|
||||
// ILogger log)
|
||||
//{
|
||||
// log.LogInformation($"[CatDemo-SimpleBinding] Run(..): C# Timer trigger function executed at: {DateTime.Now}."
|
||||
// + $" ActivitySpanId = \"{Activity.Current.SpanId.ToHexString() ?? "null"}\";"
|
||||
// + $" TestDisplayName = \"{testInfo.TestDisplayName ?? "null"}\";"
|
||||
// + $" LocationDisplayName = \"{testInfo.LocationDisplayName ?? "null"}\";"
|
||||
// + $" LocationId = \"{testInfo.LocationId ?? "null"}\".");
|
||||
|
||||
|
||||
// string responseContent;
|
||||
// using (var http = new HttpClient())
|
||||
// {
|
||||
// using (HttpResponseMessage response = await http.GetAsync("https://availabilitymonitoring-extension-monitoredappsample.azurewebsites.net/Home/MonitoredPage"))
|
||||
// {
|
||||
// response.EnsureSuccessStatusCode();
|
||||
// responseContent = await response.Content.ReadAsStringAsync();
|
||||
// }
|
||||
// }
|
||||
|
||||
// bool hasExpectedContent = responseContent.Contains("<title>Monitored Page</title>", StringComparison.OrdinalIgnoreCase)
|
||||
// && responseContent.Contains("(App Version Id: 2)", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// AvailabilityTelemetry result = testInfo.DefaultAvailabilityResult;
|
||||
|
||||
// result.Properties["UserProperty"] = "User Value";
|
||||
// result.Success = hasExpectedContent;
|
||||
// return result;
|
||||
//}
|
||||
|
||||
[FunctionName("CatDemo-SimpleBinding-LoadConfigFromHostSettings")]
|
||||
[return: AvailabilityTestResult]
|
||||
public static async Task<AvailabilityTelemetry> Run(
|
||||
//[TimerTrigger(AvailabilityTestInterval.Minute01)] TimerInfo timerInfo,
|
||||
[TimerTrigger("*/5 * * * * *")] TimerInfo timerInfo,
|
||||
[AvailabilityTestInfo] AvailabilityTestInfo testInfo,
|
||||
ILogger log)
|
||||
{
|
||||
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
|
||||
testInfo.AvailabilityResult.Success = true;
|
||||
log.LogInformation($"[CatDemo-SimpleBinding-LoadConfigFromHostSettings] Run(..): C# Timer trigger function executed at: {DateTime.Now}."
|
||||
+ $" ActivitySpanId = \"{Activity.Current.SpanId.ToHexString() ?? "null"}\";"
|
||||
+ $" TestDisplayName = \"{testInfo.TestDisplayName ?? "null"}\";"
|
||||
+ $" LocationDisplayName = \"{testInfo.LocationDisplayName ?? "null"}\";"
|
||||
+ $" LocationId = \"{testInfo.LocationId ?? "null"}\".");
|
||||
|
||||
|
||||
string responseContent;
|
||||
using (var http = new HttpClient())
|
||||
{
|
||||
using (HttpResponseMessage response = await http.GetAsync("https://availabilitymonitoring-extension-monitoredappsample.azurewebsites.net/Home/MonitoredPage"))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
responseContent = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
bool hasExpectedContent = responseContent.Contains("<title>Monitored Page</title>", StringComparison.OrdinalIgnoreCase)
|
||||
&& responseContent.Contains("(App Version Id: 2)", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
AvailabilityTelemetry result = testInfo.DefaultAvailabilityResult;
|
||||
|
||||
result.Properties["UserProperty"] = "User Value";
|
||||
result.Success = hasExpectedContent;
|
||||
return result;
|
||||
}
|
||||
|
||||
//[FunctionName("CatDemo-PlainSimplePrototype-ThrowsException")]
|
||||
//[return: AvailabilityTestResult(TestDisplayName = "An AvailabilityTestResult test!")]
|
||||
//public static bool RunWithException(
|
||||
// //[TimerTrigger(AvailabilityTestInterval.Minute01)] TimerInfo timerInfo,
|
||||
// [TimerTrigger("*/5 * * * * *")] TimerInfo timerInfo,
|
||||
// ILogger log)
|
||||
//{
|
||||
// log.LogInformation($"[CatDemo-PlainSimplePrototype-ThrowsException] RunWithException(..): C# Timer trigger function executed at: {DateTime.Now}."
|
||||
// + $" ActivitySpanId = \"{Activity.Current.SpanId.ToHexString() ?? "null"}\".");
|
||||
|
||||
// throw new HypotheticalTestException("This is a hypothetical test exception.");
|
||||
//}
|
||||
|
||||
//[FunctionName("CatDemo-PlainSimplePrototype-ShouldTimeout")]
|
||||
//[return: AvailabilityTestResult]
|
||||
//public static async Task<bool> RunWithTimeout(
|
||||
// [TimerTrigger(AvailabilityTestInterval.Minute01)] TimerInfo timerInfo,
|
||||
// ILogger log)
|
||||
//{
|
||||
// log.LogInformation($"[CatDemo-PlainSimplePrototype-ShouldTimeout] RunWithTimeout(..): C# Timer trigger function executed at: {DateTime.Now}."
|
||||
// + $" ActivitySpanId = \"{Activity.Current.SpanId.ToHexString() ?? "null"}\".");
|
||||
|
||||
// Console.WriteLine();
|
||||
|
||||
// DateTimeOffset startTime = DateTimeOffset.Now;
|
||||
|
||||
// TimeSpan passed = DateTimeOffset.Now - startTime;
|
||||
// while (passed < TimeSpan.FromSeconds(120))
|
||||
// {
|
||||
// Console.Write($"...{(int)passed.TotalSeconds}");
|
||||
// await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
// passed = DateTimeOffset.Now - startTime;
|
||||
// }
|
||||
|
||||
// Console.WriteLine();
|
||||
|
||||
// return true;
|
||||
//}
|
||||
|
||||
public class HypotheticalTestException : Exception
|
||||
{
|
||||
public HypotheticalTestException() : base() {}
|
||||
public HypotheticalTestException(string msg) : base(msg) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_DemoFunction
|
||||
{
|
||||
public static class EnvironmentSpoofer
|
||||
{
|
||||
//[FunctionName("EnvironmentSpoofer")]
|
||||
public static IActionResult Run(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
|
||||
ILogger log)
|
||||
{
|
||||
var result = new
|
||||
{
|
||||
environment = GetEnvironment()
|
||||
};
|
||||
|
||||
return new OkObjectResult(result);
|
||||
}
|
||||
|
||||
private static List<KeyValuePair<string, string>> GetEnvironment()
|
||||
{
|
||||
var environment = new Dictionary<object, object>();
|
||||
|
||||
IDictionary environmentDict = Environment.GetEnvironmentVariables();
|
||||
foreach (DictionaryEntry entry in environmentDict)
|
||||
{
|
||||
environment.Add(entry.Key, entry.Value);
|
||||
}
|
||||
|
||||
List<KeyValuePair<string, string>> orderedEnvironment = environment.Select(kvp => KeyValuePair.Create(kvp.Key.ToString(), kvp.Value.ToString()))
|
||||
.Select( (kvp) => KeyValuePair.Create(kvp.Key,
|
||||
kvp.Key.Contains("Key", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Passw", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Pass", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Pwd", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Sig", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Key", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Passw", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Pass", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Pwd", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Sig", StringComparison.OrdinalIgnoreCase)
|
||||
? "..."
|
||||
: kvp.Value) )
|
||||
.OrderBy((kvp) => kvp.Key)
|
||||
.ToList();
|
||||
return orderedEnvironment;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_DemoFunction
|
||||
{
|
||||
public static class TimeServer
|
||||
{
|
||||
public class TimeInfo
|
||||
{
|
||||
public DateTimeOffset UtcTime { get; set; }
|
||||
public DateTimeOffset LocalTime { get; set; }
|
||||
public string LocalTimeZone { get; set; }
|
||||
public string LocationInfo { get; set; }
|
||||
}
|
||||
|
||||
[FunctionName("TimeServer")]
|
||||
public static IActionResult Run(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
|
||||
ILogger log)
|
||||
{
|
||||
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
|
||||
|
||||
log.LogInformation("Function \"TimeServer\" was invoked.");
|
||||
TimeInfo timeInfo = GetLocalTimeInfo();
|
||||
return new OkObjectResult(timeInfo);
|
||||
}
|
||||
|
||||
private static TimeInfo GetLocalTimeInfo()
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
TimeZoneInfo tz = TimeZoneInfo.Local;
|
||||
|
||||
return new TimeInfo()
|
||||
{
|
||||
UtcTime = now.ToUniversalTime(),
|
||||
LocalTime = now,
|
||||
LocalTimeZone = tz.IsDaylightSavingTime(now) ? tz.DaylightName : tz.StandardName,
|
||||
LocationInfo = Environment.GetEnvironmentVariable("COMPUTERNAME") ?? Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,12 @@
|
|||
"samplingSettings": {
|
||||
"isEnabled": true
|
||||
}
|
||||
},
|
||||
"logLevel": {
|
||||
"default": "Trace"
|
||||
}
|
||||
},
|
||||
"availabilityTestResults": {
|
||||
"LocationDisplayName": "Location Name for this App"
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
|
||||
"APPINSIGHTS_INSTRUMENTATIONKEY": "4f58be6b-a928-45cc-8c72-952a6c85d8d9",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "3.1.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RootNamespace>AvailabilityMonitoring_Extension_MonitoredAppSample</RootNamespace>
|
||||
<ApplicationInsightsResourceId>/subscriptions/b338896e-c087-475e-b161-6d35b3c38374/resourcegroups/GregP-LinuxFuncs01/providers/microsoft.insights/components/AvailabilityMonitoring-Extension-MonitoredAppSample</ApplicationInsightsResourceId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.12.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<WCFMetadata Include="Connected Services" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\PublishProfiles\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
|
||||
"Version": "16.0.0.0",
|
||||
"GettingStartedDocument": {
|
||||
"Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using AvailabilityMonitoring_Extension_MonitoredAppSample.Models;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections;
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_MonitoredAppSample.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly ILogger<HomeController> _logger;
|
||||
|
||||
public HomeController(ILogger<HomeController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> MonitoredPage()
|
||||
{
|
||||
MonitoredPageViewModel model = await (new TimeController()).GetAsync();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using AvailabilityMonitoring_Extension_MonitoredAppSample.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_MonitoredAppSample.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class TimeController : ControllerBase
|
||||
{
|
||||
// GET: api/Time
|
||||
[HttpGet]
|
||||
public async Task<MonitoredPageViewModel> GetAsync()
|
||||
{
|
||||
var model = new MonitoredPageViewModel()
|
||||
{
|
||||
PublicTime = await GetRemoteTimeInfoAsync(),
|
||||
LocalTime = GetLocalTimeInfo(),
|
||||
FunctionTime = await GetFunctionTimeInfoAsync(),
|
||||
LocalEnvironment = new List<KeyValuePair<string, string>>() // GetEnvironment()
|
||||
};
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private async Task<MonitoredPageViewModel.TimeInfo> GetRemoteTimeInfoAsync()
|
||||
{
|
||||
string responseContent;
|
||||
|
||||
using (var http = new HttpClient())
|
||||
{
|
||||
using (HttpResponseMessage response = await http.GetAsync("http://worldtimeapi.org/api/ip"))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
responseContent = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
dynamic timeInfo = JObject.Parse(responseContent);
|
||||
|
||||
var timeInfoObj = new MonitoredPageViewModel.TimeInfo()
|
||||
{
|
||||
UtcTime = timeInfo.utc_datetime,
|
||||
LocalTime = timeInfo.datetime,
|
||||
LocalTimeZone = $"{timeInfo.timezone} ({timeInfo.abbreviation})",
|
||||
LocationInfo = "worldtimeapi.org"
|
||||
};
|
||||
|
||||
// Is this really a bug in Newtonsoft that caused the time to be deserialized to local??
|
||||
timeInfoObj.UtcTime = timeInfoObj.UtcTime.ToUniversalTime();
|
||||
timeInfoObj.LocalTime = timeInfoObj.LocalTime.ToLocalTime();
|
||||
|
||||
return timeInfoObj;
|
||||
}
|
||||
|
||||
private async Task<MonitoredPageViewModel.TimeInfo> GetFunctionTimeInfoAsync()
|
||||
{
|
||||
string responseContent;
|
||||
|
||||
using (var http = new HttpClient())
|
||||
{
|
||||
using (HttpResponseMessage response = await http.GetAsync("https://gregp-cat-test11.azurewebsites.net/api/TimeServer"))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
responseContent = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
MonitoredPageViewModel.TimeInfo timeInfo = JsonConvert.DeserializeObject<MonitoredPageViewModel.TimeInfo>(responseContent);
|
||||
return timeInfo;
|
||||
}
|
||||
|
||||
private MonitoredPageViewModel.TimeInfo GetLocalTimeInfo()
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
TimeZoneInfo tz = TimeZoneInfo.Local;
|
||||
|
||||
return new MonitoredPageViewModel.TimeInfo()
|
||||
{
|
||||
UtcTime = now.ToUniversalTime(),
|
||||
LocalTime = now,
|
||||
LocalTimeZone = tz.IsDaylightSavingTime(now) ? tz.DaylightName : tz.StandardName,
|
||||
LocationInfo = Environment.GetEnvironmentVariable("COMPUTERNAME") ?? Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "*"
|
||||
};
|
||||
}
|
||||
|
||||
private static List<KeyValuePair<string, string>> GetEnvironment()
|
||||
{
|
||||
var environment = new Dictionary<object, object>();
|
||||
|
||||
IDictionary environmentDict = Environment.GetEnvironmentVariables();
|
||||
foreach (DictionaryEntry entry in environmentDict)
|
||||
{
|
||||
environment.Add(entry.Key, entry.Value);
|
||||
}
|
||||
|
||||
List<KeyValuePair<string, string>> orderedEnvironment = environment.Select(kvp => KeyValuePair.Create(kvp.Key.ToString(), kvp.Value.ToString()))
|
||||
.Select((kvp) => KeyValuePair.Create(kvp.Key,
|
||||
kvp.Key.Contains("Key", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Passw", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Pass", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Pwd", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Key.Contains("Sig", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Key", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Passw", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Pass", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Pwd", StringComparison.OrdinalIgnoreCase)
|
||||
|| kvp.Value.Contains("Sig", StringComparison.OrdinalIgnoreCase)
|
||||
? "..."
|
||||
: kvp.Value))
|
||||
.OrderBy((kvp) => kvp.Key)
|
||||
.ToList();
|
||||
return orderedEnvironment;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_MonitoredAppSample.Models
|
||||
{
|
||||
public class ErrorViewModel
|
||||
{
|
||||
public string RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !String.IsNullOrEmpty(RequestId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_MonitoredAppSample.Models
|
||||
{
|
||||
public class MonitoredPageViewModel
|
||||
{
|
||||
public class TimeInfo
|
||||
{
|
||||
public DateTimeOffset UtcTime { get; set; }
|
||||
public DateTimeOffset LocalTime { get; set; }
|
||||
public string LocalTimeZone { get; set; }
|
||||
public string LocationInfo { get; set; }
|
||||
}
|
||||
|
||||
public TimeInfo LocalTime { get; set; }
|
||||
|
||||
public TimeInfo PublicTime { get; set; }
|
||||
|
||||
public TimeInfo FunctionTime { get; set; }
|
||||
|
||||
public IList<KeyValuePair<string, string>> LocalEnvironment { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_MonitoredAppSample
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:4128",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"AvailabilityMonitoring_Extension_MonitoredAppSample": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace AvailabilityMonitoring_Extension_MonitoredAppSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllersWithViews();
|
||||
services.AddApplicationInsightsTelemetry();
|
||||
services.AddControllers();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
}
|
||||
app.UseStaticFiles();
|
||||
|
||||
//app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
@{
|
||||
ViewData["Title"] = "Home Page";
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Welcome, Sashechka-Busechka</h1>
|
||||
<p>Azure Monitor rocks!</p>
|
||||
</div>
|
|
@ -0,0 +1,81 @@
|
|||
@model MonitoredPageViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Monitored Page";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
|
||||
<h2>Local Time</h2>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Location info:</td>
|
||||
<td>@Model.LocalTime.LocationInfo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UTC time:</td>
|
||||
<td>@Model.LocalTime.UtcTime.ToString("o")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local time:</td>
|
||||
<td>@Model.LocalTime.LocalTime.ToString("o")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local time zone:</td>
|
||||
<td>@Model.LocalTime.LocalTimeZone</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Public Time</h2>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Location info:</td>
|
||||
<td>@Model.PublicTime.LocationInfo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UTC time:</td>
|
||||
<td>@Model.PublicTime.UtcTime.ToString("o")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local time:</td>
|
||||
<td>@Model.PublicTime.LocalTime.ToString("o")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local time zone:</td>
|
||||
<td>@Model.PublicTime.LocalTimeZone</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Function Time</h2>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Location info:</td>
|
||||
<td>@Model.FunctionTime.LocationInfo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UTC time:</td>
|
||||
<td>@Model.FunctionTime.UtcTime.ToString("o")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local time:</td>
|
||||
<td>@Model.FunctionTime.LocalTime.ToString("o")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local time zone:</td>
|
||||
<td>@Model.FunctionTime.LocalTimeZone</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@*<h2>Local Environment</h2>
|
||||
|
||||
<table>
|
||||
@foreach (KeyValuePair<string, string> envVar in Model.LocalEnvironment)
|
||||
{
|
||||
<tr>
|
||||
<td>@envVar.Key:</td>
|
||||
<td>@envVar.Value</td>
|
||||
</tr>
|
||||
}
|
||||
</table>*@
|
|
@ -0,0 +1,25 @@
|
|||
@model ErrorViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
|
@ -0,0 +1,54 @@
|
|||
@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"]</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" />
|
||||
@Html.Raw(JavaScriptSnippet.FullScript)
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">AvailabilityMonitoring-Extension: Monitored App Sample</a>
|
||||
<b>(App Version Id: 2)</b>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="MonitoredPage">Monitored Page</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="api" asp-action="Time">Monitored API</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container">
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="border-top footer text-muted">
|
||||
<div class="container">
|
||||
© 2020 Microsoft
|
||||
</div>
|
||||
</footer>
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,2 @@
|
|||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
|
@ -0,0 +1,3 @@
|
|||
@using AvailabilityMonitoring_Extension_MonitoredAppSample
|
||||
@using AvailabilityMonitoring_Extension_MonitoredAppSample.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
@ -0,0 +1,3 @@
|
|||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ApplicationInsights": {
|
||||
"InstrumentationKey": "6e204f56-fd43-4c08-afda-cd716bf41d49"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
for details on configuring this project to bundle and minify static web assets. */
|
||||
|
||||
a.navbar-brand {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Provide sufficient contrast against white background */
|
||||
a {
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
button.accept-policy {
|
||||
font-size: 1rem;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Margin bottom by footer height */
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
line-height: 60px; /* Vertically center the text there */
|
||||
}
|
Двоичные данные
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/favicon.ico
Normal file
Двоичные данные
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/favicon.ico
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 31 KiB |
|
@ -0,0 +1,4 @@
|
|||
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
// for details on configuring this project to bundle and minify static web assets.
|
||||
|
||||
// Write your JavaScript code.
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2018 Twitter, Inc.
|
||||
Copyright (c) 2011-2018 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
3719
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
поставляемый
Normal file
3719
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
331
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
поставляемый
Normal file
331
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
поставляемый
Normal file
|
@ -0,0 +1,331 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2019 The Bootstrap Authors
|
||||
* Copyright 2011-2019 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[type="button"]:not(:disabled),
|
||||
[type="reset"]:not(:disabled),
|
||||
[type="submit"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2019 The Bootstrap Authors
|
||||
* Copyright 2011-2019 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
10038
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.css
поставляемый
Normal file
10038
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.css
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
поставляемый
Normal file
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
7
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
поставляемый
Normal file
7
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
поставляемый
Normal file
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
7013
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
поставляемый
Normal file
7013
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
4435
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.js
поставляемый
Normal file
4435
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.js
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
поставляемый
Normal file
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
7
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
поставляемый
Normal file
7
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
поставляемый
Normal file
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,12 @@
|
|||
Copyright (c) .NET Foundation. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
these files except in compliance with the License. You may obtain a copy of the
|
||||
License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed
|
||||
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations under the License.
|
|
@ -0,0 +1,432 @@
|
|||
// Unobtrusive validation support library for jQuery and jQuery Validate
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// @version v3.2.11
|
||||
|
||||
/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */
|
||||
/*global document: false, jQuery: false */
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define("jquery.validate.unobtrusive", ['jquery-validation'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// CommonJS-like environments that support module.exports
|
||||
module.exports = factory(require('jquery-validation'));
|
||||
} else {
|
||||
// Browser global
|
||||
jQuery.validator.unobtrusive = factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
var $jQval = $.validator,
|
||||
adapters,
|
||||
data_validation = "unobtrusiveValidation";
|
||||
|
||||
function setValidationValues(options, ruleName, value) {
|
||||
options.rules[ruleName] = value;
|
||||
if (options.message) {
|
||||
options.messages[ruleName] = options.message;
|
||||
}
|
||||
}
|
||||
|
||||
function splitAndTrim(value) {
|
||||
return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g);
|
||||
}
|
||||
|
||||
function escapeAttributeValue(value) {
|
||||
// As mentioned on http://api.jquery.com/category/selectors/
|
||||
return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
|
||||
}
|
||||
|
||||
function getModelPrefix(fieldName) {
|
||||
return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
function appendModelPrefix(value, prefix) {
|
||||
if (value.indexOf("*.") === 0) {
|
||||
value = value.replace("*.", prefix);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function onError(error, inputElement) { // 'this' is the form element
|
||||
var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
|
||||
replaceAttrValue = container.attr("data-valmsg-replace"),
|
||||
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
|
||||
|
||||
container.removeClass("field-validation-valid").addClass("field-validation-error");
|
||||
error.data("unobtrusiveContainer", container);
|
||||
|
||||
if (replace) {
|
||||
container.empty();
|
||||
error.removeClass("input-validation-error").appendTo(container);
|
||||
}
|
||||
else {
|
||||
error.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function onErrors(event, validator) { // 'this' is the form element
|
||||
var container = $(this).find("[data-valmsg-summary=true]"),
|
||||
list = container.find("ul");
|
||||
|
||||
if (list && list.length && validator.errorList.length) {
|
||||
list.empty();
|
||||
container.addClass("validation-summary-errors").removeClass("validation-summary-valid");
|
||||
|
||||
$.each(validator.errorList, function () {
|
||||
$("<li />").html(this.message).appendTo(list);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onSuccess(error) { // 'this' is the form element
|
||||
var container = error.data("unobtrusiveContainer");
|
||||
|
||||
if (container) {
|
||||
var replaceAttrValue = container.attr("data-valmsg-replace"),
|
||||
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null;
|
||||
|
||||
container.addClass("field-validation-valid").removeClass("field-validation-error");
|
||||
error.removeData("unobtrusiveContainer");
|
||||
|
||||
if (replace) {
|
||||
container.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onReset(event) { // 'this' is the form element
|
||||
var $form = $(this),
|
||||
key = '__jquery_unobtrusive_validation_form_reset';
|
||||
if ($form.data(key)) {
|
||||
return;
|
||||
}
|
||||
// Set a flag that indicates we're currently resetting the form.
|
||||
$form.data(key, true);
|
||||
try {
|
||||
$form.data("validator").resetForm();
|
||||
} finally {
|
||||
$form.removeData(key);
|
||||
}
|
||||
|
||||
$form.find(".validation-summary-errors")
|
||||
.addClass("validation-summary-valid")
|
||||
.removeClass("validation-summary-errors");
|
||||
$form.find(".field-validation-error")
|
||||
.addClass("field-validation-valid")
|
||||
.removeClass("field-validation-error")
|
||||
.removeData("unobtrusiveContainer")
|
||||
.find(">*") // If we were using valmsg-replace, get the underlying error
|
||||
.removeData("unobtrusiveContainer");
|
||||
}
|
||||
|
||||
function validationInfo(form) {
|
||||
var $form = $(form),
|
||||
result = $form.data(data_validation),
|
||||
onResetProxy = $.proxy(onReset, form),
|
||||
defaultOptions = $jQval.unobtrusive.options || {},
|
||||
execInContext = function (name, args) {
|
||||
var func = defaultOptions[name];
|
||||
func && $.isFunction(func) && func.apply(form, args);
|
||||
};
|
||||
|
||||
if (!result) {
|
||||
result = {
|
||||
options: { // options structure passed to jQuery Validate's validate() method
|
||||
errorClass: defaultOptions.errorClass || "input-validation-error",
|
||||
errorElement: defaultOptions.errorElement || "span",
|
||||
errorPlacement: function () {
|
||||
onError.apply(form, arguments);
|
||||
execInContext("errorPlacement", arguments);
|
||||
},
|
||||
invalidHandler: function () {
|
||||
onErrors.apply(form, arguments);
|
||||
execInContext("invalidHandler", arguments);
|
||||
},
|
||||
messages: {},
|
||||
rules: {},
|
||||
success: function () {
|
||||
onSuccess.apply(form, arguments);
|
||||
execInContext("success", arguments);
|
||||
}
|
||||
},
|
||||
attachValidation: function () {
|
||||
$form
|
||||
.off("reset." + data_validation, onResetProxy)
|
||||
.on("reset." + data_validation, onResetProxy)
|
||||
.validate(this.options);
|
||||
},
|
||||
validate: function () { // a validation function that is called by unobtrusive Ajax
|
||||
$form.validate();
|
||||
return $form.valid();
|
||||
}
|
||||
};
|
||||
$form.data(data_validation, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
$jQval.unobtrusive = {
|
||||
adapters: [],
|
||||
|
||||
parseElement: function (element, skipAttach) {
|
||||
/// <summary>
|
||||
/// Parses a single HTML element for unobtrusive validation attributes.
|
||||
/// </summary>
|
||||
/// <param name="element" domElement="true">The HTML element to be parsed.</param>
|
||||
/// <param name="skipAttach" type="Boolean">[Optional] true to skip attaching the
|
||||
/// validation to the form. If parsing just this single element, you should specify true.
|
||||
/// If parsing several elements, you should specify false, and manually attach the validation
|
||||
/// to the form when you are finished. The default is false.</param>
|
||||
var $element = $(element),
|
||||
form = $element.parents("form")[0],
|
||||
valInfo, rules, messages;
|
||||
|
||||
if (!form) { // Cannot do client-side validation without a form
|
||||
return;
|
||||
}
|
||||
|
||||
valInfo = validationInfo(form);
|
||||
valInfo.options.rules[element.name] = rules = {};
|
||||
valInfo.options.messages[element.name] = messages = {};
|
||||
|
||||
$.each(this.adapters, function () {
|
||||
var prefix = "data-val-" + this.name,
|
||||
message = $element.attr(prefix),
|
||||
paramValues = {};
|
||||
|
||||
if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy)
|
||||
prefix += "-";
|
||||
|
||||
$.each(this.params, function () {
|
||||
paramValues[this] = $element.attr(prefix + this);
|
||||
});
|
||||
|
||||
this.adapt({
|
||||
element: element,
|
||||
form: form,
|
||||
message: message,
|
||||
params: paramValues,
|
||||
rules: rules,
|
||||
messages: messages
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$.extend(rules, { "__dummy__": true });
|
||||
|
||||
if (!skipAttach) {
|
||||
valInfo.attachValidation();
|
||||
}
|
||||
},
|
||||
|
||||
parse: function (selector) {
|
||||
/// <summary>
|
||||
/// Parses all the HTML elements in the specified selector. It looks for input elements decorated
|
||||
/// with the [data-val=true] attribute value and enables validation according to the data-val-*
|
||||
/// attribute values.
|
||||
/// </summary>
|
||||
/// <param name="selector" type="String">Any valid jQuery selector.</param>
|
||||
|
||||
// $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one
|
||||
// element with data-val=true
|
||||
var $selector = $(selector),
|
||||
$forms = $selector.parents()
|
||||
.addBack()
|
||||
.filter("form")
|
||||
.add($selector.find("form"))
|
||||
.has("[data-val=true]");
|
||||
|
||||
$selector.find("[data-val=true]").each(function () {
|
||||
$jQval.unobtrusive.parseElement(this, true);
|
||||
});
|
||||
|
||||
$forms.each(function () {
|
||||
var info = validationInfo(this);
|
||||
if (info) {
|
||||
info.attachValidation();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
adapters = $jQval.unobtrusive.adapters;
|
||||
|
||||
adapters.add = function (adapterName, params, fn) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
|
||||
/// <param name="params" type="Array" optional="true">[Optional] An array of parameter names (strings) that will
|
||||
/// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and
|
||||
/// mmmm is the parameter name).</param>
|
||||
/// <param name="fn" type="Function">The function to call, which adapts the values from the HTML
|
||||
/// attributes into jQuery Validate rules and/or messages.</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
if (!fn) { // Called with no params, just a function
|
||||
fn = params;
|
||||
params = [];
|
||||
}
|
||||
this.push({ name: adapterName, params: params, adapt: fn });
|
||||
return this;
|
||||
};
|
||||
|
||||
adapters.addBool = function (adapterName, ruleName) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
|
||||
/// the jQuery Validate validation rule has no parameter values.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
|
||||
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
|
||||
/// of adapterName will be used instead.</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
return this.add(adapterName, function (options) {
|
||||
setValidationValues(options, ruleName || adapterName, true);
|
||||
});
|
||||
};
|
||||
|
||||
adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
|
||||
/// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and
|
||||
/// one for min-and-max). The HTML parameters are expected to be named -min and -max.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
|
||||
/// <param name="minRuleName" type="String">The name of the jQuery Validate rule to be used when you only
|
||||
/// have a minimum value.</param>
|
||||
/// <param name="maxRuleName" type="String">The name of the jQuery Validate rule to be used when you only
|
||||
/// have a maximum value.</param>
|
||||
/// <param name="minMaxRuleName" type="String">The name of the jQuery Validate rule to be used when you
|
||||
/// have both a minimum and maximum value.</param>
|
||||
/// <param name="minAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
|
||||
/// contains the minimum value. The default is "min".</param>
|
||||
/// <param name="maxAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
|
||||
/// contains the maximum value. The default is "max".</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) {
|
||||
var min = options.params.min,
|
||||
max = options.params.max;
|
||||
|
||||
if (min && max) {
|
||||
setValidationValues(options, minMaxRuleName, [min, max]);
|
||||
}
|
||||
else if (min) {
|
||||
setValidationValues(options, minRuleName, min);
|
||||
}
|
||||
else if (max) {
|
||||
setValidationValues(options, maxRuleName, max);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
adapters.addSingleVal = function (adapterName, attribute, ruleName) {
|
||||
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
|
||||
/// the jQuery Validate validation rule has a single value.</summary>
|
||||
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
|
||||
/// in the data-val-nnnn HTML attribute(where nnnn is the adapter name).</param>
|
||||
/// <param name="attribute" type="String">[Optional] The name of the HTML attribute that contains the value.
|
||||
/// The default is "val".</param>
|
||||
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
|
||||
/// of adapterName will be used instead.</param>
|
||||
/// <returns type="jQuery.validator.unobtrusive.adapters" />
|
||||
return this.add(adapterName, [attribute || "val"], function (options) {
|
||||
setValidationValues(options, ruleName || adapterName, options.params[attribute]);
|
||||
});
|
||||
};
|
||||
|
||||
$jQval.addMethod("__dummy__", function (value, element, params) {
|
||||
return true;
|
||||
});
|
||||
|
||||
$jQval.addMethod("regex", function (value, element, params) {
|
||||
var match;
|
||||
if (this.optional(element)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match = new RegExp(params).exec(value);
|
||||
return (match && (match.index === 0) && (match[0].length === value.length));
|
||||
});
|
||||
|
||||
$jQval.addMethod("nonalphamin", function (value, element, nonalphamin) {
|
||||
var match;
|
||||
if (nonalphamin) {
|
||||
match = value.match(/\W/g);
|
||||
match = match && match.length >= nonalphamin;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
if ($jQval.methods.extension) {
|
||||
adapters.addSingleVal("accept", "mimtype");
|
||||
adapters.addSingleVal("extension", "extension");
|
||||
} else {
|
||||
// for backward compatibility, when the 'extension' validation method does not exist, such as with versions
|
||||
// of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for
|
||||
// validating the extension, and ignore mime-type validations as they are not supported.
|
||||
adapters.addSingleVal("extension", "extension", "accept");
|
||||
}
|
||||
|
||||
adapters.addSingleVal("regex", "pattern");
|
||||
adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");
|
||||
adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range");
|
||||
adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength");
|
||||
adapters.add("equalto", ["other"], function (options) {
|
||||
var prefix = getModelPrefix(options.element.name),
|
||||
other = options.params.other,
|
||||
fullOtherName = appendModelPrefix(other, prefix),
|
||||
element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0];
|
||||
|
||||
setValidationValues(options, "equalTo", element);
|
||||
});
|
||||
adapters.add("required", function (options) {
|
||||
// jQuery Validate equates "required" with "mandatory" for checkbox elements
|
||||
if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") {
|
||||
setValidationValues(options, "required", true);
|
||||
}
|
||||
});
|
||||
adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
|
||||
var value = {
|
||||
url: options.params.url,
|
||||
type: options.params.type || "GET",
|
||||
data: {}
|
||||
},
|
||||
prefix = getModelPrefix(options.element.name);
|
||||
|
||||
$.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) {
|
||||
var paramName = appendModelPrefix(fieldName, prefix);
|
||||
value.data[paramName] = function () {
|
||||
var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']");
|
||||
// For checkboxes and radio buttons, only pick up values from checked fields.
|
||||
if (field.is(":checkbox")) {
|
||||
return field.filter(":checked").val() || field.filter(":hidden").val() || '';
|
||||
}
|
||||
else if (field.is(":radio")) {
|
||||
return field.filter(":checked").val() || '';
|
||||
}
|
||||
return field.val();
|
||||
};
|
||||
});
|
||||
|
||||
setValidationValues(options, "remote", value);
|
||||
});
|
||||
adapters.add("password", ["min", "nonalphamin", "regex"], function (options) {
|
||||
if (options.params.min) {
|
||||
setValidationValues(options, "minlength", options.params.min);
|
||||
}
|
||||
if (options.params.nonalphamin) {
|
||||
setValidationValues(options, "nonalphamin", options.params.nonalphamin);
|
||||
}
|
||||
if (options.params.regex) {
|
||||
setValidationValues(options, "regex", options.params.regex);
|
||||
}
|
||||
});
|
||||
adapters.add("fileextensions", ["extensions"], function (options) {
|
||||
setValidationValues(options, "extension", options.params.extensions);
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$jQval.unobtrusive.parse(document);
|
||||
});
|
||||
|
||||
return $jQval.unobtrusive;
|
||||
}));
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright Jörn Zaefferer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
1158
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery-validation/dist/additional-methods.js
поставляемый
Normal file
1158
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery-validation/dist/additional-methods.js
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1601
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery-validation/dist/jquery.validate.js
поставляемый
Normal file
1601
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery-validation/dist/jquery.validate.js
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,36 @@
|
|||
Copyright JS Foundation and other contributors, https://js.foundation/
|
||||
|
||||
This software consists of voluntary contributions made by many
|
||||
individuals. For exact contribution history, see the revision history
|
||||
available at https://github.com/jquery/jquery
|
||||
|
||||
The following license applies to all parts of this software except as
|
||||
documented below:
|
||||
|
||||
====
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
All files located in the node_modules and external directories are
|
||||
externally maintained libraries used by this software which have their
|
||||
own licenses; we recommend you read them, as their terms may differ from
|
||||
the terms above.
|
10364
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery/dist/jquery.js
поставляемый
Normal file
10364
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery/dist/jquery.js
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
2
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery/dist/jquery.min.js
поставляемый
Normal file
2
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery/dist/jquery.min.js
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery/dist/jquery.min.map
поставляемый
Normal file
1
src/AvailabilityMonitoring-Extension-MonitoredAppSample/wwwroot/lib/jquery/dist/jquery.min.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,118 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
internal static class Format
|
||||
{
|
||||
private const string NullWord = "null";
|
||||
|
||||
public static string Guid(Guid functionInstanceId)
|
||||
{
|
||||
return functionInstanceId.ToString("D");
|
||||
}
|
||||
|
||||
public static string SpanOperationName(string testDisplayName, string locationDisplayName)
|
||||
{
|
||||
return String.Format("AvailabilityTest={{TestDisplayName=\"{0}\", LocationDisplayName=\"{1}\"}}", SpellIfNull(testDisplayName), SpellIfNull(locationDisplayName));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string SpanId(Activity activity)
|
||||
{
|
||||
return SpellIfNull(activity?.SpanId.ToHexString());
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string SpellIfNull(string str)
|
||||
{
|
||||
return str ?? NullWord;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static object SpellIfNull(object val)
|
||||
{
|
||||
return val ?? NullWord;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AsTextLines<TKey, TValue>(IEnumerable<KeyValuePair<TKey, TValue>> table)
|
||||
{
|
||||
string QuoteIfString<T>(T val)
|
||||
{
|
||||
if (val == null)
|
||||
{
|
||||
return NullWord;
|
||||
}
|
||||
|
||||
string str = (val is string) ? '"' + val.ToString() + '"' : val.ToString();
|
||||
return str;
|
||||
}
|
||||
|
||||
if (table == null)
|
||||
{
|
||||
yield return NullWord;
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach(KeyValuePair<TKey, TValue> row in table)
|
||||
{
|
||||
string rowStr = $"[{QuoteIfString(row.Key)}] = {QuoteIfString(row.Value)}";
|
||||
yield return rowStr;
|
||||
}
|
||||
}
|
||||
|
||||
public static string LocationNameAsId(string locationDisplayName)
|
||||
{
|
||||
string locationId = locationDisplayName?.Trim()?.ToLowerInvariant()?.Replace(' ', '-');
|
||||
return locationId;
|
||||
}
|
||||
|
||||
public static string LimitLength(object value, int maxLength, bool trim)
|
||||
{
|
||||
string valueStr = value?.ToString();
|
||||
return LimitLength(valueStr, maxLength, trim);
|
||||
}
|
||||
|
||||
public static string LimitLength(string value, int maxLength, bool trim)
|
||||
{
|
||||
if (maxLength < 0)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(maxLength)} may not be smaller than zero, but it was {maxLength}.");
|
||||
}
|
||||
|
||||
const string FillStr = "...";
|
||||
int fillStrLen = FillStr.Length;
|
||||
|
||||
value = SpellIfNull(value);
|
||||
value = trim ? value.Trim() : value;
|
||||
int valueLen = value.Length;
|
||||
|
||||
if (valueLen <= maxLength)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (maxLength < fillStrLen + 2)
|
||||
{
|
||||
string superShortResult = value.Substring(0, maxLength);
|
||||
return superShortResult;
|
||||
}
|
||||
|
||||
int postLen = (maxLength - fillStrLen) / 2;
|
||||
int preLen = maxLength - fillStrLen - postLen;
|
||||
|
||||
string postStr = value.Substring(valueLen - postLen, postLen);
|
||||
string preStr = value.Substring(0, preLen);
|
||||
|
||||
var shortResult = new StringBuilder(preStr, maxLength);
|
||||
shortResult.Append(FillStr);
|
||||
shortResult.Append(postStr);
|
||||
|
||||
return shortResult.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
internal interface IAvailabilityTestConfiguration
|
||||
{
|
||||
string TestDisplayName { get; }
|
||||
|
||||
string LocationDisplayName { get; }
|
||||
|
||||
string LocationId { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring{
|
||||
internal static class LogExtensions
|
||||
{
|
||||
private class NoOpDisposable : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly IDisposable NoOpDisposableSingelton = new NoOpDisposable();
|
||||
|
||||
public static IDisposable BeginScopeSafe<TState>(this ILogger log, TState state) where TState : class
|
||||
{
|
||||
if (log == null || state == null)
|
||||
{
|
||||
return NoOpDisposableSingelton;
|
||||
}
|
||||
|
||||
return log.BeginScope(state);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring{
|
||||
internal class MinimalConsoleLogger : ILogger
|
||||
{
|
||||
private class MinimalConsoleLoggerScope : IDisposable
|
||||
{
|
||||
private MinimalConsoleLogger _logger = null;
|
||||
private readonly int _scopeId;
|
||||
|
||||
public MinimalConsoleLoggerScope(MinimalConsoleLogger logger, int scopeId)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeId = scopeId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
MinimalConsoleLogger logger = Interlocked.Exchange(ref _logger, null);
|
||||
if (logger != null)
|
||||
{
|
||||
logger.EndScope(_scopeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const string TabString = " ";
|
||||
private static readonly Random Rnd = new Random();
|
||||
|
||||
private const int Column1EndOffs = 12;
|
||||
private const int Column2EndOffs = 36;
|
||||
|
||||
private int _indentDepth = 0;
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
int scopeId = Rnd.Next();
|
||||
var scope = new MinimalConsoleLoggerScope(this, scopeId);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(FormatLine(
|
||||
column1: "[BeginScope]",
|
||||
column2: $"ScopeId = {scopeId.ToString("X")} {{",
|
||||
column3: null));
|
||||
|
||||
if (state is IEnumerable<KeyValuePair<string, object>> stateTable)
|
||||
{
|
||||
int lineNum = 0;
|
||||
foreach (string line in Format.AsTextLines(stateTable))
|
||||
{
|
||||
Console.WriteLine(FormatLine(
|
||||
column1: (lineNum++ == 0) ? "[ScopeState]" : null,
|
||||
column2: null,
|
||||
column3: state?.ToString() ?? "null"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(FormatLine(
|
||||
column1: "[ScopeState]",
|
||||
column2: null,
|
||||
column3: state?.ToString() ?? "null"));
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
Interlocked.Increment(ref _indentDepth);
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void EndScope(int scopeId)
|
||||
{
|
||||
Interlocked.Decrement(ref _indentDepth);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(FormatLine(
|
||||
column1: "[EndScope]",
|
||||
column2: $"}} ScopeId = {scopeId.ToString("X")}",
|
||||
column3: null));
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
Validate.NotNull(formatter, nameof(formatter));
|
||||
|
||||
string message = formatter(state, exception);
|
||||
|
||||
string column1 = $"[{logLevel.ToString()}]";
|
||||
|
||||
string column2 = $"Event={{Id={eventId.Id}, Name=\"{eventId.Name}\"";
|
||||
|
||||
string column3 = null;
|
||||
if (false == String.IsNullOrEmpty(message) && exception != null)
|
||||
{
|
||||
column3 = $"Message = \"{message}\",{TabString}Exception = \"{exception}\"";
|
||||
}
|
||||
else if (false == String.IsNullOrEmpty(message))
|
||||
{
|
||||
column3 = message;
|
||||
}
|
||||
else if (exception != null)
|
||||
{
|
||||
column3 = exception.ToString();
|
||||
}
|
||||
|
||||
string line = FormatLine(column1, column2, column3);
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
|
||||
private string FormatLine(string column1, string column2, string column3)
|
||||
{
|
||||
if (String.IsNullOrEmpty(column1) && String.IsNullOrEmpty(column2) && String.IsNullOrEmpty(column3))
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
var str = new StringBuilder();
|
||||
|
||||
str.Append(column1 ?? String.Empty);
|
||||
if (String.IsNullOrEmpty(column2) && String.IsNullOrEmpty(column3))
|
||||
{
|
||||
return str.ToString();
|
||||
}
|
||||
|
||||
while (str.Length < Column1EndOffs)
|
||||
{
|
||||
str.Append(" ");
|
||||
}
|
||||
|
||||
str.Append(column2 ?? String.Empty);
|
||||
if (String.IsNullOrEmpty(column3))
|
||||
{
|
||||
return str.ToString();
|
||||
}
|
||||
|
||||
while (str.Length < Column2EndOffs)
|
||||
{
|
||||
str.Append(" ");
|
||||
}
|
||||
|
||||
for (int i = 0; i < _indentDepth; i++)
|
||||
{
|
||||
str.Append(TabString);
|
||||
}
|
||||
|
||||
str.Append(column3);
|
||||
return str.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
internal static class Validate
|
||||
{
|
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using Microsoft.ApplicationInsights;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
public static class AvailabilityTest
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class Logging
|
||||
{
|
||||
internal static readonly Logging SingeltonInstance = new Logging();
|
||||
|
||||
private Logging() { }
|
||||
|
||||
public bool UseConsoleIfNoLoggerAvailable { get; set; }
|
||||
|
||||
internal ILogger CreateFallbackLogIfRequired(ILogger log)
|
||||
{
|
||||
if (log == null && this.UseConsoleIfNoLoggerAvailable)
|
||||
{
|
||||
return new MinimalConsoleLogger();
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static Logging Log { get { return AvailabilityTest.Logging.SingeltonInstance; } }
|
||||
|
||||
|
||||
internal static AvailabilityTestScope StartNew(IAvailabilityTestConfiguration testConfig,
|
||||
TelemetryConfiguration telemetryConfig,
|
||||
bool flushOnDispose,
|
||||
ILogger log)
|
||||
{
|
||||
return StartNew(testConfig, telemetryConfig, flushOnDispose, log, logScope: null);
|
||||
}
|
||||
|
||||
internal static AvailabilityTestScope StartNew(IAvailabilityTestConfiguration testConfig,
|
||||
TelemetryConfiguration telemetryConfig,
|
||||
bool flushOnDispose,
|
||||
ILogger log,
|
||||
object logScope)
|
||||
{
|
||||
Validate.NotNull(testConfig, nameof(testConfig));
|
||||
return StartNew(testConfig.TestDisplayName, testConfig.LocationDisplayName, testConfig.LocationId, telemetryConfig, flushOnDispose, log, logScope);
|
||||
}
|
||||
|
||||
public static AvailabilityTestScope StartNew(string testDisplayName,
|
||||
string locationDisplayName,
|
||||
TelemetryConfiguration telemetryConfig,
|
||||
bool flushOnDispose,
|
||||
ILogger log)
|
||||
|
||||
{
|
||||
string locationId = Format.LocationNameAsId(locationDisplayName);
|
||||
return StartNew(testDisplayName, locationDisplayName, locationId, telemetryConfig, flushOnDispose, log, logScope: null);
|
||||
}
|
||||
|
||||
public static AvailabilityTestScope StartNew(string testDisplayName,
|
||||
string locationDisplayName,
|
||||
string locationId,
|
||||
TelemetryConfiguration telemetryConfig,
|
||||
bool flushOnDispose,
|
||||
ILogger log)
|
||||
|
||||
{
|
||||
return StartNew(testDisplayName, locationDisplayName, locationId, telemetryConfig, flushOnDispose, log, logScope: null);
|
||||
}
|
||||
|
||||
public static AvailabilityTestScope StartNew(string testDisplayName,
|
||||
string locationDisplayName,
|
||||
string locationId,
|
||||
TelemetryConfiguration telemetryConfig,
|
||||
bool flushOnDispose,
|
||||
ILogger log,
|
||||
object logScope)
|
||||
|
||||
{
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
|
||||
var testScope = new AvailabilityTestScope(testDisplayName, locationDisplayName, locationId, telemetryConfig, flushOnDispose, log, logScope);
|
||||
testScope.Start();
|
||||
|
||||
return testScope;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
public class AvailabilityTestInfo : IAvailabilityTestConfiguration
|
||||
{
|
||||
[JsonProperty]
|
||||
public string TestDisplayName { get; private set; }
|
||||
|
||||
[JsonProperty]
|
||||
public string LocationDisplayName { get; private set; }
|
||||
|
||||
[JsonProperty]
|
||||
public string LocationId { get; private set; }
|
||||
|
||||
[JsonProperty]
|
||||
public DateTimeOffset StartTime { get; private set; }
|
||||
|
||||
[JsonProperty]
|
||||
public AvailabilityTelemetry DefaultAvailabilityResult { get; private set; }
|
||||
|
||||
public AvailabilityTestInfo()
|
||||
{
|
||||
this.TestDisplayName = null;
|
||||
this.LocationDisplayName = null;
|
||||
this.LocationId = null;
|
||||
this.StartTime = default;
|
||||
this.DefaultAvailabilityResult = null;
|
||||
}
|
||||
|
||||
|
||||
internal AvailabilityTestInfo(
|
||||
string testDisplayName,
|
||||
string locationDisplayName,
|
||||
string locationId,
|
||||
DateTimeOffset startTime,
|
||||
AvailabilityTelemetry defaultAvailabilityResult)
|
||||
{
|
||||
Validate.NotNullOrWhitespace(testDisplayName, nameof(testDisplayName));
|
||||
Validate.NotNullOrWhitespace(locationDisplayName, nameof(locationDisplayName));
|
||||
Validate.NotNullOrWhitespace(locationId, nameof(locationId));
|
||||
Validate.NotNull(defaultAvailabilityResult, nameof(defaultAvailabilityResult));
|
||||
|
||||
this.TestDisplayName = testDisplayName;
|
||||
this.LocationDisplayName = locationDisplayName;
|
||||
this.LocationId = locationId;
|
||||
this.StartTime = startTime;
|
||||
this.DefaultAvailabilityResult = defaultAvailabilityResult;
|
||||
}
|
||||
|
||||
internal bool IsInitialized()
|
||||
{
|
||||
return (this.DefaultAvailabilityResult != null);
|
||||
}
|
||||
|
||||
internal void CopyFrom(AvailabilityTestInfo availabilityTestInfo)
|
||||
{
|
||||
Validate.NotNull(availabilityTestInfo, nameof(availabilityTestInfo));
|
||||
|
||||
Validate.NotNullOrWhitespace(availabilityTestInfo.TestDisplayName, "availabilityTestInfo.TestDisplayName");
|
||||
Validate.NotNullOrWhitespace(availabilityTestInfo.LocationDisplayName, "availabilityTestInfo.LocationDisplayName");
|
||||
Validate.NotNullOrWhitespace(availabilityTestInfo.LocationId, "availabilityTestInfo.LocationId");
|
||||
Validate.NotNull(availabilityTestInfo.DefaultAvailabilityResult, "availabilityTestInfo.DefaultAvailabilityResult");
|
||||
|
||||
this.TestDisplayName = availabilityTestInfo.TestDisplayName;
|
||||
this.LocationDisplayName = availabilityTestInfo.LocationDisplayName;
|
||||
this.LocationId = availabilityTestInfo.LocationId;
|
||||
this.StartTime = availabilityTestInfo.StartTime;
|
||||
this.DefaultAvailabilityResult = availabilityTestInfo.DefaultAvailabilityResult;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,531 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using Microsoft.ApplicationInsights;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
public class AvailabilityTestScope : IDisposable
|
||||
{
|
||||
public enum Stage : Int32
|
||||
{
|
||||
New = 10,
|
||||
Started = 20,
|
||||
Completed = 30,
|
||||
SentResults = 40
|
||||
}
|
||||
|
||||
private const string DefaultResultMessage_NoError_Pass = "Passed: Availability Test completed normally and reported Success.";
|
||||
private const string DefaultResultMessage_NoError_Fail = "Failed: Availability Test completed normally and reported Failure.";
|
||||
private const string DefaultResultMessage_Error_Exception = "Error: Availability Test resulted in an exception.";
|
||||
private const string DefaultResultMessage_Error_Timeout = "Error: Availability Test timed out.";
|
||||
private const string DefaultResultMessage_NoResult_Disposed = "No Result: Availability Test was disposed, but no result was set. A Failure is assumed.";
|
||||
private const string DefaultResultMessage_NoResult_NotDisposed = "No Result: Availability Test was not disposed, and no result was set. A Failure is assumed.";
|
||||
|
||||
private const string PropertyName_WebtestLocationId = "WebtestLocationId";
|
||||
|
||||
private const string PropertyName_AssociatedException_Type = "AssociatedException.Type";
|
||||
private const string PropertyName_AssociatedException_Message = "AssociatedException.Message";
|
||||
private const string PropertyName_AssociatedException_IsTimeout = "AssociatedException.IsTimeout";
|
||||
|
||||
private const string PropertyName_AssociatedAvailabilityResult_Name = "AssociatedAvailabilityResult.Name";
|
||||
private const string PropertyName_AssociatedAvailabilityResult_RunLocation = "AssociatedAvailabilityResult.RunLocation";
|
||||
private const string PropertyName_AssociatedAvailabilityResult_Id = "AssociatedAvailabilityResult.Id";
|
||||
private const string PropertyName_AssociatedAvailabilityResult_IsTimeout = "AssociatedAvailabilityResult.IsTimeout";
|
||||
|
||||
private readonly string _instrumentationKey;
|
||||
private readonly TelemetryClient _telemetryClient;
|
||||
private readonly bool _flushOnDispose;
|
||||
private readonly ILogger _log;
|
||||
private readonly object _logScope;
|
||||
|
||||
private int _currentStage;
|
||||
|
||||
private Activity _activitySpan = null;
|
||||
private string _activitySpanId = null;
|
||||
private DateTimeOffset _startTime = default;
|
||||
private DateTimeOffset _endTime = default;
|
||||
private AvailabilityTelemetry _finalAvailabilityResult = null;
|
||||
|
||||
public AvailabilityTestScope.Stage CurrentStage { get { return (AvailabilityTestScope.Stage) _currentStage; } }
|
||||
|
||||
public string TestDisplayName { get; }
|
||||
|
||||
public string LocationDisplayName { get; }
|
||||
|
||||
public string LocationId { get; }
|
||||
|
||||
public AvailabilityTestScope(string testDisplayName, string locationDisplayName, string locationId, TelemetryConfiguration telemetryConfig, bool flushOnDispose, ILogger log, object logScope)
|
||||
{
|
||||
Validate.NotNullOrWhitespace(testDisplayName, nameof(testDisplayName));
|
||||
Validate.NotNullOrWhitespace(locationDisplayName, nameof(locationDisplayName));
|
||||
Validate.NotNullOrWhitespace(locationId, nameof(locationId));
|
||||
Validate.NotNull(telemetryConfig, nameof(telemetryConfig));
|
||||
|
||||
this.TestDisplayName = testDisplayName;
|
||||
this.LocationDisplayName = locationDisplayName;
|
||||
this.LocationId = locationId;
|
||||
|
||||
_instrumentationKey = telemetryConfig.InstrumentationKey;
|
||||
_telemetryClient = new TelemetryClient(telemetryConfig);
|
||||
|
||||
_flushOnDispose = flushOnDispose;
|
||||
|
||||
_log = log;
|
||||
_logScope = logScope;
|
||||
|
||||
_currentStage = (int) Stage.New;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
using (_log.BeginScopeSafe(_logScope))
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Start)} beginning:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId);
|
||||
|
||||
TransitionStage(from: Stage.New, to: Stage.Started);
|
||||
|
||||
// Start activity:
|
||||
string activityOperationName = Format.SpanOperationName(TestDisplayName, LocationDisplayName);
|
||||
_activitySpan = new Activity(activityOperationName).Start();
|
||||
_activitySpanId = Format.SpanId(_activitySpan);
|
||||
|
||||
// Start the timer:
|
||||
_startTime = DateTimeOffset.Now;
|
||||
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Start)} finished:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\", StartTime=\"{StartTime}\", OperationName=\"{OperationName}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
_activitySpanId, _startTime.ToString("o"), activityOperationName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Complete(bool success)
|
||||
{
|
||||
using (_log.BeginScopeSafe(_logScope))
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} invoked with Success={{Success}}:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\"}}",
|
||||
success,
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId));
|
||||
|
||||
EnsureStage(Stage.Started);
|
||||
|
||||
AvailabilityTelemetry availabilityResult = CreateDefaultAvailabilityResult();
|
||||
availabilityResult.Success = success;
|
||||
|
||||
availabilityResult.Message = success
|
||||
? DefaultResultMessage_NoError_Pass
|
||||
: DefaultResultMessage_NoError_Fail;
|
||||
|
||||
Complete(availabilityResult);
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteOnDisposeOrFinalize(bool disposing)
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(CompleteOnDisposeOrFinalize)} invoked with Disposing={{Disposing}}:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\"}}."
|
||||
+ " This indicates that the test result was not set by calling Complete(..); a Failure will be assumed.",
|
||||
disposing,
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId));
|
||||
|
||||
EnsureStage(Stage.Started);
|
||||
|
||||
AvailabilityTelemetry availabilityResult = CreateDefaultAvailabilityResult();
|
||||
availabilityResult.Success = false;
|
||||
|
||||
availabilityResult.Message = disposing
|
||||
? DefaultResultMessage_NoResult_Disposed
|
||||
: DefaultResultMessage_NoResult_NotDisposed;
|
||||
|
||||
Complete(availabilityResult);
|
||||
}
|
||||
|
||||
public void Complete(Exception error)
|
||||
{
|
||||
Complete(error, isTimeout: false);
|
||||
}
|
||||
|
||||
public void Complete(Exception error, bool isTimeout)
|
||||
{
|
||||
using (_log.BeginScopeSafe(_logScope))
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} invoked with"
|
||||
+ " (ExceptionType={ExceptionType}, ExceptionMessage=\"{ExceptionMessage}\", IsTimeout={IsTimeout}):"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\"}}",
|
||||
Format.SpellIfNull(error?.GetType()?.Name), Format.LimitLength(error.Message, 100, trim: true), isTimeout,
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId));
|
||||
|
||||
EnsureStage(Stage.Started);
|
||||
|
||||
AvailabilityTelemetry availabilityResult = CreateDefaultAvailabilityResult();
|
||||
availabilityResult.Success = false;
|
||||
|
||||
availabilityResult.Message = isTimeout
|
||||
? DefaultResultMessage_Error_Timeout
|
||||
: DefaultResultMessage_Error_Exception;
|
||||
|
||||
availabilityResult.Properties[PropertyName_AssociatedException_Type] = Format.SpellIfNull(error?.GetType()?.Name);
|
||||
availabilityResult.Properties[PropertyName_AssociatedException_Message] = Format.SpellIfNull(error?.Message);
|
||||
availabilityResult.Properties[PropertyName_AssociatedException_IsTimeout] = isTimeout.ToString();
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
error.Data[PropertyName_AssociatedAvailabilityResult_Name] = availabilityResult.Name;
|
||||
error.Data[PropertyName_AssociatedAvailabilityResult_RunLocation] = availabilityResult.RunLocation;
|
||||
error.Data[PropertyName_AssociatedAvailabilityResult_Id] = availabilityResult.Id;
|
||||
error.Data[PropertyName_AssociatedAvailabilityResult_IsTimeout] = isTimeout.ToString();
|
||||
}
|
||||
|
||||
Complete(availabilityResult);
|
||||
}
|
||||
}
|
||||
|
||||
public void Complete(AvailabilityTelemetry availabilityResult)
|
||||
{
|
||||
using (_log.BeginScopeSafe(_logScope))
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} beginning:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId));
|
||||
|
||||
Validate.NotNull(availabilityResult, nameof(availabilityResult));
|
||||
|
||||
TransitionStage(from: Stage.Started, to: Stage.Completed);
|
||||
|
||||
// Stop the timer:
|
||||
_endTime = DateTimeOffset.Now;
|
||||
|
||||
// Stop activity:
|
||||
_activitySpan.Stop();
|
||||
|
||||
// Examine several properties of the Availability Result.
|
||||
// If the user set them, use the user's value. Otherwise, initialize appropriately:
|
||||
|
||||
if (String.IsNullOrWhiteSpace(availabilityResult.Message))
|
||||
{
|
||||
availabilityResult.Message = availabilityResult.Success
|
||||
? DefaultResultMessage_NoError_Pass
|
||||
: DefaultResultMessage_NoError_Fail;
|
||||
}
|
||||
|
||||
if (availabilityResult.Timestamp == default(DateTimeOffset))
|
||||
{
|
||||
availabilityResult.Timestamp = _startTime;
|
||||
}
|
||||
else if (availabilityResult.Timestamp.ToUniversalTime() != _startTime.ToUniversalTime())
|
||||
{
|
||||
_log?.LogDebug($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} (SpanId=\"{{SpanId}}\") detected that the Timestamp of the"
|
||||
+ $" specified Availability Result is different from the corresponding value of this {nameof(AvailabilityTestScope)}."
|
||||
+ $" The value specified in the Availability Result takes precedence for tracking."
|
||||
+ " AvailabilityTestScope_StartTime=\"{AvailabilityTestScope_StartTime}\". AvailabilityResult_Timestamp=\"{AvailabilityResult_Timestamp}\"",
|
||||
_activitySpanId, _startTime.ToUniversalTime().ToString("o"), availabilityResult.Timestamp.ToUniversalTime().ToString("o"));
|
||||
}
|
||||
|
||||
TimeSpan duration = _endTime - availabilityResult.Timestamp;
|
||||
|
||||
if (availabilityResult.Duration == TimeSpan.Zero)
|
||||
{
|
||||
availabilityResult.Duration = duration;
|
||||
}
|
||||
else if (availabilityResult.Duration != duration)
|
||||
{
|
||||
_log?.LogDebug($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} (SpanId=\"{{SpanId}}\") detected that the Duration of the"
|
||||
+ $" specified Availability Result is different from the corresponding value of this {nameof(AvailabilityTestScope)}."
|
||||
+ $" The value specified in the Availability Result takes precedence for tracking."
|
||||
+ " AvailabilityTestScope_Duration=\"{AvailabilityTestScope_Duration}\". AvailabilityResult_Duration=\"{AvailabilityResult_Duration}\"",
|
||||
_activitySpanId, duration, availabilityResult.Duration);
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(availabilityResult.Name))
|
||||
{
|
||||
availabilityResult.Name = TestDisplayName;
|
||||
}
|
||||
else if (! availabilityResult.Name.Equals(TestDisplayName, StringComparison.Ordinal))
|
||||
{
|
||||
_log?.LogDebug($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} (SpanId=\"{{SpanId}}\") detected that the Name of the"
|
||||
+ $" specified Availability Result is different from the corresponding value of this {nameof(AvailabilityTestScope)}."
|
||||
+ $" The value specified in the Availability Result takes precedence for tracking."
|
||||
+ " AvailabilityTestScopeTestDisplayName=\"{AvailabilityTestScope_TestDisplayName}\". AvailabilityResult_Name=\"{AvailabilityResult_Name}\"",
|
||||
_activitySpanId, TestDisplayName, availabilityResult.Name);
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(availabilityResult.RunLocation))
|
||||
{
|
||||
availabilityResult.RunLocation = LocationDisplayName;
|
||||
}
|
||||
else if (! availabilityResult.RunLocation.Equals(LocationDisplayName, StringComparison.Ordinal))
|
||||
{
|
||||
_log?.LogDebug($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} (SpanId=\"{{SpanId}}\") detected that the RunLocation of the"
|
||||
+ $" specified Availability Result is different from the corresponding value of this {nameof(AvailabilityTestScope)}."
|
||||
+ $" The value specified in the Availability Result takes precedence for tracking."
|
||||
+ " AvailabilityTestScope_LocationDisplayName=\"{AvailabilityTestScope_LocationDisplayName}\". AvailabilityResult_RunLocation=\"{AvailabilityResult_RunLocation}\"",
|
||||
_activitySpanId, LocationDisplayName, availabilityResult.RunLocation);
|
||||
}
|
||||
|
||||
if (! availabilityResult.Properties.TryGetValue(PropertyName_WebtestLocationId, out string availabilityResultLocationId))
|
||||
{
|
||||
availabilityResultLocationId = null;
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(availabilityResultLocationId))
|
||||
{
|
||||
availabilityResult.Properties[PropertyName_WebtestLocationId] = LocationId;
|
||||
}
|
||||
else if (! availabilityResultLocationId.Equals(LocationId, StringComparison.Ordinal))
|
||||
{
|
||||
_log?.LogDebug($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} (SpanId=\"{{SpanId}}\") detected that the WebtestLocationId of the"
|
||||
+ $" specified Availability Result is different from the corresponding value of this {nameof(AvailabilityTestScope)}."
|
||||
+ $" The value specified in the Availability Result takes precedence for tracking."
|
||||
+ " AvailabilityTestScope_LocationId=\"{AvailabilityTestScope_LocationId}\". AvailabilityResult_WebtestLocationId=\"{AvailabilityResult_WebtestLocationId}\"",
|
||||
_activitySpanId, LocationId, availabilityResultLocationId);
|
||||
}
|
||||
|
||||
// The user may or may not have set the ID of the availability result telemetry.
|
||||
// Either way, we myst set it to the right value, otherwise distributed tracing will break:
|
||||
availabilityResult.Id = _activitySpanId;
|
||||
|
||||
// Store the result, but do not send it until SendResult() is called:
|
||||
_finalAvailabilityResult = availabilityResult;
|
||||
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} finished"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\", StartTime=\"{StartTime}\", EndTime=\"{EndTime}\", Duration=\"{Duration}\", Success=\"{Success}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
_activitySpanId, _startTime.ToString("o"), _endTime.ToString("o"), duration, availabilityResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public AvailabilityTestInfo CreateAvailabilityTestInfo()
|
||||
{
|
||||
AvailabilityTelemetry defaultAvailabilityResult = CreateDefaultAvailabilityResult();
|
||||
var testInfo = new AvailabilityTestInfo(TestDisplayName, LocationDisplayName, LocationId, _startTime, defaultAvailabilityResult);
|
||||
return testInfo;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~AvailabilityTestScope()
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
using (_log.BeginScopeSafe(_logScope))
|
||||
{
|
||||
_log?.LogError(ex,
|
||||
$"{nameof(AvailabilityTestScope)} finalizer threw an exception:"
|
||||
+ " {{SpanId=\"{SpanId}\", ExceptionType=\"{ExceptionType}\", ExceptionMessage=\"{ExceptionMessage}\"}}",
|
||||
Format.SpellIfNull(_activitySpanId), ex.GetType().Name, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We are on the Finalizer thread, so the user has no chance of catching an exception.
|
||||
// We make our best attempt at logging it and then swallow it to avoid tearing down the application.
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
using (_log.BeginScopeSafe(_logScope))
|
||||
{
|
||||
Stage stage = CurrentStage;
|
||||
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Dispose)} beginning:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", Disposing=\"{Disposing}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId), stage, disposing);
|
||||
|
||||
switch (stage)
|
||||
{
|
||||
case Stage.New:
|
||||
break;
|
||||
|
||||
case Stage.Started:
|
||||
CompleteOnDisposeOrFinalize(disposing);
|
||||
SendResult();
|
||||
FlushIfRequested();
|
||||
break;
|
||||
|
||||
case Stage.Completed:
|
||||
SendResult();
|
||||
FlushIfRequested();
|
||||
break;
|
||||
|
||||
case Stage.SentResults:
|
||||
FlushIfRequested();
|
||||
break;
|
||||
}
|
||||
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Dispose)} finished:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", Disposing=\"{Disposing}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId), CurrentStage, disposing);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendResult()
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(SendResult)} beginning:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId));
|
||||
|
||||
TransitionStage(from: Stage.Completed, to: Stage.SentResults);
|
||||
|
||||
AvailabilityTelemetry availabilityResult = _finalAvailabilityResult;
|
||||
if (availabilityResult == null)
|
||||
{
|
||||
throw new InvalidOperationException($"This {nameof(AvailabilityTestScope)} was in the {Stage.Completed}-stage,"
|
||||
+ $" but Final Availability Result was not initialized. This indicated that"
|
||||
+ $" this {nameof(AvailabilityTestScope)} may be used from multiple threads."
|
||||
+ $" That is currently not supported.");
|
||||
}
|
||||
|
||||
_telemetryClient.TrackAvailability(availabilityResult);
|
||||
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(SendResult)} finished:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId));
|
||||
}
|
||||
|
||||
private void FlushIfRequested()
|
||||
{
|
||||
if (_flushOnDispose)
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)} is flushing its {nameof(TelemetryClient)}:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", FlushOnDispose=\"{FlushOnDispose}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId), CurrentStage, _flushOnDispose);
|
||||
|
||||
_telemetryClient.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)} is NOT flushing its {nameof(TelemetryClient)}:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", LocationDisplayName=\"{LocationDisplayName}\", LocationId=\"{LocationId}\","
|
||||
+ " SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", FlushOnDispose=\"{FlushOnDispose}\"}}",
|
||||
TestDisplayName, LocationDisplayName, LocationId,
|
||||
Format.SpellIfNull(_activitySpanId), CurrentStage, _flushOnDispose);
|
||||
}
|
||||
}
|
||||
|
||||
private AvailabilityTelemetry CreateDefaultAvailabilityResult()
|
||||
{
|
||||
// We cannot create a default result if we already Completed. We should use the actual (final) result then.
|
||||
EnsureStage(Stage.New, Stage.Started);
|
||||
|
||||
//const string mockApplicationInsightsAppId = "00000000-0000-0000-0000-000000000000";
|
||||
//const string mockApplicationInsightsArmResourceName = "Application-Insights-Component";
|
||||
|
||||
// Note: this method is not thread-safe in respect to Stage transitions
|
||||
// (e.g. we may have just transitioned into Strated, but not yet set the start time).
|
||||
|
||||
var availabilityResult = new AvailabilityTelemetry();
|
||||
|
||||
if (CurrentStage == Stage.New)
|
||||
{
|
||||
availabilityResult.Timestamp = default(DateTimeOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
availabilityResult.Timestamp = _startTime.ToUniversalTime();
|
||||
availabilityResult.Id = Format.SpanId(_activitySpan);
|
||||
}
|
||||
|
||||
availabilityResult.Duration = TimeSpan.Zero;
|
||||
availabilityResult.Success = false;
|
||||
|
||||
availabilityResult.Name = TestDisplayName;
|
||||
availabilityResult.RunLocation = LocationDisplayName;
|
||||
availabilityResult.Properties["WebtestLocationId"] = this.LocationId;
|
||||
|
||||
//availabilityResult.Properties["SyntheticMonitorId"] = $"default_{this.TestArmResourceName}_{this.LocationId}";
|
||||
//availabilityResult.Properties["WebtestArmResourceName"] = this.TestArmResourceName;
|
||||
|
||||
//availabilityResult.Properties["SourceId"] = $"sid://{mockApplicationInsightsAppId}.visualstudio.com"
|
||||
// + $"/applications/{mockApplicationInsightsArmResourceName}"
|
||||
// + $"/features/{this.TestArmResourceName}"
|
||||
// + $"/locations/{this.LocationId}";
|
||||
|
||||
if (! String.IsNullOrWhiteSpace(_instrumentationKey))
|
||||
{
|
||||
availabilityResult.Context.InstrumentationKey = _instrumentationKey;
|
||||
}
|
||||
|
||||
return availabilityResult;
|
||||
}
|
||||
|
||||
private void TransitionStage(AvailabilityTestScope.Stage from, AvailabilityTestScope.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(AvailabilityTestScope)}.{nameof(CurrentStage)}"
|
||||
+ $" to \'{to}\' (={toStage}): Previous {nameof(CurrentStage)} was expected to"
|
||||
+ $" be \'{from}\' (={fromStage}), but it was actually \'{((Stage) prevStage)}\' (={prevStage}).");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureStage(AvailabilityTestScope.Stage required)
|
||||
{
|
||||
int requiredStage = (int) required;
|
||||
int currStage = _currentStage;
|
||||
|
||||
if (currStage != requiredStage)
|
||||
{
|
||||
throw new InvalidOperationException($"For this operation {nameof(AvailabilityTestScope)}.{nameof(CurrentStage)}"
|
||||
+ $" is required to be \'{required}\' (={requiredStage}),"
|
||||
+ $" but it is actually \'{((Stage) currStage)}\' (={currStage}).");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureStage(AvailabilityTestScope.Stage requiredA, AvailabilityTestScope.Stage requiredB)
|
||||
{
|
||||
int requiredStageA = (int) requiredA, requiredStageB = (int) requiredB;
|
||||
int currStage = _currentStage;
|
||||
|
||||
if (currStage != requiredStageA && currStage != requiredStageB)
|
||||
{
|
||||
throw new InvalidOperationException($"For this operation {nameof(AvailabilityTestScope)}.{nameof(CurrentStage)}"
|
||||
+ $" is required to be \'{requiredA}\' (={requiredStageA}) or \'{requiredB}\' (={requiredStageB}),"
|
||||
+ $" but it is actually \'{((Stage) currStage)}\' (={currStage}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
using System;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.Extensions
|
||||
{
|
||||
public static class AvailabilityMonitoringWebJobsBuilderExtensions
|
||||
{
|
||||
|
@ -11,15 +12,20 @@ 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<INameResolver, AvailabilityMonitoringNameResolver>();
|
||||
//serviceCollection.AddSingleton<ITelemetryInitializer, AvailabilityMonitoringTelemetryInitializer>();
|
||||
|
||||
serviceCollection.AddSingleton<AvailabilityTestRegistry>();
|
||||
|
||||
// Type 'IFunctionFilter' (and other Filter-related types) is marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
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>();
|
||||
#pragma warning restore CS0618
|
||||
|
||||
builder.AddExtension<AvailabilityMonitoringExtensionConfigProvider>();
|
||||
return builder;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,29 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- @ToDo Need to remove this later! -->
|
||||
<SkipFunctionsDepsCopy>true</SkipFunctionsDepsCopy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="Version.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<Authors>Microsoft Application Insights</Authors>
|
||||
<Company>Microsoft</Company>
|
||||
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Product>Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring</Product>
|
||||
<RepositoryUrl>https://github.com/Azure/azure-functions-availability-monitoring-extension</RepositoryUrl>
|
||||
<PackageTags>Microsoft Azure WebJobs AzureFunctions AzureMonitor CodedAvailabilityTest AvailabilityTest Synthetic SyntheticMonitor SyntheticTest Application Insights ApplicationInsights Availability</PackageTags>
|
||||
<Description></Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.6" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Logging.ApplicationInsights" Version="3.0.14" />
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
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
|
||||
{
|
||||
public AvailabilityMonitoringExtensionConfigProvider()
|
||||
{
|
||||
}
|
||||
|
||||
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.BindToInput<AvailabilityTestInfo>(CreateAvailabilityTestInvocation);
|
||||
rule.BindToInput<AvailabilityTelemetry>(CreateAvailabilityTelemetry);
|
||||
rule.BindToInput<JObject>(CreateJObject);
|
||||
}
|
||||
|
||||
private static Task<AvailabilityTestInfo> CreateAvailabilityTestInvocation(AvailabilityTestAttribute attribute, ValueBindingContext context)
|
||||
{
|
||||
Validate.NotNull(attribute, nameof(attribute));
|
||||
Validate.NotNull(context, nameof(context));
|
||||
|
||||
AvailabilityTestInfo invocationInfo = CreateAndRegisterInvocation(attribute, context.FunctionInstanceId, typeof(AvailabilityTestInfo));
|
||||
return Task.FromResult(invocationInfo);
|
||||
}
|
||||
|
||||
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,17 +0,0 @@
|
|||
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; }
|
||||
|
||||
}
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
public class AvailabilityTestInfo
|
||||
{
|
||||
[JsonProperty]
|
||||
public string TestDisplayName { get; }
|
||||
|
||||
[JsonProperty]
|
||||
public string TestArmResourceName { get; }
|
||||
|
||||
[JsonProperty]
|
||||
public string LocationDisplayName { get; }
|
||||
|
||||
[JsonProperty]
|
||||
public string LocationId { get; }
|
||||
|
||||
[JsonProperty]
|
||||
public DateTimeOffset StartTime { get; private set; }
|
||||
|
||||
[JsonProperty]
|
||||
public AvailabilityTelemetry AvailabilityResult { get; }
|
||||
|
||||
[JsonProperty]
|
||||
internal Guid Identity { get; }
|
||||
|
||||
public AvailabilityTestInfo(
|
||||
string testDisplayName,
|
||||
string testArmResourceName,
|
||||
string locationDisplayName,
|
||||
string locationId)
|
||||
{
|
||||
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 = default(DateTimeOffset);
|
||||
|
||||
this.AvailabilityResult = CreateNewAvailabilityResult();
|
||||
|
||||
this.Identity = Guid.NewGuid();
|
||||
}
|
||||
|
||||
public AvailabilityTestInfo(AvailabilityTelemetry availabilityResult)
|
||||
: this(Convert.NotNullOrWord(availabilityResult?.Name),
|
||||
Convert.GetPropertyOrNullWord(availabilityResult, "WebtestArmResourceName"),
|
||||
Convert.NotNullOrWord(availabilityResult?.RunLocation),
|
||||
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 AvailabilityTestInfo(
|
||||
string testDisplayName,
|
||||
string testArmResourceName,
|
||||
string locationDisplayName,
|
||||
string locationId,
|
||||
Guid identity,
|
||||
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.Identity = identity;
|
||||
this.StartTime = startTime;
|
||||
this.AvailabilityResult = availabilityResult;
|
||||
}
|
||||
|
||||
internal void SetStartTime (DateTimeOffset startTime)
|
||||
{
|
||||
this.StartTime = startTime;
|
||||
this.AvailabilityResult.Timestamp = startTime.ToUniversalTime();
|
||||
}
|
||||
|
||||
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["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}";
|
||||
|
||||
OutputTelemetryFormat.AddAvailabilityTestInfoIdentity(availabilityResult, Identity);
|
||||
|
||||
return availabilityResult;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
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 object NotNullOrWord(object 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(AvailabilityTestInfo availabilityTestInvocation)
|
||||
{
|
||||
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
|
||||
JObject jObject = JObject.FromObject(availabilityTestInvocation);
|
||||
return jObject;
|
||||
}
|
||||
|
||||
public static AvailabilityTestInfo JObjectToAvailabilityTestInvocation(JObject availabilityTestInvocation)
|
||||
{
|
||||
Validate.NotNull(availabilityTestInvocation, nameof(availabilityTestInvocation));
|
||||
|
||||
try
|
||||
{
|
||||
AvailabilityTestInfo stronglyTypedTestInvocation = availabilityTestInvocation.ToObject<AvailabilityTestInfo>();
|
||||
return stronglyTypedTestInvocation;
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 AvailabilityTestInfo AvailabilityTelemetryToAvailabilityTestInvocation(AvailabilityTelemetry availabilityResult)
|
||||
{
|
||||
Validate.NotNull(availabilityResult, nameof(availabilityResult));
|
||||
return new AvailabilityTestInfo(availabilityResult);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,354 +0,0 @@
|
|||
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}\".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
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}).");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
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,177 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Config;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
[Extension("AvailabilityMonitoring")]
|
||||
internal class AvailabilityMonitoringExtensionConfigProvider : IExtensionConfigProvider
|
||||
{
|
||||
|
||||
private readonly ILogger _log;
|
||||
private readonly AvailabilityTestRegistry _availabilityTestRegistry;
|
||||
|
||||
public AvailabilityMonitoringExtensionConfigProvider(AvailabilityTestRegistry availabilityTestRegistry, ILoggerFactory loggerFactory)
|
||||
{
|
||||
Validate.NotNull(availabilityTestRegistry, nameof(availabilityTestRegistry));
|
||||
Validate.NotNull(loggerFactory, nameof(loggerFactory));
|
||||
|
||||
_availabilityTestRegistry = availabilityTestRegistry;
|
||||
_log = loggerFactory.CreateLogger(LogMonikers.Categories.Extension);
|
||||
}
|
||||
|
||||
public void Initialize(ExtensionConfigContext extensionConfigContext)
|
||||
{
|
||||
_log?.LogInformation("Availability Monitoring Extension is initializing:"
|
||||
+ " {{Version=\"{Version}\"}}",
|
||||
this.GetType().Assembly.GetName().Version);
|
||||
|
||||
Validate.NotNull(extensionConfigContext, nameof(extensionConfigContext));
|
||||
|
||||
// A Coded Availablity Test is defined as such by returning a value that is bound by AvailabilityTestResult-Attribute.
|
||||
// A paramater bound by AvailabilityTestInfo-Attribute is optional.
|
||||
// Such parameter can be used to programmatically get information about the current availablity test, or it can be omitted.
|
||||
|
||||
// FluentBindingRule<T> 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 by disabling it.
|
||||
#pragma warning disable CS0618
|
||||
FluentBindingRule<AvailabilityTestResultAttribute> testResultRule = extensionConfigContext.AddBindingRule<AvailabilityTestResultAttribute>();
|
||||
FluentBindingRule<AvailabilityTestInfoAttribute> testInfoRule = extensionConfigContext.AddBindingRule<AvailabilityTestInfoAttribute>();
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// This binding is used to get and process the return value of the function:
|
||||
testResultRule.BindToCollector<AvailabilityTestResultAttribute, AvailabilityTelemetry>(CreateAvailabilityTelemetryAsyncCollector);
|
||||
testResultRule.BindToCollector<AvailabilityTestResultAttribute, bool>(CreateBoolAsyncCollector);
|
||||
extensionConfigContext.AddConverter<string, AvailabilityTelemetry>(Convert.StringToAvailabilityTelemetry);
|
||||
|
||||
// This is an optional In-parameter that allows user code to get runtime info about the availablity test:
|
||||
testInfoRule.BindToInput<AvailabilityTestInfo>(CreateAvailabilityTestInfo);
|
||||
extensionConfigContext.AddConverter<AvailabilityTestInfo, string>(Convert.AvailabilityTestInfoToString);
|
||||
extensionConfigContext.AddConverter<AvailabilityTestInfo, AvailabilityTelemetry>(Convert.AvailabilityTestInfoToAvailabilityTelemetry);
|
||||
}
|
||||
|
||||
private Task<IAsyncCollector<AvailabilityTelemetry>> CreateAvailabilityTelemetryAsyncCollector(AvailabilityTestResultAttribute attribute, ValueBindingContext valueBindingContext)
|
||||
{
|
||||
AvailabilityResultAsyncCollector resultCollector = CreateAvailabilityResultAsyncCollector(attribute, valueBindingContext);
|
||||
return Task.FromResult<IAsyncCollector<AvailabilityTelemetry>>(resultCollector);
|
||||
}
|
||||
|
||||
private Task<IAsyncCollector<bool>> CreateBoolAsyncCollector(AvailabilityTestResultAttribute attribute, ValueBindingContext valueBindingContext)
|
||||
{
|
||||
AvailabilityResultAsyncCollector resultCollector = CreateAvailabilityResultAsyncCollector(attribute, valueBindingContext);
|
||||
return Task.FromResult<IAsyncCollector<bool>>(resultCollector);
|
||||
}
|
||||
|
||||
private AvailabilityResultAsyncCollector CreateAvailabilityResultAsyncCollector(AvailabilityTestResultAttribute attribute, ValueBindingContext valueBindingContext)
|
||||
{
|
||||
// A function is defined as an Availability Test iff is has a return value marked with [AvailabilityTestResult].
|
||||
// If that is the case, this method will be invoked as some point to construct a collector for the return value.
|
||||
// Depending on the kind of the function, this will happen in different ways:
|
||||
//
|
||||
// - For .Net functions (in-proc), this method runs BEFORE function filters:
|
||||
// a) We will register this function as an Availability Test in the Functions registry (this is a NoOp for all,
|
||||
// except the very first invocation).
|
||||
// b) We will create new invocation state bag and register it with the Invocations registry.
|
||||
// c) We will instantiate a result collector and attach it to the state bag.
|
||||
// d) Later on, BEFORE the function body runs, the runtime execute the pre-function filter. At that point we will:
|
||||
// ~ Initialize an Availablity Test Scope and attach it to the invocation state bag;
|
||||
// ~ Link the results collector and the test scope.
|
||||
// e) Subsequently, AFTER the function body runs the result will be set in one of two ways:
|
||||
// ~ If no error: the runtime will add the return value to the result collector -> the collector will Complete the Test Scope;
|
||||
// ~ If error/exception: the runtime will invoke the post-function filter -> the filter will Complete the Test Scope.
|
||||
//
|
||||
// - For non-.Net functions (out-of-proc), this method runs AFTER function filters (and, potentially, even AFTER the function body has completed):
|
||||
// a) Registering this function as an Availability Test in the Functions registry will be a NoOp.
|
||||
// b) We will receive an existing invocation state bag; the Availablity Test Scope will be already set in the state bag.
|
||||
// c&d) We will instantiate a result collector and link it with the test scope right away; we will attach the collector to the state bag.
|
||||
// e) The results will be set in a simillar manner as for .Net described above.
|
||||
|
||||
Validate.NotNull(attribute, nameof(attribute));
|
||||
Validate.NotNull(valueBindingContext, nameof(valueBindingContext));
|
||||
|
||||
string functionName = valueBindingContext.FunctionContext.MethodName;
|
||||
|
||||
using (_log.BeginScope(LogMonikers.Scopes.CreateForTestInvocation(functionName)))
|
||||
{
|
||||
// Register this Function as an Availability Test (NoOp for all invocations of this method, except the very first one):
|
||||
_availabilityTestRegistry.Functions.Register(functionName, attribute, _log);
|
||||
|
||||
// Register this particular invocation of this function:
|
||||
Guid functionInstanceId = valueBindingContext.FunctionInstanceId;
|
||||
AvailabilityTestInvocationState invocationState = _availabilityTestRegistry.Invocations.GetOrRegister(functionInstanceId, _log);
|
||||
|
||||
// Create the result collector:
|
||||
var resultCollector = new AvailabilityResultAsyncCollector();
|
||||
|
||||
// If the test scope is already set (out-of-proc function), then link it with the collector:
|
||||
bool isTestScopeInitialized = invocationState.TryGetTestScope(out AvailabilityTestScope testScope);
|
||||
if (isTestScopeInitialized)
|
||||
{
|
||||
resultCollector.Initialize(testScope);
|
||||
}
|
||||
|
||||
// Attache the collector to the invocation state bag:
|
||||
invocationState.AttachResultCollector(resultCollector);
|
||||
|
||||
// Done:
|
||||
return resultCollector;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<AvailabilityTestInfo> CreateAvailabilityTestInfo(AvailabilityTestInfoAttribute attribute, ValueBindingContext valueBindingContext)
|
||||
{
|
||||
// A function is an Availability Test iff is has a return value marked with [AvailabilityTestResult];
|
||||
// whereas a [AvailabilityTestInfo] is OPTIONAL to get test information at runtime.
|
||||
// User could have marked a parameter with [AvailabilityTestInfo] but no return value with [AvailabilityTestResult]:
|
||||
// That does not make sense, but we need to do something graceful.
|
||||
// There is no telling what will run first: this method, or CreateAvailabilityTelemetryAsyncCollector(..) above.
|
||||
// From here we cannot call _availabilityTestRegistry.Functions.Register(..), becasue the attribute type we get
|
||||
// here does not contain any configuration.
|
||||
// We will attach a raw test info object to this invocation.
|
||||
// If a test-RESULT-attribute is attached to this function later, it will supply configuration eventually.
|
||||
// If not, the test info will remain raw and we must remember to clear the invocation from the registry in the post-function filter.
|
||||
|
||||
Validate.NotNull(attribute, nameof(attribute));
|
||||
Validate.NotNull(valueBindingContext, nameof(valueBindingContext));
|
||||
|
||||
string functionName = valueBindingContext.FunctionContext.MethodName;
|
||||
|
||||
using (_log.BeginScope(LogMonikers.Scopes.CreateForTestInvocation(functionName)))
|
||||
{
|
||||
// Register this particular invocation of this function:
|
||||
Guid functionInstanceId = valueBindingContext.FunctionInstanceId;
|
||||
AvailabilityTestInvocationState invocationState = _availabilityTestRegistry.Invocations.GetOrRegister(functionInstanceId, _log);
|
||||
|
||||
// Create the test info:
|
||||
var testInfo = new AvailabilityTestInfo();
|
||||
|
||||
// If the test scope is already set (out-of-proc function), then use it to initialize the test info:
|
||||
bool isTestScopeInitialized = invocationState.TryGetTestScope(out AvailabilityTestScope testScope);
|
||||
if (isTestScopeInitialized)
|
||||
{
|
||||
testInfo.CopyFrom(testScope.CreateAvailabilityTestInfo());
|
||||
}
|
||||
|
||||
// Attach the test info to the invocation state bag:
|
||||
invocationState.AttachTestInfo(testInfo);
|
||||
|
||||
// Done:
|
||||
return Task.FromResult(testInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
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 AvailabilityMonitoringNameResolver : INameResolver
|
||||
{
|
||||
private readonly INameResolver _defaultNameResolver;
|
||||
|
||||
public AvailabilityMonitoringNameResolver(IConfiguration config)
|
||||
{
|
||||
if (config != null)
|
||||
{
|
||||
_defaultNameResolver = new DefaultNameResolver(config);
|
||||
}
|
||||
}
|
||||
|
||||
public string Resolve(string name)
|
||||
{
|
||||
// If this is a Availability Test Interval specification (has the right prefix), then resolve it:
|
||||
if (AvailabilityTestInterval.IsSpecification(name))
|
||||
{
|
||||
return ResolveAvailabilityTestInterval(name);
|
||||
}
|
||||
|
||||
// If we have a default ame resolver, use it:
|
||||
if (_defaultNameResolver != null)
|
||||
{
|
||||
return _defaultNameResolver.Resolve(name);
|
||||
}
|
||||
|
||||
// Do nothing:
|
||||
return name;
|
||||
}
|
||||
|
||||
private string ResolveAvailabilityTestInterval(string testIntervalSpec)
|
||||
{
|
||||
int minuteInterval = AvailabilityTestInterval.Parse(testIntervalSpec);
|
||||
|
||||
string cronSpec = AvailabilityTestInterval.CreateCronIntervalSpecWithRandomOffset(minuteInterval);
|
||||
return cronSpec;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using Microsoft.ApplicationInsights.Channel;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityMonitoringTelemetryInitializer : ITelemetryInitializer
|
||||
{
|
||||
public void Initialize(ITelemetry telemetryItem)
|
||||
{
|
||||
//Console.WriteLine();
|
||||
|
||||
//const string prefix = "*********** Availability Monitoring Telemetry Processor";
|
||||
//Console.WriteLine($"{prefix}: Item type: {Format.SpellIfNull(telemetryItem?.GetType()?.Name)}");
|
||||
|
||||
//if (telemetryItem == null)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//Console.WriteLine($"{prefix}: Operation.Id: {Format.SpellIfNull(telemetryItem.Context.Operation.Id)}");
|
||||
//Console.WriteLine($"{prefix}: Operation.Name: {Format.SpellIfNull(telemetryItem.Context.Operation.Name)}");
|
||||
//Console.WriteLine($"{prefix}: Operation.ParentId: {Format.SpellIfNull(telemetryItem.Context.Operation.ParentId)}");
|
||||
//Console.WriteLine($"{prefix}: Operation.SyntheticSource:{Format.SpellIfNull(telemetryItem.Context.Operation.SyntheticSource)}");
|
||||
|
||||
//if (telemetryItem is TraceTelemetry traceTelemetry)
|
||||
//{
|
||||
// Console.WriteLine($"{prefix}: Message: {Format.SpellIfNull(traceTelemetry.Message)}");
|
||||
//}
|
||||
|
||||
//if (telemetryItem is RequestTelemetry requestTelemetry)
|
||||
//{
|
||||
// Console.WriteLine($"{prefix}: Name: {Format.SpellIfNull(requestTelemetry.Name)}");
|
||||
// Console.WriteLine($"{prefix}: Id: {Format.SpellIfNull(requestTelemetry.Id)}");
|
||||
//}
|
||||
|
||||
//if (telemetryItem is DependencyTelemetry dependencyTelemetry)
|
||||
//{
|
||||
// Console.WriteLine($"{prefix}: Name: {Format.SpellIfNull(dependencyTelemetry.Name)}");
|
||||
// Console.WriteLine($"{prefix}: Id: {Format.SpellIfNull(dependencyTelemetry.Id)}");
|
||||
//}
|
||||
|
||||
//if (telemetryItem is AvailabilityTelemetry availabilityTelemetry)
|
||||
//{
|
||||
// Console.WriteLine($"{prefix}: Name: {Format.SpellIfNull(availabilityTelemetry.Name)}");
|
||||
// Console.WriteLine($"{prefix}: Id: {Format.SpellIfNull(availabilityTelemetry.Id)}");
|
||||
//}
|
||||
|
||||
//Console.WriteLine();
|
||||
//Console.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using Microsoft.Azure.WebJobs.Hosting;
|
||||
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.Extensions;
|
||||
|
||||
[assembly: WebJobsStartup(typeof(AvailabilityMonitoringWebJobsStartup))]
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityMonitoringWebJobsStartup : IWebJobsStartup
|
||||
{
|
||||
public void Configure(IWebJobsBuilder builder)
|
||||
{
|
||||
builder.AddAvailabilityMonitoring();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityResultAsyncCollector : IAsyncCollector<AvailabilityTelemetry>, IAsyncCollector<bool>
|
||||
{
|
||||
private AvailabilityTestScope _availabilityTestScope = null;
|
||||
|
||||
public AvailabilityResultAsyncCollector()
|
||||
{
|
||||
}
|
||||
|
||||
public void Initialize(AvailabilityTestScope availabilityTestScope)
|
||||
{
|
||||
Validate.NotNull(availabilityTestScope, nameof(availabilityTestScope));
|
||||
|
||||
_availabilityTestScope = availabilityTestScope;
|
||||
}
|
||||
|
||||
public Task AddAsync(bool availbilityResultSuccess, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AvailabilityTestScope testScope = GetValidatedTestScope();
|
||||
|
||||
testScope.Complete(availbilityResultSuccess);
|
||||
testScope.Dispose();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AddAsync(AvailabilityTelemetry availbilityResult, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AvailabilityTestScope testScope = GetValidatedTestScope();
|
||||
|
||||
testScope.Complete(availbilityResult);
|
||||
testScope.Dispose();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FlushAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private AvailabilityTestScope GetValidatedTestScope()
|
||||
{
|
||||
AvailabilityTestScope testScope = _availabilityTestScope;
|
||||
|
||||
if (testScope == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot execute {nameof(AddAsync)}(..) on this instance of"
|
||||
+ $" {nameof(AvailabilityResultAsyncCollector)} becasue no"
|
||||
+ $" {nameof(AvailabilityTestScope)} was set by calling {nameof(Initialize)}(..).");
|
||||
}
|
||||
|
||||
return testScope;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityTestFunctionRegistry
|
||||
{
|
||||
private class AvailabilityTestRegistration
|
||||
{
|
||||
public string FunctionName { get; private set; }
|
||||
public IAvailabilityTestConfiguration Config { get; private set; }
|
||||
public bool IsAvailabilityTest { get; private set; }
|
||||
|
||||
public AvailabilityTestRegistration(string functionName, IAvailabilityTestConfiguration config, bool isAvailabilityTest)
|
||||
{
|
||||
Validate.NotNullOrWhitespace(functionName, nameof(functionName));
|
||||
Validate.NotNull(config, nameof(config));
|
||||
|
||||
this.FunctionName = functionName;
|
||||
this.Config = config;
|
||||
this.IsAvailabilityTest = isAvailabilityTest;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private readonly ConcurrentDictionary<string, AvailabilityTestRegistration> _registeredAvailabilityTests;
|
||||
|
||||
|
||||
public AvailabilityTestFunctionRegistry()
|
||||
{
|
||||
_registeredAvailabilityTests = new ConcurrentDictionary<string, AvailabilityTestRegistration>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
public void Register(string functionName, IAvailabilityTestConfiguration testConfig, ILogger log)
|
||||
{
|
||||
GetOrRegister(functionName, testConfig, isAvailabilityTest: true, log, "based on a code attribute annotation");
|
||||
}
|
||||
|
||||
|
||||
// Type 'FunctionInvocationContext' (and other Filter-related types) is marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
public bool IsAvailabilityTest(FunctionInvocationContext functionInvocationContext, out string functionName, out IAvailabilityTestConfiguration testConfig)
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
Validate.NotNull(functionInvocationContext, nameof(functionInvocationContext));
|
||||
|
||||
functionName = functionInvocationContext.FunctionName;
|
||||
Validate.NotNullOrWhitespace(functionName, "functionInvocationContext.FunctionName");
|
||||
|
||||
// In most cases we have already registered the Function:
|
||||
// either by callign this method from the filter during an earlier execution (out-of-proc languages)
|
||||
// or by calling Register(..) from the binding (.Net (in-proc) functions).
|
||||
|
||||
if (_registeredAvailabilityTests.TryGetValue(functionName, out AvailabilityTestRegistration registration))
|
||||
{
|
||||
testConfig = registration.Config;
|
||||
return registration.IsAvailabilityTest;
|
||||
}
|
||||
|
||||
ILogger log = functionInvocationContext.Logger;
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
|
||||
// Getting here means that we are executing out-of-proc language function for the first time.
|
||||
// In such cases, bindings happen late and dynamically, AFTER filters. Thus, NO binding has yet occurred.
|
||||
// We will read the function metadata to see if the return value of the function is tagged with the right attribute.
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to parse the function metadata file. This will throw if something goes wrong.
|
||||
// We will catch immediately, but this is rare if it happens at all) and helps attaching debuggers.
|
||||
GetTestConfigFromMetadata(functionName, functionInvocationContext, log, out bool isAvailabilityTest, out testConfig);
|
||||
|
||||
// We got here becasue the function was not registered, so take the insertion path right away:
|
||||
GetOrRegisterSlow(functionName, testConfig, isAvailabilityTest, log, "based on the function metadata file");
|
||||
return isAvailabilityTest;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
log.LogError(ex,
|
||||
$"Error while processing function metadata file to determine whether this function is a Coded Availability Test:"
|
||||
+ " FunctionName=\"{FunctionName}\", {{ErrorType=\"{ErrorType}\", {{ErrorMessage=\"{ErrorMessage}\"}}",
|
||||
functionName,
|
||||
ex.GetType().Name,
|
||||
ex.Message);
|
||||
|
||||
// We could not conclusively determine the aswer from metadata.
|
||||
// We assume "NOT an Availability Test", but we do not cache this, so we will keep checking in case this was some transient IO error.
|
||||
// We are not worried about the resulting perf impace, bacause this should not happen anyway.
|
||||
|
||||
testConfig = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private IAvailabilityTestConfiguration GetOrRegister(string functionName,
|
||||
IAvailabilityTestConfiguration testConfig,
|
||||
bool isAvailabilityTest,
|
||||
ILogger log,
|
||||
string causeDescriptionMsg)
|
||||
{
|
||||
Validate.NotNullOrWhitespace(functionName, nameof(functionName));
|
||||
Validate.NotNull(testConfig, nameof(testConfig));
|
||||
causeDescriptionMsg = causeDescriptionMsg ?? "unknown reason";
|
||||
|
||||
// The test will be already registered in all cases, except the first invocation.
|
||||
// Optimize for that and pay a small perf premium during the very first invocation.
|
||||
|
||||
if (_registeredAvailabilityTests.TryGetValue(functionName, out AvailabilityTestRegistration registration))
|
||||
{
|
||||
if (registration.IsAvailabilityTest != isAvailabilityTest)
|
||||
{
|
||||
throw new InvalidOperationException($"Registering Funtion \"{functionName}\"as {(isAvailabilityTest ? "" : "NOT")} "
|
||||
+ $"a Coded Availability Test ({causeDescriptionMsg}),"
|
||||
+ " but a Function with the same name is already registered as with the opposite"
|
||||
+ " IsAvailabilityTest-setting. Are you mixing .Net-based (in-proc) and"
|
||||
+ " non-.Net (out-of-proc) Functions in the same App and share the same Function name?"
|
||||
+ " That scenario that is not supported.");
|
||||
}
|
||||
|
||||
return registration.Config;
|
||||
}
|
||||
|
||||
// We did not have a registration. Let's try to insert one:
|
||||
return GetOrRegisterSlow(functionName, testConfig, isAvailabilityTest, log, causeDescriptionMsg);
|
||||
}
|
||||
|
||||
|
||||
private IAvailabilityTestConfiguration GetOrRegisterSlow(string functionName,
|
||||
IAvailabilityTestConfiguration testConfig,
|
||||
bool isAvailabilityTest,
|
||||
ILogger log,
|
||||
string causeDescriptionMsg)
|
||||
{
|
||||
AvailabilityTestRegistration newRegistration = null;
|
||||
AvailabilityTestRegistration usedRegistration = _registeredAvailabilityTests.GetOrAdd(
|
||||
functionName,
|
||||
(fn) =>
|
||||
{
|
||||
newRegistration = new AvailabilityTestRegistration(functionName, testConfig, isAvailabilityTest);
|
||||
return newRegistration;
|
||||
});
|
||||
|
||||
if (usedRegistration == newRegistration)
|
||||
{
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
|
||||
if (isAvailabilityTest)
|
||||
{
|
||||
log?.LogInformation($"A new Coded Availability Test was discovered ({causeDescriptionMsg}):"
|
||||
+ " {{ FunctionName=\"{FunctionName}\" }}",
|
||||
functionName);
|
||||
}
|
||||
else
|
||||
{
|
||||
log?.LogInformation($"A Function was registered as NOT a Coded Availability Test ({causeDescriptionMsg}):"
|
||||
+ " {{ FunctionName=\"{FunctionName}\" }}",
|
||||
functionName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (usedRegistration.IsAvailabilityTest != isAvailabilityTest)
|
||||
{
|
||||
throw new InvalidOperationException($"Registering Funtion \"{functionName}\"as {(isAvailabilityTest ? "" : "NOT")} "
|
||||
+ $"a Coded Availability Test ({causeDescriptionMsg}),"
|
||||
+ " but a Function with the same name is already registered as with the opposite"
|
||||
+ " IsAvailabilityTest-setting. Are you mixing .Net-based (in-proc) and"
|
||||
+ " non-.Net (out-of-proc) Functions in the same App and share the same Function name?"
|
||||
+ " That scenario that is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
return usedRegistration.Config;
|
||||
}
|
||||
|
||||
|
||||
// Type 'FunctionInvocationContext' (and other Filter-related types) is marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
private static void GetTestConfigFromMetadata(string functionName,
|
||||
FunctionInvocationContext functionInvocationContext,
|
||||
ILogger log,
|
||||
out bool isAvailabilityTest,
|
||||
out IAvailabilityTestConfiguration testConfig)
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
// We will do very verbose error checking and logging via exception here to aid supportability
|
||||
// in case out assumptions about Function Runtime behaviur get violated.
|
||||
|
||||
const string BeginAnalysisLogMessage = "Analysis of function metadata file to determine whether this function"
|
||||
+ " is a Coded Availability Test beginning:"
|
||||
+ " {{FunctionName=\"{FunctionName}\"}}";
|
||||
|
||||
const string FinishAnalysisLogMessage = "Analysis of function metadata file to determine whether this function"
|
||||
+ " is a Coded Availability Test finished:"
|
||||
+ " {{FunctionName=\"{FunctionName}\", IsAvailabilityTest=\"{IsAvailabilityTest}\"}}";
|
||||
|
||||
log?.LogDebug(BeginAnalysisLogMessage, functionName);
|
||||
|
||||
string metadataFileContent = ReadFunctionMetadataFile(functionInvocationContext);
|
||||
|
||||
FunctionMetadata functionMetadata = JsonConvert.DeserializeObject<FunctionMetadata>(metadataFileContent);
|
||||
|
||||
if (functionMetadata == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not parse the function metadata for function \"{functionName}\".");
|
||||
}
|
||||
|
||||
if (functionMetadata.Bindings == null)
|
||||
{
|
||||
throw new InvalidOperationException($"The function metadata for function \"{functionName}\" was parsed,"
|
||||
+ " but it did not contain a list of bindings.");
|
||||
}
|
||||
|
||||
if (functionMetadata.Bindings == null)
|
||||
{
|
||||
throw new InvalidOperationException($"The function metadata for function \"{functionName}\" was parsed;"
|
||||
+ " it contained a list of bindings, but the list had no entries.");
|
||||
}
|
||||
|
||||
foreach (BindingMetadata bindingMetadata in functionMetadata.Bindings)
|
||||
{
|
||||
if (bindingMetadata == null || bindingMetadata.Type == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bindingMetadata.Type.Equals(AvailabilityTestResultAttribute.BindingTypeName, StringComparison.OrdinalIgnoreCase)
|
||||
|| bindingMetadata.Type.Equals(nameof(AvailabilityTestResultAttribute), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isAvailabilityTest = true;
|
||||
testConfig = bindingMetadata;
|
||||
|
||||
log?.LogDebug(FinishAnalysisLogMessage, functionName, isAvailabilityTest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isAvailabilityTest = false;
|
||||
testConfig = null;
|
||||
|
||||
log?.LogDebug(FinishAnalysisLogMessage, functionName, isAvailabilityTest);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Type 'FunctionInvocationContext' (and other Filter-related types) is marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
private static string ReadFunctionMetadataFile(FunctionInvocationContext functionInvocationContext)
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
// We will do very verbose error checking and logging via exceptions here to aid supportability
|
||||
// in case out assumptions about Function Runtime behaviur get violated.
|
||||
|
||||
// For out-of-proc languages, the _context parameter should contain info about the runtime environment.
|
||||
// It should be of type ExecutionContext.
|
||||
// ExecutionContext should have info about the location of the function metadata file.
|
||||
|
||||
Validate.NotNull(functionInvocationContext.Arguments, "functionInvocationContext.Arguments");
|
||||
|
||||
const string NeedContextArgumentErrorPrefix = "For non-.Net (out-of-proc) functions, the Arguments table of the specified"
|
||||
+ " FunctionInvocationContext is expected to have a an entry with the key \"_context\""
|
||||
+ " and a value of type \"ExecutionContext\".";
|
||||
|
||||
if (! functionInvocationContext.Arguments.TryGetValue("_context", out object execContextObj))
|
||||
{
|
||||
throw new InvalidOperationException(NeedContextArgumentErrorPrefix + " However, such entry does not exist.");
|
||||
}
|
||||
|
||||
if (execContextObj == null)
|
||||
{
|
||||
throw new InvalidOperationException(NeedContextArgumentErrorPrefix + " Such entry exists, but the value is null.");
|
||||
}
|
||||
|
||||
string metadataFilePath;
|
||||
if (execContextObj is ExecutionContext execContext)
|
||||
{
|
||||
metadataFilePath = GetFullFunctionMetadataPath(execContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(NeedContextArgumentErrorPrefix
|
||||
+ $" Such entry exists, but is has the wrong type (\"{execContextObj.GetType().Name}\").");
|
||||
}
|
||||
|
||||
string metadataFileContent = File.ReadAllText(metadataFilePath);
|
||||
return metadataFileContent;
|
||||
}
|
||||
|
||||
private static string GetFullFunctionMetadataPath(ExecutionContext execContext)
|
||||
{
|
||||
const string functionJson = "function.json";
|
||||
|
||||
string functionDir = execContext.FunctionDirectory ?? String.Empty;
|
||||
string metadataFilePathInFuncDir = Path.Combine(functionDir, functionJson);
|
||||
|
||||
if (File.Exists(metadataFilePathInFuncDir))
|
||||
{
|
||||
return metadataFilePathInFuncDir;
|
||||
}
|
||||
|
||||
// We did not find function.json where it should be (in FunctionDirectory).
|
||||
// Let us attempt to look in FunctionAppDirectory as a fallback.
|
||||
// @ToDo: Is this reqired / safe?
|
||||
|
||||
string functionAppDir = execContext.FunctionAppDirectory ?? String.Empty;
|
||||
string metadataFilePathInAppDir = Path.Combine(functionAppDir, functionJson);
|
||||
|
||||
if (File.Exists(metadataFilePathInAppDir))
|
||||
{
|
||||
return metadataFilePathInAppDir;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Looked for the Function Metadata File (\"{functionJson}\") first in"
|
||||
+ $" \"{metadataFilePathInFuncDir}\" and then in \"{metadataFilePathInAppDir}\","
|
||||
+ " but that file does not exist.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityTestInvocationRegistry
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, AvailabilityTestInvocationState> _registeredInvocations;
|
||||
|
||||
public AvailabilityTestInvocationRegistry()
|
||||
{
|
||||
_registeredInvocations = new ConcurrentDictionary<Guid, AvailabilityTestInvocationState>();
|
||||
}
|
||||
|
||||
public AvailabilityTestInvocationState GetOrRegister(Guid functionInstanceId, ILogger log)
|
||||
{
|
||||
if (_registeredInvocations.TryGetValue(functionInstanceId, out AvailabilityTestInvocationState invocationState))
|
||||
{
|
||||
return invocationState;
|
||||
}
|
||||
|
||||
return GetOrRegisterSlow(functionInstanceId, log);
|
||||
}
|
||||
|
||||
public bool TryDeregister(Guid functionInstanceId, ILogger log, out AvailabilityTestInvocationState invocationState)
|
||||
{
|
||||
bool wasRegistered = _registeredInvocations.TryRemove(functionInstanceId, out invocationState);
|
||||
|
||||
if (wasRegistered)
|
||||
{
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
|
||||
log?.LogInformation($"A Coded Availability Test invocation instance was deregistered (completed):"
|
||||
+ " {{ FunctionInstanceId=\"{FunctionInstanceId}\" }}",
|
||||
functionInstanceId);
|
||||
}
|
||||
|
||||
return wasRegistered;
|
||||
}
|
||||
|
||||
private AvailabilityTestInvocationState GetOrRegisterSlow(Guid functionInstanceId, ILogger log)
|
||||
{
|
||||
AvailabilityTestInvocationState newRegistration = null;
|
||||
AvailabilityTestInvocationState usedRegistration = _registeredInvocations.GetOrAdd(
|
||||
functionInstanceId,
|
||||
(id) =>
|
||||
{
|
||||
newRegistration = new AvailabilityTestInvocationState(id);
|
||||
return newRegistration;
|
||||
});
|
||||
|
||||
if (usedRegistration == newRegistration)
|
||||
{
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
|
||||
log?.LogInformation($"A new Coded Availability Test invocation instance was registered:"
|
||||
+ " {{ FunctionInstanceId=\"{FunctionInstanceId}\" }}",
|
||||
functionInstanceId);
|
||||
}
|
||||
|
||||
return usedRegistration;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityTestInvocationState
|
||||
{
|
||||
private readonly Guid _functionInstanceId;
|
||||
private AvailabilityTestScope _availabilityTestScope = null;
|
||||
private AvailabilityResultAsyncCollector _resultCollector = null;
|
||||
private IList<AvailabilityTestInfo> _availabilityTestInfos = null;
|
||||
|
||||
public AvailabilityTestInvocationState(Guid functionInstanceId)
|
||||
{
|
||||
_functionInstanceId = functionInstanceId;
|
||||
}
|
||||
|
||||
public void AttachTestScope(AvailabilityTestScope testScope)
|
||||
{
|
||||
Validate.NotNull(testScope, nameof(testScope));
|
||||
|
||||
_availabilityTestScope = testScope;
|
||||
}
|
||||
|
||||
public bool TryGetTestScope(out AvailabilityTestScope testScope)
|
||||
{
|
||||
testScope = _availabilityTestScope;
|
||||
return (testScope != null);
|
||||
}
|
||||
|
||||
public void AttachResultCollector(AvailabilityResultAsyncCollector resultCollector)
|
||||
{
|
||||
Validate.NotNull(resultCollector, nameof(resultCollector));
|
||||
|
||||
_resultCollector = resultCollector;
|
||||
}
|
||||
|
||||
public bool TryGetResultCollector(out AvailabilityResultAsyncCollector resultCollector)
|
||||
{
|
||||
resultCollector = _resultCollector;
|
||||
return (resultCollector != null);
|
||||
}
|
||||
|
||||
public void AttachTestInfo(AvailabilityTestInfo testInfo)
|
||||
{
|
||||
IList<AvailabilityTestInfo> testInfos = _availabilityTestInfos;
|
||||
if (testInfos == null)
|
||||
{
|
||||
var newTestInfos = new List<AvailabilityTestInfo>();
|
||||
IList<AvailabilityTestInfo> prevTestInfos = Interlocked.CompareExchange(ref _availabilityTestInfos, newTestInfos, null);
|
||||
testInfos = prevTestInfos ?? newTestInfos;
|
||||
}
|
||||
|
||||
testInfos.Add(testInfo);
|
||||
}
|
||||
|
||||
public bool TryGetTestInfos(out IEnumerable<AvailabilityTestInfo> testInfos)
|
||||
{
|
||||
testInfos = _availabilityTestInfos;
|
||||
return (testInfos != null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityTestRegistry
|
||||
{
|
||||
public AvailabilityTestFunctionRegistry Functions { get; }
|
||||
public AvailabilityTestInvocationRegistry Invocations { get; }
|
||||
|
||||
public AvailabilityTestRegistry()
|
||||
{
|
||||
this.Functions = new AvailabilityTestFunctionRegistry();
|
||||
this.Invocations = new AvailabilityTestInvocationRegistry();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class AvailabilityTestScopeSettingsResolver
|
||||
{
|
||||
public static class ConfigurationKeys
|
||||
{
|
||||
public static class SectionNames
|
||||
{
|
||||
public const string AvailabilityTestResults = "AvailabilityTestResults";
|
||||
public const string AzureFunctionsJobHost = "AzureFunctionsJobHost";
|
||||
}
|
||||
|
||||
public static class KeyNames
|
||||
{
|
||||
public const string TestDisplayName = "TestDisplayName";
|
||||
public const string LocationDisplayName = "LocationDisplayName";
|
||||
public const string LocationId = "LocationId";
|
||||
}
|
||||
|
||||
public static class EnvironmentVariableNames
|
||||
{
|
||||
public const string TestDisplayName = SectionNames.AvailabilityTestResults + "." + KeyNames.TestDisplayName;
|
||||
|
||||
public const string LocationDisplayName = SectionNames.AvailabilityTestResults + "." + KeyNames.LocationDisplayName;
|
||||
public const string LocationDisplayName_Fallback1 = "REGION_NAME";
|
||||
public const string LocationDisplayName_Fallback2 = "Location";
|
||||
|
||||
public const string LocationId = SectionNames.AvailabilityTestResults + "." + KeyNames.LocationId;
|
||||
}
|
||||
}
|
||||
|
||||
private class AvailabilityTestConfiguration : IAvailabilityTestConfiguration
|
||||
{
|
||||
public string TestDisplayName { get; }
|
||||
public string LocationDisplayName { get; }
|
||||
public string LocationId { get; }
|
||||
public AvailabilityTestConfiguration(string testDisplayName, string locationDisplayName, string locationId)
|
||||
{
|
||||
this.TestDisplayName = testDisplayName;
|
||||
this.LocationDisplayName = locationDisplayName;
|
||||
this.LocationId = locationId;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly INameResolver _nameResolver;
|
||||
|
||||
public AvailabilityTestScopeSettingsResolver(IConfiguration configuration, INameResolver nameResolver)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_nameResolver = nameResolver;
|
||||
}
|
||||
|
||||
public IAvailabilityTestConfiguration Resolve(IAvailabilityTestConfiguration testConfig, string functionName)
|
||||
{
|
||||
// Whenever a setting is missing, attempt to fill it from the config ir the environment:
|
||||
|
||||
// Test Display Name:
|
||||
string testDisplayName = testConfig?.TestDisplayName;
|
||||
|
||||
testDisplayName = TryFillValueFromConfig(
|
||||
testDisplayName,
|
||||
ConfigurationKeys.SectionNames.AvailabilityTestResults,
|
||||
ConfigurationKeys.KeyNames.TestDisplayName);
|
||||
|
||||
testDisplayName = TryFillValueFromEnvironment(
|
||||
testDisplayName,
|
||||
ConfigurationKeys.EnvironmentVariableNames.TestDisplayName);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(testDisplayName))
|
||||
{
|
||||
testDisplayName = functionName;
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(testDisplayName))
|
||||
{
|
||||
throw new ArgumentException("The Availability Test Display Name must be set, but it was not."
|
||||
+ " To set that value, use one of the following (in order of precedence):"
|
||||
+ $" (a) Explicitly set the property \"{nameof(AvailabilityTestResultAttribute.TestDisplayName)}\""
|
||||
+ $" on the '{AvailabilityTestResultAttribute.BindingTypeName}'-binding"
|
||||
+ " (via the attribute or via function.json) (%%-tags are supported);"
|
||||
+ $" (b) Use the App Setting \"{ConfigurationKeys.KeyNames.TestDisplayName}\" in"
|
||||
+ $" configuration section \"{ConfigurationKeys.SectionNames.AvailabilityTestResults}\";"
|
||||
+ $" (c) Use an environment variable \"{ConfigurationKeys.EnvironmentVariableNames.TestDisplayName}\";"
|
||||
+ " (d) The name of the Azure Function will be used as a fallback.");
|
||||
}
|
||||
|
||||
|
||||
// Location Display Name:
|
||||
string locationDisplayName = testConfig?.LocationDisplayName;
|
||||
|
||||
locationDisplayName = TryFillValueFromConfig(
|
||||
locationDisplayName,
|
||||
ConfigurationKeys.SectionNames.AvailabilityTestResults,
|
||||
ConfigurationKeys.KeyNames.LocationDisplayName);
|
||||
|
||||
locationDisplayName = TryFillValueFromEnvironment(
|
||||
locationDisplayName,
|
||||
ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName);
|
||||
|
||||
locationDisplayName = TryFillValueFromEnvironment(
|
||||
locationDisplayName,
|
||||
ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback1);
|
||||
|
||||
locationDisplayName = TryFillValueFromEnvironment(
|
||||
locationDisplayName,
|
||||
ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback2);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(locationDisplayName))
|
||||
{
|
||||
throw new ArgumentException("The Location Display Name of the Availability Test must be set, but it was not."
|
||||
+ " To set that value, use one of the following (in order of precedence):"
|
||||
+ $" (a) Explicitly set the property \"{nameof(AvailabilityTestResultAttribute.LocationDisplayName)}\""
|
||||
+ $" on the '{AvailabilityTestResultAttribute.BindingTypeName}'-binding"
|
||||
+ " (via the attribute or via function.json) (%%-tags are supported);"
|
||||
+ $" (b) Use the App Setting \"{ConfigurationKeys.KeyNames.LocationDisplayName}\" in"
|
||||
+ $" configuration section \"{ConfigurationKeys.SectionNames.AvailabilityTestResults}\";"
|
||||
+ $" (c) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName}\";"
|
||||
+ $" (d) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback1}\";"
|
||||
+ $" (e) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationDisplayName_Fallback2}\".");
|
||||
}
|
||||
|
||||
// Location Id:
|
||||
string locationId = testConfig?.LocationId;
|
||||
|
||||
locationId = TryFillValueFromConfig(
|
||||
locationId,
|
||||
ConfigurationKeys.SectionNames.AvailabilityTestResults,
|
||||
ConfigurationKeys.KeyNames.LocationId);
|
||||
|
||||
locationId = TryFillValueFromEnvironment(
|
||||
locationId,
|
||||
ConfigurationKeys.EnvironmentVariableNames.LocationId);
|
||||
|
||||
if (locationId == null)
|
||||
{
|
||||
locationId = Format.LocationNameAsId(locationDisplayName);
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(locationId))
|
||||
{
|
||||
throw new ArgumentException($"The Location Id of the Availability Test must be set, but it was not."
|
||||
+ $" To set that value, use one of the following (in order of precedence):"
|
||||
+ $" (a) Explicitly set the property \"{nameof(AvailabilityTestResultAttribute.LocationId)}\""
|
||||
+ $" on the '{AvailabilityTestResultAttribute.BindingTypeName}'-binding"
|
||||
+ " (via the attribute or via function.json) (%%-tags are supported);"
|
||||
+ $" (b) Use the App Setting \"{ConfigurationKeys.KeyNames.LocationId}\" in"
|
||||
+ $" configuration section \"{ConfigurationKeys.SectionNames.AvailabilityTestResults}\";"
|
||||
+ $" (c) Use the environment variable \"{ConfigurationKeys.EnvironmentVariableNames.LocationId}\";"
|
||||
+ $" (d) As a fallback, a Location Id will be derived from the Location Display Name (only if that is set).");
|
||||
}
|
||||
|
||||
// We did our best to get the config.
|
||||
|
||||
var resolvedConfig = new AvailabilityTestScopeSettingsResolver.AvailabilityTestConfiguration(testDisplayName, locationDisplayName, locationId);
|
||||
return resolvedConfig;
|
||||
}
|
||||
|
||||
private string TryFillValueFromConfig(string value, string configSectionName, string configKeyName)
|
||||
{
|
||||
// If we already have value, we are done:
|
||||
if (false == String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// Try getting from configuration:
|
||||
string valueFromConfig = null;
|
||||
try
|
||||
{
|
||||
if (_configuration != null && configKeyName != null)
|
||||
{
|
||||
if (configSectionName == null)
|
||||
{
|
||||
// Try WITHOUT using the 'AzureFunctionsJobHost'-root:
|
||||
valueFromConfig = _configuration[configKeyName];
|
||||
|
||||
// Try WITH using the 'AzureFunctionsJobHost'-root:
|
||||
if (String.IsNullOrWhiteSpace(valueFromConfig))
|
||||
{
|
||||
IConfiguration jobHostConfigSection = _configuration.GetSection(ConfigurationKeys.SectionNames.AzureFunctionsJobHost);
|
||||
valueFromConfig = jobHostConfigSection[configKeyName];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try WITHOUT using the 'AzureFunctionsJobHost'-root:
|
||||
IConfiguration configSection = _configuration.GetSection(configSectionName);
|
||||
valueFromConfig = configSection[configKeyName];
|
||||
|
||||
// Try WITH using the 'AzureFunctionsJobHost'-root:
|
||||
if (String.IsNullOrWhiteSpace(valueFromConfig))
|
||||
{
|
||||
IConfiguration jobHostConfigSection = _configuration.GetSection(ConfigurationKeys.SectionNames.AzureFunctionsJobHost);
|
||||
IConfiguration configSubsection = jobHostConfigSection.GetSection(configSectionName);
|
||||
valueFromConfig = configSubsection[configKeyName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
// Apply name resolution:
|
||||
if (valueFromConfig != null)
|
||||
{
|
||||
value = NameResolveWholeStringRecursive(valueFromConfig);
|
||||
}
|
||||
|
||||
// Nothing else we can do:
|
||||
return value;
|
||||
}
|
||||
|
||||
private string TryFillValueFromEnvironment(string value, string environmentVariableName)
|
||||
{
|
||||
// If we already have value, we are done:
|
||||
if (false == String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// In case we had no configuration, try looking in the environment explicitly:
|
||||
string valueFromEnvironment = null;
|
||||
try
|
||||
{
|
||||
valueFromEnvironment = Environment.GetEnvironmentVariable(environmentVariableName);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
// Apply name resolution
|
||||
if (valueFromEnvironment != null)
|
||||
{
|
||||
value = NameResolveWholeStringRecursive(valueFromEnvironment);
|
||||
}
|
||||
|
||||
// Nothing else we can do:
|
||||
return value;
|
||||
}
|
||||
|
||||
private string NameResolveWholeStringRecursive(string name)
|
||||
{
|
||||
if (_nameResolver == null || String.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
string resolvedName = _nameResolver.ResolveWholeString(name);
|
||||
while (false == String.IsNullOrWhiteSpace(resolvedName) && false == resolvedName.Equals(name, StringComparison.Ordinal))
|
||||
{
|
||||
name = resolvedName;
|
||||
resolvedName = _nameResolver.ResolveWholeString(name);
|
||||
}
|
||||
|
||||
return resolvedName;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal class FunctionMetadata
|
||||
{
|
||||
public IList<BindingMetadata> Bindings { get; set; }
|
||||
}
|
||||
|
||||
internal class BindingMetadata : IAvailabilityTestConfiguration
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Direction { get; set; }
|
||||
public string TestDisplayName { get; set; }
|
||||
public string LocationDisplayName { get; set; }
|
||||
public string LocationId { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal static class Convert
|
||||
{
|
||||
public static string AvailabilityTestInfoToString(AvailabilityTestInfo availabilityTestInfo)
|
||||
{
|
||||
Validate.NotNull(availabilityTestInfo, nameof(availabilityTestInfo));
|
||||
string str = JsonConvert.SerializeObject(availabilityTestInfo, Formatting.Indented);
|
||||
return str;
|
||||
}
|
||||
|
||||
public static AvailabilityTelemetry AvailabilityTestInfoToAvailabilityTelemetry(AvailabilityTestInfo availabilityTestInfo)
|
||||
{
|
||||
Validate.NotNull(availabilityTestInfo, nameof(availabilityTestInfo));
|
||||
return availabilityTestInfo.DefaultAvailabilityResult;
|
||||
}
|
||||
|
||||
public static AvailabilityTelemetry StringToAvailabilityTelemetry(string str)
|
||||
{
|
||||
if (str == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AvailabilityTelemetry availabilityResult = JsonConvert.DeserializeObject<AvailabilityTelemetry>(str);
|
||||
return availabilityResult;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Config;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
using PatternMatcherFactory = Func<Func<AvailabilityTestResultAttribute, ValueBindingContext, Task<IAsyncCollector<AvailabilityTelemetry>>>, object>;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary workaround until the required overload of the BindToCollector(..) method is added to the SDK.
|
||||
/// </summary>
|
||||
internal static class FluentBindingRuleExtensions
|
||||
{
|
||||
private const string PatternMatcherFactoryMethodName = "New";
|
||||
private const string FluentBindingRuleBindToCollectorMethodName = "BindToCollector";
|
||||
private const string PatternMatcherTypeName = "Microsoft.Azure.WebJobs.Host.Bindings.PatternMatcher";
|
||||
private static readonly ParameterModifier[] NoParameterModifiers = new ParameterModifier[0];
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public static void BindToCollector<TAttribute, TMessage>(
|
||||
this FluentBindingRule<TAttribute> bindingRule,
|
||||
Func<TAttribute, ValueBindingContext, Task<IAsyncCollector<TMessage>>> asyncCollectorFactory)
|
||||
where TAttribute : Attribute
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
// We could speed this up 10x - 100x by creating and caching delegates to the methods we accell vie reflection.
|
||||
// However, since this is a temp workaround until the methods are available in the SDK, we will avoid the complexity.
|
||||
|
||||
#pragma warning disable CS0618
|
||||
Type fluentBindingRuleType = typeof(FluentBindingRule<TAttribute>);
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// First, reflect to invoke the method
|
||||
// public static PatternMatcher New<TSource, TDest>(Func<TSource, ValueBindingContext, Task<TDest>> func)
|
||||
// on the PatternMatcher class:
|
||||
|
||||
Type patternMatcherType = fluentBindingRuleType.Assembly.GetType(PatternMatcherTypeName);
|
||||
MethodInfo patternMatcherFactoryBound = null;
|
||||
{
|
||||
Type[] genericMethodParamTypes = new Type[] { typeof(TAttribute), typeof(IAsyncCollector<TMessage>) };
|
||||
Type requiredParamType = typeof(Func<TAttribute, ValueBindingContext, Task<IAsyncCollector<TMessage>>>);
|
||||
|
||||
foreach (MethodInfo method in patternMatcherType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
|
||||
{
|
||||
if (method.IsGenericMethod && method.GetParameters().Length == 1 && method.GetGenericArguments().Length == 2)
|
||||
{
|
||||
MethodInfo methodBound = method.MakeGenericMethod(genericMethodParamTypes);
|
||||
if (methodBound.GetParameters()[0].ParameterType.Equals(requiredParamType))
|
||||
{
|
||||
patternMatcherFactoryBound = methodBound;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a PatternMatcher instance that wraps asyncCollectorFactory:
|
||||
object patternMatcherInstance = patternMatcherFactoryBound.Invoke(obj: null, parameters: new object[] { asyncCollectorFactory });
|
||||
|
||||
// Next, reflect to invoke
|
||||
// private void BindToCollector<TMessage>(PatternMatcher pm)
|
||||
// in the FluentBindingRule<TAttribute> class:
|
||||
|
||||
MethodInfo bindToCollectorMethodGeneric = fluentBindingRuleType.GetMethod(
|
||||
FluentBindingRuleBindToCollectorMethodName,
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
|
||||
binder: null,
|
||||
new Type[] { patternMatcherType },
|
||||
NoParameterModifiers);
|
||||
|
||||
MethodInfo bindToCollectorMethodBound = bindToCollectorMethodGeneric.MakeGenericMethod(new Type[] { typeof(TMessage) });
|
||||
|
||||
// Bind asyncCollectorFactory wrapped into the patternMatcherInstance to the binding rule:
|
||||
bindToCollectorMethodBound.Invoke(obj: bindingRule, parameters: new object[] { patternMatcherInstance });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ApplicationInsights;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
// Type 'IFunctionInvocationFilter' (and other Filter-related types) is marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
internal class FunctionInvocationManagementFilter : IFunctionInvocationFilter, IFunctionExceptionFilter
|
||||
#pragma warning restore CS0618 // Type or member is obsolete (Filter-related types are obsolete, but we want to use them)
|
||||
{
|
||||
private readonly AvailabilityTestRegistry _availabilityTestRegistry;
|
||||
private readonly TelemetryConfiguration _telemetryConfiguration;
|
||||
private readonly AvailabilityTestScopeSettingsResolver _availabilityTestScopeSettingsResolver;
|
||||
|
||||
public FunctionInvocationManagementFilter(AvailabilityTestRegistry availabilityTestRegistry, TelemetryConfiguration telemetryConfiguration, IConfiguration configuration, INameResolver nameResolver)
|
||||
{
|
||||
Validate.NotNull(availabilityTestRegistry, nameof(availabilityTestRegistry));
|
||||
Validate.NotNull(telemetryConfiguration, nameof(telemetryConfiguration));
|
||||
|
||||
_availabilityTestRegistry = availabilityTestRegistry;
|
||||
_telemetryConfiguration = telemetryConfiguration;
|
||||
_availabilityTestScopeSettingsResolver = new AvailabilityTestScopeSettingsResolver(configuration, nameResolver);
|
||||
}
|
||||
|
||||
// Types 'FunctionExecutingContext' and 'IFunctionFilter' (and other Filter-related types) are marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancelControl)
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
// A few lines which we need for attaching a debugger during development.
|
||||
// @ToDo: Remove before shipping.
|
||||
Console.WriteLine($"Filter Entry Point: {nameof(FunctionInvocationManagementFilter)}.{nameof(OnExecutingAsync)}(..).");
|
||||
Console.WriteLine($"FunctionInstanceId: {Format.SpellIfNull(executingContext?.FunctionInstanceId)}.");
|
||||
Process proc = Process.GetCurrentProcess();
|
||||
Console.WriteLine($"Process name: \"{proc.ProcessName}\", Process Id: \"{proc.Id}\".");
|
||||
// --
|
||||
|
||||
Validate.NotNull(executingContext, nameof(executingContext));
|
||||
|
||||
// Grab the invocation id and the logger:
|
||||
Guid functionInstanceId = executingContext.FunctionInstanceId;
|
||||
ILogger log = executingContext.Logger;
|
||||
|
||||
// Check if this is an Availability Test.
|
||||
// There are 3 cases:
|
||||
// 1) This IS an Availability Test and this is an in-proc/.Net functuion:
|
||||
// This filter runs AFTER the bindings.
|
||||
// The current function was already registered, becasue the attribute binding was already executed.
|
||||
// 2) This IS an Availability Test and this is an out-of-proc/non-.Net function:
|
||||
// This filter runs BEFORE the bindings.
|
||||
// a) If this is the first time the filter runs for the current function, TryGetTestConfig(..) will
|
||||
// read the metadata file, extract the config and return True.
|
||||
// b) If this is not the first time, the function is already registered as described in (a).
|
||||
// 3) This is NOT an Availability Test:
|
||||
// We will get False here and do nothing.
|
||||
|
||||
bool isAvailabilityTest = _availabilityTestRegistry.Functions.IsAvailabilityTest(executingContext, out string functionName, out IAvailabilityTestConfiguration testConfig);
|
||||
if (! isAvailabilityTest)
|
||||
{
|
||||
if (log != null)
|
||||
{
|
||||
using (log.BeginScope(LogMonikers.Scopes.CreateForTestInvocation(functionName)))
|
||||
{
|
||||
log.LogDebug($"Availability Test Pre-Function routine was invoked and determned that this function is NOT an Availability Test:"
|
||||
+ " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\"}}",
|
||||
functionName, functionInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// If configured, use a fall-back logger:
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
|
||||
IReadOnlyDictionary<string, object> logScopeInfo = LogMonikers.Scopes.CreateForTestInvocation(functionName);
|
||||
using (log.BeginScopeSafe(logScopeInfo))
|
||||
{
|
||||
log?.LogDebug($"Availability Test Pre-Function routine was invoked:"
|
||||
+ " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\","
|
||||
+ " TestConfiguration={{TestDisplayNameTemplate=\"{TestDisplayNameTemplate}\","
|
||||
+ " LocationDisplayNameTemplate=\"{LocationDisplayNameTemplate}\","
|
||||
+ " LocationIdTemplate=\"{LocationIdTemplate}\"}} }}",
|
||||
functionName, functionInstanceId, testConfig.TestDisplayName, testConfig.LocationDisplayName, testConfig.LocationId);
|
||||
|
||||
// - In case (1) described above, we have already registered this invocation:
|
||||
// The function parameters have been instantiated, and attached to the invocationState.
|
||||
// However, the parameters are NOT yet initialized, as we did not have a AvailabilityTestScope instance yet.
|
||||
// We will set up an AvailabilityTestScope and attach it to the invocationState.
|
||||
// Then we will initialize the parameters using data from that scope.
|
||||
// - In case (2) described above, we have not yet registered the invocation:
|
||||
// A new invocationState will end up being created now.
|
||||
// We will set up an AvailabilityTestScope and attach it to the invocationState.
|
||||
// Subsequently, when the binings eventually get invoked by the Functions tuntime,
|
||||
// they will instantiate and initialize the parameters using data from that scope.
|
||||
|
||||
// Get the invocation state bag:
|
||||
|
||||
AvailabilityTestInvocationState invocationState = _availabilityTestRegistry.Invocations.GetOrRegister(functionInstanceId, log);
|
||||
|
||||
// If test configuration makes reference to configuration, resolve the settings
|
||||
IAvailabilityTestConfiguration resolvedTestConfig = _availabilityTestScopeSettingsResolver.Resolve(testConfig, functionName);
|
||||
|
||||
// Start the availability test scope (this will start timers and set up the activity span):
|
||||
AvailabilityTestScope testScope = AvailabilityTest.StartNew(resolvedTestConfig, _telemetryConfiguration, flushOnDispose: true, log, logScopeInfo);
|
||||
invocationState.AttachTestScope(testScope);
|
||||
|
||||
// If we have previously instantiated a result collector, initialize it now:
|
||||
if (invocationState.TryGetResultCollector(out AvailabilityResultAsyncCollector resultCollector))
|
||||
{
|
||||
resultCollector.Initialize(testScope);
|
||||
}
|
||||
|
||||
// If we have previously instantiated a test info, initialize it now:
|
||||
if (invocationState.TryGetTestInfos(out IEnumerable<AvailabilityTestInfo> testInfos))
|
||||
{
|
||||
AvailabilityTestInfo model = testScope.CreateAvailabilityTestInfo();
|
||||
foreach (AvailabilityTestInfo testInfo in testInfos)
|
||||
{
|
||||
testInfo.CopyFrom(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Types 'FunctionExceptionContext' and 'IFunctionFilter' (and other Filter-related types) are marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
public Task OnExceptionAsync(FunctionExceptionContext exceptionContext, CancellationToken cancelControl)
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
// A few lines which we need for attaching a debugger during development.
|
||||
// @ToDo: Remove before shipping.
|
||||
Console.WriteLine($"Filter Entry Point: {nameof(FunctionInvocationManagementFilter)}.{nameof(OnExceptionAsync)}(..).");
|
||||
Console.WriteLine($"FunctionInstanceId: {Format.SpellIfNull(exceptionContext?.FunctionInstanceId)}.");
|
||||
Process proc = Process.GetCurrentProcess();
|
||||
Console.WriteLine($"Process name: \"{proc.ProcessName}\", Process Id: \"{proc.Id}\".");
|
||||
// --
|
||||
|
||||
// Get error:
|
||||
Exception error = exceptionContext?.Exception
|
||||
?? new Exception("OnExceptionAsync(..) is invoked, but no Exception information is available. ");
|
||||
|
||||
OnPostFunctionError(exceptionContext, error, nameof(OnExceptionAsync));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Types 'FunctionExecutedContext' and 'IFunctionFilter' (and other Filter-related types) are marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
public Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancelControl)
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
// A few lines which we need for attaching a debugger during development.
|
||||
// @ToDo: Remove before shipping.
|
||||
Console.WriteLine($"Filter Entry Point: {nameof(FunctionInvocationManagementFilter)}.{nameof(OnExecutedAsync)}(..).");
|
||||
Console.WriteLine($"FunctionInstanceId: {Format.SpellIfNull(executedContext?.FunctionInstanceId)}.");
|
||||
Process proc = Process.GetCurrentProcess();
|
||||
Console.WriteLine($"Process name: \"{proc.ProcessName}\", Process Id: \"{proc.Id}\".");
|
||||
// --
|
||||
|
||||
Exception error = null;
|
||||
if (executedContext?.FunctionResult?.Succeeded != true)
|
||||
{
|
||||
error = executedContext?.FunctionResult?.Exception;
|
||||
error = error ?? new Exception("FunctionResult.Succeeded is false, but no Exception information is available.");
|
||||
}
|
||||
|
||||
OnPostFunctionError(executedContext, error, nameof(OnExecutedAsync));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Types 'FunctionFilterContext' and 'IFunctionFilter' (and other Filter-related types) are marked as preview/obsolete,
|
||||
// but the guidance from the Azure Functions team is to use it, so we disable the warning.
|
||||
#pragma warning disable CS0618
|
||||
private void OnPostFunctionError(FunctionFilterContext filterContext, Exception error, string entryPointName)
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
// The functions runtime communicates some exceptions only via OnExceptionAsync(..) (e.g., timeouts).
|
||||
// Some other exceptions may be also be communicated via OnExecutedAsync(..).
|
||||
// Rather than trying to predict this flaky behaviour, we are being defensve and are processing both callbacks.
|
||||
// Whichever happens first will call this method. We will deregister the invocation and process the error.
|
||||
// The second call (if it happens) will find this invocation no longer registered it will return.
|
||||
// If no error occurred at all, the result is handeled by the result collector (IAsyncCollector<>).
|
||||
// So, for no-error cases, all we need to do here is to deregister the invocation and return right away.
|
||||
|
||||
Validate.NotNull(filterContext, nameof(filterContext));
|
||||
|
||||
// Grab the invocation id, the logger and the function name:
|
||||
Guid functionInstanceId = filterContext.FunctionInstanceId;
|
||||
ILogger log = filterContext.Logger;
|
||||
string functionName = filterContext.FunctionName;
|
||||
|
||||
// Unwrap generic function exception:
|
||||
while (error != null
|
||||
&& error is FunctionInvocationException funcInvocEx
|
||||
&& funcInvocEx.InnerException != null)
|
||||
{
|
||||
error = funcInvocEx.InnerException;
|
||||
}
|
||||
|
||||
// If configured, use a fall-back logger:
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
const int MaxErrorMessageLength = 100;
|
||||
|
||||
IReadOnlyDictionary<string, object> logScopeInfo = LogMonikers.Scopes.CreateForTestInvocation(functionName);
|
||||
using (log?.BeginScopeSafe(logScopeInfo))
|
||||
{
|
||||
log?.LogDebug($"Availability Test Post-Function error handling routine (via {entryPointName}) beginning:"
|
||||
+ " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\","
|
||||
+ " ErrorType=\"{ErrorType}\", ErrorMessage=\"{ErrorMessage}\"}}",
|
||||
functionName, functionInstanceId,
|
||||
Format.SpellIfNull(error?.GetType()?.Name), Format.LimitLength(error?.Message, MaxErrorMessageLength, trim: true));
|
||||
|
||||
// A function is an Availability Test iff is has a return value marked with [AvailabilityTestResult];
|
||||
// whereas a [AvailabilityTestInfo] is optional to get test information at runtime.
|
||||
// User could have marked a parameter with [AvailabilityTestInfo] but no return value with [AvailabilityTestResult]:
|
||||
// That does not make sense, but we need to do something graceful. Since in the binder (see CreateAvailabilityTestInfo) we
|
||||
// did not have a way of knowing whether the return value is tagged, we have initialized the test info and registered the invocation.
|
||||
// We need to clean it up now even if the function is not an Availability Test.
|
||||
|
||||
bool isTrackedInvocation = _availabilityTestRegistry.Invocations.TryDeregister(functionInstanceId, log, out AvailabilityTestInvocationState invocationState);
|
||||
if (! isTrackedInvocation)
|
||||
{
|
||||
log?.LogDebug($"Availability Test Post-Function error handling routine (via {entryPointName}) finished:"
|
||||
+ " This function invocation instance is not being tracked."
|
||||
+ " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\","
|
||||
+ " ErrorType=\"{ErrorType}\", ErrorMessage=\"{ErrorMessage}\"}}",
|
||||
functionName, functionInstanceId,
|
||||
Format.SpellIfNull(error?.GetType()?.Name), Format.LimitLength(error?.Message, MaxErrorMessageLength, trim: true));
|
||||
return;
|
||||
}
|
||||
|
||||
// If no exception was thrown by the function, the results collector will be called to set the return value.
|
||||
// It will Complete the Availability Test Scope, so there is nothing to do here.
|
||||
|
||||
if (error == null)
|
||||
{
|
||||
log?.LogDebug($"Availability Test Post-Function error handling routine (via {entryPointName}) finished:"
|
||||
+ " No error to be handled."
|
||||
+ " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\","
|
||||
+ " ErrorType=\"{ErrorType}\", , ErrorMessage=\"{ErrorMessage}\"}}",
|
||||
functionName, functionInstanceId,
|
||||
Format.SpellIfNull(null), Format.LimitLength(null, MaxErrorMessageLength, trim: true));
|
||||
return;
|
||||
}
|
||||
|
||||
// An exception has occurred in the function, so we need to complete the Availability Test Scope here.
|
||||
|
||||
if (! invocationState.TryGetTestScope(out AvailabilityTestScope testScope))
|
||||
{
|
||||
// This should never happen!
|
||||
|
||||
log?.LogError($"Availability Test Post-Function error handling routine (via {entryPointName}) finished:"
|
||||
+ " Error: No AvailabilityTestScope was attached to the invocation state - Cannot continue processing!"
|
||||
+ " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\"}}"
|
||||
+ " ErrorType=\"{ErrorType}\", ErrorMessage=\"{ErrorMessage}\"}}",
|
||||
functionName, functionInstanceId,
|
||||
error.GetType().Name, error.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
bool isTimeout = (error is FunctionTimeoutException);
|
||||
|
||||
testScope.Complete(error, isTimeout);
|
||||
testScope.Dispose();
|
||||
|
||||
log?.LogDebug($"Availability Test Post-Function error handling routine (via {entryPointName}) finished" +
|
||||
$":"
|
||||
+ $" {nameof(AvailabilityTestScope)} was completed and disposed."
|
||||
+ " {{FunctionName=\"{FunctionName}\", FunctionInstanceId=\"{FunctionInstanceId}\","
|
||||
+ " ErrorType=\"{ErrorType}\", ErrorMessage=\"{ErrorMessage}\","
|
||||
+ " TestConfiguration={{TestDisplayName=\"{TestDisplayName}\","
|
||||
+ " LocationDisplayName=\"{LocationDisplayName}\","
|
||||
+ " LocationId=\"{LocationId}\"}} }}",
|
||||
functionName, functionInstanceId,
|
||||
error.GetType().Name, Format.LimitLength(error.Message, MaxErrorMessageLength, trim: true),
|
||||
testScope.TestDisplayName, testScope.LocationDisplayName, testScope.LocationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
internal static class LogMonikers
|
||||
{
|
||||
public static class Categories
|
||||
{
|
||||
public const string Extension = "Host.Extensions.AvailabilityMonitoring";
|
||||
|
||||
public static string CreateForTestInvocation(string functionName)
|
||||
{
|
||||
string category = $"Function.AvailabilityTest.{Format.SpellIfNull(functionName)}";
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Scopes
|
||||
{
|
||||
public static IReadOnlyDictionary<string, object> CreateForTestInvocation(string functionName)
|
||||
{
|
||||
var scope = new Dictionary<string, object>(capacity: 2)
|
||||
{
|
||||
[LogConstants.CategoryNameKey] = Categories.CreateForTestInvocation(functionName),
|
||||
[LogConstants.LogLevelKey] = LogLevel.Information
|
||||
};
|
||||
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче