diff --git a/src/ApplicationInsights.Kubernetes/Extensions/ApplicationInsightsExtensions.cs b/src/ApplicationInsights.Kubernetes/Extensions/ApplicationInsightsExtensions.cs index 5880b14..94ae6da 100644 --- a/src/ApplicationInsights.Kubernetes/Extensions/ApplicationInsightsExtensions.cs +++ b/src/ApplicationInsights.Kubernetes/Extensions/ApplicationInsightsExtensions.cs @@ -18,14 +18,18 @@ public static partial class ApplicationInsightsExtensions /// Enables Application Insights for Kubernetes on the Default TelemetryConfiguration in the dependency injection container with custom options. /// /// Collection of service descriptors. - /// Action to customize the configuration of Application Insights for Kubernetes. /// Sets the diagnostics log levels for the enricher. + /// If true, the Application Insights enricher library will not use any background (hosted) services, + /// and Kubernetes information will not be fetched automatically. Hosted services are not allowed in some environments, e.g. Azure Function. + /// For more information see https://github.com/Azure/azure-functions-host/issues/5447#issuecomment-575368316 + /// Action to customize the configuration of Application Insights for Kubernetes. /// Provides a custom implementation to check whether it is inside kubernetes cluster or not. /// The service collection for chaining the next operation. public static IServiceCollection AddApplicationInsightsKubernetesEnricher( this IServiceCollection services, - Action? applyOptions = default, LogLevel? diagnosticLogLevel = LogLevel.None, + bool disableBackgroundService = false, + Action? applyOptions = default, IClusterEnvironmentCheck? clusterCheck = default) { diagnosticLogLevel ??= LogLevel.None; // Default to None. @@ -37,7 +41,7 @@ public static IServiceCollection AddApplicationInsightsKubernetesEnricher( if (!KubernetesTelemetryInitializerExists(services)) { - services.ConfigureKubernetesTelemetryInitializer(applyOptions, clusterCheck); + services.ConfigureKubernetesTelemetryInitializer(applyOptions, clusterCheck, disableBackgroundService); } return services; } @@ -48,7 +52,7 @@ public static IServiceCollection AddApplicationInsightsKubernetesEnricher( public static void StartApplicationInsightsKubernetesEnricher(this IServiceProvider serviceProvider) { IK8sInfoBootstrap? k8sInfoBootstrap = serviceProvider.GetService(); - if(k8sInfoBootstrap is null) + if (k8sInfoBootstrap is null) { _logger.LogInformation("No service registered by type {0}. Either not running in a Kubernetes cluster or `{1}()` wasn't called on the service collection.", nameof(IK8sInfoBootstrap), nameof(AddApplicationInsightsKubernetesEnricher)); return; @@ -69,9 +73,10 @@ private static bool KubernetesTelemetryInitializerExists(IServiceCollection serv internal static void ConfigureKubernetesTelemetryInitializer( this IServiceCollection services, Action? overwriteOptions, - IClusterEnvironmentCheck? clusterCheck) + IClusterEnvironmentCheck? clusterCheck, + bool skipRegisterBackendService = false) { - IKubernetesServiceCollectionBuilder kubernetesServiceCollectionBuilder = new KubernetesServiceCollectionBuilder(overwriteOptions, clusterCheck); + IKubernetesServiceCollectionBuilder kubernetesServiceCollectionBuilder = new KubernetesServiceCollectionBuilder(overwriteOptions, clusterCheck, skipRegisterBackendService); _ = kubernetesServiceCollectionBuilder.RegisterServices(services); } } diff --git a/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs b/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs index 44686e9..35964e8 100644 --- a/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs +++ b/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs @@ -17,6 +17,7 @@ namespace Microsoft.Extensions.DependencyInjection; internal class KubernetesServiceCollectionBuilder : IKubernetesServiceCollectionBuilder { private readonly IClusterEnvironmentCheck _clusterCheck; + private readonly bool _skipRegisterBackendService; private readonly Action? _customizeOptions; private readonly ApplicationInsightsKubernetesDiagnosticSource _logger = ApplicationInsightsKubernetesDiagnosticSource.Instance; @@ -29,10 +30,12 @@ internal class KubernetesServiceCollectionBuilder : IKubernetesServiceCollection /// public KubernetesServiceCollectionBuilder( Action? customizeOptions, - IClusterEnvironmentCheck? clusterCheck) + IClusterEnvironmentCheck? clusterCheck, + bool skipRegisterBackendService) { _customizeOptions = customizeOptions; _clusterCheck = clusterCheck ?? new ClusterEnvironmentCheck(); + _skipRegisterBackendService = skipRegisterBackendService; } /// @@ -142,7 +145,13 @@ protected virtual void RegisterK8sEnvironmentFactory(IServiceCollection serviceC serviceCollection.TryAddScoped(); serviceCollection.TryAddSingleton(_ => K8sEnvironmentHolder.Instance); + _logger.LogTrace("Registering bootstrap and hosted service."); serviceCollection.TryAddSingleton(); - serviceCollection.AddHostedService(); + if (!_skipRegisterBackendService) + { + _logger.LogInformation("Skip registering {0} by user configuration.", nameof(K8sInfoBackgroundService)); + serviceCollection.AddHostedService(); + } + _logger.LogTrace("Registered bootstrap and hosted service."); } } diff --git a/src/ApplicationInsights.Kubernetes/IK8sInfoBootstrap.cs b/src/ApplicationInsights.Kubernetes/IK8sInfoBootstrap.cs index e1c69bb..1d2ab56 100644 --- a/src/ApplicationInsights.Kubernetes/IK8sInfoBootstrap.cs +++ b/src/ApplicationInsights.Kubernetes/IK8sInfoBootstrap.cs @@ -8,7 +8,7 @@ namespace Microsoft.ApplicationInsights.Kubernetes; /// The intention is for the client to have a handle to start getting Kubernetes info to be consumed by the . /// Remark: This is supposed to only be used in Console Application. Do NOT use this in ASP.NET or Worker, where the hosted service exists. /// -internal interface IK8sInfoBootstrap +public interface IK8sInfoBootstrap { /// /// Bootstrap the fetch of Kubernetes information. diff --git a/tests/UnitTests/AppInsightsKubernetesOptionsTests.cs b/tests/UnitTests/AppInsightsKubernetesOptionsTests.cs index bd93781..04035d4 100644 --- a/tests/UnitTests/AppInsightsKubernetesOptionsTests.cs +++ b/tests/UnitTests/AppInsightsKubernetesOptionsTests.cs @@ -18,7 +18,7 @@ public void ShouldHaveDefaultOptions() clusterCheck.Setup(c => c.IsInCluster).Returns(true); - KubernetesServiceCollectionBuilder builder = new KubernetesServiceCollectionBuilder(customizeOptions: default, clusterCheck.Object); + KubernetesServiceCollectionBuilder builder = new KubernetesServiceCollectionBuilder(customizeOptions: default, clusterCheck.Object, skipRegisterBackendService: false); IServiceCollection services = new ServiceCollection(); IConfiguration configuration = (new ConfigurationBuilder()).Build(); @@ -44,7 +44,7 @@ public void ShouldTakeOptionFromIConfiguration() clusterCheck.Setup(c => c.IsInCluster).Returns(true); - KubernetesServiceCollectionBuilder builder = new KubernetesServiceCollectionBuilder(customizeOptions: default, clusterCheck.Object); + KubernetesServiceCollectionBuilder builder = new KubernetesServiceCollectionBuilder(customizeOptions: default, clusterCheck.Object, skipRegisterBackendService: false); IServiceCollection services = new ServiceCollection(); IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() @@ -71,7 +71,7 @@ public void ShouldTakeDelegateOverwriteForSettings() KubernetesServiceCollectionBuilder builder = new KubernetesServiceCollectionBuilder(customizeOptions: opt => { opt.InitializationTimeout = TimeSpan.FromSeconds(10); // The user settings through code will take precedence. - }, clusterCheck.Object); + }, clusterCheck.Object, skipRegisterBackendService: false); IServiceCollection services = new ServiceCollection(); IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() @@ -101,7 +101,7 @@ public void ShouldAllowSetTelemetryKeyProcessorByCode() KubernetesServiceCollectionBuilder builder = new KubernetesServiceCollectionBuilder(customizeOptions: opt => { opt.TelemetryKeyProcessor = keyTransformer; - }, clusterCheck.Object); + }, clusterCheck.Object, skipRegisterBackendService: false); IServiceCollection services = new ServiceCollection(); IConfiguration configuration = new ConfigurationBuilder().Build(); diff --git a/tests/UnitTests/ApplicationInsightsExtensionsTests.cs b/tests/UnitTests/ApplicationInsightsExtensionsTests.cs index 0e65e2e..3c80212 100644 --- a/tests/UnitTests/ApplicationInsightsExtensionsTests.cs +++ b/tests/UnitTests/ApplicationInsightsExtensionsTests.cs @@ -1,6 +1,9 @@ using System; +using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Moq; using Xunit; namespace Microsoft.ApplicationInsights.Kubernetes.Tests; @@ -29,7 +32,7 @@ public void ShouldAllowOverwriteOptions() IServiceCollection collection = new ServiceCollection(); // If there's compile error, check if the signature of AddApplicationInsightsKubernetesEnricher was changed. - collection = collection.AddApplicationInsightsKubernetesEnricher(opt => opt.InitializationTimeout = TimeSpan.FromMinutes(15)); + collection = collection.AddApplicationInsightsKubernetesEnricher(applyOptions: opt => opt.InitializationTimeout = TimeSpan.FromMinutes(15)); Assert.NotNull(collection); } @@ -52,9 +55,35 @@ public void ShouldAllowOverwritingOptionsAndDiagnosticLoggingLevel() // If there's compile error, check if the signature of AddApplicationInsightsKubernetesEnricher was changed. collection = collection.AddApplicationInsightsKubernetesEnricher( - opt => opt.InitializationTimeout = TimeSpan.FromMinutes(15), - diagnosticLogLevel: LogLevel.Trace); + diagnosticLogLevel: LogLevel.Trace, + applyOptions: opt => opt.InitializationTimeout = TimeSpan.FromMinutes(15)); Assert.NotNull(collection); } + + [Theory] + [InlineData(false, true)] + [InlineData(true, false)] + public void ShouldNotRegisterHostedServiceWhenSet(bool disableBackgroundService, bool expectServiceRegistered) + { + IServiceCollection collection = new ServiceCollection(); + + Mock clusterCheck = new(); + clusterCheck.Setup(c => c.IsInCluster).Returns(true); + + // If there's compile error, check if the signature of AddApplicationInsightsKubernetesEnricher was changed. + collection = collection.AddApplicationInsightsKubernetesEnricher(disableBackgroundService: disableBackgroundService, clusterCheck: clusterCheck.Object); + + Assert.NotNull(collection); + bool registered = collection.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IHostedService)); + + if (expectServiceRegistered) + { + Assert.True(registered); + } + else + { + Assert.False(registered); + } + } } diff --git a/tests/UnitTests/KubernetesEnablementTests.cs b/tests/UnitTests/KubernetesEnablementTests.cs index bf7569f..b0a21db 100644 --- a/tests/UnitTests/KubernetesEnablementTests.cs +++ b/tests/UnitTests/KubernetesEnablementTests.cs @@ -21,7 +21,7 @@ public void ServicesRegistered() clusterCheckMock.Setup(c => c.IsInCluster).Returns(true); - KubernetesServiceCollectionBuilder target = new KubernetesServiceCollectionBuilder(customizeOptions: null, clusterCheckMock.Object); + KubernetesServiceCollectionBuilder target = new KubernetesServiceCollectionBuilder(customizeOptions: null, clusterCheckMock.Object, skipRegisterBackendService: false); target.RegisterServices(services); Assert.NotNull(services.FirstOrDefault(sd => sd.ImplementationType == typeof(KubernetesTelemetryInitializer)));