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

[AzureMonitorOpenTelemetryDistro] AddAzureMonitorOpenTelemetry extension methods and AzureMonitorOpenTelemetryOptions #34198

Merged
merged 13 commits into from
Feb 24, 2023
Merged
2 changes: 2 additions & 0 deletions eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@

<!-- OpenTelemetry dependency approved for Azure.Monitor.OpenTelemetry.Exporter package only -->
<PackageReference Update="OpenTelemetry" Version="[1.4.0-rc.4]" Condition="'$(MSBuildProjectName)' == 'Azure.Monitor.OpenTelemetry.Exporter'" />
<PackageReference Update="OpenTelemetry.Extensions.Hosting" Version="1.4.0-rc.4" />
<PackageReference Update="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9.13" />
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
<PackageReference Update="OpenTelemetry.Extensions.PersistentStorage" Version="1.0.0-beta.1" Condition="'$(MSBuildProjectName)' == 'Azure.Monitor.OpenTelemetry.Exporter'" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependent Projects", "Depen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Monitor.OpenTelemetry.Exporter", "..\Azure.Monitor.OpenTelemetry.Exporter\src\Azure.Monitor.OpenTelemetry.Exporter.csproj", "{4158B8A7-971D-4A57-B4C8-5077BFD10AE6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Monitor.OpenTelemetry.Demo", "tests\Azure.Monitor.OpenTelemetry.Demo\Azure.Monitor.OpenTelemetry.Demo.csproj", "{6F094CAA-F6BB-4A9C-9D1F-1165F0758C99}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -30,6 +32,10 @@ Global
{4158B8A7-971D-4A57-B4C8-5077BFD10AE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4158B8A7-971D-4A57-B4C8-5077BFD10AE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4158B8A7-971D-4A57-B4C8-5077BFD10AE6}.Release|Any CPU.Build.0 = Release|Any CPU
{6F094CAA-F6BB-4A9C-9D1F-1165F0758C99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F094CAA-F6BB-4A9C-9D1F-1165F0758C99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F094CAA-F6BB-4A9C-9D1F-1165F0758C99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F094CAA-F6BB-4A9C-9D1F-1165F0758C99}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Azure.Monitor.OpenTelemetry
{
public static partial class AzureMonitorOpenTelemetryExtensions
{
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAzureMonitorOpenTelemetry(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Azure.Monitor.OpenTelemetry.AzureMonitorOpenTelemetryOptions? options = null) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAzureMonitorOpenTelemetry(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfiguration configuration) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAzureMonitorOpenTelemetry(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Azure.Monitor.OpenTelemetry.AzureMonitorOpenTelemetryOptions> configureAzureMonitorOpenTelemetry, string? name = null) { throw null; }
}
public partial class AzureMonitorOpenTelemetryOptions : Azure.Monitor.OpenTelemetry.Exporter.AzureMonitorExporterOptions
{
public AzureMonitorOpenTelemetryOptions() { }
public bool EnableLogs { get { throw null; } set { } }
public bool EnableMetrics { get { throw null; } set { } }
public bool EnableTraces { get { throw null; } set { } }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<IncludeOperationsSharedSource>true</IncludeOperationsSharedSource>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Azure.Monitor.OpenTelemetry.Exporter\src\Azure.Monitor.OpenTelemetry.Exporter.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Azure.Monitor.OpenTelemetry
{
/// <summary>
/// Extension methods for setting up Azure Monitor OpenTelemetry in an <see cref="IServiceCollection" />.
/// </summary>
public static class AzureMonitorOpenTelemetryExtensions
{
/// <summary>
/// Adds Azure Monitor OpenTelemetry into service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="options">The <see cref="AzureMonitorOpenTelemetryOptions" /> instance for configuration.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddAzureMonitorOpenTelemetry(this IServiceCollection services, AzureMonitorOpenTelemetryOptions? options = null)
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
{
return AzureMonitorOpenTelemetryImplementations.AddAzureMonitorOpenTelemetryWithOptions(services, options);
}

/// <summary>
/// Adds Azure Monitor OpenTelemetry into service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="configuration"><see cref="IConfiguration"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddAzureMonitorOpenTelemetry(this IServiceCollection services, IConfiguration configuration)
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
{
var options = new AzureMonitorOpenTelemetryOptions();
configuration.Bind(options);
return services.AddAzureMonitorOpenTelemetry(options);
}

/// <summary>
/// Adds Azure Monitor OpenTelemetry into service collection.
/// </summary>
/// <param name="services"><see cref="IServiceCollection"/>.</param>
/// <param name="configureAzureMonitorOpenTelemetry">Callback action for configuring <see cref="AzureMonitorOpenTelemetryOptions"/>.</param>
/// <param name="name">Name which is used when retrieving options.</param>
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
/// <returns><see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddAzureMonitorOpenTelemetry(this IServiceCollection services, Action<AzureMonitorOpenTelemetryOptions> configureAzureMonitorOpenTelemetry, string? name = null)
{
return AzureMonitorOpenTelemetryImplementations.AddAzureMonitorOpenTelemetryWithAction(services, configureAzureMonitorOpenTelemetry, name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace Azure.Monitor.OpenTelemetry
{
internal class AzureMonitorOpenTelemetryImplementations
{
internal static IServiceCollection AddAzureMonitorOpenTelemetryWithOptions(IServiceCollection services, AzureMonitorOpenTelemetryOptions? options)
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

options ??= new AzureMonitorOpenTelemetryOptions();
var builder = services.AddOpenTelemetry();

if (options.EnableTraces)
{
builder.WithTracing(b => b
.AddAspNetCoreInstrumentation()
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
.AddAzureMonitorTraceExporter(o =>
{
o.ConnectionString = options.ConnectionString;
o.DisableOfflineStorage = options.DisableOfflineStorage;
o.StorageDirectory = options.StorageDirectory;
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
}));
}

if (options.EnableMetrics)
{
builder.WithMetrics(b => b
.AddAspNetCoreInstrumentation()
.AddAzureMonitorMetricExporter(o =>
{
o.ConnectionString = options.ConnectionString;
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
o.DisableOfflineStorage = options.DisableOfflineStorage;
o.StorageDirectory = options.StorageDirectory;
}));
}

return services;
}

internal static IServiceCollection AddAzureMonitorOpenTelemetryWithAction(IServiceCollection services, Action<AzureMonitorOpenTelemetryOptions> configureAzureMonitorOpenTelemetry, string? name)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

name ??= Options.DefaultName;

if (configureAzureMonitorOpenTelemetry != null)
{
services.Configure(name, configureAzureMonitorOpenTelemetry);
}

var builder = services.AddOpenTelemetry();

builder.WithTracing(b => b
.AddAspNetCoreInstrumentation()
.AddAzureMonitorTraceExporter());

builder.WithMetrics(b => b
.AddAspNetCoreInstrumentation()
.AddAzureMonitorMetricExporter());

ServiceDescriptor? sdkTracerProviderServiceRegistration = null;
ServiceDescriptor? sdkMeterProviderServiceRegistration = null;

foreach (var service in services)
{
if (service.ServiceType == typeof(TracerProvider))
{
sdkTracerProviderServiceRegistration = service;
}
else if (service.ServiceType == typeof(MeterProvider))
{
sdkMeterProviderServiceRegistration = service;
}
}

if (sdkTracerProviderServiceRegistration?.ImplementationFactory == null ||
sdkMeterProviderServiceRegistration?.ImplementationFactory == null)
{
throw new InvalidOperationException("OpenTelemetry SDK has changed its registration mechanism.");
}

// We looped through the registered services so that we can take over
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
// the SDK registrations.

services.Remove(sdkTracerProviderServiceRegistration);
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
services.Remove(sdkMeterProviderServiceRegistration);

// Now we register our own services for TracerProvider & MeterProvider
// so that we can return no-op versions when it isn't enabled.

services.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptionsMonitor<AzureMonitorOpenTelemetryOptions>>().Get(name);
if (!options.EnableTraces)
{
return new NoopTracerProvider();
}
else
{
SetValueToExporterOptions(sp, options);
var sdkProviderWrapper = sp.GetRequiredService<SdkProviderWrapper>();
sdkProviderWrapper.SdkTracerProvider = (TracerProvider)sdkTracerProviderServiceRegistration.ImplementationFactory(sp);
return sdkProviderWrapper.SdkTracerProvider;
}
});

services.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptionsMonitor<AzureMonitorOpenTelemetryOptions>>().Get(name);
if (!options.EnableMetrics)
{
return new NoopMeterProvider();
}
else
{
SetValueToExporterOptions(sp, options);
var sdkProviderWrapper = sp.GetRequiredService<SdkProviderWrapper>();
sdkProviderWrapper.SdkMeterProvider = (MeterProvider)sdkMeterProviderServiceRegistration.ImplementationFactory(sp);
return sdkProviderWrapper.SdkMeterProvider;
}
});

// SdkProviderWrapper is here to make sure the SDK services get properly
// shutdown when the service provider is disposed.
services.AddSingleton<SdkProviderWrapper>();

return services;
}

private static void SetValueToExporterOptions(IServiceProvider sp, AzureMonitorOpenTelemetryOptions options)
{
var exporterOptions = sp.GetRequiredService<IOptionsMonitor<AzureMonitorExporterOptions>>().Get("");

// TODO: Remove the hard-coded ConnectionString.
// If ConnectionString is not provided in config, exporter will throw.
// Modify exporter to include environment variable.
exporterOptions.ConnectionString = options.ConnectionString ?? "InstrumentationKey=00000000-0000-0000-0000-000000000000";
exporterOptions.DisableOfflineStorage = options.DisableOfflineStorage;
exporterOptions.StorageDirectory = options.StorageDirectory;
}

private sealed class NoopTracerProvider : TracerProvider
{
}

private sealed class NoopMeterProvider : MeterProvider
{
}

private sealed class SdkProviderWrapper : IDisposable
{
public TracerProvider? SdkTracerProvider;
public MeterProvider? SdkMeterProvider;

public void Dispose()
{
this.SdkTracerProvider?.Dispose();
this.SdkMeterProvider?.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable disable
TimothyMothra marked this conversation as resolved.
Show resolved Hide resolved

using Azure.Monitor.OpenTelemetry.Exporter;

namespace Azure.Monitor.OpenTelemetry
{
/// <summary>
/// Options that allow users to configure the Azure Monitor OpenTelemetry.
/// </summary>
public class AzureMonitorOpenTelemetryOptions : AzureMonitorExporterOptions
rajkumar-rangaraj marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Gets or sets a value indicating whether Traces should be enabled.
/// </summary>
public bool EnableTraces { get; set; } = true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to give users options to enable/disable metrics/logs/traces individually? ApplicationInsights SDK only has a single DisableTelemetry option as best I can tell and I'm not sure if there was much customer feedback around that or some other motivation to do things differently?

If we do start adding options to enable/disable specific subsets of telemetry then it would be good to understand a bit of a roadmap for it so we don't paint ourselves into an awkward corner. For example if we have EnableTraces first, but then later we add a List<string> of enabled trace sources because users needed more control, and then later still we decide to break the glass exposing OpenTelemetryTracerProviderBuilder for even more advanced usage it becomes unclear how these mechanisms would compose together.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplicationInsights SDK only has a single DisableTelemetry option as best I can tel

Application Insights has on/off for each auto-collection module.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something to be discussed more. I think most users should be good with the defaults. But then they can ask for additional things like - add custom meter, activitysource, other things.. We should not say "sorry not possible. you have to get rid of distro, and do everything by hand". I am hoping we can say "you can customize otel completely while retaining the distro's one-line, by registering your own Action to DI"..

For example:
AddAzMon() - adds 4 instrumentaion libraries.
User want to add a 5th one.
it should be something like

AddAzmon() - no change here.
ConfigureTracerProviderBuilder(builder.AddMyNewInstrumentation());

^ Is this your expectation too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it earlier, customer would be able to get additional instrumentations, custom ActivitySource, etc., Syntax is something like you explained ^.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this still leaves several questions...

  1. Will users figure that plan out?
  2. Will it be a common occurrence that people want to add a little extra, and when they do they will have this (IMO) odd looking configuration code?
  3. Assuming they do figure it out and people start mixing and matching the different configuration APIs, how likely is it that a new user seeing this code understands what output it produces?
services.AddAzureMonitorOpenTelemetry(o =>
{
    o.EnableMetrics = false;
}
services.ConfigureMeterProviderBuilder(b => b.AddMeter("Foo"));

If I ignore what I've seen of the implementation and just guess what behavior this should have I don't know what to expect.

It is good to know what works but I also want to be a little pointed that my concern here is about does the API look nice, will it feel easy to use and be easily understandable. I am thinking of this API as the front door of Azure Monitor for .NET developers for the next decade. It will show up in samples everywhere, it will be first (and perhaps only) lines of code they write, and it will set their first impression.

Application Insights has on/off for each auto-collection module.

Can you point me at them? I didn't find them either by searching the web or looking at some of the source. Its likely I didn't guess the right terms to search for.

Copy link
Contributor

@cijothomas cijothomas Feb 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will users figure that plan out?

I do not see any reason why users have trouble figuring this out... This has been the pattern in ApplicationInsights (does not mean its a perfect solution! It certainly has a lot of ugliness as it was built over time, and had to keep back-compat, etc, but overall it feels clean IMHO.).

The overall flow is like:

  1. Basic usage with all defaults. (used in hello world, pretty much the face of app insights)
    services.AddApplicationInsightsTelemetry();
  • enables app insights with all defaults (auto collection modules, few initializers, sampling processor, metric extraction, livemetrics, etc.), and reads ConnectionString from IConfig.
  1. User want turn on/off things - They are given knobs for most things (on/off for most common feature, not all).
  2. User want to add more things like more processor, initializers - They just add it to DI, and it'll be picked up.
  3. User want to customize each auto-collection module - ConfigureTelemetryModule<DependencyTrackingTelemetryModule> with an action to fully customize the auto-collection modules.

The doc for ref: https://learn.microsoft.com/en-us/azure/azure-monitor/app/asp-net-core?tabs=netcorenew%2Cnetcore6#configure-the-application-insights-sdk

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I ignore what I've seen of the implementation and just guess what behavior this should have I don't know what to expect.

Thats fair. I can see it can be hard to predict the behavior without reading accompanying doc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my view, these public API offer customers a quick and easy way to collect telemetry and send data to application insights. We could document limited customization options, such as adding new instrumentation or changing sampling percentages, etc., However, when complex configuration updates are needed for most instrumentations within the TracerProvider/MeterProvider, we should suggest that customers use the "piece meal" approach. This approach involves manually building the TracerProvider/MeterProvider with Azure Monitor Exporter using the documentation provided by OpenTelemetry .NET SDK, without utilizing any of the distro APIs.

The plan is same for .NET Worker Service, to start with we will not have any package and customer need to take a piece meal approach.


/// <summary>
/// Gets or sets a value indicating whether Metrics should be enabled.
/// </summary>
public bool EnableMetrics { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether Logs should be enabled.
/// </summary>
public bool EnableLogs { get; set; } = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Azure.Monitor.OpenTelemetry.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#if NET6_0_OR_GREATER
using System.Diagnostics;
using Azure.Monitor.OpenTelemetry;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Instrumentation.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.Configure<AspNetCoreInstrumentationOptions>(o =>
{
o.EnrichWithHttpRequest = (activity, httpRequest) =>
{
activity.SetTag("requestProtocol", httpRequest.Protocol);
};
o.EnrichWithHttpResponse = (activity, httpResponse) =>
{
activity.SetTag("responseLength", httpResponse.ContentLength);
};
o.EnrichWithException = (activity, exception) =>
{
activity.SetTag("exceptionType", exception.GetType().ToString());
};
});

// builder.Services.AddAzureMonitorOpenTelemetry();

// builder.Services.AddAzureMonitorOpenTelemetry(enableTraces: true, enableMetrics: true);

// builder.Services.AddAzureMonitorOpenTelemetry(builder.Configuration.GetSection("AzureMonitorOpenTelemetry"));

builder.Services.AddAzureMonitorOpenTelemetry(o =>
{
o.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000";
});

var app = builder.Build();
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");

app.Run();
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#if NET461

namespace Azure.Monitor.OpenTelemetry.Demo
{
public partial class Program
{
public static void Main(string[] args)
{
}
}
}

#endif
Loading