Skip to content
This repository was archived by the owner on Jul 5, 2020. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
namespace Microsoft.ApplicationInsights.WindowsServer
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights.WindowsServer.Implementation;
using Microsoft.ApplicationInsights.WindowsServer.Implementation.DataContracts;
using Microsoft.ApplicationInsights.WindowsServer.Mock;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Assert = Xunit.Assert;

[TestClass]
public class AppServiceEnvVarMonitorTests
{
// used to clean up the environment variables after we've run this test
static private Dictionary<string, string> environmentInitialState;

[ClassInitialize]
static public void InitializeTests(TestContext context)
{
environmentInitialState = GetCurrentAppServiceEnvironmentVariableValues(false);
}

[ClassCleanup]
static public void CleanupTests()
{
foreach (var kvp in environmentInitialState)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
}

environmentInitialState = null;
}

[TestMethod]
public void ConfirmIntervalCheckEnforced()
{
var envVars = GetCurrentAppServiceEnvironmentVariableValues();

foreach (var kvp in envVars)
{
string val = string.Empty;
AppServiceEnvVarMonitor.GetUpdatedEnvironmentVariable(kvp.Key, ref val);
// set the value to something new
Environment.SetEnvironmentVariable(kvp.Key, string.Concat("UPDATED-", val, "-UPDATED"));
}

// set the next-check time to a value that won't get hit
AppServiceEnvVarMonitor.NextCheckTime = DateTime.MaxValue;

// ensure the current values are indeed different
var currentEnvVars = GetCurrentAppServiceEnvironmentVariableValues();

// ensure the values are cached and aren't getting re-read at this time
foreach (var kvp in envVars)
{
string cachedVal = string.Empty;
AppServiceEnvVarMonitor.GetUpdatedEnvironmentVariable(kvp.Key, ref cachedVal);
Assert.Equal(kvp.Value, cachedVal, StringComparer.Ordinal);
Assert.NotEqual(cachedVal, currentEnvVars[kvp.Key], StringComparer.Ordinal);
}
}

[TestMethod]
public void ConfirmUpdatedEnvironmentIsCaptured()
{
var envVars = GetCurrentAppServiceEnvironmentVariableValues();

foreach (var kvp in envVars)
{
string val = string.Empty;
AppServiceEnvVarMonitor.GetUpdatedEnvironmentVariable(kvp.Key, ref val);
// set the value to something new
Environment.SetEnvironmentVariable(kvp.Key, string.Concat("UPDATED-", val, "-UPDATED"));
}

// set the next-check time to a value that will re-read the values immediately
AppServiceEnvVarMonitor.NextCheckTime = DateTime.MinValue;

// ensure the current values are indeed different
var currentEnvVars = GetCurrentAppServiceEnvironmentVariableValues();

// ensure the values are re-read
foreach (var kvp in envVars)
{
string cachedVal = string.Empty;
AppServiceEnvVarMonitor.GetUpdatedEnvironmentVariable(kvp.Key, ref cachedVal);
Assert.Equal(currentEnvVars[kvp.Key], cachedVal, StringComparer.Ordinal);
Assert.NotEqual(cachedVal, kvp.Value, StringComparer.Ordinal);
}
}

static private Dictionary<string,string> GetCurrentAppServiceEnvironmentVariableValues(bool supplyValue = true)
{
int testValueCount = 0;
Dictionary<string, string> envVars = new Dictionary<string, string>();

foreach (var kvp in AppServiceEnvVarMonitor.CheckedValues)
{
string envVar = Environment.GetEnvironmentVariable(kvp.Key);
if (supplyValue && string.IsNullOrEmpty(envVar))
{
envVar = $"{testValueCount}_Stand-inValue_{testValueCount}";
testValueCount++;
Environment.SetEnvironmentVariable(kvp.Key, envVar);
}
envVars.Add(kvp.Key, envVar);
}

return envVars;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Import_RootNamespace>Microsoft.ApplicationInsights.WindowsServer</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)AppServiceEnvVarMonitorTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AzureInstanceMetadataEndToEndTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AzureWebAppRoleEnvironmentTelemetryInitializerTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AzureInstanceMetadataTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
namespace Microsoft.ApplicationInsights.WindowsServer
{
using System;
using System.Threading;

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation;
Expand All @@ -19,9 +17,6 @@ public class AzureWebAppRoleEnvironmentTelemetryInitializer : ITelemetryInitiali
/// <summary>Predefined suffix for Azure Web App Hostname.</summary>
private const string WebAppSuffix = ".azurewebsites.net";

private string nodeName;
private string roleName;

/// <summary>
/// Initializes a new instance of the <see cref="AzureWebAppRoleEnvironmentTelemetryInitializer" /> class.
/// </summary>
Expand All @@ -38,14 +33,12 @@ public void Initialize(ITelemetry telemetry)
{
if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName))
{
string name = LazyInitializer.EnsureInitialized(ref this.roleName, this.GetRoleName);
telemetry.Context.Cloud.RoleName = name;
telemetry.Context.Cloud.RoleName = this.GetRoleName();
}

