Skip to content

Commit 6323cca

Browse files
jviauRohitRanjanMS
andauthored
Support identity auth for otel azure monitor (#10615)
* Support identity auth for otel azure monitor Co-authored-by: Rohit Ranjan <[email protected]>
1 parent dae16f9 commit 6323cca

File tree

3 files changed

+158
-5
lines changed

3 files changed

+158
-5
lines changed

release_notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<!-- Please add your release notes in the following format:
44
- My change description (#PR)
55
-->
6+
- Add support for managed identity when using open telemetry + azure monitor (#10615)
67
- Update Java Worker Version to [2.18.0](https://github.com/Azure/azure-functions-java-worker/releases/tag/2.18.0)
7-
88
- Allow for an output binding value of an invocation result to be null (#10698)
99
- Updated dotnet-isolated worker to 1.0.12.
1010
- [Corrected the path for the prelaunch app location.](https://github.com/Azure/azure-functions-dotnet-worker/pull/2897)

src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.ComponentModel;
66
using System.Diagnostics;
77
using System.Diagnostics.Tracing;
8+
using Azure.Core;
9+
using Azure.Identity;
810
using Azure.Monitor.OpenTelemetry.Exporter;
911
using Azure.Monitor.OpenTelemetry.LiveMetrics;
1012
using Microsoft.Extensions.Configuration;
@@ -15,6 +17,7 @@
1517
using OpenTelemetry.Metrics;
1618
using OpenTelemetry.Resources;
1719
using OpenTelemetry.Trace;
20+
using AppInsightsCredentialOptions = Microsoft.Azure.WebJobs.Logging.ApplicationInsights.TokenCredentialOptions;
1821

1922
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.OpenTelemetry
2023
{
@@ -23,6 +26,8 @@ internal static class OpenTelemetryConfigurationExtensions
2326
internal static void ConfigureOpenTelemetry(this ILoggingBuilder loggingBuilder, HostBuilderContext context)
2427
{
2528
string azMonConnectionString = GetConfigurationValue(EnvironmentSettingNames.AppInsightsConnectionString, context.Configuration);
29+
TokenCredential credential = GetTokenCredential(context.Configuration);
30+
2631
bool enableOtlp = false;
2732
if (!string.IsNullOrEmpty(GetConfigurationValue(EnvironmentSettingNames.OtlpEndpoint, context.Configuration)))
2833
{
@@ -39,7 +44,7 @@ internal static void ConfigureOpenTelemetry(this ILoggingBuilder loggingBuilder,
3944
}
4045
if (!string.IsNullOrEmpty(azMonConnectionString))
4146
{
42-
o.AddAzureMonitorLogExporter(options => options.ConnectionString = azMonConnectionString);
47+
o.AddAzureMonitorLogExporter(options => ConfigureAzureMonitorOptions(options, azMonConnectionString, credential));
4348
}
4449
o.IncludeFormattedMessage = true;
4550
o.IncludeScopes = false;
@@ -68,18 +73,21 @@ internal static void ConfigureOpenTelemetry(this ILoggingBuilder loggingBuilder,
6873
o.FilterHttpRequestMessage = _ =>
6974
{
7075
Activity activity = Activity.Current?.Parent;
71-
return (activity == null || !activity.Source.Name.Equals("Azure.Core.Http")) ? true : false;
76+
return activity == null || !activity.Source.Name.Equals("Azure.Core.Http");
7277
};
7378
});
79+
7480
if (enableOtlp)
7581
{
7682
b.AddOtlpExporter();
7783
}
84+
7885
if (!string.IsNullOrEmpty(azMonConnectionString))
7986
{
80-
b.AddAzureMonitorTraceExporter(options => options.ConnectionString = azMonConnectionString);
81-
b.AddLiveMetrics(options => options.ConnectionString = azMonConnectionString);
87+
b.AddAzureMonitorTraceExporter(options => ConfigureAzureMonitorOptions(options, azMonConnectionString, credential));
88+
b.AddLiveMetrics(options => ConfigureAzureMonitorOptions(options, azMonConnectionString, credential));
8289
}
90+
8391
b.AddProcessor(ActivitySanitizingProcessor.Instance);
8492
b.AddProcessor(TraceFilterProcessor.Instance);
8593
});
@@ -127,5 +135,34 @@ private static string GetConfigurationValue(string key, IConfiguration configura
127135
return null;
128136
}
129137
}
138+
139+
private static TokenCredential GetTokenCredential(IConfiguration configuration)
140+
{
141+
if (GetConfigurationValue(EnvironmentSettingNames.AppInsightsAuthenticationString, configuration) is string authString)
142+
{
143+
AppInsightsCredentialOptions credOptions = AppInsightsCredentialOptions.ParseAuthenticationString(authString);
144+
return new ManagedIdentityCredential(credOptions.ClientId);
145+
}
146+
147+
return null;
148+
}
149+
150+
private static void ConfigureAzureMonitorOptions(AzureMonitorExporterOptions options, string connectionString, TokenCredential credential)
151+
{
152+
options.ConnectionString = connectionString;
153+
if (credential is not null)
154+
{
155+
options.Credential = credential;
156+
}
157+
}
158+
159+
private static void ConfigureAzureMonitorOptions(LiveMetricsExporterOptions options, string connectionString, TokenCredential credential)
160+
{
161+
options.ConnectionString = connectionString;
162+
if (credential is not null)
163+
{
164+
options.Credential = credential;
165+
}
166+
}
130167
}
131168
}

test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.Linq;
8+
using System.Reflection;
9+
using Azure.Identity;
10+
using Azure.Monitor.OpenTelemetry.Exporter;
811
using FluentAssertions;
912
using Microsoft.ApplicationInsights;
1013
using Microsoft.ApplicationInsights.Extensibility;
@@ -231,6 +234,78 @@ public void ResourceDetectorLocalDevelopment()
231234
Assert.Equal(4, resource.Attributes.Count());
232235
}
233236

237+
[Fact]
238+
public void ConfigureTelemetry_Should_UseOpenTelemetryWhenModeSetAndAppInsightsAuthStringClientIdPresent()
239+
{
240+
// Arrange
241+
var clientId = Guid.NewGuid();
242+
IServiceCollection serviceCollection = default;
243+
244+
var hostBuilder = new HostBuilder()
245+
.ConfigureAppConfiguration(config =>
246+
{
247+
config.AddInMemoryCollection(new Dictionary<string, string>
248+
{
249+
{ "APPLICATIONINSIGHTS_AUTHENTICATION_STRING", $"Authorization=AAD;ClientId={clientId}" },
250+
{ "APPLICATIONINSIGHTS_CONNECTION_STRING", "InstrumentationKey=key" },
251+
{ ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "telemetryMode"), TelemetryMode.OpenTelemetry.ToString() }
252+
});
253+
})
254+
.ConfigureDefaultTestWebScriptHost()
255+
.ConfigureLogging((context, loggingBuilder) => loggingBuilder.ConfigureTelemetry(context))
256+
.ConfigureServices(services => serviceCollection = services);
257+
258+
using var host = hostBuilder.Build();
259+
260+
// Act
261+
var tracerProviderDescriptors = GetTracerProviderDescriptors(serviceCollection);
262+
var resolvedClient = ExtractClientFromDescriptors(tracerProviderDescriptors);
263+
264+
// Extract the clientId from the client object
265+
var clientIdValue = resolvedClient?.GetType().GetProperty("ClientId", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(resolvedClient)?.ToString();
266+
267+
// Assert
268+
serviceCollection.Should().NotBeNullOrEmpty();
269+
clientIdValue.Should().Be(clientId.ToString());
270+
resolvedClient.GetType().Name.Should().Be("ManagedIdentityClient");
271+
}
272+
273+
[Fact]
274+
public void ConfigureTelemetry_Should_UseOpenTelemetryWhenModeSetAndAppInsightsAuthStringPresent()
275+
{
276+
// Arrange
277+
IServiceCollection serviceCollection = default;
278+
279+
var hostBuilder = new HostBuilder()
280+
.ConfigureAppConfiguration(config =>
281+
{
282+
config.AddInMemoryCollection(new Dictionary<string, string>
283+
{
284+
{ "APPLICATIONINSIGHTS_AUTHENTICATION_STRING", $"Authorization=AAD" },
285+
{ "APPLICATIONINSIGHTS_CONNECTION_STRING", "InstrumentationKey=key" },
286+
{ ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "telemetryMode"), TelemetryMode.OpenTelemetry.ToString() }
287+
});
288+
})
289+
.ConfigureDefaultTestWebScriptHost()
290+
.ConfigureLogging((context, loggingBuilder) => loggingBuilder.ConfigureTelemetry(context))
291+
.ConfigureServices(services => serviceCollection = services);
292+
293+
using var host = hostBuilder.Build();
294+
295+
// Act
296+
var tracerProviderDescriptors = GetTracerProviderDescriptors(serviceCollection);
297+
var resolvedClient = ExtractClientFromDescriptors(tracerProviderDescriptors);
298+
299+
// Extract the clientId from the client object
300+
var clientIdValue = resolvedClient?.GetType().GetProperty("ClientId", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(resolvedClient)?.ToString();
301+
302+
// Assert
303+
serviceCollection.Should().NotBeNullOrEmpty();
304+
// No clientId should be present as it was not provided
305+
clientIdValue.Should().BeNull();
306+
resolvedClient.GetType().Name.Should().Be("ManagedIdentityClient");
307+
}
308+
234309
// The OpenTelemetryEventListener is fine because it's a no-op if there are no otel events to listen to
235310
private bool HasOtelServices(IServiceCollection sc) => sc.Any(sd => sd.ServiceType != typeof(OpenTelemetryEventListener) && sd.ServiceType.FullName.Contains("OpenTelemetry"));
236311

@@ -244,5 +319,46 @@ private static IDisposable SetupDefaultEnvironmentVariables()
244319
{ "REGION_NAME", "EastUS" }
245320
});
246321
}
322+
323+
private static List<ServiceDescriptor> GetTracerProviderDescriptors(IServiceCollection services)
324+
{
325+
return services
326+
.Where(descriptor =>
327+
descriptor.Lifetime == ServiceLifetime.Singleton &&
328+
descriptor.ServiceType.Name == "IConfigureTracerProviderBuilder" &&
329+
descriptor.ImplementationInstance?.GetType().Name == "ConfigureTracerProviderBuilderCallbackWrapper")
330+
.ToList();
331+
}
332+
333+
private static object ExtractClientFromDescriptors(List<ServiceDescriptor> descriptors)
334+
{
335+
foreach (var descriptor in descriptors)
336+
{
337+
var implementation = descriptor.ImplementationInstance;
338+
if (implementation is null)
339+
{
340+
continue;
341+
}
342+
343+
// Reflection starts here
344+
var configureField = implementation.GetType().GetField("configure", BindingFlags.Instance | BindingFlags.NonPublic);
345+
if (configureField?.GetValue(implementation) is Action<IServiceProvider, TracerProviderBuilder> configureDelegate)
346+
{
347+
var targetType = configureDelegate.Target.GetType();
348+
var configureDelegateTarget = targetType.GetField("configure", BindingFlags.Instance | BindingFlags.Public);
349+
350+
if (configureDelegateTarget?.GetValue(configureDelegate.Target) is Action<AzureMonitorExporterOptions> exporterOptionsDelegate)
351+
{
352+
var credentialField = exporterOptionsDelegate.Target.GetType().GetField("credential", BindingFlags.Instance | BindingFlags.Public);
353+
if (credentialField?.GetValue(exporterOptionsDelegate.Target) is ManagedIdentityCredential managedIdentityCredential)
354+
{
355+
var clientProperty = managedIdentityCredential.GetType().GetProperty("Client", BindingFlags.Instance | BindingFlags.NonPublic);
356+
return clientProperty?.GetValue(managedIdentityCredential);
357+
}
358+
}
359+
}
360+
}
361+
return null;
362+
}
247363
}
248364
}

0 commit comments

Comments
 (0)