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
@@ -0,0 +1,2 @@
Microsoft.ApplicationInsights.Web.WebApplicationInsightsInitializer
static Microsoft.ApplicationInsights.Web.WebApplicationInsightsInitializer.Initialize() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,15 @@ public void SetAzureTokenCredential(TokenCredential tokenCredential)
/// </summary>
internal void PrependOpenTelemetryBuilderConfiguration(Action<IOpenTelemetryBuilder> configure)
{
this.ThrowIfBuilt();

var previousConfiguration = this.builderConfiguration;
this.builderConfiguration = builder =>
if (!this.isBuilt)
{
configure(builder);
previousConfiguration(builder);
};
var previousConfiguration = this.builderConfiguration;
this.builderConfiguration = builder =>
{
configure(builder);
previousConfiguration(builder);
};
}
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- [Fix #3163: `TelemetryConfiguration.CreateDefault()` on classic ASP.NET now returns a configuration already populated from `ApplicationInsights.config` (connection string, sampling, storage, live metrics, request/dependency tracking, etc.). A new `PreApplicationStartMethod` (`WebApplicationInsightsInitializer`) materializes and populates the singleton before `Application_Start` runs, so customer code calling `TelemetryConfiguration.CreateDefault()` from `Global.asax.cs` no longer gets an empty configuration. The `ApplicationInsightsHttpModule` no longer carries the config-reading bookkeeping. `TelemetryConfiguration.PrependOpenTelemetryBuilderConfiguration` no longer throws if the configuration was already built — it silently skips, so a second `TelemetryClient` constructed against an already-built configuration no longer throws `InvalidOperationException`.](https://github.com/microsoft/ApplicationInsights-dotnet/pull/3183)

## Version 3.1.1
- [Update OpenTelemetry and Azure Monitor dependencies to address known security advisories (e.g. [GHSA-g94r-2vxg-569j](https://github.com/advisories/GHSA-g94r-2vxg-569j) in `OpenTelemetry.Api` 1.15.1).](https://github.com/microsoft/ApplicationInsights-dotnet/pull/3174)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Microsoft.ApplicationInsights.Web.Tests
using Microsoft.ApplicationInsights.Web.Implementation;
using Xunit;

[Collection("ApplicationInsightsHttpModule")] // Same collection as ApplicationInsightsHttpModuleTests; both write the same ApplicationInsights.config file in the test base directory.
public class ApplicationInsightsConfigurationReaderTests : IDisposable
{
private readonly string configFilePath;
Expand Down
28 changes: 8 additions & 20 deletions WEB/Src/Web/Web.Tests/ApplicationInsightsHttpModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,36 +443,24 @@ private TelemetryConfiguration GetTelemetryConfigurationFromModule(ApplicationIn

private void ResetStaticState()
{
// Use reflection to reset static state for test isolation
var type = typeof(ApplicationInsightsHttpModule);

var sharedConfigField = type.GetField("sharedTelemetryConfiguration", BindingFlags.Static | BindingFlags.NonPublic);
if (sharedConfigField != null)
{
sharedConfigField.SetValue(null, null);
}

var isInitializedField = type.GetField("isInitialized", BindingFlags.Static | BindingFlags.NonPublic);
// Use reflection to reset static state for test isolation.
// The HTTP module no longer carries its own static state; configuration loading
// moved to WebApplicationInsightsInitializer, which has its own one-shot guard.
var initializerType = typeof(WebApplicationInsightsInitializer);
var isInitializedField = initializerType.GetField("isInitialized", BindingFlags.Static | BindingFlags.NonPublic);
if (isInitializedField != null)
{
isInitializedField.SetValue(null, false);
}

var initCountField = type.GetField("initializationCount", BindingFlags.Static | BindingFlags.NonPublic);
if (initCountField != null)
{
initCountField.SetValue(null, 0);
}

// This next block is added because of tests that check the value of exporter options for sampling settings.
// Also reset the TelemetryConfiguration.DefaultInstance static Lazy field
// This is necessary because CreateDefault() returns a singleton that can only be built once
// Also reset the TelemetryConfiguration.DefaultInstance static Lazy field.
// This is necessary because CreateDefault() returns a singleton that can only be built once.
var telemetryConfigType = typeof(TelemetryConfiguration);
var defaultInstanceField = telemetryConfigType.GetField("DefaultInstance", BindingFlags.Static | BindingFlags.NonPublic);
if (defaultInstanceField != null)
{
// Create a new Lazy<TelemetryConfiguration> instance to replace the existing one
var lazyType = typeof(Lazy<TelemetryConfiguration>);
// Create a new Lazy<TelemetryConfiguration> instance to replace the existing one.
var newLazy = new Lazy<TelemetryConfiguration>(() => new TelemetryConfiguration(), LazyThreadSafetyMode.ExecutionAndPublication);
defaultInstanceField.SetValue(null, newLazy);
}
Expand Down
203 changes: 15 additions & 188 deletions WEB/Src/Web/Web/ApplicationInsightsHttpModule.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
namespace Microsoft.ApplicationInsights.Web
{
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Internal;
using Microsoft.ApplicationInsights.Web.Extensions;
using Microsoft.ApplicationInsights.Web.Implementation;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

/// <summary>
/// Platform agnostic module for web application instrumentation.
/// </summary>
/// <remarks>
/// The shared <see cref="TelemetryConfiguration"/> is populated from
/// <c>ApplicationInsights.config</c> by <see cref="WebApplicationInsightsInitializer"/>,
/// which ASP.NET invokes before <c>Application_Start</c> via
/// <see cref="System.Web.PreApplicationStartMethodAttribute"/>.
/// </remarks>
public sealed class ApplicationInsightsHttpModule : IHttpModule
{
// Static fields to track initialization across all module instances
private static readonly object StaticLockObject = new object();
private static int initializationCount = 0;
private static TelemetryConfiguration sharedTelemetryConfiguration;
private static bool isInitialized = false;

private readonly object lockObject = new object();
private TelemetryConfiguration telemetryConfiguration;
private TelemetryClient telemetryClient;
Expand All @@ -47,101 +37,14 @@ public void Init(HttpApplication context)
throw new ArgumentNullException(nameof(context));
}

lock (StaticLockObject)
{
initializationCount++;
System.Diagnostics.Debug.WriteLine($"Module Init called #{initializationCount} at {DateTime.Now:HH:mm:ss.fff}");
System.Diagnostics.Debug.WriteLine($"AppDomain: {AppDomain.CurrentDomain.Id}");

// Only initialize the shared configuration once per AppDomain
if (!isInitialized)
{
System.Diagnostics.Debug.WriteLine("Performing first-time initialization");

sharedTelemetryConfiguration = TelemetryConfiguration.CreateDefault();

sharedTelemetryConfiguration.ExtensionVersion = VersionUtils.ExtensionLabelShimWeb + VersionUtils.GetVersion(typeof(ApplicationInsightsExtensions));

// Read all configuration options from applicationinsights.config
ApplicationInsightsConfigOptions configOptions = ApplicationInsightsConfigurationReader.GetConfigurationOptions();

if (configOptions != null)
{
// Apply Group 1: Direct TelemetryConfiguration properties (before Build)
if (!string.IsNullOrEmpty(configOptions.ConnectionString))
{
sharedTelemetryConfiguration.ConnectionString = configOptions.ConnectionString;
WebEventSource.Log.ConnectionStringLoadedFromConfig(configOptions.ConnectionString);
}
else
{
WebEventSource.Log.NoConnectionStringFoundInConfig();
}

if (configOptions.DisableTelemetry.HasValue)
{
sharedTelemetryConfiguration.DisableTelemetry = configOptions.DisableTelemetry.Value;
}
// Defensive: PreApplicationStartMethod normally runs before this, but call again
// in case the module is loaded without our assembly being scanned early.
// Initialize() is idempotent.
WebApplicationInsightsInitializer.Initialize();

if (configOptions.TracesPerSecond.HasValue)
{
sharedTelemetryConfiguration.TracesPerSecond = configOptions.TracesPerSecond.Value;
}
// CreateDefault returns the singleton already populated by the initializer above.
this.telemetryConfiguration = TelemetryConfiguration.CreateDefault();

if (configOptions.SamplingRatio.HasValue)
{
sharedTelemetryConfiguration.SamplingRatio = configOptions.SamplingRatio.Value;
if (!configOptions.TracesPerSecond.HasValue)
{
sharedTelemetryConfiguration.TracesPerSecond = null;
}
}

if (!string.IsNullOrEmpty(configOptions.StorageDirectory))
{
sharedTelemetryConfiguration.StorageDirectory = configOptions.StorageDirectory;
}

if (configOptions.DisableOfflineStorage.HasValue)
{
sharedTelemetryConfiguration.DisableOfflineStorage = configOptions.DisableOfflineStorage.Value;
}

if (configOptions.EnableTraceBasedLogsSampler.HasValue)
{
sharedTelemetryConfiguration.EnableTraceBasedLogsSampler = configOptions.EnableTraceBasedLogsSampler.Value;
}

// EnableQuickPulseMetricStream -> EnableLiveMetrics (TelemetryConfiguration property)
if (configOptions.EnableQuickPulseMetricStream.HasValue)
{
sharedTelemetryConfiguration.EnableLiveMetrics = configOptions.EnableQuickPulseMetricStream.Value;
}

// Configure OpenTelemetry builder for properties that require OpenTelemetry API
sharedTelemetryConfiguration.ConfigureOpenTelemetryBuilder(
builder => ConfigureOpenTelemetryWithOptions(builder, configOptions));
}
else
{
WebEventSource.Log.NoConnectionStringFoundInConfig();

sharedTelemetryConfiguration.ConfigureOpenTelemetryBuilder(
builder => builder.UseApplicationInsightsAspNetTelemetry());
}

isInitialized = true;
}
else
{
System.Diagnostics.Debug.WriteLine("Skipping duplicate initialization - using shared configuration");
}

// Use the shared configuration for this instance
this.telemetryConfiguration = sharedTelemetryConfiguration;
}

// Subscribe to events (this is safe to do multiple times as ASP.NET handles duplicate subscriptions)
context.BeginRequest += this.OnBeginRequest;
}

Expand All @@ -150,89 +53,13 @@ public void Init(HttpApplication context)
/// </summary>
public void Dispose()
{
// Don't dispose the shared configuration, as other instances might still be using it
// It will be cleaned up when the AppDomain unloads

// Note: If you need to dispose, you'd need a reference counting mechanism
System.Diagnostics.Debug.WriteLine("Dispose called");
}

/// <summary>
/// Configures OpenTelemetry builder with options that require OpenTelemetry API access.
/// Note: Classic ASP.NET doesn't use DI, so we can only configure things through the builder's direct API.
/// </summary>
private static void ConfigureOpenTelemetryWithOptions(IOpenTelemetryBuilder builder, ApplicationInsightsConfigOptions configOptions)
{
builder.UseApplicationInsightsAspNetTelemetry();

// Configure AzureMonitorExporterOptions for internal properties using reflection
// Even though classic ASP.NET doesn't use DI, the OpenTelemetry builder does internally
builder.Services.Configure<AzureMonitorExporterOptions>(exporterOptions =>
{
// EnablePerformanceCounterCollectionModule -> EnablePerfCounters (internal property)
if (configOptions.EnablePerformanceCounterCollectionModule.HasValue)
{
TrySetInternalProperty(exporterOptions, "EnablePerfCounters", configOptions.EnablePerformanceCounterCollectionModule.Value);
}

// AddAutoCollectedMetricExtractor -> EnableStandardMetrics (internal property)
if (configOptions.AddAutoCollectedMetricExtractor.HasValue)
{
TrySetInternalProperty(exporterOptions, "EnableStandardMetrics", configOptions.AddAutoCollectedMetricExtractor.Value);
}
});

// Handle EnableDependencyTrackingTelemetryModule and EnableRequestTrackingTelemetryModule - add activity filter processor
bool enableDependencyTracking = configOptions.EnableDependencyTrackingTelemetryModule ?? true;
bool enableRequestTracking = configOptions.EnableRequestTrackingTelemetryModule ?? true;

// Only add processor if either feature is disabled
if (!enableDependencyTracking || !enableRequestTracking)
{
// Use WithTracing to get TracerProviderBuilder which has AddProcessor method
builder.WithTracing(tracerBuilder =>
{
tracerBuilder.AddProcessor(new ActivityFilterProcessor(enableDependencyTracking, enableRequestTracking));
});
}

// Handle ApplicationVersion - add to resource attributes
if (!string.IsNullOrEmpty(configOptions.ApplicationVersion))
{
builder.ConfigureResource(resourceBuilder =>
{
resourceBuilder.AddAttributes(new[]
{
new KeyValuePair<string, object>("service.version", configOptions.ApplicationVersion),
});
});
}
}

/// <summary>
/// Tries to set an internal property on an object using reflection.
/// Used to configure internal properties on AzureMonitorExporterOptions.
/// </summary>
private static void TrySetInternalProperty(object target, string propertyName, bool value)
{
try
{
var property = target.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property != null && property.CanWrite && property.PropertyType == typeof(bool))
{
property.SetValue(target, value);
}
}
catch
{
// Silently ignore if property doesn't exist or can't be set
// This allows forward/backward compatibility across versions
}
// The shared TelemetryConfiguration singleton is owned by TelemetryConfiguration
// itself and is cleaned up on AppDomain unload. Nothing to dispose here.
}

private void OnBeginRequest(object sender, EventArgs eventArgs)
{
// Ensure TelemetryClient is created only once per module instance using double-check locking pattern
// Ensure TelemetryClient is created only once per module instance using double-check locking.
if (this.telemetryClient == null)
{
lock (this.lockObject)
Expand Down
Loading
Loading