if (string.IsNullOrEmpty(telemetry.Context.GetInternalContext().NodeName))
{
string name = LazyInitializer.EnsureInitialized(ref this.nodeName, this.GetNodeName);
telemetry.Context.GetInternalContext().NodeName = name;
telemetry.Context.GetInternalContext().NodeName = this.GetNodeName();
}
}

Expand All @@ -62,7 +55,9 @@ private string GetRoleName()

private string GetNodeName()
{
return Environment.GetEnvironmentVariable(WebAppHostNameEnvironmentVariable) ?? string.Empty;
string nodeName = string.Empty;
AppServiceEnvVarMonitor.GetUpdatedEnvironmentVariable(WebAppHostNameEnvironmentVariable, ref nodeName);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer lazy initialized and we've added ~3 extra stack frames to every request/dependency in the customer app. Variable will be read synchronously every 30 seconds (may be fine if we do not Expand..() it, though).
We may think of an asynchronous approach here. Discussed offline with Derek.

return nodeName;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace Microsoft.ApplicationInsights.WindowsServer.Implementation
{
using System;
using System.Collections.Generic;

/// <summary>
/// Utility to monitor the value of environment variables which may change
/// during the run of an application. Checks the environment variables
/// intermittently.
/// </summary>
internal static class AppServiceEnvVarMonitor
{
// Environment variables tracked by this monitor. (internal to allow tests to modify them)
internal static readonly Dictionary<string, string> CheckedValues = new Dictionary<string, string>()
{
{ "WEBSITE_SITE_NAME", string.Empty },
{ "WEBSITE_HOME_STAMPNAME", string.Empty },
{ "WEBSITE_HOSTNAME", string.Empty }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In comments for environment variable extraction in this PR, it's noted that WEBSITE_HOSTNAME is not a reliable source:

//...refactored so that it did not use WEBSITE_HOSTNAME, which is determined to be unreliable for functions during slot swaps.

Do we want to rely on that field, or we may use some tricks, e.g. from the same PR to build something more reliable from what we have.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I say we should resort to these tactics if we have an issue or have proof that App Services are affected by the same failing that Azure Functions are. I have put an issue together to verify that we can detect slot swap using only WEBSITE_HOSTNAME, and to update to using the WEBSITE_SITE_NAME + WEBSITE_SLOT_NAME if the need arises. Note that I've included the WEBSITE_SLOT_NAME in the list of environment variables that the monitor watches for now.

};

// When is the next time we will allow a check to occur? (internal to allow tests to modify this to avoid waits)
internal static DateTime NextCheckTime = DateTime.MinValue;

// how often we allow the code to re-check the environment
private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(30);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let user configure it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do, not sure how much value would be there. I propose we raise an issue and chat about it for the next release.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not overexpose API we are not certain in and may want to pull in the very next PR. We can make it configurable later if we stick with this solution, we cannot unmake public API once it's in stable release without the breaking change.


/// <summary>
/// Get the latest value assigned to an environment variable.
/// </summary>
/// <param name="envVarName">Name of the environment variable to acquire.</param>
/// <param name="value">Value of the environment variable (updated within the update interval).</param>
public static void GetUpdatedEnvironmentVariable(string envVarName, ref string value)
{
if (!string.IsNullOrEmpty(envVarName))
{
CheckVariablesIntermittent();
CheckedValues.TryGetValue(envVarName, out value);
}
}

/// <summary>
/// Simply update the stored environment variables if the last time we
/// checked from now is greater than the check interval.
/// </summary>
private static void CheckVariablesIntermittent()
{
DateTime rightNow = DateTime.UtcNow;
if (rightNow > NextCheckTime)
{
NextCheckTime = rightNow + CheckInterval;

List<string> keys = new List<string>(CheckedValues.Keys);
foreach (var key in keys)
{
CheckedValues[key] = Environment.GetEnvironmentVariable(key);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Compile Include="$(MSBuildThisFileDirectory)BuildInfoConfigComponentVersionTelemetryInitializer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DeveloperModeWithDebuggerAttachedTelemetryModule.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DomainNameRoleInstanceTelemetryInitializer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Implementation\AppServiceEnvVarMonitor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Implementation\AzureComputeMetadataHeartbeatPropertyProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Implementation\AzureMetadataRequestor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Implementation\DataContracts\AzureInstanceComputeMetadata.cs" />
Expand Down