Коммит
f2f86b1a8a
|
@ -1,3 +0,0 @@
|
|||
Do not delete or move this file!
|
||||
This file is required by the binary output placement logic for the entire repository defined in the Directory.Build.props.
|
||||
Make sure you understand that logic before touching this file!
|
|
@ -1,79 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.ApplicationInsights.Channel;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring.Extensions
|
||||
{
|
||||
internal class AvailabilityMonitoringTelemetryInitializer : ITelemetryInitializer
|
||||
{
|
||||
public void Initialize(ITelemetry telemetryItem)
|
||||
{
|
||||
if (telemetryItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We want to annotate telemetry that results from USER CODE of an Availability Test with SyntheticSource information.
|
||||
// This includes all telemetry types.
|
||||
// However, telemetry that results from the Functions Runtime, or from the Availability Monitoring Extension should not me annotated.
|
||||
// Since we use the AvailabilityTestScope's Activity for driving this logic, the above rule is not followed perfectly:
|
||||
// There are some telemetry items that are emitted within the scope of the availability test, but outside of user code.
|
||||
// We cannot avoid it, becasue we have co controll over execution order of the bindings.
|
||||
// Also, some trace telemetry items are affected by this.
|
||||
// Best-effort attmpt to excule such items:
|
||||
|
||||
if (telemetryItem is TraceTelemetry traceTelemetry)
|
||||
{
|
||||
bool isUserTrace = false;
|
||||
if (traceTelemetry.Properties.TryGetValue("Category", out string categoryName))
|
||||
{
|
||||
if (categoryName != null && categoryName.EndsWith(".User", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isUserTrace = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! isUserTrace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we wanted to only include DependencyTelemetry, we would do it something like this:
|
||||
// if (false == (telemetryItem is DependencyTelemetry))
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
Activity activity = Activity.Current;
|
||||
if (TryPopulateContextFromActivity(telemetryItem, activity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity = activity?.Parent;
|
||||
TryPopulateContextFromActivity(telemetryItem, activity);
|
||||
}
|
||||
|
||||
private bool TryPopulateContextFromActivity(ITelemetry telemetryItem, Activity activity)
|
||||
{
|
||||
if (activity != null && activity.IsAvailabilityTestSpan(out string testInfoDescriptor, out string testInvocationInstanceDescriptor))
|
||||
{
|
||||
// The Context fields below are chosen in-line with
|
||||
// https://github.com/microsoft/ApplicationInsights-dotnet/blob/d1865fcba9ad9cbb27b623dd8a1bcdc112bf987e/WEB/Src/Web/Web/WebTestTelemetryInitializer.cs#L47
|
||||
// and the respective value format is adapted for the Coded Test scenario.
|
||||
|
||||
telemetryItem.Context.Operation.SyntheticSource = Format.AvailabilityTest.TelemetryOperationSyntheticSourceMoniker;
|
||||
telemetryItem.Context.User.Id = $"{{{testInfoDescriptor}, OperationId=\"{testInvocationInstanceDescriptor}\"}}";
|
||||
telemetryItem.Context.Session.Id = testInvocationInstanceDescriptor;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
internal static class ActivityExtensions
|
||||
{
|
||||
public static bool IsAvailabilityTestSpan(this Activity activity, out string testInfoDescriptor, out string testInvocationInstanceDescriptor)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
testInfoDescriptor = null;
|
||||
testInvocationInstanceDescriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
string activityOperationName = activity.OperationName;
|
||||
|
||||
if (activityOperationName == null || false == activityOperationName.StartsWith(Format.AvailabilityTest.SpanOperationNameObjectName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
testInfoDescriptor = null;
|
||||
testInvocationInstanceDescriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
testInfoDescriptor = activityOperationName;
|
||||
testInvocationInstanceDescriptor = activity.RootId;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
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");
|
||||
}
|
||||
|
||||
[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 string QuoteOrSpellNull(string str)
|
||||
{
|
||||
if (str == null)
|
||||
{
|
||||
return NullWord;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append('"');
|
||||
builder.Append(str);
|
||||
builder.Append('"');
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AsTextLines<TKey, TValue>(IEnumerable<KeyValuePair<TKey, TValue>> table)
|
||||
{
|
||||
string QuoteIfString<T>(T val)
|
||||
{
|
||||
if (val == null)
|
||||
{
|
||||
return NullWord;
|
||||
}
|
||||
|
||||
if (val is string valStr)
|
||||
{
|
||||
return QuoteOrSpellNull(valStr);
|
||||
}
|
||||
|
||||
return val.ToString();
|
||||
}
|
||||
|
||||
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 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();
|
||||
}
|
||||
|
||||
internal static class AvailabilityTest
|
||||
{
|
||||
public static class HttpHeaderNames
|
||||
{
|
||||
// The names of the headers do not perfectly describe the intent, but we use them for compatibility reasons with existing headers used by GSM.
|
||||
// See here:
|
||||
// https://github.com/microsoft/ApplicationInsights-dotnet/blob/d1865fcba9ad9cbb27b623dd8a1bcdc112bf987e/WEB/Src/Web/Web/WebTestTelemetryInitializer.cs#L15
|
||||
|
||||
public const string TestInvocationInstanceDescriptor = "SyntheticTest-RunId";
|
||||
public const string TestInfoDescriptor = "SyntheticTest-Location";
|
||||
}
|
||||
|
||||
public const string SpanOperationNameObjectName = nameof(AvailabilityTestScope);
|
||||
|
||||
public const string TelemetryOperationSyntheticSourceMoniker = nameof(Microsoft) + "." + nameof(Microsoft.Azure) + "." + nameof(Microsoft.Azure.AvailabilityMonitoring) + "." + nameof(AvailabilityTestScope);
|
||||
|
||||
public static string SpanOperationName(string testDisplayName)
|
||||
{
|
||||
return String.Format("{0}={{TestDisplayName=\"{1}\"}}",
|
||||
SpanOperationNameObjectName,
|
||||
SpellIfNull(testDisplayName));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string SpanId(Activity activity)
|
||||
{
|
||||
return SpellIfNull(activity?.SpanId.ToHexString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
internal interface IAvailabilityTestConfiguration
|
||||
{
|
||||
string TestDisplayName { get; }
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
internal interface IAvailabilityTestInternalConfiguration
|
||||
{
|
||||
string TestDisplayName { get; }
|
||||
|
||||
string LocationDisplayName { get; }
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
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,69 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
internal static class Validate
|
||||
{
|
||||
private const string FallbackParameterName = "specified parameter";
|
||||
|
||||
/// <summary>
|
||||
/// Parameter check for Null.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to be checked.</param>
|
||||
/// <param name="name">Name of the parameter being checked.</param>
|
||||
/// <exception cref="ArgumentNullException">If the value is null.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void NotNull(object value, string name)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(name ?? Validate.FallbackParameterName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// String parameter check with a more informative exception that specifies whether
|
||||
/// the problem was that the string was null or empty.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to be checked.</param>
|
||||
/// <param name="name">Name of the parameter being checked.</param>
|
||||
/// <exception cref="ArgumentNullException">If the value is null.</exception>
|
||||
/// <exception cref="ArgumentException">If the value is an empty string.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void NotNullOrEmpty(string value, string name)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(name ?? Validate.FallbackParameterName);
|
||||
}
|
||||
|
||||
if (value.Length == 0)
|
||||
{
|
||||
throw new ArgumentException((name ?? Validate.FallbackParameterName) + " may not be empty.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// String parameter check with a more informative exception that specifies whether
|
||||
/// the problem was that the string was null, empty or whitespace only.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to be checked.</param>
|
||||
/// <param name="name">Name of the parameter being checked.</param>
|
||||
/// <exception cref="ArgumentNullException">If the value is null.</exception>
|
||||
/// <exception cref="ArgumentException">If the value is an empty string or a string containing whitespaces only;
|
||||
/// the message describes which of these two applies.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void NotNullOrWhitespace(string value, string name)
|
||||
{
|
||||
NotNullOrEmpty(value, name);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException((name ?? Validate.FallbackParameterName) + " may not be whitespace only.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Net.Http;
|
||||
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(IAvailabilityTestInternalConfiguration testConfig,
|
||||
TelemetryConfiguration telemetryConfig,
|
||||
bool flushOnDispose,
|
||||
ILogger log,
|
||||
object logScope)
|
||||
{
|
||||
Validate.NotNull(testConfig, nameof(testConfig));
|
||||
return StartNew(testConfig.TestDisplayName, testConfig.LocationDisplayName, telemetryConfig, flushOnDispose, log, logScope);
|
||||
}
|
||||
|
||||
public static AvailabilityTestScope StartNew(string testDisplayName,
|
||||
string locationDisplayName,
|
||||
TelemetryConfiguration telemetryConfig,
|
||||
bool flushOnDispose,
|
||||
ILogger log,
|
||||
object logScope)
|
||||
|
||||
{
|
||||
log = AvailabilityTest.Log.CreateFallbackLogIfRequired(log);
|
||||
|
||||
var testScope = new AvailabilityTestScope(testDisplayName, locationDisplayName, telemetryConfig, flushOnDispose, log, logScope);
|
||||
testScope.Start();
|
||||
|
||||
return testScope;
|
||||
}
|
||||
|
||||
public static HttpClient NewHttpClient(AvailabilityTestScope availabilityTestScope)
|
||||
{
|
||||
Validate.NotNull(availabilityTestScope, nameof(availabilityTestScope));
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
httpClient.SetAvailabilityTestRequestHeaders(availabilityTestScope);
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public static HttpClient NewHttpClient()
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
httpClient.SetAvailabilityTestRequestHeaders();
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
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 DateTimeOffset StartTime { get; private set; }
|
||||
|
||||
[JsonProperty]
|
||||
public AvailabilityTelemetry DefaultAvailabilityResult { get; private set; }
|
||||
|
||||
public AvailabilityTestInfo()
|
||||
{
|
||||
this.TestDisplayName = null;
|
||||
this.StartTime = default;
|
||||
this.DefaultAvailabilityResult = null;
|
||||
}
|
||||
|
||||
|
||||
internal AvailabilityTestInfo(
|
||||
string testDisplayName,
|
||||
DateTimeOffset startTime,
|
||||
AvailabilityTelemetry defaultAvailabilityResult)
|
||||
{
|
||||
Validate.NotNullOrWhitespace(testDisplayName, nameof(testDisplayName));
|
||||
Validate.NotNull(defaultAvailabilityResult, nameof(defaultAvailabilityResult));
|
||||
|
||||
this.TestDisplayName = testDisplayName;
|
||||
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.NotNull(availabilityTestInfo.DefaultAvailabilityResult, "availabilityTestInfo.DefaultAvailabilityResult");
|
||||
|
||||
this.TestDisplayName = availabilityTestInfo.TestDisplayName;
|
||||
this.StartTime = availabilityTestInfo.StartTime;
|
||||
this.DefaultAvailabilityResult = availabilityTestInfo.DefaultAvailabilityResult;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,512 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using Microsoft.ApplicationInsights;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.ApplicationInsights.Extensibility.Implementation;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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_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 const string SdkVersion = "azcat:1.0.0";
|
||||
|
||||
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 string _activitySpanOperationName = null;
|
||||
private string _distributedOperationId = 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 ActivitySpanOperationName
|
||||
{
|
||||
get
|
||||
{
|
||||
string activitySpanOperationName = _activitySpanOperationName;
|
||||
if (activitySpanOperationName == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(ActivitySpanOperationName)} is not available before this {nameof(AvailabilityTestScope)} has been started.");
|
||||
}
|
||||
|
||||
return activitySpanOperationName;
|
||||
}
|
||||
}
|
||||
|
||||
public string DistributedOperationId
|
||||
{
|
||||
get
|
||||
{
|
||||
string distributedOperationId = _distributedOperationId;
|
||||
if (distributedOperationId == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(DistributedOperationId)} is not available before this {nameof(AvailabilityTestScope)} has been started.");
|
||||
}
|
||||
|
||||
return distributedOperationId;
|
||||
}
|
||||
}
|
||||
|
||||
public AvailabilityTestScope(string testDisplayName, string locationDisplayName, TelemetryConfiguration telemetryConfig, bool flushOnDispose, ILogger log, object logScope)
|
||||
{
|
||||
Validate.NotNullOrWhitespace(testDisplayName, nameof(testDisplayName));
|
||||
Validate.NotNullOrWhitespace(locationDisplayName, nameof(locationDisplayName));
|
||||
Validate.NotNull(telemetryConfig, nameof(telemetryConfig));
|
||||
|
||||
this.TestDisplayName = testDisplayName;
|
||||
this.LocationDisplayName = locationDisplayName;
|
||||
|
||||
_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}\"}}",
|
||||
TestDisplayName);
|
||||
|
||||
TransitionStage(from: Stage.New, to: Stage.Started);
|
||||
|
||||
// Start activity:
|
||||
_activitySpanOperationName = Format.AvailabilityTest.SpanOperationName(TestDisplayName);
|
||||
_activitySpan = new Activity(_activitySpanOperationName).Start();
|
||||
_activitySpanId = Format.AvailabilityTest.SpanId(_activitySpan);
|
||||
|
||||
_distributedOperationId = _activitySpan.RootId;
|
||||
|
||||
// Start the timer:
|
||||
_startTime = DateTimeOffset.Now;
|
||||
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Start)} finished:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", "
|
||||
+ " SpanId=\"{SpanId}\", SpanIdFormat=\"{SpanIdFormat}\", StartTime=\"{StartTime}\", OperationName=\"{OperationName}\"}}",
|
||||
TestDisplayName, _activitySpanId, _activitySpan.IdFormat, _startTime.ToString("o"), _activitySpanOperationName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Complete(bool success)
|
||||
{
|
||||
using (_log.BeginScopeSafe(_logScope))
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} invoked with Success={{Success}}:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", SpanId=\"{SpanId}\"}}",
|
||||
success, TestDisplayName, 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}\", SpanId=\"{SpanId}\"}}."
|
||||
+ " This indicates that the test result was not set by calling Complete(..); a Failure will be assumed.",
|
||||
disposing, TestDisplayName, 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}\", SpanId=\"{SpanId}\"}}",
|
||||
Format.SpellIfNull(error?.GetType()?.Name), Format.LimitLength(error.Message, 100, trim: true), isTimeout,
|
||||
TestDisplayName, 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}\", SpanId=\"{SpanId}\"}}",
|
||||
TestDisplayName, 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 = this.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 = this.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);
|
||||
}
|
||||
|
||||
// The user may or may not have set the ID of the availability result telemetry.
|
||||
// Either way, we must set it to the right value, otherwise distributed tracing will break:
|
||||
availabilityResult.Id = _activitySpanId;
|
||||
|
||||
// Similarly, whatever iKey the user set, we insist on the value from this scope's telemetry configuration to make
|
||||
// sure everything ends up in the right place.
|
||||
// Users may request a feature to allow sending availabuility results to an iKey that is different from other telemetry.
|
||||
// If so, we should consider exposing a corresponsing parameter on the ctor of this class and - corresponsingly - on
|
||||
// the AvailabilityTestResultAttribute. In that case we must also do the appropriate thing with the traces sent by this
|
||||
// class. Sending them and the telemetry result to different destinations may be a failure pit for the user.
|
||||
availabilityResult.Context.InstrumentationKey = _instrumentationKey;
|
||||
|
||||
// Set custom SDK version to differentiate CAT tests from the regular availability telemetry
|
||||
availabilityResult.Context.GetInternalContext().SdkVersion = SdkVersion;
|
||||
|
||||
// Store the result, but do not send it until SendResult() is called:
|
||||
_finalAvailabilityResult = availabilityResult;
|
||||
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(Complete)} finished"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", "
|
||||
+ " SpanId=\"{SpanId}\", StartTime=\"{StartTime}\", EndTime=\"{EndTime}\", Duration=\"{Duration}\", Success=\"{Success}\"}}",
|
||||
TestDisplayName, _activitySpanId, _startTime.ToString("o"), _endTime.ToString("o"), duration, availabilityResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public AvailabilityTestInfo CreateAvailabilityTestInfo()
|
||||
{
|
||||
AvailabilityTelemetry defaultAvailabilityResult = CreateDefaultAvailabilityResult();
|
||||
var testInfo = new AvailabilityTestInfo(TestDisplayName, _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}\", SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", Disposing=\"{Disposing}\"}}",
|
||||
TestDisplayName, 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}\", SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", Disposing=\"{Disposing}\"}}",
|
||||
TestDisplayName, Format.SpellIfNull(_activitySpanId), CurrentStage, disposing);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendResult()
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)}.{nameof(SendResult)} beginning:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", SpanId=\"{SpanId}\"}}",
|
||||
TestDisplayName, 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}\", SpanId=\"{SpanId}\"}}",
|
||||
TestDisplayName, Format.SpellIfNull(_activitySpanId));
|
||||
}
|
||||
|
||||
private void FlushIfRequested()
|
||||
{
|
||||
if (_flushOnDispose)
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)} is flushing its {nameof(TelemetryClient)}:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", FlushOnDispose=\"{FlushOnDispose}\"}}",
|
||||
TestDisplayName, Format.SpellIfNull(_activitySpanId), CurrentStage, _flushOnDispose);
|
||||
|
||||
_telemetryClient.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
_log?.LogInformation($"{nameof(AvailabilityTestScope)} is NOT flushing its {nameof(TelemetryClient)}:"
|
||||
+ " {{TestDisplayName=\"{TestDisplayName}\", SpanId=\"{SpanId}\", CurrentStage=\"{CurrentStage}\", FlushOnDispose=\"{FlushOnDispose}\"}}",
|
||||
TestDisplayName, 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);
|
||||
|
||||
// Note: this method is not thread-safe in respect to Stage transitions
|
||||
// (e.g. we may have just transitioned into Started, 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 = _activitySpanId;
|
||||
}
|
||||
|
||||
availabilityResult.Duration = TimeSpan.Zero;
|
||||
availabilityResult.Success = false;
|
||||
availabilityResult.Name = TestDisplayName;
|
||||
|
||||
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,63 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.Azure.AvailabilityMonitoring
|
||||
{
|
||||
public static class HttpClientExtensions
|
||||
{
|
||||
public static void SetAvailabilityTestRequestHeaders(this HttpClient httpClient)
|
||||
{
|
||||
Validate.NotNull(httpClient, nameof(httpClient));
|
||||
|
||||
Activity currentActivity = Activity.Current;
|
||||
if (currentActivity == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot set Availability Monitoring request headers for this {nameof(HttpClient)} because there is no"
|
||||
+ $" valid Activity.Current value representing an availability test span."
|
||||
+ $" Ensure that you are calling this API inside a valid and active {nameof(AvailabilityTestScope)},"
|
||||
+ $" or explicitly specify an {nameof(AvailabilityTestScope)} or an {nameof(Activity)}-span representing such a scope,"
|
||||
+ $" so that correct header values can be determined.");
|
||||
}
|
||||
|
||||
httpClient.SetAvailabilityTestRequestHeaders(currentActivity);
|
||||
}
|
||||
|
||||
public static void SetAvailabilityTestRequestHeaders(this HttpClient httpClient, AvailabilityTestScope availabilityTestScope)
|
||||
{
|
||||
Validate.NotNull(httpClient, nameof(httpClient));
|
||||
Validate.NotNull(availabilityTestScope, nameof(availabilityTestScope));
|
||||
|
||||
string testInfoDescriptor = availabilityTestScope.ActivitySpanOperationName;
|
||||
string testInvocationInstanceDescriptor = availabilityTestScope.DistributedOperationId;
|
||||
|
||||
httpClient.SetAvailabilityTestRequestHeaders(testInfoDescriptor, testInvocationInstanceDescriptor);
|
||||
}
|
||||
|
||||
public static void SetAvailabilityTestRequestHeaders(this HttpClient httpClient, Activity activitySpan)
|
||||
{
|
||||
Validate.NotNull(httpClient, nameof(httpClient));
|
||||
Validate.NotNull(activitySpan, nameof(activitySpan));
|
||||
|
||||
if (! activitySpan.IsAvailabilityTestSpan(out string testInfoDescriptor, out string testInvocationInstanceDescriptor))
|
||||
{
|
||||
throw new ArgumentException($"The specified {nameof(activitySpan)} does not represent an activity span that was set up by an {nameof(AvailabilityTestScope)}"
|
||||
+ $" ({nameof(activitySpan.OperationName)}={Format.QuoteOrSpellNull(testInfoDescriptor)}).");
|
||||
}
|
||||
|
||||
httpClient.SetAvailabilityTestRequestHeaders(testInfoDescriptor, testInvocationInstanceDescriptor);
|
||||
}
|
||||
|
||||
public static void SetAvailabilityTestRequestHeaders(this HttpClient httpClient, string testInfoDescriptor, string testInvocationInstanceDescriptor)
|
||||
{
|
||||
Validate.NotNull(httpClient, nameof(httpClient));
|
||||
Validate.NotNullOrWhitespace(testInfoDescriptor, nameof(testInfoDescriptor));
|
||||
Validate.NotNullOrWhitespace(testInvocationInstanceDescriptor, nameof(testInvocationInstanceDescriptor));
|
||||
|
||||
HttpRequestHeaders headers = httpClient.DefaultRequestHeaders;
|
||||
headers.Add(Format.AvailabilityTest.HttpHeaderNames.TestInfoDescriptor, Format.SpellIfNull(testInfoDescriptor));
|
||||
headers.Add(Format.AvailabilityTest.HttpHeaderNames.TestInvocationInstanceDescriptor, Format.SpellIfNull(testInvocationInstanceDescriptor));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.AvailabilityMonitoring.Extensions;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.Extensions
|
||||
{
|
||||
public static class AvailabilityMonitoringWebJobsBuilderExtensions
|
||||
{
|
||||
public static IWebJobsBuilder AddAvailabilityMonitoring(this IWebJobsBuilder builder)
|
||||
{
|
||||
Validate.NotNull(builder, nameof(builder));
|
||||
|
||||
IServiceCollection serviceCollection = builder.Services;
|
||||
|
||||
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
|
||||
|
||||
builder.AddExtension<AvailabilityMonitoringExtensionConfigProvider>();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,176 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,330 +0,0 @@
|
|||
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.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"The function metadata for function \"{functionName}\" was parsed;"
|
||||
+ " it contained a list of bindings, but the list had no entries.");
|
||||
}
|
||||
|
||||
foreach (BindingMetadata bindingMetadata in functionMetadata.Bindings)
|
||||
{
|
||||
if (bindingMetadata == null || bindingMetadata.Type == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bindingMetadata.Type.Equals(AvailabilityTestResultAttribute.BindingTypeName, StringComparison.OrdinalIgnoreCase)
|
||||
|| bindingMetadata.Type.Equals(nameof(AvailabilityTestResultAttribute), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isAvailabilityTest = true;
|
||||
testConfig = bindingMetadata;
|
||||
|
||||
log?.LogDebug(FinishAnalysisLogMessage, functionName, isAvailabilityTest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isAvailabilityTest = false;
|
||||
testConfig = null;
|
||||
|
||||
log?.LogDebug(FinishAnalysisLogMessage, functionName, isAvailabilityTest);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
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 EnvironmentVariableNames
|
||||
{
|
||||
public const string LocationDisplayName1 = "REGION_NAME";
|
||||
public const string LocationDisplayName2 = "Location";
|
||||
}
|
||||
|
||||
private class AvailabilityTestConfiguration : IAvailabilityTestInternalConfiguration
|
||||
{
|
||||
public string TestDisplayName { get; }
|
||||
public string LocationDisplayName { get; }
|
||||
public AvailabilityTestConfiguration(string testDisplayName, string locationDisplayName)
|
||||
{
|
||||
this.TestDisplayName = testDisplayName;
|
||||
this.LocationDisplayName = locationDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly INameResolver _nameResolver;
|
||||
|
||||
public AvailabilityTestScopeSettingsResolver(INameResolver nameResolver)
|
||||
{
|
||||
_nameResolver = nameResolver;
|
||||
}
|
||||
|
||||
public IAvailabilityTestInternalConfiguration Resolve(IAvailabilityTestConfiguration testConfig, string functionName)
|
||||
{
|
||||
// Test Display Name:
|
||||
string testDisplayName = testConfig?.TestDisplayName;
|
||||
if (String.IsNullOrWhiteSpace(testDisplayName))
|
||||
{
|
||||
testDisplayName = functionName;
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(testDisplayName))
|
||||
{
|
||||
throw new ArgumentException("The Availability Test Display Name must be set, but it was not."
|
||||
+ " To set that value, explicitly set the property \"{nameof(AvailabilityTestResultAttribute.TestDisplayName)}\""
|
||||
+ $" Otherwise the name of the Azure Function will be used as a fallback.");
|
||||
}
|
||||
|
||||
|
||||
// Location Display Name:
|
||||
string locationDisplayName = TryFillValueFromEnvironment(null, EnvironmentVariableNames.LocationDisplayName1);
|
||||
locationDisplayName = TryFillValueFromEnvironment(locationDisplayName, EnvironmentVariableNames.LocationDisplayName2);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(locationDisplayName))
|
||||
{
|
||||
throw new ArgumentException("The Location Display Name of the Availability Test must be set, but it was not."
|
||||
+ " Check that one of the following environment variables are set:"
|
||||
+ $" (a) \"{EnvironmentVariableNames.LocationDisplayName1}\";"
|
||||
+ $" (b) \"{EnvironmentVariableNames.LocationDisplayName2}\".");
|
||||
}
|
||||
|
||||
// We did our best to get the config.
|
||||
|
||||
var resolvedConfig = new AvailabilityTestScopeSettingsResolver.AvailabilityTestConfiguration(testDisplayName, locationDisplayName);
|
||||
return resolvedConfig;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
AvailabilityTelemetry availabilityResult = JsonConvert.DeserializeObject<AvailabilityTelemetry>(str);
|
||||
return availabilityResult;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
throw new FormatException($"Cannot parse the availability result."
|
||||
+ $" The availability results must be a valid JSON representation of an {nameof(AvailabilityTelemetry)} object (partial objects are OK).",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,284 +0,0 @@
|
|||
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, INameResolver nameResolver)
|
||||
{
|
||||
Validate.NotNull(availabilityTestRegistry, nameof(availabilityTestRegistry));
|
||||
Validate.NotNull(telemetryConfiguration, nameof(telemetryConfiguration));
|
||||
|
||||
_availabilityTestRegistry = availabilityTestRegistry;
|
||||
_telemetryConfiguration = telemetryConfiguration;
|
||||
_availabilityTestScopeSettingsResolver = new AvailabilityTestScopeSettingsResolver(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
|
||||
{
|
||||
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}\" }} }}",
|
||||
functionName, functionInstanceId, testConfig.TestDisplayName);
|
||||
|
||||
// - 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
|
||||
IAvailabilityTestInternalConfiguration 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}\" }} }}",
|
||||
functionName, functionInstanceId,
|
||||
error.GetType().Name, Format.LimitLength(error.Message, MaxErrorMessageLength, trim: true),
|
||||
testScope.TestDisplayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
[Binding]
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class AvailabilityTestInfoAttribute : Attribute
|
||||
{
|
||||
internal const string BindingTypeName = "AvailabilityTestInfo";
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
public static class AvailabilityTestInterval
|
||||
{
|
||||
private const string Moniker = "AvailabilityTestInterval";
|
||||
|
||||
public const string Minute01 = "% AvailabilityTestInterval.Minute01 %";
|
||||
public const string Minutes05 = "% AvailabilityTestInterval.Minutes05 %";
|
||||
public const string Minutes10 = "% AvailabilityTestInterval.Minutes10 %";
|
||||
public const string Minutes15 = "% AvailabilityTestInterval.Minutes15 %";
|
||||
|
||||
private static class ValidSpecifiers
|
||||
{
|
||||
public static readonly string Minutes01 = RemoveEnclosingNameResolverTags("% AvailabilityTestInterval.Minutes01 %");
|
||||
public static readonly string Minute01 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minute01);
|
||||
public static readonly string Minutes05 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minutes05);
|
||||
public static readonly string Minutes10 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minutes10);
|
||||
public static readonly string Minutes15 = RemoveEnclosingNameResolverTags(AvailabilityTestInterval.Minutes15);
|
||||
}
|
||||
|
||||
private static readonly Random Rnd = new Random();
|
||||
|
||||
internal static bool IsSpecification(string testIntervalSpec)
|
||||
{
|
||||
// Ignore nulls and empty strings:
|
||||
if (String.IsNullOrWhiteSpace(testIntervalSpec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If starts AND ends with '%', throw those away (this also trims):
|
||||
testIntervalSpec = RemoveEnclosingNameResolverTags(testIntervalSpec);
|
||||
|
||||
// Check that the specified 'name' starts with the right prefix:
|
||||
return testIntervalSpec.StartsWith(Moniker, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static int Parse(string testIntervalSpec)
|
||||
{
|
||||
// Ensure not null:
|
||||
testIntervalSpec = Format.SpellIfNull(testIntervalSpec);
|
||||
|
||||
// Remove '%' (if any) and trim:
|
||||
testIntervalSpec = RemoveEnclosingNameResolverTags(testIntervalSpec);
|
||||
|
||||
if (AvailabilityTestInterval.ValidSpecifiers.Minute01.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (AvailabilityTestInterval.ValidSpecifiers.Minutes01.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (AvailabilityTestInterval.ValidSpecifiers.Minutes05.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (AvailabilityTestInterval.ValidSpecifiers.Minutes10.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
if (AvailabilityTestInterval.ValidSpecifiers.Minutes15.Equals(testIntervalSpec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 15;
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid availability test interval specification: \"{testIntervalSpec}\""
|
||||
+ $" (Expected format is \"{Moniker}.MinSpec\", where \'MinSpec\' is one of"
|
||||
+ $" {{\"{nameof(ValidSpecifiers.Minute01)}\", \"{nameof(ValidSpecifiers.Minutes05)}\","
|
||||
+ $" \"{nameof(ValidSpecifiers.Minutes10)}\", \"{nameof(ValidSpecifiers.Minutes15)}\"}}).");
|
||||
}
|
||||
|
||||
internal static string CreateCronIntervalSpecWithRandomOffset(int intervalMins)
|
||||
{
|
||||
// The basic format of the CRON expressions is:
|
||||
// {second} {minute} {hour} {day} {month} {day of the week}
|
||||
// E.g.
|
||||
// TimerTrigger("15 2/5 * * * *")
|
||||
// means every 5 minutes starting at 2, on 15 secs past the minute, i.e., 02:15, 07:15, 12:15, 17:15, ...
|
||||
|
||||
if (intervalMins != 1 && intervalMins != 5 && intervalMins != 10 && intervalMins != 15)
|
||||
{
|
||||
throw new ArgumentException($"Invalid number of minutes in the interval: valid values are M = 1, 5, 10 or 15; specified value is \'{intervalMins}\'.");
|
||||
}
|
||||
|
||||
int intervalTotalSecs = intervalMins * 60;
|
||||
int rndOffsTotalSecs = Rnd.Next(0, intervalTotalSecs);
|
||||
int rndOffsWholeMins = rndOffsTotalSecs / 60;
|
||||
int rndOffsSubminSecs = rndOffsTotalSecs % 60;
|
||||
|
||||
string cronSpec = $"{rndOffsSubminSecs} {rndOffsWholeMins}/{intervalMins} * * * *";
|
||||
return cronSpec;
|
||||
}
|
||||
|
||||
private static string RemoveEnclosingNameResolverTags(string s)
|
||||
{
|
||||
// Trim:
|
||||
s = s.Trim();
|
||||
|
||||
// If starts AND ends with '%', remove those:
|
||||
while (s.Length > 2 && s.StartsWith("%") && s.EndsWith("%"))
|
||||
{
|
||||
s = s.Substring(1, s.Length - 2);
|
||||
s = s.Trim();
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.Azure.AvailabilityMonitoring;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring
|
||||
{
|
||||
[Binding]
|
||||
[AttributeUsage(AttributeTargets.ReturnValue)]
|
||||
public class AvailabilityTestResultAttribute : Attribute, IAvailabilityTestConfiguration
|
||||
{
|
||||
internal const string BindingTypeName = "AvailabilityTestResult";
|
||||
|
||||
[AutoResolve]
|
||||
public string TestDisplayName { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
<Project>
|
||||
|
||||
<!-- Include this file into your project file using <Import Project="Version.props" /> or similar. -->
|
||||
<!-- Follow the instructions in the comments to set the version and the version-date. -->
|
||||
<!-- This will make sure, your assmebly and file versions, as well as your NuGet package version are managed correctly. -->
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Update this section EVERY time the component is shipped/released. -->
|
||||
<!-- (See Semantic Versioning at https://semver.org for guidelines on specifying the version.) -->
|
||||
|
||||
<!-- You MUST update the version every 7 years for the 'VersionTimestamp' to work correctly. -->
|
||||
<!-- Any part of the version can be updated; use 'VersionPatch' if nothing else has changed. -->
|
||||
|
||||
<VersionMajor>1</VersionMajor>
|
||||
<VersionMinor>0</VersionMinor>
|
||||
<VersionPatch>0</VersionPatch>
|
||||
|
||||
<!-- Date when Version was changed last. -->
|
||||
<!-- - You MUST update this when any of 'VersionMajor', 'VersionMinor' or 'VersionPatch' is changed. -->
|
||||
<!-- - You MUST NOT update this when 'VersionPrerelease' is changed. -->
|
||||
<!-- (If you do, 'VersionTimestamp' will restart resulting in higher package versions having files with lower versions. E.g.: -->
|
||||
<!-- Package ver '1.2.3-beta.1' has File ver '1.2.3.22222', and -->
|
||||
<!-- Package ver '1.2.3-beta.2' has File ver '1.2.3.11111'.) -->
|
||||
<!-- - You MUST update the version every 7 years. -->
|
||||
<!-- (Any part of the version can be updated; use 'VersionPatch' if nothing else has changed. -->
|
||||
<!-- (If you do not change the version at these intervals, -->
|
||||
<!-- then 'VersionTimestamp' will restart and lower file versions will follow after higher file versions.) -->
|
||||
<VersionDate>2020-04-09</VersionDate>
|
||||
|
||||
<!-- VersionPrerelease format examples: alpha.1, alpha.2, beta.1, beta.2; EMPTY for stable releases. -->
|
||||
<VersionPrerelease>alpha.3</VersionPrerelease>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
<!-- 'VersionTimestamp' is the number of HOURS that passed since 'VersionDate'. -->
|
||||
<!-- Assembly and File version components must fit into a UInt16 (max 65535), so we apply modulo 65534. -->
|
||||
<!-- This means that 'VersionTimestamp' cycles approx. every 7 years + a few 5 months. -->
|
||||
<!-- Thus, including a safety perion, we MUST increase the version at least every 7 years, even if nothing else changes. -->
|
||||
<VersionTimestamp>$([System.Math]::Truncate(
|
||||
$([MSBuild]::Modulo( $([System.DateTime]::UtcNow.Subtract( $([System.DateTime]::Parse($(VersionDate))) ).TotalHours), 65534))
|
||||
))</VersionTimestamp>
|
||||
|
||||
<!-- 'BuildTimestamp' does not have the same 16-bit restriction as 'VersionTimestamp'. So we use a desctriprive date timestamp. -->
|
||||
<BuildTimestamp>$([System.DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))</BuildTimestamp>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$(VersionPrerelease) == ''">
|
||||
<!-- If 'VersionPrerelease' is NOT set, then 'VersionMonikerSuffix' is not used. -->
|
||||
<VersionMonikerSuffix></VersionMonikerSuffix>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$(VersionPrerelease) != ''">
|
||||
<!-- If 'VersionPrerelease' is set, then 'VersionMonikerSuffix' is comprised of 'VersionPrerelease' and 'BuildTimestamp'. -->
|
||||
<!-- We also need to turn off the warning about SemVer 2 (https://docs.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu5105). -->
|
||||
<NoWarn>NU5105</NoWarn>
|
||||
<VersionMonikerSuffix>-$(VersionPrerelease)+$(BuildTimestamp)</VersionMonikerSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionMonikerBase>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionMonikerBase>
|
||||
<AssemblyVersion>$(VersionMonikerBase).$(VersionTimestamp)</AssemblyVersion>
|
||||
<FileVersion>$(VersionMonikerBase).$(VersionTimestamp)</FileVersion>
|
||||
<Version>$(VersionMonikerBase)$(VersionMonikerSuffix)</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
|
@ -1,75 +0,0 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29519.181
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring", "AvailabilityMonitoring-Extension\Microsoft.Azure.WebJobs.Extensions.AvailabilityMonitoring.csproj", "{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvailabilityMonitoring-Extension-DemoConsole", "Demos\AvailabilityMonitoring-Extension-DemoConsole\AvailabilityMonitoring-Extension-DemoConsole.csproj", "{0AA499F5-F903-4FAC-BF3B-EA75D6735167}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvailabilityMonitoring-Extension-DemoFunction", "Demos\AvailabilityMonitoring-Extension-DemoFunction\AvailabilityMonitoring-Extension-DemoFunction.csproj", "{32B33932-2F92-4236-A286-3A085781E11C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvailabilityMonitoring-Extension-MonitoredAppSample", "Demos\AvailabilityMonitoring-Extension-MonitoredAppSample\AvailabilityMonitoring-Extension-MonitoredAppSample.csproj", "{D52BC15A-37C0-45A5-94C5-1AC546785846}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlainSimplePrototype", "PlainSimplePrototype\PlainSimplePrototype.csproj", "{9275BF08-E979-44A3-96C1-7202CB1A52D0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demos", "Demos", "{D87E0179-8A11-477C-9AA6-6ACD0F155BC5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvailabilityMonitoring-Extension-MonitoredFuncSample", "Demos\AvailabilityMonitoring-Extension-MonitoredFuncSample\AvailabilityMonitoring-Extension-MonitoredFuncSample.csproj", "{2DCF0A71-BE90-4942-B152-68AD127FE4E5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvailabilityMonitoring-Extension-SimpleScenarioCatFunction", "Demos\AvailabilityMonitoring-Extension-SimpleScenarioCatFunction\AvailabilityMonitoring-Extension-SimpleScenarioCatFunction.csproj", "{4DAE7E06-63DB-42D1-BDC7-04D90BB72961}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{30D20E09-DFAB-4CB4-8915-1A2D794F3FCE}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
Directory.Build.props = Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13CD7C8A-600D-4F5B-84B4-40D2575E21F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0AA499F5-F903-4FAC-BF3B-EA75D6735167}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{32B33932-2F92-4236-A286-3A085781E11C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{32B33932-2F92-4236-A286-3A085781E11C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{32B33932-2F92-4236-A286-3A085781E11C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{32B33932-2F92-4236-A286-3A085781E11C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D52BC15A-37C0-45A5-94C5-1AC546785846}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D52BC15A-37C0-45A5-94C5-1AC546785846}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D52BC15A-37C0-45A5-94C5-1AC546785846}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D52BC15A-37C0-45A5-94C5-1AC546785846}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9275BF08-E979-44A3-96C1-7202CB1A52D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9275BF08-E979-44A3-96C1-7202CB1A52D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9275BF08-E979-44A3-96C1-7202CB1A52D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9275BF08-E979-44A3-96C1-7202CB1A52D0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2DCF0A71-BE90-4942-B152-68AD127FE4E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2DCF0A71-BE90-4942-B152-68AD127FE4E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2DCF0A71-BE90-4942-B152-68AD127FE4E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2DCF0A71-BE90-4942-B152-68AD127FE4E5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4DAE7E06-63DB-42D1-BDC7-04D90BB72961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4DAE7E06-63DB-42D1-BDC7-04D90BB72961}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4DAE7E06-63DB-42D1-BDC7-04D90BB72961}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4DAE7E06-63DB-42D1-BDC7-04D90BB72961}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0AA499F5-F903-4FAC-BF3B-EA75D6735167} = {D87E0179-8A11-477C-9AA6-6ACD0F155BC5}
|
||||
{32B33932-2F92-4236-A286-3A085781E11C} = {D87E0179-8A11-477C-9AA6-6ACD0F155BC5}
|
||||
{D52BC15A-37C0-45A5-94C5-1AC546785846} = {D87E0179-8A11-477C-9AA6-6ACD0F155BC5}
|
||||
{2DCF0A71-BE90-4942-B152-68AD127FE4E5} = {D87E0179-8A11-477C-9AA6-6ACD0F155BC5}
|
||||
{4DAE7E06-63DB-42D1-BDC7-04D90BB72961} = {D87E0179-8A11-477C-9AA6-6ACD0F155BC5}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B62196E4-D124-4879-B726-71519C486A1A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
Двоичные данные
src/Demos/AppSettings.png
Двоичные данные
src/Demos/AppSettings.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 27 KiB |
Двоичные данные
src/Demos/AvailabilityResults.png
Двоичные данные
src/Demos/AvailabilityResults.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 39 KiB |
|
@ -1,326 +0,0 @@
|
|||
# Azure Function integration with Availability testing in Azure Monitor*
|
||||
|
||||
1. [API testing](#API-testing-in-Azure-Function)
|
||||
2. [Browser testing](#Browser-testing-in-Azure-Function)
|
||||
3. [Authentication in Azure Functions](#Authentication-in-Azure-Functions)
|
||||
|
||||
*this is a part of private preview in Azure Monitor, please contact casocha@microsoft.com for more details if you would like to participate in.
|
||||
|
||||
<br/>
|
||||
|
||||
# API testing in Azure Function
|
||||
|
||||
### Create Azure Function with your custom code
|
||||
|
||||
1) Create Azure Function template in VS (VS 2017 or VS 2019) for C# or VSCode for JavaScript and choose HttpTrigger as an initial template configuration
|
||||
2) Write custom code to ping your application
|
||||
3) Deploy your code to Azure Function app and connect Function app to Application Insights
|
||||
|
||||
|
||||
### Setup availability test
|
||||
1) Get and copy your Azure Function URL by navigation to Function App -> Functions -> your function Overview blade in the Azure Portal
|
||||
|
||||
![](./GetFunctionUrl.png)
|
||||
|
||||
2) Navigate to Availability blade in the Azure Portal and create new web test, choose regular URL ping test and paste saved Azure Function URL
|
||||
|
||||
**NOTE** Selected locations actually do not matter, so you can choose only one or you might want to choose mutiple to have test execution more frequently (by choosing only one location you will have 5mins default test frequency).
|
||||
|
||||
![](./CreateTest.png)
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
### Optional AppInsights SDK configuration for JavaScript
|
||||
|
||||
Dependency calls are utomatically collected for C# but you need to do manual enablement of AppInsights SDK for Node.JS in order to collect generated outgoing dependency calls from your Function.
|
||||
Full documentation for Node.JS SDK can be found [here](https://github.com/microsoft/ApplicationInsights-node.js/blob/develop/README.md).
|
||||
|
||||
1) Enable Application Insights SDK for Node.JS:
|
||||
|
||||
``` powershell
|
||||
npm install applicationinsights --save
|
||||
```
|
||||
|
||||
2) Setup SDK in the Function code:
|
||||
|
||||
``` javascript
|
||||
const appInsights = require('applicationinsights');
|
||||
appInsights.setup().start();
|
||||
```
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# Browser testing in Azure Function
|
||||
|
||||
Headless browser support for Chromium was recently added to the Azure Function consumption plan in Linux (not supported in Windows consumption plan) and you can either use it with some customizations (see references below) or build custom Docker image that includes chromium or other browser of your choice and deploy it to the Premium plan.
|
||||
|
||||
## 1. Playwright
|
||||
|
||||
### Prerequisites
|
||||
Ensure that you have correct tooling for Azure Functions v3:
|
||||
|
||||
1. Install NodeJs
|
||||
|
||||
You must use NodeJs versions 10.x.x or 12.x.x. Use official [NodeJS website](https://nodejs.org/en/download/) to download and install the latest 12.x.x version. To check the installed version run the following command:
|
||||
``` powershell
|
||||
node -v
|
||||
```
|
||||
|
||||
2. Install Azure Core Tools
|
||||
|
||||
The installation in Windows is through NodeJs, running the following command line in PowerShell or cmd:
|
||||
``` powershell
|
||||
npm install -g azure-functions-core-tools@3
|
||||
```
|
||||
To check the installed version run the following command:
|
||||
``` powershell
|
||||
func --version
|
||||
```
|
||||
|
||||
### Pre-configuration
|
||||
1. Create Azure Function template in VSCode for JavaScript and choose HttpTrigger as an initial template configuration. Ensure that Function v3 was created by checking **.vscode/settings.json** file - it should have projectRuntime set to 3:
|
||||
|
||||
``` json
|
||||
"azureFunctions.projectRuntime": "~3",
|
||||
```
|
||||
|
||||
2. Install Playwright npm module. Ensure that you are using the latest **1.5.\*** version, otherwise upgrade it to the latest.
|
||||
|
||||
``` powershell
|
||||
npm install playwright-chromium
|
||||
```
|
||||
|
||||
3. To have full integration with Playwright and have an ability to view all the actions taken during the testing, including failures and screenshots collected for each step you also need to install experimental appinsights-playwright npm package from myget.
|
||||
|
||||
``` powershell
|
||||
npm install appinsights-playwright --registry=https://www.myget.org/F/applicationinsights-cat/npm/
|
||||
```
|
||||
|
||||
### Generate the Playwright code
|
||||
|
||||
The easiest way to generate the actual test script is to use Record & Replay [CLI tool](https://github.com/microsoft/playwright-cli). This is also a great way to learn the Playwright API. Run **npx playwright-cli codegen** in your terminal and try it out now!
|
||||
|
||||
### Enable Application Insights collection
|
||||
|
||||
Use this sample as a prototype and replace you custom Playwright execution in this [function template](https://github.com/Azure/azure-functions-availability-monitoring-extension/tree/master/src/Demos/JavaScript-Monitoring-Samples).
|
||||
|
||||
Details:
|
||||
- Initialize AppInsightsContextListener at the beggining of your function code
|
||||
- Wrap actual test execution in the try/catch/finally to have data still being collected and sent in the case of failure
|
||||
- Serialize collected data in the response at the **finally** section
|
||||
|
||||
Resulting code should look like:
|
||||
|
||||
``` javascript
|
||||
const { chromium } = require('playwright-chromium');
|
||||
const { AppInsightsContextListener } = require('appinsights-playwright')
|
||||
|
||||
// initialize AppInsightsListener to collect information about Playwright execution
|
||||
// set input parameter to:
|
||||
// - 'AutoCollect' to collect screenshots after every action taken
|
||||
// - 'OnFailure' to collect screenshots only for the failed actions
|
||||
// - 'No' to skip the screenshots collection. Default value.
|
||||
const listener = new AppInsightsContextListener('AutoCollect');
|
||||
|
||||
module.exports = async function (context, req) {
|
||||
try {
|
||||
// your custom Playwright code
|
||||
} catch (err) {
|
||||
context.log.error(err);
|
||||
} finally {
|
||||
// Serialize collected data into the response
|
||||
context.res = listener.serializeData();
|
||||
context.done();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**NOTE**
|
||||
If you're using Record & Replay tool for code generation do the following modifications:
|
||||
1) Rename **context** field to something else as it will conflict with context defined in HttpTrigger template and will cause compilcation issues. The following lines should be updated:
|
||||
``` javascript
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
// playwright actions...
|
||||
await context.close();
|
||||
```
|
||||
2) Set **headless** parameter in **chromium.launch()** function to **true** (or remove it as **true** is default value)
|
||||
|
||||
|
||||
Here is the full sample before and after the code changes:
|
||||
|
||||
1. Before:
|
||||
``` javascript
|
||||
const { chromium } = require('playwright-chromium');
|
||||
const { AppInsightsContextListener } = require('appinsights-playwright')
|
||||
|
||||
module.exports = async function (context, req) {
|
||||
context.log("Function entered.");
|
||||
const browser = await chromium.launch({
|
||||
headless: false
|
||||
});
|
||||
const context = await browser.newContext();
|
||||
|
||||
// Open new page
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('https://www.bing.com/?toWww=1&redig=037D00CBA156451D8AFBC24E86985CDF');
|
||||
|
||||
// Close page
|
||||
await page.close();
|
||||
|
||||
// ---------------------
|
||||
await context.close();
|
||||
await browser.close();
|
||||
};
|
||||
```
|
||||
|
||||
1. After:
|
||||
``` javascript
|
||||
const { chromium } = require('playwright-chromium');
|
||||
const { AppInsightsContextListener } = require('appinsights-playwright')
|
||||
|
||||
// initialize AppInsightsListener to collect information about Playwright execution
|
||||
// set input parameter to:
|
||||
// - 'AutoCollect' to collect screenshots after every action taken
|
||||
// - 'OnFailure' to collect screenshots only for the failed actions
|
||||
// - 'No' to skip the screenshots collection. Default value.
|
||||
const listener = new AppInsightsContextListener('AutoCollect');
|
||||
|
||||
module.exports = async function (context, req) {
|
||||
context.log("Function entered.");
|
||||
|
||||
try {
|
||||
const browser = await chromium.launch();
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
// Open new page
|
||||
const page = await browserContext.newPage();
|
||||
|
||||
await page.goto('https://www.bing.com/?toHttps=1&redig=69CC3FCA85A84B3AAFA1D638964EA2B1');
|
||||
|
||||
// Close page
|
||||
await page.close();
|
||||
|
||||
// ---------------------
|
||||
await browserContext.close();
|
||||
await browser.close();
|
||||
|
||||
} catch (err) {
|
||||
context.log.error(err);
|
||||
} finally {
|
||||
// Serialize collected data into the response
|
||||
context.res = listener.serializeData();
|
||||
context.done();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
### Configuring Chromium download location
|
||||
|
||||
By default, Playwright downloads Chromium to a location outside the function app's folder. In order to include Chromium in the build artifacts, we need to instruct Playwright to install Chromium in the app's node_modules folder. To do this, navigate to Configuration blade of your Function App and create an app setting named **PLAYWRIGHT_BROWSERS_PATH** with a value of **0** in the function app in Azure. This setting is also used by Playwright at run-time to locate Chromium in node_modules.
|
||||
|
||||
![](./AppSettings.png)
|
||||
|
||||
Here you can also ensure that **FUNCTIONS_WORKER_RUNTIME** is set to **node** and **FUNCTIONS_EXTENSION_VERSION** is set to **~3**.
|
||||
|
||||
### Configuring VSCode for remote build
|
||||
|
||||
1. Enable scmDoBuildDuringDeployment setting
|
||||
By default, the Azure Functions VS Code extension will deploy the app using local build, which means it'll run npm install locally and deploy the app package. For remote build, we update the app's **.vscode/settings.json** to enable **scmDoBuildDuringDeployment**.
|
||||
|
||||
``` json
|
||||
{
|
||||
"azureFunctions.deploySubpath": ".",
|
||||
"azureFunctions.projectLanguage": "JavaScript",
|
||||
"azureFunctions.projectRuntime": "~3",
|
||||
"debug.internalConsoleOptions": "neverOpen",
|
||||
"azureFunctions.scmDoBuildDuringDeployment": true
|
||||
}
|
||||
```
|
||||
|
||||
We can also remove the postDeployTask and preDeployTask settings that runs npm commands before and after the deployment; they're not needed because we're running the build remotely.
|
||||
|
||||
2. Add node_modules folder to .funcignore
|
||||
This excludes the **node_modules folder** from the deployment package to make the upload as small as possible and use only remote versions of the packages. File should look like [this](https://github.com/Azure/azure-functions-availability-monitoring-extension/tree/master/src/Demos/JavaScript-Monitoring-Samples/.funcignore).
|
||||
|
||||
3. Enable myget registry for remote build
|
||||
As appinsights-playwright package is currently deployed only to myget the feed needs to be included as an additional npm feed during deloyment. To enable this create **.npmrc** file in the project root folder and put the following line, file should look like [this](https://github.com/Azure/azure-functions-availability-monitoring-extension/tree/master/src/Demos/JavaScript-Monitoring-Samples/.npmrc).
|
||||
|
||||
``` text
|
||||
registry=https://www.myget.org/F/applicationinsights-cat/npm/
|
||||
```
|
||||
|
||||
### Publish code
|
||||
|
||||
1. Using VSCode
|
||||
|
||||
Use the **Azure Functions: Deploy to Function App...** command to publish the app. It'll recognize the settings we configured earlier and automatically use remote build.
|
||||
|
||||
2. Using Azure Functions Core Tools
|
||||
|
||||
Run the command with the **--build remote** flag:
|
||||
|
||||
``` powershell
|
||||
func azure functionapp publish <YourAzureFunctionName> --build remote
|
||||
```
|
||||
|
||||
### Create availability test
|
||||
|
||||
Follow instructions above in the **Setup availability test** section how to create the availability URL ping test.
|
||||
|
||||
After setup is done, wait till results appear in the Availability blade:
|
||||
|
||||
![](./AvailabilityResults.png)
|
||||
|
||||
|
||||
And click on Load test steps button to see more details of Playwright test execution:
|
||||
|
||||
![](./E2EDetails.png)
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you don't see any additional Playwright steps in E2E to blade, try to do the following:
|
||||
1) Navigate to Function App -> Functions -> your function Code + Test blade
|
||||
2) Click Test/Run button in the command bar
|
||||
3) Change HttpMethod type to GET
|
||||
4) Click Run
|
||||
5) Open different tab in browser and call you function URL
|
||||
6) Move back to Code + Test blade and check opened Logs console panel for exceptions
|
||||
|
||||
![](./FunctiontestAndRun.png)
|
||||
|
||||
If you see the following exception generated it means that remote build didn't work for you. Please double check that you configured remote build in VSCode and specified **PLAYWRIGHT_BROWSERS_PATH** app setting correctly:
|
||||
|
||||
```diff
|
||||
- 2020-10-14T23:36:20.789 [Error] browserType.launch: Failed to launch chromium because executable doesn't exist at D:\home\site\wwwroot\node_modules\playwright-chromium\.local-browsers\chromium-815036\chrome-win\chrome.exeTry re-installing playwright with "npm install playwright"
|
||||
```
|
||||
|
||||
If there are no exceptions in the log but you're getting empty response that looks like JSON below then mostly likely you forgot to include node_modules folder to .funcignore. Please carefully review all steps in **Configuring VSCode for remote build** section.
|
||||
|
||||
```json
|
||||
{"type":"playwright","steps":[]}
|
||||
```
|
||||
|
||||
### Additional useful information
|
||||
|
||||
- Some generic samples like authentication can be found [here](https://github.com/microsoft/playwright/tree/master/docs/examples).
|
||||
- If you would like to use custom Docker image instead of Consumption plan you can find the documentation with already included browsers [here](https://github.com/microsoft/playwright/tree/master/docs/docker).
|
||||
|
||||
|
||||
|
||||
## 2. Selenium
|
||||
|
||||
- JavaScript documentation can be found [here](https://www.selenium.dev/selenium/docs/api/javascript/). We don't have default integration as of today.
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# Authentication in Azure Functions
|
||||
|
||||
One of the most common questions is how to use secrets in Azure Function when you want to do authentication during API test for example. The newest and the most recommended approach is to use the integration between Azure Functions (App Services) and a Key Vault.
|
||||
[This article](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) shows you how to work with secrets from Azure Key Vault in your App Service or Azure Functions application without requiring any code changes.
|
Двоичные данные
src/Demos/CreateTest.png
Двоичные данные
src/Demos/CreateTest.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 31 KiB |
Двоичные данные
src/Demos/E2EDetails.png
Двоичные данные
src/Demos/E2EDetails.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 410 KiB |
Двоичные данные
src/Demos/FunctionTestAndRun.png
Двоичные данные
src/Demos/FunctionTestAndRun.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 139 KiB |
Двоичные данные
src/Demos/GetFunctionUrl.png
Двоичные данные
src/Demos/GetFunctionUrl.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 22 KiB |
|
@ -1,9 +0,0 @@
|
|||
*.js.map
|
||||
*.ts
|
||||
.git*
|
||||
.vscode
|
||||
local.settings.json
|
||||
test
|
||||
tsconfig.json
|
||||
node_modules
|
||||
*.webm
|
|
@ -1,96 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TypeScript output
|
||||
dist
|
||||
out
|
||||
|
||||
# Azure Functions artifacts
|
||||
bin
|
||||
obj
|
||||
appsettings.json
|
||||
local.settings.json
|
||||
|
||||
*.webm
|
|
@ -1 +0,0 @@
|
|||
registry=https://www.myget.org/F/applicationinsights-cat/npm/
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"ms-azuretools.vscode-azurefunctions"
|
||||
]
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to Node Functions",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 9229,
|
||||
"preLaunchTask": "func: host start"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"azureFunctions.deploySubpath": ".",
|
||||
"azureFunctions.projectLanguage": "JavaScript",
|
||||
"azureFunctions.projectRuntime": "~3",
|
||||
"debug.internalConsoleOptions": "neverOpen",
|
||||
"azureFunctions.scmDoBuildDuringDeployment": true
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "func",
|
||||
"command": "host start",
|
||||
"problemMatcher": "$func-watch",
|
||||
"isBackground": true,
|
||||
"dependsOn": "npm install"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "npm install",
|
||||
"command": "npm install"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "npm prune",
|
||||
"command": "npm prune --production",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"bindings": [
|
||||
{
|
||||
"authLevel": "anonymous",
|
||||
"type": "httpTrigger",
|
||||
"direction": "in",
|
||||
"name": "req",
|
||||
"methods": [
|
||||
"get",
|
||||
"post"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "http",
|
||||
"direction": "out",
|
||||
"name": "res"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
const { chromium } = require('playwright-chromium');
|
||||
const { AppInsightsContextListener } = require('appinsights-playwright')
|
||||
|
||||
// initialize AppInsightsListener to collect information about Playwright execution
|
||||
// set input parameter to:
|
||||
// - 'AutoCollect' to collect screenshots after every action taken
|
||||
// - 'OnFailure' to collect screenshots only for the failed actions
|
||||
// - 'No' to skip the screenshots collection. Default value.
|
||||
const listener = new AppInsightsContextListener('AutoCollect');
|
||||
|
||||
module.exports = async function (context, req) {
|
||||
context.log("Function entered.");
|
||||
|
||||
try {
|
||||
const browser = await chromium.launch();
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
// Open new page
|
||||
const page = await browserContext.newPage();
|
||||
page.setDefaultTimeout(5000);
|
||||
|
||||
// Go to https://www.bing.com/?toHttps=1&redig=69CC3FCA85A84B3AAFA1D638964EA2B1
|
||||
await page.goto('https://www.bing.com/?toHttps=1&redig=69CC3FCA85A84B3AAFA1D638964EA2B1');
|
||||
|
||||
// Click input[aria-label="Enter your search term"]
|
||||
await page.click('input[aria-label="Enter your search term"]');
|
||||
|
||||
// Fill input[aria-label="Enter your search term"]
|
||||
await page.fill('input[aria-label="Enter your search term"]', 'Playwright');
|
||||
|
||||
// Click //label[normalize-space(@aria-label)='Search the web']/*[local-name()="svg"]
|
||||
await page.click('//label[normalize-space(@aria-label)=\'Search the web\']/*[local-name()="svg"]');
|
||||
// assert.equal(page.url(), 'https://www.bing.com/search?q=Playwright&form=QBLH&sp=-1&pq=playwright&sc=8-10&qs=n&sk=&cvid=A5708CE6F75940C79891958DC561761B');
|
||||
|
||||
// Click text="Playwright"
|
||||
await page.click('text="Playwright"');
|
||||
// assert.equal(page.url(), 'https://playwright.dev/');
|
||||
|
||||
// Close page
|
||||
await page.close();
|
||||
|
||||
// ---------------------
|
||||
await browserContext.close();
|
||||
await browser.close();
|
||||
|
||||
} catch (err) {
|
||||
context.log.error(err);
|
||||
} finally {
|
||||
// Serialize collected data into the response
|
||||
context.res = listener.serializeData();
|
||||
context.done();
|
||||
}
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingSettings": {
|
||||
"isEnabled": true,
|
||||
"excludedTypes": "Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[1.*, 2.0.0)"
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "node",
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
|
||||
}
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
{
|
||||
"name": "functions-playwright",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "14.11.8",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/@types/node/-/@types/node-14.11.8.tgz",
|
||||
"integrity": "sha1-/iAS8jVeTOCLykSus6u7Ic+I0z8=",
|
||||
"optional": true
|
||||
},
|
||||
"@types/yauzl": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/@types/yauzl/-/@types/yauzl-2.9.1.tgz",
|
||||
"integrity": "sha1-0Q9p+fUi7vPPmOMK+2hKHh7JI68=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/agent-base/-/agent-base-6.0.1.tgz",
|
||||
"integrity": "sha1-gIAH5OWGfeywq2qy+Sj721pZbbQ=",
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"appinsights-playwright": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/appinsights-playwright/-/appinsights-playwright-1.0.1.tgz",
|
||||
"integrity": "sha1-Fa7CpteGDrudJujgu0N6aO/lcKw=",
|
||||
"requires": {
|
||||
"playwright-chromium": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/debug/-/debug-4.2.0.tgz",
|
||||
"integrity": "sha1-fxUPk5IOlMWPVXTC/QGjEQ7/5/E=",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha1-WuZKX0UFe682JuwU2gyl5LJDHrA=",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha1-Zj3KVv5G34kNXxMe9KBtIruLoTo=",
|
||||
"requires": {
|
||||
"@types/yauzl": "^2.9.1",
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
}
|
||||
},
|
||||
"fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
|
||||
"requires": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha1-SWaheV7lrOZecGxLe+txJX1uItM=",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha1-FB8zuBp8JJLhJVlDB0gMRmeSeKY=",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/graceful-fs/-/graceful-fs-4.2.4.tgz",
|
||||
"integrity": "sha1-Ila94U02MpWMRl68ltxGfKB6Kfs="
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha1-4qkFQqu2inYuCghQ9sntrf2FBrI=",
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w="
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/jpeg-js/-/jpeg-js-0.4.2.tgz",
|
||||
"integrity": "sha1-izRbGuSr3mTC2i/mfqIWoRSsJ50="
|
||||
},
|
||||
"mime": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/mime/-/mime-2.4.6.tgz",
|
||||
"integrity": "sha1-5bQHyQ20QvK+tbFiNz0Htpr/pNE="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
|
||||
},
|
||||
"playwright-chromium": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/playwright-chromium/-/playwright-chromium-1.5.0.tgz",
|
||||
"integrity": "sha1-E6oIXNAvlZaj4ur0tRxNqNhGuQk=",
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"extract-zip": "^2.0.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"jpeg-js": "^0.4.2",
|
||||
"mime": "^2.4.6",
|
||||
"pngjs": "^5.0.0",
|
||||
"progress": "^2.0.3",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"ws": "^7.3.1"
|
||||
}
|
||||
},
|
||||
"pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha1-553SshV2f9nARWHAEjbflgvOf7s="
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha1-foz42PW48jnBvGi+tOt4Vn1XLvg="
|
||||
},
|
||||
"proper-lockfile": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/proper-lockfile/-/proper-lockfile-4.1.1.tgz",
|
||||
"integrity": "sha1-KEz5254wqQ5kevrWnet8sGiBJiw=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.11",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha1-4QLxbKNVQkhldV0sno6k8k1Yw+I="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha1-tKIRaBW94vTh6mAjVOjHVWUQemQ=",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha1-8aVAK6YiCtUswSgrrBrjqkn9Bho=",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha1-oUEMLt2PB3sItOJTyOrPyvBXRhw="
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/ws/-/ws-7.3.1.tgz",
|
||||
"integrity": "sha1-0FR79n985PEqct/jEmLGjX3FUcg="
|
||||
},
|
||||
"yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://www.myget.org/F/applicationinsights-cat/npm/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
|
||||
"requires": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "functions-playwright",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "func start",
|
||||
"test": "echo \"No tests yet...\""
|
||||
},
|
||||
"dependencies": {
|
||||
"appinsights-playwright": "^1.0.1",
|
||||
"playwright-chromium": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
|
@ -1,228 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- This Directory.Build.props file needs to apply to all projects and solutions in this repository. It is placed near the rot of the repo accordingly. -->
|
||||
<!-- If you use more specific Directory.Build.props files, you need to reference this file from there. You can do it with a statement such as this: -->
|
||||
<!-- <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> -->
|
||||
<!-- For details see: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build?view=vs-2019#use-case-multi-level-merging -->
|
||||
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- -->
|
||||
<!-- # Purpose -->
|
||||
<!-- -->
|
||||
<!-- This file redirects binary output for all MSBuild projects in this repository into a common directory outside the repository. -->
|
||||
<!-- This allows keeping things clean and concise. -->
|
||||
<!-- You also avoid the need to search for the binaries all over the repository, to clean them manually and/or to collect them in one place later. -->
|
||||
<!-- -->
|
||||
<!-- Generally, the recommended local directory structure for code projects is as follows: -->
|
||||
<!-- -->
|
||||
<!-- some root that contains all your code projects\ -->
|
||||
<!-- short descriptive moniker for project X\ -->
|
||||
<!-- repository X name\ -->
|
||||
<!-- files contained within repository X name -->
|
||||
<!-- . . . -->
|
||||
<!-- short descriptive moniker for project Y\ -->
|
||||
<!-- repository Y name\ -->
|
||||
<!-- files contained within repository Y name -->
|
||||
<!-- . . . -->
|
||||
<!-- . . . -->
|
||||
<!-- -->
|
||||
<!-- A specific example: -->
|
||||
<!-- -->
|
||||
<!-- c:\Code\GitHub\ -->
|
||||
<!-- AvlMon-Ext\ -->
|
||||
<!-- azure-functions-availability-monitoring-extension\ -->
|
||||
<!-- readme.md -->
|
||||
<!-- license -->
|
||||
<!-- . . . -->
|
||||
<!-- AzWedJobs\ -->
|
||||
<!-- azure-webjobs-sdk\ -->
|
||||
<!-- readme.md -->
|
||||
<!-- licence.txt -->
|
||||
<!-- . . . -->
|
||||
<!-- . . . -->
|
||||
<!-- -->
|
||||
<!-- So, this basically means that the “git clone” command is issued from an just-created empty directory for the descriptive monitor. E.g.: -->
|
||||
<!-- C:\Code\GitHub>mkdir AvlMon-Ext -->
|
||||
<!-- C:\Code\GitHub>cd AvlMon-Ext -->
|
||||
<!-- C:\Code\GitHub\AvlMon-Ext>git clone https://github.com/Azure/azure-functions-availability-monitoring-extension.git -->
|
||||
<!-- Cloning into 'azure-functions-availability-monitoring-extension'... -->
|
||||
<!-- -->
|
||||
<!-- This structure has two beneficial properties: -->
|
||||
<!-- - You can use different monikers to enlist into the same repo (or a fork) many times, keeping an identical internal structure. -->
|
||||
<!-- - You can place your binary output OUTSIDE the repo, keeping things clean and concise. -->
|
||||
<!-- -->
|
||||
<!-- This PROPS file modifies the MSBuild properties that point to output directories to collect the binaries for all projects in a single place. -->
|
||||
<!-- -->
|
||||
<!-- # Details -->
|
||||
<!-- -->
|
||||
<!-- The logic in this file uses two marker files placed in the repo: “.EnlistmentRoot.marker” and “.SourceRoot.marker”: -->
|
||||
<!-- -->
|
||||
<!-- - “.EnlistmentRoot.marker” must be placed into the root of the repository. -->
|
||||
<!-- It is the top-most folder; typically, it is the folder that contains the ".git/" subfolder. -->
|
||||
<!-- -->
|
||||
<!-- - “.SourceRoot.marker” must be placed into the root folder of all code sources in the repository. -->
|
||||
<!-- This “source root” may be the same as the repository root (you may place both files next to each other), or it may be a subfolder. -->
|
||||
<!-- For example, the repo root may contain other sub-folders in the root, containing other files, such as documentation. -->
|
||||
<!-- -->
|
||||
<!-- After the binary output was redirected by this PROPS file, your directory structure will look something like this: -->
|
||||
<!-- -->
|
||||
<!-- c:\Code\GitHub\ -->
|
||||
<!-- AvlMon-Ext\ -->
|
||||
<!-- obj\ (the OBJ folders for all projects are redirected here) -->
|
||||
<!-- ... -->
|
||||
<!-- bin\ (the BIN folders for all projects are redirected here) -->
|
||||
<!-- ... -->
|
||||
<!-- createdPackages\ (the NuGet files for all projects are created here) -->
|
||||
<!-- ... -->
|
||||
<!-- azure-functions-availability-monitoring-extension\ (the repo root) -->
|
||||
<!-- src\ (the source root folder, all code sources are contained within here) -->
|
||||
<!-- Code Project A\ -->
|
||||
<!-- CodeProjectA.csproj -->
|
||||
<!-- SomeFile.cs -->
|
||||
<!-- ... -->
|
||||
<!-- Code Project B\ -->
|
||||
<!-- CodeProjectB.csproj -->
|
||||
<!-- SomeFile.cs -->
|
||||
<!-- ... -->
|
||||
<!-- .SourceRoot.marker (the source-root marker file) -->
|
||||
<!-- Directory.Build.props (this file) -->
|
||||
<!-- ... -->
|
||||
<!-- user manuals\ (some folder that does not contain sources) -->
|
||||
<!-- ... -->
|
||||
<!-- .EnlistmentRoot.marker (the repository-root marker file) -->
|
||||
<!-- readme.md -->
|
||||
<!-- license -->
|
||||
<!-- . . . -->
|
||||
<!-- -->
|
||||
|
||||
<!-- Control options: -->
|
||||
<PropertyGroup>
|
||||
|
||||
<!-- Set AvoidRedirectingBinaryOutput to True to turn off all redirects in tis file. -->
|
||||
<!-- This will result in the same build behavior as if this file did not exist, except for an informative log message during build. -->
|
||||
<AvoidRedirectingBinaryOutput>false</AvoidRedirectingBinaryOutput>
|
||||
|
||||
<!-- Set ForceBinaryOutputIntoEnlistment to True to redirect all binary output into a ".BinaryOutput\" directory WITHIN the repository root, -->
|
||||
<!-- instead of individual folders ABOVE the repo root. -->
|
||||
<!-- This is not recommended in general, but it can be useful if repo enlistments are all contained within the same parent directory without the -->
|
||||
<!-- intermediate project monikers described above. In that case, this will ensure that binaries from different repository copies are separated. -->
|
||||
<!-- If you use this option, make sure to add ".BinaryOutput\" to your .gitignore. -->
|
||||
<ForceBinaryOutputIntoEnlistment>false</ForceBinaryOutputIntoEnlistment>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(AvoidRedirectingBinaryOutput)' != 'True' ">
|
||||
|
||||
<!-- EnlistmentRoot is the root folder of the repository. Typically, it is the folder that contains the ".git/" subfolder. -->
|
||||
<!-- The binary output root directories ("bin/" and "obj/") will be typically placed placed immediately above it. -->
|
||||
<!-- You can use ForceBinaryOutputIntoEnlistment to control that behaviour. -->
|
||||
<EnlistmentRoot>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '.EnlistmentRoot.marker'))</EnlistmentRoot>
|
||||
|
||||
<!-- SourceRoot is the root folder of the source code in the repository. It may be the same as EnlistmentRoot, or it may be -->
|
||||
<!-- a subfolder, if there are other sub-folders in the root, containing other files, such as documentation. -->
|
||||
<!-- It us used to compute RelativeOutputPathBase below. -->
|
||||
<SourceRoot>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '.SourceRoot.marker'))</SourceRoot>
|
||||
|
||||
<!-- RelativeOutputPathBase is relative path from SourceRoot to the project file being built -->
|
||||
<!-- will be the relative path from the binary roots to the respective bianary files. -->
|
||||
<RelativeOutputPathBase>$(MSBuildProjectDirectory.Substring($(SourceRoot.Length)))</RelativeOutputPathBase>
|
||||
|
||||
<!-- By default, binary output directories ("bin/" and "obj/", ...) should be placed directly ABOVE the root of the repository. -->
|
||||
<!-- However, ForceBinaryOutputIntoEnlistment allows putting the binarlies INTO the repository root. -->
|
||||
<BinaryOutputParent Condition=" '$(ForceBinaryOutputIntoEnlistment)' == 'True' ">$(EnlistmentRoot)\.BinaryOutput\</BinaryOutputParent>
|
||||
<BinaryOutputParent Condition=" '$(ForceBinaryOutputIntoEnlistment)' != 'True' ">$(EnlistmentRoot)\..\</BinaryOutputParent>
|
||||
|
||||
<!-- By default, Configuration is DEBUG, but user can (and will) overwrite it. -->
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
|
||||
<!-- By default, PlatformName is AnyCPU, but user can (and may) overwrite it. -->
|
||||
<PlatformName Condition=" '$(PlatformName)' == '' ">AnyCPU</PlatformName>
|
||||
|
||||
<OutputConfigMoniker Condition=" '$(PlatformName)' == 'AnyCPU' ">$(Configuration)</OutputConfigMoniker>
|
||||
<OutputConfigMoniker Condition=" '$(PlatformName)' != 'AnyCPU' ">$(PlatformName)\$(Configuration)</OutputConfigMoniker>
|
||||
|
||||
<!-- IntermediateOutputPath is the OBJ folder for the project. It is within BaseIntermediateOutputPath, which is the OBJ root for all projects. -->
|
||||
<BaseIntermediateOutputPath>$(BinaryOutputParent)\obj</BaseIntermediateOutputPath>
|
||||
<BaseIntermediateOutputPath>$([System.IO.Path]::GetFullPath( $(BaseIntermediateOutputPath) ))</BaseIntermediateOutputPath>
|
||||
|
||||
<IntermediateOutputPath>$(BaseIntermediateOutputPath)\$(OutputConfigMoniker)\$(RelativeOutputPathBase)</IntermediateOutputPath>
|
||||
<IntermediateOutputPath>$([System.IO.Path]::GetFullPath( $(IntermediateOutputPath) ))\</IntermediateOutputPath>
|
||||
|
||||
<MSBuildProjectExtensionsPath>$(IntermediateOutputPath)</MSBuildProjectExtensionsPath>
|
||||
|
||||
<!-- OutputPath is the BIN folder for the project. It is within BaseOutputPath, which is the BIN root for all projects. -->
|
||||
<BaseOutputPath>$(BinaryOutputParent)\bin</BaseOutputPath>
|
||||
<BaseOutputPath>$([System.IO.Path]::GetFullPath( $(BaseOutputPath) ))</BaseOutputPath>
|
||||
|
||||
<OutputPath>$(BaseOutputPath)\$(OutputConfigMoniker)\$(RelativeOutputPathBase)</OutputPath>
|
||||
<OutputPath>$([System.IO.Path]::GetFullPath( $(OutputPath) ))\</OutputPath>
|
||||
|
||||
<!-- PackageOutputPath is where all the generated NuGet packages are going to go. Tnis is different to where we are storing the importned packages. -->
|
||||
<PackageOutputPath>$(BinaryOutputParent)\createdPackages\$(OutputConfigMoniker)</PackageOutputPath>
|
||||
<PackageOutputPath>$([System.IO.Path]::GetFullPath( $(PackageOutputPath) ))</PackageOutputPath>
|
||||
|
||||
<!-- PackagesDir is where the local importend packages will go. -->
|
||||
<PackagesDir>$(BinaryOutputParent)\ImportedPackages</PackagesDir>
|
||||
<PackagesDir>$([System.IO.Path]::GetFullPath( $(PackagesDir) ))</PackagesDir>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Enable NuGet package restore during build -->
|
||||
<RestorePackages>true</RestorePackages>
|
||||
<RequireRestoreConsent>false</RequireRestoreConsent>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="SetOutputDirectories" BeforeTargets="Build" KeepDuplicateOutputs="True">
|
||||
|
||||
<!-- This target will run before the build, but after the redefinitions in this file took place. -->
|
||||
<!-- It can be used to view the details of the redefinitions, but be careful when using it for debugging. -->
|
||||
|
||||
<PropertyGroup>
|
||||
<SodMgsPrefix>Set Output Directories</SodMgsPrefix>
|
||||
</PropertyGroup>
|
||||
|
||||
<Message Condition=" '$(AvoidRedirectingBinaryOutput)' != 'True' "
|
||||
Text="[$(SodMgsPrefix)] Binary output directories were redirected for project "$(MSBuildProjectName)". For details, uncomment the Message Logs in the Set-Output-Dirs PROPS file and rebuild."
|
||||
Importance="high" />
|
||||
|
||||
<Message Condition=" '$(AvoidRedirectingBinaryOutput)' == 'True' "
|
||||
Text="[$(SodMgsPrefix)] Binary output directories were NOT redirected for project "$(MSBuildProjectName)", because "AvoidRedirectingBinaryOutput" is set. For details, uncomment the Message Logs in the Set-Output-Dirs PROPS file and rebuild."
|
||||
Importance="high" />
|
||||
|
||||
<Message Text="[$(SodMgsPrefix)] Project file: "$(MSBuildProjectFullPath)"." Importance="high"/>
|
||||
<Message Text="[$(SodMgsPrefix)] Set-Output-Dirs PROPS file: "$(MSBuildThisFileFullPath)"." Importance="high"/>
|
||||
|
||||
<!-- !! The commented section below contains useful code and must be maintained when changes to this file are made !! -->
|
||||
<!-- Comment/uncomment the lines below to control whether you see the detailed results of the output folder(s) redirection. -->
|
||||
|
||||
<!--
|
||||
<Message Text="[$(SodMgsPrefix)] AvoidRedirectingBinaryOutput: "$(AvoidRedirectingBinaryOutput)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] ForceBinaryOutputIntoEnlistment: "$(ForceBinaryOutputIntoEnlistment)"." Importance="high" />
|
||||
|
||||
<Message Text="[$(SodMgsPrefix)] MSBuildProjectDirectory: "$(MSBuildProjectDirectory)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] EnlistmentRoot: "$(EnlistmentRoot)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] SourceRoot: "$(SourceRoot)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] RelativeOutputPathBase: "$(RelativeOutputPathBase)"." Importance="high" />
|
||||
|
||||
<Message Text="[$(SodMgsPrefix)] BinaryOutputParent: "$(BinaryOutputParent)"." Importance="high" />
|
||||
|
||||
<Message Text="[$(SodMgsPrefix)] Configuration: "$(Configuration)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] PlatformName: "$(PlatformName)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] OutputConfigMoniker: "$(OutputConfigMoniker)"." Importance="high" />
|
||||
|
||||
<Message Text="[$(SodMgsPrefix)] BaseIntermediateOutputPath: "$(BaseIntermediateOutputPath)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] IntermediateOutputPath: "$(IntermediateOutputPath)"." Importance="high" />
|
||||
|
||||
<Message Text="[$(SodMgsPrefix)] BaseOutputPath: "$(BaseOutputPath)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] OutputPath: "$(OutputPath)"." Importance="high" />
|
||||
|
||||
<Message Text="[$(SodMgsPrefix)] MSBuildProjectExtensionsPath: "$(MSBuildProjectExtensionsPath)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] PackageOutputPath: "$(PackageOutputPath)"." Importance="high" />
|
||||
<Message Text="[$(SodMgsPrefix)] PackagesDir: "$(PackagesDir)"." Importance="high" />
|
||||
-->
|
||||
|
||||
</Target>
|
||||
|
||||
</Project>
|
|
@ -1,338 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Config;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Script.WebHost
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public static class AvailabilityTestWebJobsBuilderExtensions
|
||||
{
|
||||
public static IWebJobsBuilder AddAvailabilityTests(this IWebJobsBuilder builder)
|
||||
{
|
||||
builder.AddExtension<AvailabilityTestExtensionConfigProvider>();
|
||||
|
||||
builder.Services.AddSingleton<IFunctionFilter, AvailabilityTestInvocationFilter>();
|
||||
builder.Services.AddSingleton<AvailabilityTestManager>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
[Extension("AvailabilityTest")]
|
||||
internal class AvailabilityTestExtensionConfigProvider : IExtensionConfigProvider
|
||||
{
|
||||
private readonly AvailabilityTestManager _manager;
|
||||
|
||||
public AvailabilityTestExtensionConfigProvider(AvailabilityTestManager manager)
|
||||
{
|
||||
_manager = manager;
|
||||
}
|
||||
|
||||
public void Initialize(ExtensionConfigContext context)
|
||||
{
|
||||
// this binding provider exists soley to allow us to inspect functions to determine
|
||||
// whether they're availability tests
|
||||
// the fluent APIs below don't allow us to do this easily
|
||||
context.AddBindingRule<AvailabilityTestContextAttribute>().Bind(new AvailabilityTestDiscoveryBindingProvider(_manager));
|
||||
|
||||
var inputRule = context.AddBindingRule<AvailabilityTestContextAttribute>()
|
||||
.BindToInput<AvailabilityTestContext>((AvailabilityTestContextAttribute attr, ValueBindingContext valueContext) =>
|
||||
{
|
||||
// new up the context to be passed to user code
|
||||
var testContext = new AvailabilityTestContext
|
||||
{
|
||||
TestDisplayName = "My Test",
|
||||
StartTime = DateTime.UtcNow
|
||||
};
|
||||
return Task.FromResult<AvailabilityTestContext>(testContext);
|
||||
});
|
||||
|
||||
var outputRule = context.AddBindingRule<AvailabilityTestResultAttribute>();
|
||||
outputRule.BindToCollector<AvailabilityTestResultAttribute, AvailabilityTestResult>((AvailabilityTestResultAttribute attr, ValueBindingContext valueContext) =>
|
||||
{
|
||||
var collector = new AvailabilityTestCollector(_manager, valueContext.FunctionInstanceId);
|
||||
return Task.FromResult<IAsyncCollector<AvailabilityTestResult>>(collector);
|
||||
});
|
||||
|
||||
context.AddConverter<AvailabilityTestContext, string>(ctxt =>
|
||||
{
|
||||
return JsonConvert.SerializeObject(ctxt);
|
||||
});
|
||||
|
||||
context.AddConverter<string, AvailabilityTestResult>(json =>
|
||||
{
|
||||
return JsonConvert.DeserializeObject<AvailabilityTestResult>(json);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class AvailabilityTestManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, bool> _availabilityTests = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<Guid, AvailabilityTestInvocationContext> _availabilityTestInvocationsContextMap = new ConcurrentDictionary<Guid, AvailabilityTestInvocationContext>();
|
||||
|
||||
public AvailabilityTestManager()
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsAvailabilityTest(string functionName)
|
||||
{
|
||||
if (_availabilityTests.TryGetValue(functionName, out bool value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsAvailabilityTest(FunctionInvocationContext context)
|
||||
{
|
||||
if (IsAvailabilityTest(context.FunctionName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// For .NET languages, binding happens BEFORE filters, so if the function is an availability test
|
||||
// we'll have known that already above
|
||||
// The following check handles OOP languages, where bindings happen late and dynamically, AFTER filters.
|
||||
// In these cases, we must read the function metadata, since NO binding has yet occurred
|
||||
if (context.Arguments.TryGetValue("_context", out object value))
|
||||
{
|
||||
var executionContext = value as ExecutionContext;
|
||||
if (executionContext != null)
|
||||
{
|
||||
var metadataPath = Path.Combine(executionContext.FunctionAppDirectory, "function.json");
|
||||
bool isAvailabilityTest = HasAvailabilityTestBinding(metadataPath);
|
||||
_availabilityTests[context.FunctionName] = isAvailabilityTest;
|
||||
return isAvailabilityTest;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RegisterTest(string functionName)
|
||||
{
|
||||
_availabilityTests[functionName] = true;
|
||||
}
|
||||
|
||||
public Task<AvailabilityTestInvocationContext> StartInvocationAsync(FunctionInvocationContext context)
|
||||
{
|
||||
var invocationContext = _availabilityTestInvocationsContextMap.GetOrAdd(context.FunctionInstanceId, n =>
|
||||
{
|
||||
return new AvailabilityTestInvocationContext
|
||||
{
|
||||
InvocationId = context.FunctionInstanceId
|
||||
};
|
||||
});
|
||||
return Task.FromResult(invocationContext);
|
||||
}
|
||||
|
||||
public AvailabilityTestInvocationContext GetInvocation(Guid instanceId)
|
||||
{
|
||||
if (_availabilityTestInvocationsContextMap.TryGetValue(instanceId, out AvailabilityTestInvocationContext invocationContext))
|
||||
{
|
||||
return invocationContext;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task CompleteInvocationAsync(FunctionInvocationContext context)
|
||||
{
|
||||
if (_availabilityTestInvocationsContextMap.TryRemove(context.FunctionInstanceId, out AvailabilityTestInvocationContext invocationContext))
|
||||
{
|
||||
// TODO: complete the invocation
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool HasAvailabilityTestBinding(string metadataPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: make this code robust
|
||||
string content = File.ReadAllText(metadataPath);
|
||||
JObject jo = JObject.Parse(content);
|
||||
var bindings = (JArray)jo["bindings"];
|
||||
bool isAvailabilityTest = bindings.Any(p =>
|
||||
string.Compare((string)p["type"], "availabilityTestContext", StringComparison.OrdinalIgnoreCase) == 0 ||
|
||||
string.Compare((string)p["type"], "availabilityTestResult", StringComparison.OrdinalIgnoreCase) == 0);
|
||||
return isAvailabilityTest;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class AvailabilityTestInvocationContext
|
||||
{
|
||||
public Guid InvocationId { get; set; }
|
||||
|
||||
public AvailabilityTestResult Result { get; set; }
|
||||
}
|
||||
|
||||
internal class AvailabilityTestDiscoveryBindingProvider : IBindingProvider
|
||||
{
|
||||
private readonly AvailabilityTestManager _manager;
|
||||
|
||||
public AvailabilityTestDiscoveryBindingProvider(AvailabilityTestManager manager)
|
||||
{
|
||||
_manager = manager;
|
||||
}
|
||||
|
||||
public Task<IBinding> TryCreateAsync(BindingProviderContext context)
|
||||
{
|
||||
// Note that this only works for .NET functions, not OOP languages, because those bindings
|
||||
// are evaluated late-bound, dynamically, and the triggering function is an IL genned wrapper
|
||||
// without any AvailabilityTest attributes at all!
|
||||
// OOP languages are handled internally by the AvailabilityTestManager by reading function metadata
|
||||
var availabilityTestAttribute = context.Parameter.GetCustomAttributes(false).OfType<AvailabilityTestResultAttribute>().SingleOrDefault();
|
||||
if (availabilityTestAttribute != null)
|
||||
{
|
||||
string functionName = GetFunctionName((MethodInfo)context.Parameter.Member);
|
||||
_manager.RegisterTest(functionName);
|
||||
}
|
||||
|
||||
return Task.FromResult<IBinding>(null);
|
||||
}
|
||||
|
||||
private static string GetFunctionName(MethodInfo methodInfo)
|
||||
{
|
||||
// the format returned here must match the same format passed to invocation filters
|
||||
// this is the same code used by the SDK for this
|
||||
var functionNameAttribute = methodInfo.GetCustomAttribute<FunctionNameAttribute>();
|
||||
return (functionNameAttribute != null) ? functionNameAttribute.Name : $"{methodInfo.DeclaringType.Name}.{methodInfo.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
// Collector used to receive test results. IAsyncCollector is the type that allows interop with
|
||||
// other languages. In .NET, user can just return an AvailabilityTestResult directly from their function -
|
||||
// the framework should pass this to your collector
|
||||
internal class AvailabilityTestCollector : IAsyncCollector<AvailabilityTestResult>
|
||||
{
|
||||
private readonly Guid _instanceId;
|
||||
private readonly AvailabilityTestManager _manager;
|
||||
|
||||
public AvailabilityTestCollector(AvailabilityTestManager manager, Guid instanceId)
|
||||
{
|
||||
_manager = manager;
|
||||
_instanceId = instanceId;
|
||||
}
|
||||
|
||||
public Task AddAsync(AvailabilityTestResult item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var invocation = _manager.GetInvocation(_instanceId);
|
||||
invocation.Result = item;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FlushAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// Attribute applied to input parameter
|
||||
[Binding]
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class AvailabilityTestContextAttribute : Attribute
|
||||
{
|
||||
// TODO: Add any configuration properties the user needs to set
|
||||
// Mark with [AutResolve] to have them resolved from app settings, etc. automatically
|
||||
}
|
||||
|
||||
// Attribute applied to return value, representing the test result
|
||||
[Binding]
|
||||
[AttributeUsage(AttributeTargets.ReturnValue)]
|
||||
public class AvailabilityTestResultAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
// Input context type passed to user function
|
||||
public class AvailabilityTestContext
|
||||
{
|
||||
public string TestDisplayName { get; set; }
|
||||
|
||||
public string TestArmResourceName { get; set; }
|
||||
|
||||
public string LocationDisplayName { get; set; }
|
||||
|
||||
public string LocationId { get; set; }
|
||||
|
||||
public DateTimeOffset StartTime { get; set; }
|
||||
}
|
||||
|
||||
// Result type the user returns from their function
|
||||
public class AvailabilityTestResult
|
||||
{
|
||||
public string Result { get; set; }
|
||||
}
|
||||
|
||||
internal class AvailabilityTestInvocationFilter : IFunctionInvocationFilter
|
||||
{
|
||||
private readonly AvailabilityTestManager _manager;
|
||||
|
||||
public AvailabilityTestInvocationFilter(AvailabilityTestManager manager)
|
||||
{
|
||||
_manager = manager;
|
||||
}
|
||||
|
||||
public async Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_manager.IsAvailabilityTest(executedContext))
|
||||
{
|
||||
await _manager.CompleteInvocationAsync(executedContext);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_manager.IsAvailabilityTest(executingContext))
|
||||
{
|
||||
await _manager.StartInvocationAsync(executingContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class RuleExtensions
|
||||
{
|
||||
public static void BindToCollector<TAttribute, TMessage>(this FluentBindingRule<TAttribute> rule, Func<TAttribute, ValueBindingContext, Task<IAsyncCollector<TMessage>>> buildFromAttribute) where TAttribute : Attribute
|
||||
{
|
||||
// TEMP: temporary workaround code effectively adding a ValueBindingContext collector overload,
|
||||
// until it's added to the SDK
|
||||
Type patternMatcherType = typeof(FluentBindingRule<>).Assembly.GetType("Microsoft.Azure.WebJobs.Host.Bindings.PatternMatcher");
|
||||
var patternMatcherNewMethodInfo = patternMatcherType.GetMethods()[4]; // TODO: get this method properly via reflection
|
||||
patternMatcherNewMethodInfo = patternMatcherNewMethodInfo.MakeGenericMethod(new Type[] { typeof(TAttribute), typeof(IAsyncCollector<TMessage>) });
|
||||
var patternMatcherInstance = patternMatcherNewMethodInfo.Invoke(null, new object[] { buildFromAttribute });
|
||||
|
||||
MethodInfo bindToCollectorMethod = typeof(FluentBindingRule<TAttribute>).GetMethod(
|
||||
"BindToCollector",
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
new Type[] { patternMatcherType },
|
||||
modifiers: null);
|
||||
bindToCollectorMethod = bindToCollectorMethod.MakeGenericMethod(new Type[] { typeof(TMessage) });
|
||||
bindToCollectorMethod.Invoke(rule, new object[] { patternMatcherInstance });
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,5 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppInsightsContextListener = void 0;
|
||||
var instrumentation_1 = require("./src/instrumentation");
|
||||
Object.defineProperty(exports, "AppInsightsContextListener", { enumerable: true, get: function () { return instrumentation_1.AppInsightsContextListener; } });
|
|
@ -1 +0,0 @@
|
|||
export { AppInsightsContextListener } from './src/instrumentation';
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"name": "appinsights-playwright",
|
||||
"version": "1.0.1",
|
||||
"description": "AppInsights Playwright",
|
||||
"repository": "github:Azure/azure-functions-availability-monitoring-extension",
|
||||
"homepage": "",
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"author": "Microsoft Application Insights Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"playwright-chromium": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.0.2",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"tslint": "^5.19.0",
|
||||
"tslint-config-prettier": "^1.18.0"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"src/**/*.js"
|
||||
]
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppInsightsActionListener = exports.AppInsightsContextListener = void 0;
|
||||
var BrowserContext = require('playwright-chromium/lib/server/browserContext');
|
||||
var AppInsightsContextListener = /** @class */ (function () {
|
||||
function AppInsightsContextListener(state) {
|
||||
this._actionListener = new AppInsightsActionListener(state || 'No');
|
||||
BrowserContext.contextListeners.add(this);
|
||||
}
|
||||
AppInsightsContextListener.prototype.dispose = function () {
|
||||
BrowserContext.contextListeners.delete(this);
|
||||
};
|
||||
AppInsightsContextListener.prototype.onContextCreated = function (context) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
// subscribe new context to actions listening
|
||||
context._actionListeners.add(this._actionListener);
|
||||
return [2 /*return*/];
|
||||
});
|
||||
});
|
||||
};
|
||||
AppInsightsContextListener.prototype.onContextDestroyed = function (context) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/];
|
||||
});
|
||||
});
|
||||
};
|
||||
AppInsightsContextListener.prototype.serializeData = function () {
|
||||
var data = {
|
||||
type: 'playwright',
|
||||
steps: this._actionListener._data
|
||||
};
|
||||
var status = this._actionListener._failed ? '500' : '200';
|
||||
// clean data
|
||||
this._actionListener._data = [];
|
||||
this._actionListener._failed = false;
|
||||
// serialize response
|
||||
return {
|
||||
body: JSON.stringify(data),
|
||||
status: status,
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
};
|
||||
};
|
||||
return AppInsightsContextListener;
|
||||
}());
|
||||
exports.AppInsightsContextListener = AppInsightsContextListener;
|
||||
var AppInsightsActionListener = /** @class */ (function () {
|
||||
function AppInsightsActionListener(state) {
|
||||
this._data = [];
|
||||
this._state = state;
|
||||
this._failed = false;
|
||||
}
|
||||
AppInsightsActionListener.prototype.dispose = function () {
|
||||
this._data = [];
|
||||
};
|
||||
AppInsightsActionListener.prototype.onAfterAction = function (result, metadata) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var pageUrl, duration, step, _a, buffer, buffer, e_1;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
_b.trys.push([0, 8, , 9]);
|
||||
pageUrl = metadata.page.mainFrame().url();
|
||||
duration = result.endTime - result.startTime;
|
||||
this._failed = this._failed || !!result.error;
|
||||
step = {
|
||||
action: metadata.type,
|
||||
target: metadata.target,
|
||||
elementValue: metadata.value,
|
||||
resultCode: !!result.error ? '500' : '200',
|
||||
success: !result.error,
|
||||
url: pageUrl,
|
||||
duration: duration,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
_a = this._state;
|
||||
switch (_a) {
|
||||
case 'AutoCollect': return [3 /*break*/, 1];
|
||||
case 'OnFailure': return [3 /*break*/, 3];
|
||||
}
|
||||
return [3 /*break*/, 6];
|
||||
case 1: return [4 /*yield*/, metadata.page.screenshot({ type: 'jpeg' })];
|
||||
case 2:
|
||||
buffer = _b.sent();
|
||||
step.screenshot = buffer.toString('base64');
|
||||
return [3 /*break*/, 7];
|
||||
case 3:
|
||||
if (!!!result.error) return [3 /*break*/, 5];
|
||||
return [4 /*yield*/, metadata.page.screenshot({ type: 'jpeg' })];
|
||||
case 4:
|
||||
buffer = _b.sent();
|
||||
step.screenshot = buffer.toString('base64');
|
||||
_b.label = 5;
|
||||
case 5: return [3 /*break*/, 7];
|
||||
case 6: return [3 /*break*/, 7];
|
||||
case 7:
|
||||
this._data.push(step);
|
||||
return [3 /*break*/, 9];
|
||||
case 8:
|
||||
e_1 = _b.sent();
|
||||
// Do not throw from instrumentation.
|
||||
console.log('Error during appinsights instrumentation: ' + e_1.message);
|
||||
return [3 /*break*/, 9];
|
||||
case 9: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return AppInsightsActionListener;
|
||||
}());
|
||||
exports.AppInsightsActionListener = AppInsightsActionListener;
|
|
@ -1,111 +0,0 @@
|
|||
const BrowserContext = require('playwright-chromium/lib/server/browserContext');
|
||||
|
||||
export type ScreenshotMode = 'AutoCollect' | 'OnFailure' | 'No';
|
||||
|
||||
interface WebTestStep {
|
||||
action: string;
|
||||
url: string;
|
||||
resultCode: string;
|
||||
success: boolean;
|
||||
duration: number;
|
||||
timestamp: number;
|
||||
screenshot?: string;
|
||||
target?: string;
|
||||
elementValue?: string;
|
||||
}
|
||||
|
||||
export class AppInsightsContextListener {
|
||||
_actionListener: AppInsightsActionListener;
|
||||
constructor(state?: ScreenshotMode) {
|
||||
this._actionListener = new AppInsightsActionListener(state || 'No');
|
||||
BrowserContext.contextListeners.add(this);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
BrowserContext.contextListeners.delete(this);
|
||||
}
|
||||
|
||||
async onContextCreated(context: any): Promise<void> {
|
||||
// subscribe new context to actions listening
|
||||
context._actionListeners.add(this._actionListener);
|
||||
}
|
||||
|
||||
async onContextDestroyed(context: any): Promise<void> {
|
||||
}
|
||||
|
||||
serializeData() {
|
||||
const data = {
|
||||
type: 'playwright',
|
||||
steps: this._actionListener._data
|
||||
};
|
||||
const status = this._actionListener._failed ? '500' : '200';
|
||||
|
||||
// clean data
|
||||
this._actionListener._data = [];
|
||||
this._actionListener._failed = false;
|
||||
|
||||
// serialize response
|
||||
return {
|
||||
body: JSON.stringify(data),
|
||||
status: status,
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class AppInsightsActionListener {
|
||||
_state: ScreenshotMode;
|
||||
_data: WebTestStep[] = [];
|
||||
_failed: boolean;
|
||||
constructor(state: ScreenshotMode) {
|
||||
this._state = state;
|
||||
this._failed = false;
|
||||
}
|
||||
dispose() {
|
||||
this._data = [];
|
||||
}
|
||||
async onAfterAction(result: any, metadata: any): Promise<void> {
|
||||
try {
|
||||
const pageUrl = metadata.page.mainFrame().url();
|
||||
const duration = result.endTime - result.startTime;
|
||||
|
||||
this._failed = this._failed || !!result.error;
|
||||
|
||||
// Track new step on completion
|
||||
let step: WebTestStep = {
|
||||
action: metadata.type,
|
||||
target: metadata.target,
|
||||
elementValue: metadata.value,
|
||||
resultCode: !!result.error ? '500' : '200',
|
||||
success: !result.error,
|
||||
url: pageUrl,
|
||||
duration: duration,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
switch (this._state) {
|
||||
case 'AutoCollect': {
|
||||
const buffer = await metadata.page.screenshot({ type: 'jpeg' });
|
||||
step.screenshot = buffer.toString('base64');
|
||||
break;
|
||||
}
|
||||
case 'OnFailure': {
|
||||
if (!!result.error) {
|
||||
const buffer = await metadata.page.screenshot({ type: 'jpeg' });
|
||||
step.screenshot = buffer.toString('base64');
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this._data.push(step);
|
||||
} catch (e) {
|
||||
// Do not throw from instrumentation.
|
||||
console.log('Error during appinsights instrumentation: ' + e.message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче