Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support identity auth for otel azure monitor #10615

Merged
merged 6 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
<!-- Please add your release notes in the following format:
- My change description (#PR)
-->

- Setting force refersh to false for CreateOrUpdate call (#10668)
- Add support for managed identity when using open telemetry + azure monitor (#10615)
- Setting force refersh to false for CreateOrUpdate call (#10668)
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();
}

jviau marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
}