Skip to content
Merged
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
Expand Up @@ -6,9 +6,10 @@
<AssemblyName>Microsoft.Azure.Functions.Worker.ApplicationInsights</AssemblyName>
<RootNamespace>Microsoft.Azure.Functions.Worker.ApplicationInsights</RootNamespace>
<MajorProductVersion>1</MajorProductVersion>
<MinorProductVersion>0</MinorProductVersion>
<MinorProductVersion>1</MinorProductVersion>
<PatchProductVersion>0</PatchProductVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<VersionSuffix>-preview1</VersionSuffix>
<BeforePack>$(BeforePack);GetReleaseNotes</BeforePack>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public static IServiceCollection ConfigureFunctionsApplicationInsights(this ISer
throw new ArgumentNullException(nameof(services));
}

services.AddSingleton<IConfigureOptions<AppServiceOptions>, AppServiceOptionsInitializer>();
services.AddSingleton<AppServiceEnvironmentVariableMonitor>();
services.AddSingleton<IOptionsChangeTokenSource<AppServiceOptions>>(p => p.GetRequiredService<AppServiceEnvironmentVariableMonitor>());
services.AddSingleton<IHostedService>(p => p.GetRequiredService<AppServiceEnvironmentVariableMonitor>());

services.AddSingleton<FunctionsRoleInstanceProvider>();

services.TryAddEnumerable(ServiceDescriptor.Singleton<ITelemetryInitializer, FunctionsTelemetryInitializer>());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers;

