Skip to content

Commit

Permalink
Support identity auth for otel azure monitor (#10615)
Browse files Browse the repository at this point in the history
* Support identity auth for otel azure monitor

Co-authored-by: Rohit Ranjan <90008725+RohitRanjanMS@users.noreply.github.com>
  • Loading branch information
jviau and RohitRanjanMS authored Jan 24, 2025
1 parent dae16f9 commit 6323cca
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 5 deletions.
2 changes: 1 addition & 1 deletion release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand All @@ -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)))
{
Expand All @@ -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;
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));

Expand All @@ -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;
}
}
}

0 comments on commit 6323cca

Please sign in to comment.