diff --git a/release_notes.md b/release_notes.md index 8d0d526632..e5306a8663 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,8 +3,8 @@ <!-- Please add your release notes in the following format: - My change description (#PR) --> +- Add support for managed identity when using open telemetry + azure monitor (#10615) - Update Java Worker Version to [2.18.0](https://github.com/Azure/azure-functions-java-worker/releases/tag/2.18.0) - - Allow for an output binding value of an invocation result to be null (#10698) - Updated dotnet-isolated worker to 1.0.12. - [Corrected the path for the prelaunch app location.](https://github.com/Azure/azure-functions-dotnet-worker/pull/2897) diff --git a/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs b/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs index 34dc8d5eda..2670c6a2ad 100644 --- a/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs +++ b/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs @@ -5,6 +5,8 @@ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.Tracing; +using Azure.Core; +using Azure.Identity; using Azure.Monitor.OpenTelemetry.Exporter; using Azure.Monitor.OpenTelemetry.LiveMetrics; using Microsoft.Extensions.Configuration; @@ -15,6 +17,7 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; +using AppInsightsCredentialOptions = Microsoft.Azure.WebJobs.Logging.ApplicationInsights.TokenCredentialOptions; namespace Microsoft.Azure.WebJobs.Script.Diagnostics.OpenTelemetry { @@ -23,6 +26,8 @@ internal static class OpenTelemetryConfigurationExtensions internal static void ConfigureOpenTelemetry(this ILoggingBuilder loggingBuilder, HostBuilderContext context) { string azMonConnectionString = GetConfigurationValue(EnvironmentSettingNames.AppInsightsConnectionString, context.Configuration); + TokenCredential credential = GetTokenCredential(context.Configuration); + bool enableOtlp = false; if (!string.IsNullOrEmpty(GetConfigurationValue(EnvironmentSettingNames.OtlpEndpoint, context.Configuration))) { @@ -39,7 +44,7 @@ internal static void ConfigureOpenTelemetry(this ILoggingBuilder loggingBuilder, } if (!string.IsNullOrEmpty(azMonConnectionString)) { - o.AddAzureMonitorLogExporter(options => options.ConnectionString = azMonConnectionString); + o.AddAzureMonitorLogExporter(options => ConfigureAzureMonitorOptions(options, azMonConnectionString, credential)); } o.IncludeFormattedMessage = true; o.IncludeScopes = false; @@ -68,18 +73,21 @@ internal static void ConfigureOpenTelemetry(this ILoggingBuilder loggingBuilder, o.FilterHttpRequestMessage = _ => { Activity activity = Activity.Current?.Parent; - return (activity == null || !activity.Source.Name.Equals("Azure.Core.Http")) ? true : false; + return activity == null || !activity.Source.Name.Equals("Azure.Core.Http"); }; }); + if (enableOtlp) { b.AddOtlpExporter(); } + if (!string.IsNullOrEmpty(azMonConnectionString)) { - b.AddAzureMonitorTraceExporter(options => options.ConnectionString = azMonConnectionString); - b.AddLiveMetrics(options => options.ConnectionString = azMonConnectionString); + b.AddAzureMonitorTraceExporter(options => ConfigureAzureMonitorOptions(options, azMonConnectionString, credential)); + b.AddLiveMetrics(options => ConfigureAzureMonitorOptions(options, azMonConnectionString, credential)); } + b.AddProcessor(ActivitySanitizingProcessor.Instance); b.AddProcessor(TraceFilterProcessor.Instance); }); @@ -127,5 +135,34 @@ private static string GetConfigurationValue(string key, IConfiguration configura return null; } } + + private static TokenCredential GetTokenCredential(IConfiguration configuration) + { + if (GetConfigurationValue(EnvironmentSettingNames.AppInsightsAuthenticationString, configuration) is string authString) + { + AppInsightsCredentialOptions credOptions = AppInsightsCredentialOptions.ParseAuthenticationString(authString); + return new ManagedIdentityCredential(credOptions.ClientId); + } + + return null; + } + + private static void ConfigureAzureMonitorOptions(AzureMonitorExporterOptions options, string connectionString, TokenCredential credential) + { + options.ConnectionString = connectionString; + if (credential is not null) + { + options.Credential = credential; + } + } + + private static void ConfigureAzureMonitorOptions(LiveMetricsExporterOptions options, string connectionString, TokenCredential credential) + { + options.ConnectionString = connectionString; + if (credential is not null) + { + options.Credential = credential; + } + } } } \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs b/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs index 2e26d76544..600a270db0 100644 --- a/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs +++ b/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs @@ -5,6 +5,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reflection; +using Azure.Identity; +using Azure.Monitor.OpenTelemetry.Exporter; using FluentAssertions; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; @@ -231,6 +234,78 @@ public void ResourceDetectorLocalDevelopment() Assert.Equal(4, resource.Attributes.Count()); } + [Fact] + public void ConfigureTelemetry_Should_UseOpenTelemetryWhenModeSetAndAppInsightsAuthStringClientIdPresent() + { + // Arrange + var clientId = Guid.NewGuid(); + IServiceCollection serviceCollection = default; + + var hostBuilder = new HostBuilder() + .ConfigureAppConfiguration(config => + { + config.AddInMemoryCollection(new Dictionary<string, string> + { + { "APPLICATIONINSIGHTS_AUTHENTICATION_STRING", $"Authorization=AAD;ClientId={clientId}" }, + { "APPLICATIONINSIGHTS_CONNECTION_STRING", "InstrumentationKey=key" }, + { ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "telemetryMode"), TelemetryMode.OpenTelemetry.ToString() } + }); + }) + .ConfigureDefaultTestWebScriptHost() + .ConfigureLogging((context, loggingBuilder) => loggingBuilder.ConfigureTelemetry(context)) + .ConfigureServices(services => serviceCollection = services); + + using var host = hostBuilder.Build(); + + // Act + var tracerProviderDescriptors = GetTracerProviderDescriptors(serviceCollection); + var resolvedClient = ExtractClientFromDescriptors(tracerProviderDescriptors); + + // Extract the clientId from the client object + var clientIdValue = resolvedClient?.GetType().GetProperty("ClientId", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(resolvedClient)?.ToString(); + + // Assert + serviceCollection.Should().NotBeNullOrEmpty(); + clientIdValue.Should().Be(clientId.ToString()); + resolvedClient.GetType().Name.Should().Be("ManagedIdentityClient"); + } + + [Fact] + public void ConfigureTelemetry_Should_UseOpenTelemetryWhenModeSetAndAppInsightsAuthStringPresent() + { + // Arrange + IServiceCollection serviceCollection = default; + + var hostBuilder = new HostBuilder() + .ConfigureAppConfiguration(config => + { + config.AddInMemoryCollection(new Dictionary<string, string> + { + { "APPLICATIONINSIGHTS_AUTHENTICATION_STRING", $"Authorization=AAD" }, + { "APPLICATIONINSIGHTS_CONNECTION_STRING", "InstrumentationKey=key" }, + { ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "telemetryMode"), TelemetryMode.OpenTelemetry.ToString() } + }); + }) + .ConfigureDefaultTestWebScriptHost() + .ConfigureLogging((context, loggingBuilder) => loggingBuilder.ConfigureTelemetry(context)) + .ConfigureServices(services => serviceCollection = services); + + using var host = hostBuilder.Build(); + + // Act + var tracerProviderDescriptors = GetTracerProviderDescriptors(serviceCollection); + var resolvedClient = ExtractClientFromDescriptors(tracerProviderDescriptors); + + // Extract the clientId from the client object + var clientIdValue = resolvedClient?.GetType().GetProperty("ClientId", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(resolvedClient)?.ToString(); + + // Assert + serviceCollection.Should().NotBeNullOrEmpty(); + // No clientId should be present as it was not provided + clientIdValue.Should().BeNull(); + resolvedClient.GetType().Name.Should().Be("ManagedIdentityClient"); + } + // The OpenTelemetryEventListener is fine because it's a no-op if there are no otel events to listen to private bool HasOtelServices(IServiceCollection sc) => sc.Any(sd => sd.ServiceType != typeof(OpenTelemetryEventListener) && sd.ServiceType.FullName.Contains("OpenTelemetry")); @@ -244,5 +319,46 @@ private static IDisposable SetupDefaultEnvironmentVariables() { "REGION_NAME", "EastUS" } }); } + + private static List<ServiceDescriptor> GetTracerProviderDescriptors(IServiceCollection services) + { + return services + .Where(descriptor => + descriptor.Lifetime == ServiceLifetime.Singleton && + descriptor.ServiceType.Name == "IConfigureTracerProviderBuilder" && + descriptor.ImplementationInstance?.GetType().Name == "ConfigureTracerProviderBuilderCallbackWrapper") + .ToList(); + } + + private static object ExtractClientFromDescriptors(List<ServiceDescriptor> descriptors) + { + foreach (var descriptor in descriptors) + { + var implementation = descriptor.ImplementationInstance; + if (implementation is null) + { + continue; + } + + // Reflection starts here + var configureField = implementation.GetType().GetField("configure", BindingFlags.Instance | BindingFlags.NonPublic); + if (configureField?.GetValue(implementation) is Action<IServiceProvider, TracerProviderBuilder> configureDelegate) + { + var targetType = configureDelegate.Target.GetType(); + var configureDelegateTarget = targetType.GetField("configure", BindingFlags.Instance | BindingFlags.Public); + + if (configureDelegateTarget?.GetValue(configureDelegate.Target) is Action<AzureMonitorExporterOptions> exporterOptionsDelegate) + { + var credentialField = exporterOptionsDelegate.Target.GetType().GetField("credential", BindingFlags.Instance | BindingFlags.Public); + if (credentialField?.GetValue(exporterOptionsDelegate.Target) is ManagedIdentityCredential managedIdentityCredential) + { + var clientProperty = managedIdentityCredential.GetType().GetProperty("Client", BindingFlags.Instance | BindingFlags.NonPublic); + return clientProperty?.GetValue(managedIdentityCredential); + } + } + } + } + return null; + } } } \ No newline at end of file