From b0b141b186d06f0629a5b450212685af08e02a91 Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Fri, 22 May 2026 16:38:06 -0700 Subject: [PATCH 1/3] Fixes 3163 --- .../Stable/PublicAPI.Unshipped.txt | 2 + .../Extensibility/TelemetryConfiguration.cs | 15 +- .../Web/Web/ApplicationInsightsHttpModule.cs | 203 +------------- .../Web/WebApplicationInsightsInitializer.cs | 227 +++++++++++++++ examples/ClassicAspNetWebApp/Web.config | 264 ++++++++++++------ .../applicationinsights.config | 2 +- 6 files changed, 427 insertions(+), 286 deletions(-) create mode 100644 WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs diff --git a/.publicApi/Microsoft.AI.Web.dll/Stable/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.AI.Web.dll/Stable/PublicAPI.Unshipped.txt index e69de29bb2..b6aa84f8d5 100644 --- a/.publicApi/Microsoft.AI.Web.dll/Stable/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.AI.Web.dll/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.ApplicationInsights.Web.WebApplicationInsightsInitializer +static Microsoft.ApplicationInsights.Web.WebApplicationInsightsInitializer.Initialize() -> void diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs index 1f4eaa371b..ba902bf95a 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs @@ -320,14 +320,15 @@ public void SetAzureTokenCredential(TokenCredential tokenCredential) /// internal void PrependOpenTelemetryBuilderConfiguration(Action 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); + }; + } } /// diff --git a/WEB/Src/Web/Web/ApplicationInsightsHttpModule.cs b/WEB/Src/Web/Web/ApplicationInsightsHttpModule.cs index 0a27516a1c..eb2d78f538 100644 --- a/WEB/Src/Web/Web/ApplicationInsightsHttpModule.cs +++ b/WEB/Src/Web/Web/ApplicationInsightsHttpModule.cs @@ -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; /// /// Platform agnostic module for web application instrumentation. /// + /// + /// The shared is populated from + /// ApplicationInsights.config by , + /// which ASP.NET invokes before Application_Start via + /// . + /// 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; @@ -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; } @@ -150,89 +53,13 @@ public void Init(HttpApplication context) /// 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"); - } - - /// - /// 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. - /// - 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(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("service.version", configOptions.ApplicationVersion), - }); - }); - } - } - - /// - /// Tries to set an internal property on an object using reflection. - /// Used to configure internal properties on AzureMonitorExporterOptions. - /// - 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) diff --git a/WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs b/WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs new file mode 100644 index 0000000000..c1930f5e5f --- /dev/null +++ b/WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs @@ -0,0 +1,227 @@ +// +// Copyright © Microsoft. All Rights Reserved. +// + +[assembly: System.Web.PreApplicationStartMethod( + typeof(Microsoft.ApplicationInsights.Web.WebApplicationInsightsInitializer), + nameof(Microsoft.ApplicationInsights.Web.WebApplicationInsightsInitializer.Initialize))] + +namespace Microsoft.ApplicationInsights.Web +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Reflection; + 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; + + /// + /// Configures the default for classic ASP.NET + /// from ApplicationInsights.config before the application's Application_Start + /// runs. This is invoked automatically by ASP.NET via + /// . + /// + /// + /// This type is public only because + /// requires the target type and method to be public. It is not intended to be called + /// directly by user code. Customers should call , + /// which returns the singleton already populated by this initializer. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class WebApplicationInsightsInitializer + { + private static readonly object SyncRoot = new object(); + private static bool isInitialized; + + /// + /// Initializes the default from + /// ApplicationInsights.config. Safe to call multiple times; subsequent + /// calls are no-ops. + /// + /// + /// This method is public only because + /// requires it to be public. It is invoked automatically by ASP.NET and is not + /// intended to be called directly by user code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Initialize() + { + if (isInitialized) + { + return; + } + + lock (SyncRoot) + { + if (isInitialized) + { + return; + } + + // Materialize the singleton. Any subsequent call to + // TelemetryConfiguration.CreateDefault() returns this same instance. + TelemetryConfiguration cfg = TelemetryConfiguration.CreateDefault(); + + cfg.ExtensionVersion = VersionUtils.ExtensionLabelShimWeb + + VersionUtils.GetVersion(typeof(ApplicationInsightsExtensions)); + + ApplicationInsightsConfigOptions configOptions = + ApplicationInsightsConfigurationReader.GetConfigurationOptions(); + + if (configOptions != null) + { + ApplyConfigOptions(cfg, configOptions); + } + else + { + WebEventSource.Log.NoConnectionStringFoundInConfig(); + cfg.ConfigureOpenTelemetryBuilder( + builder => builder.UseApplicationInsightsAspNetTelemetry()); + } + + isInitialized = true; + } + } + + /// + /// Applies the values read from ApplicationInsights.config to the given + /// . + /// + private static void ApplyConfigOptions(TelemetryConfiguration cfg, ApplicationInsightsConfigOptions configOptions) + { + // Direct TelemetryConfiguration properties (must be set before Build). + if (!string.IsNullOrEmpty(configOptions.ConnectionString)) + { + cfg.ConnectionString = configOptions.ConnectionString; + WebEventSource.Log.ConnectionStringLoadedFromConfig(configOptions.ConnectionString); + } + else + { + WebEventSource.Log.NoConnectionStringFoundInConfig(); + } + + if (configOptions.DisableTelemetry.HasValue) + { + cfg.DisableTelemetry = configOptions.DisableTelemetry.Value; + } + + if (configOptions.TracesPerSecond.HasValue) + { + cfg.TracesPerSecond = configOptions.TracesPerSecond.Value; + } + + if (configOptions.SamplingRatio.HasValue) + { + cfg.SamplingRatio = configOptions.SamplingRatio.Value; + if (!configOptions.TracesPerSecond.HasValue) + { + cfg.TracesPerSecond = null; + } + } + + if (!string.IsNullOrEmpty(configOptions.StorageDirectory)) + { + cfg.StorageDirectory = configOptions.StorageDirectory; + } + + if (configOptions.DisableOfflineStorage.HasValue) + { + cfg.DisableOfflineStorage = configOptions.DisableOfflineStorage.Value; + } + + if (configOptions.EnableTraceBasedLogsSampler.HasValue) + { + cfg.EnableTraceBasedLogsSampler = configOptions.EnableTraceBasedLogsSampler.Value; + } + + // EnableQuickPulseMetricStream -> EnableLiveMetrics (TelemetryConfiguration property). + if (configOptions.EnableQuickPulseMetricStream.HasValue) + { + cfg.EnableLiveMetrics = configOptions.EnableQuickPulseMetricStream.Value; + } + + // Configure OpenTelemetry builder for properties that require OpenTelemetry API. + cfg.ConfigureOpenTelemetryBuilder( + builder => ConfigureOpenTelemetryWithOptions(builder, configOptions)); + } + + /// + /// 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. + /// + 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(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) + { + 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("service.version", configOptions.ApplicationVersion), + }); + }); + } + } + + /// + /// Tries to set an internal property on an object using reflection. + /// Used to configure internal properties on AzureMonitorExporterOptions. + /// + 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. + } + } + } +} diff --git a/examples/ClassicAspNetWebApp/Web.config b/examples/ClassicAspNetWebApp/Web.config index 33e3127392..aef1589f83 100644 --- a/examples/ClassicAspNetWebApp/Web.config +++ b/examples/ClassicAspNetWebApp/Web.config @@ -22,96 +22,180 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/ClassicAspNetWebApp/applicationinsights.config b/examples/ClassicAspNetWebApp/applicationinsights.config index 1884d9be9c..b286f93555 100644 --- a/examples/ClassicAspNetWebApp/applicationinsights.config +++ b/examples/ClassicAspNetWebApp/applicationinsights.config @@ -27,6 +27,6 @@ true true true - false + true 1.0.0 From baec9496bb803a4dc65a1c890a07cd027aa32ca4 Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Fri, 22 May 2026 16:48:21 -0700 Subject: [PATCH 2/3] Update CHANGELOG for #3183 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64206ccf76..8db3c0bb42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) From e7a8fef2854ade8260c7c2bd2704ad8c0f2fb2d2 Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Fri, 22 May 2026 17:00:21 -0700 Subject: [PATCH 3/3] Address Copilot review on #3183: guard against built config, tighten reflection catch, fix test isolation --- ...icationInsightsConfigurationReaderTests.cs | 1 + .../ApplicationInsightsHttpModuleTests.cs | 28 +++------- .../Web/WebApplicationInsightsInitializer.cs | 54 ++++++++++++------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/WEB/Src/Web/Web.Tests/ApplicationInsightsConfigurationReaderTests.cs b/WEB/Src/Web/Web.Tests/ApplicationInsightsConfigurationReaderTests.cs index 20a4931b0d..2eda69a954 100644 --- a/WEB/Src/Web/Web.Tests/ApplicationInsightsConfigurationReaderTests.cs +++ b/WEB/Src/Web/Web.Tests/ApplicationInsightsConfigurationReaderTests.cs @@ -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; diff --git a/WEB/Src/Web/Web.Tests/ApplicationInsightsHttpModuleTests.cs b/WEB/Src/Web/Web.Tests/ApplicationInsightsHttpModuleTests.cs index 99c6e50b7e..8af7500fbe 100644 --- a/WEB/Src/Web/Web.Tests/ApplicationInsightsHttpModuleTests.cs +++ b/WEB/Src/Web/Web.Tests/ApplicationInsightsHttpModuleTests.cs @@ -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 instance to replace the existing one - var lazyType = typeof(Lazy); + // Create a new Lazy instance to replace the existing one. var newLazy = new Lazy(() => new TelemetryConfiguration(), LazyThreadSafetyMode.ExecutionAndPublication); defaultInstanceField.SetValue(null, newLazy); } diff --git a/WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs b/WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs index c1930f5e5f..7f07238856 100644 --- a/WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs +++ b/WEB/Src/Web/Web/WebApplicationInsightsInitializer.cs @@ -65,25 +65,38 @@ public static void Initialize() return; } - // Materialize the singleton. Any subsequent call to - // TelemetryConfiguration.CreateDefault() returns this same instance. - TelemetryConfiguration cfg = TelemetryConfiguration.CreateDefault(); + try + { + // Materialize the singleton. Any subsequent call to + // TelemetryConfiguration.CreateDefault() returns this same instance. + TelemetryConfiguration cfg = TelemetryConfiguration.CreateDefault(); - cfg.ExtensionVersion = VersionUtils.ExtensionLabelShimWeb - + VersionUtils.GetVersion(typeof(ApplicationInsightsExtensions)); + cfg.ExtensionVersion = VersionUtils.ExtensionLabelShimWeb + + VersionUtils.GetVersion(typeof(ApplicationInsightsExtensions)); - ApplicationInsightsConfigOptions configOptions = - ApplicationInsightsConfigurationReader.GetConfigurationOptions(); + ApplicationInsightsConfigOptions configOptions = + ApplicationInsightsConfigurationReader.GetConfigurationOptions(); - if (configOptions != null) - { - ApplyConfigOptions(cfg, configOptions); + if (configOptions != null) + { + ApplyConfigOptions(cfg, configOptions); + } + else + { + WebEventSource.Log.NoConnectionStringFoundInConfig(); + cfg.ConfigureOpenTelemetryBuilder( + builder => builder.UseApplicationInsightsAspNetTelemetry()); + } } - else + catch (InvalidOperationException ex) { - WebEventSource.Log.NoConnectionStringFoundInConfig(); - cfg.ConfigureOpenTelemetryBuilder( - builder => builder.UseApplicationInsightsAspNetTelemetry()); + // The singleton TelemetryConfiguration was already built by user code + // (e.g., a TelemetryClient was constructed before PreApplicationStartMethod + // ran). Property setters and builder-config registrations throw in that + // state. There is nothing we can apply at this point — surface a + // diagnostic and move on. Subsequent calls become no-ops via isInitialized. + WebEventSource.Log.ApplicationInsightsConfigReadError( + "WebApplicationInsightsInitializer skipped: TelemetryConfiguration was already built. " + ex.Message); } isInitialized = true; @@ -217,10 +230,15 @@ private static void TrySetInternalProperty(object target, string propertyName, b property.SetValue(target, value); } } - catch - { - // Silently ignore if property doesn't exist or can't be set. - // This allows forward/backward compatibility across versions. + catch (Exception ex) when ( + ex is AmbiguousMatchException + || ex is TargetException + || ex is TargetInvocationException + || ex is ArgumentException + || ex is MethodAccessException) + { + // Silently ignore if the property is missing or cannot be set. + // This allows forward/backward compatibility across exporter versions. } } }