internal class AppServiceEnvironmentVariableMonitor : BackgroundService, IOptionsChangeTokenSource<AppServiceOptions>
{
private readonly TimeSpan _refreshInterval;

private IChangeToken _changeToken;
private CancellationTokenSource _cancellationTokenSource = new();

private readonly Dictionary<string, string?> _monitoredVariableCache = new(StringComparer.OrdinalIgnoreCase);

public AppServiceEnvironmentVariableMonitor() : this(TimeSpan.FromSeconds(5))
{
}

public AppServiceEnvironmentVariableMonitor(TimeSpan refreshInterval)
{
_refreshInterval = refreshInterval;
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
}

public string Name => string.Empty;

public IChangeToken GetChangeToken() => _changeToken;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
bool changeDetected = false;

foreach (string envVar in AppServiceOptionsInitializer.EnvironmentVariablesToMonitor)
{
string? currentVal = Environment.GetEnvironmentVariable(envVar);
_monitoredVariableCache.TryGetValue(envVar, out string? cachedVal);

if (!string.Equals(currentVal, cachedVal, StringComparison.Ordinal))
{
changeDetected = true;
_monitoredVariableCache[envVar] = currentVal;
}
}

if (changeDetected)
{
var oldTokenSource = Interlocked.Exchange(ref _cancellationTokenSource, new CancellationTokenSource());
Interlocked.Exchange(ref _changeToken, new CancellationChangeToken(_cancellationTokenSource.Token));

if (!oldTokenSource.IsCancellationRequested)
{
oldTokenSource.Cancel();
oldTokenSource.Dispose();
}
}

try
{
await Task.Delay(_refreshInterval, stoppingToken);
Comment thread
brettsam marked this conversation as resolved.
}
catch (OperationCanceledException)
{
// happens during normal shutdown
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers;

internal class AppServiceOptions
{
public string? AzureWebsiteName { get; set; }

public string? AzureWebsiteSlotName { get; set; }

public string? AzureWebsiteCloudRoleName { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers;
using Microsoft.Extensions.Options;

internal class AppServiceOptionsInitializer : IConfigureOptions<AppServiceOptions>
{
internal const string AzureWebsiteName = "WEBSITE_SITE_NAME";
internal const string AzureWebsiteSlotName = "WEBSITE_SLOT_NAME";
internal const string AzureWebsiteCloudRoleName = "WEBSITE_CLOUD_ROLENAME";
internal const string DefaultProductionSlotName = "production";

internal static string[] EnvironmentVariablesToMonitor = new[] { AzureWebsiteName, AzureWebsiteSlotName, AzureWebsiteCloudRoleName };

public void Configure(AppServiceOptions options)
{
options.AzureWebsiteName = Environment.GetEnvironmentVariable(AzureWebsiteName);
Comment thread
brettsam marked this conversation as resolved.
options.AzureWebsiteCloudRoleName = Environment.GetEnvironmentVariable(AzureWebsiteCloudRoleName);

// Compute the slot name by appending non-production slot to the site name (i.e. mysite-staging)
string slotName = Environment.GetEnvironmentVariable(AzureWebsiteSlotName);
options.AzureWebsiteSlotName = GetAzureWebsiteUniqueSlotName(options.AzureWebsiteName, slotName);
}

/// <summary>
/// Gets a value that uniquely identifies the site and slot.
/// </summary>
private static string? GetAzureWebsiteUniqueSlotName(string? websiteName, string? slotName)
{
if (!string.IsNullOrEmpty(slotName) &&
!string.Equals(slotName, DefaultProductionSlotName, StringComparison.OrdinalIgnoreCase))
{
websiteName += $"-{slotName}";
}

return websiteName?.ToLowerInvariant();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers
{
Expand All @@ -17,13 +18,15 @@ namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers
/// </summary>
internal class FunctionsRoleEnvironmentTelemetryInitializer : ITelemetryInitializer
{
internal const string AzureWebsiteName = "WEBSITE_SITE_NAME";
internal const string AzureWebsiteSlotName = "WEBSITE_SLOT_NAME";
internal const string AzureWebsiteCloudRoleName = "WEBSITE_CLOUD_ROLENAME";
private const string DefaultProductionSlotName = "production";
private const string WebAppSuffix = ".azurewebsites.net";
internal const string WebAppSuffix = ".azurewebsites.net";

private readonly ConcurrentDictionary<string, string> _siteNodeNames = new(StringComparer.OrdinalIgnoreCase);
private readonly IOptionsMonitor<AppServiceOptions> _appServiceOptions;

public FunctionsRoleEnvironmentTelemetryInitializer(IOptionsMonitor<AppServiceOptions> appServiceOptions)
{
_appServiceOptions = appServiceOptions;
}

/// <summary>
/// Initializes <see cref="ITelemetry" /> device context.
Expand All @@ -36,49 +39,27 @@ public void Initialize(ITelemetry telemetry)
return;
}

var siteSlotName = new Lazy<string?>(() =>
{
// We cannot cache these values as the environment variables can change on the fly.
return GetAzureWebsiteUniqueSlotName();
});

var websiteCloudRoleName = Environment.GetEnvironmentVariable(AzureWebsiteCloudRoleName);
var options = _appServiceOptions.CurrentValue;

if (!string.IsNullOrEmpty(websiteCloudRoleName))
if (!string.IsNullOrEmpty(options.AzureWebsiteCloudRoleName))
{
telemetry.Context.Cloud.RoleName = websiteCloudRoleName;
telemetry.Context.Cloud.RoleName = options.AzureWebsiteCloudRoleName;
}
else
{
telemetry.Context.Cloud.RoleName = siteSlotName.Value;
telemetry.Context.Cloud.RoleName = options.AzureWebsiteSlotName;
}

var internalContext = telemetry.Context.GetInternalContext();
if (!string.IsNullOrEmpty(siteSlotName.Value))
if (!string.IsNullOrEmpty(options.AzureWebsiteSlotName))
{
internalContext.NodeName = _siteNodeNames.GetOrAdd(siteSlotName.Value!, p =>
internalContext.NodeName = _siteNodeNames.GetOrAdd(options.AzureWebsiteSlotName!, p =>
{
// maintain previous behavior of node having the full url
return p += WebAppSuffix;
});
}
}

/// <summary>
/// Gets a value that uniquely identifies the site and slot.
/// </summary>
private static string? GetAzureWebsiteUniqueSlotName()
{
var name = Environment.GetEnvironmentVariable(AzureWebsiteName);
var slotName = Environment.GetEnvironmentVariable(AzureWebsiteSlotName);

if (!string.IsNullOrEmpty(slotName) &&
!string.Equals(slotName, DefaultProductionSlotName, StringComparison.OrdinalIgnoreCase))
{
name += $"-{slotName}";
}

return name?.ToLowerInvariant();
}
}
}

2 changes: 1 addition & 1 deletion src/DotNetWorker.ApplicationInsights/release_notes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## What's Changed

- GA release (no functional changes)
- Moving hot-path environment variable checks to a background task (#1996)
Loading