[Tracing] Improve dependency injection support in tracing build-up using SDK#3533
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #3533 +/- ##
==========================================
+ Coverage 87.02% 87.34% +0.31%
==========================================
Files 277 280 +3
Lines 9953 10056 +103
==========================================
+ Hits 8662 8783 +121
+ Misses 1291 1273 -18
|
… that there is no breakage during upgrades.
| [Obsolete("Call ConfigureServices instead this method will be removed in a future version.")] | ||
| public static IServiceCollection? GetServices(this TracerProviderBuilder tracerProviderBuilder) |
There was a problem hiding this comment.
I'm beginning to question if we should be less conservative here. That is, just dump GetServices. Likewise dump Configure.
Yes, it'll break folks who are currently using OpenTelemetry.Extensions.Hosting, but my sense of these methods are that they're less frequently used.
There was a problem hiding this comment.
@alanwest I pushed an update which removed them from SDK but I left them in Hosting and added [Obsolete] there. I don't feel passionately that we shouldn't remove them outright, but I feel like the Hosting API has existed for so long it would be nice for us to not break anyone. LMK what you think about that.
alanwest
left a comment
There was a problem hiding this comment.
👍 I think this is definitely the right direction. It'll be good to land this sooner than later to unblock bringing these patterns to metrics and logs. I think there'll be ample opportunity to continue to iron things out as needed.
cijothomas
left a comment
There was a problem hiding this comment.
LGTM.
We need to document/add some examples demonstrating the new capabilities ( i.e everything from PR desc. into docs/examples) later, so people can easily leverage these.
Totally! Once I am done getting traces/metrics/logs all squared away I'll take a stab at spinning up some documentation. |
Fixes #1215 - Multiple registrations now all configure the same builder/provider
Relates to #3086 - Adds a mechanism to do this (see scenarios)
Relates to #1642 - Moves all of the dependency bits into SDK, only thing in "hosting" is the
IHostedService. May or may not help with the name 😄Relates to #2980 - Makes it possible to solve this
Relates to #2971 - Makes it possible to solve this
Changes
Moves dependency injection features based on
ServiceCollectionfrom hosting library directly into SDK so they are universally available. This is similar to what was done on [Logs] Support dependency injection in logging build-up #3504 for logs.Enabled a few more detached scenarios.
We decided on the SIG meeting 8/2/2022 that only a single provider should be supported in a given
ServicesCollectionthis PR makes that a reality.SDK public API
namespace Microsoft.Extensions.DependencyInjection { + public static class TracerProviderBuilderServiceCollectionExtensions + { // These are basically what existed previously in Hosting minus the IHostedService registration + public static IServiceCollection ConfigureOpenTelemetryTracing(this IServiceCollection services) {} + public static IServiceCollection ConfigureOpenTelemetryTracing(this IServiceCollection services, Action<TracerProviderBuilder> configure) {} + } } namespace OpenTelemetry.Trace { public static class TracerProviderBuilderExtensions { // These existed previously in Hosting, now they are part of the SDK and can be used anywhere SDK is referenced + public static TracerProviderBuilder AddInstrumentation<T>(this TracerProviderBuilder tracerProviderBuilder) {} + public static TracerProviderBuilder AddProcessor<T>(this TracerProviderBuilder tracerProviderBuilder) where T : BaseProcessor<Activity> {} + public static TracerProviderBuilder ConfigureBuilder(this TracerProviderBuilder tracerProviderBuilder, Action<IServiceProvider, TracerProviderBuilder> configure) {} + public static TracerProviderBuilder ConfigureServices(this TracerProviderBuilder tracerProviderBuilder, Action<IServiceCollection> configure) {} + public static TracerProviderBuilder SetSampler<T>(this TracerProviderBuilder tracerProviderBuilder) where T : Sampler {} // These are new + public static TracerProviderBuilder AddExporter(this TracerProviderBuilder tracerProviderBuilder, ExportProcessorType exportProcessorType, BaseExporter<Activity> exporter) {} + public static TracerProviderBuilder AddExporter(this TracerProviderBuilder tracerProviderBuilder, ExportProcessorType exportProcessorType, BaseExporter<Activity> exporter, Action<ExportActivityProcessorOptions> configure) {} + public static TracerProviderBuilder AddExporter<T>(this TracerProviderBuilder tracerProviderBuilder, ExportProcessorType exportProcessorType) where T : BaseExporter<Activity> {} + public static TracerProviderBuilder AddExporter<T>(this TracerProviderBuilder tracerProviderBuilder, ExportProcessorType exportProcessorType, Action<ExportActivityProcessorOptions> configure) where T : BaseExporter<Activity> {} } // This class is similar to MetricReaderOptions and is used by AddExporter extensions + public class ExportActivityProcessorOptions + { + public ExportProcessorType ExportProcessorType { get; set; } + public BatchExportActivityProcessorOptions BatchExportProcessorOptions { get; set; } + } }OpenTelemetry.Extensions.Hosting public API
namespace OpenTelemetry.Trace { public static class TracerProviderBuilderExtensions { - public static TracerProviderBuilder AddInstrumentation<T>(this TracerProviderBuilder tracerProviderBuilder) {} - public static TracerProviderBuilder AddProcessor<T>(this TracerProviderBuilder tracerProviderBuilder) where T : BaseProcessor<Activity> {} - public static TracerProviderBuilder SetSampler<T>(this TracerProviderBuilder tracerProviderBuilder) where T : Sampler {} - public static TracerProvider Build(this TracerProviderBuilder tracerProviderBuilder, IServiceProvider serviceProvider) {} // Obsoleted these so no one breaks upgrading + [Obsolete("Call ConfigureBuilder instead this method will be removed in a future version.")] public static TracerProviderBuilder Configure(this TracerProviderBuilder tracerProviderBuilder, Action<IServiceProvider, TracerProviderBuilder> configure) {} + [Obsolete("Call ConfigureServices instead this method will be removed in a future version.")] public static IServiceCollection? GetServices(this TracerProviderBuilder tracerProviderBuilder) {} } }Existing scenarios made available in SDK
All of this stuff you could already do using the hosting library. But now you can do it just with the SDK. That makes it more universal. Notice below the root is
Sdk.CreateTracerProviderBuilderpreviously that builder had zero support for any kind dependency injection. Now it has the same surface as the builder returned by theAddOpenTelemetryTracingAPI. All of this will work for .NET Framework as well as .NET/Core.The hidden value being created here is really for library/extension authors. Now I can build a library with this extension...
...and it will work equally well for .NET Framework users as it will .NET/Core users, with only a reference to SDK.
It also allows us to clean up some of the strange registration code we have. For example this...
...becomes this...
New scenarios enabled
Register an exporter with the builder
This can be done either by providing the exporter instance or using dependency injection.
The
BatchExportActivityProcessorOptionsused (when batching is configured) is retrieved through the options API. Users will be able to bindBatchExportActivityProcessorOptionstoIConfigurationand manage it through the configuration pipeline (JSON, envvars, command-line, whatever they happen to have configured). There is a bit more work to smooth out how environment variables are retrieved (which is what #2980 is about) but I'll tackle that separately.Detached configuration method
Some users like @martinjt have asked for a way to configure things completely detached from the builder.
The SDK
ConfigureOpenTelemetryTracingextension is safe to be called multiple times. Only oneTracerProviderwill exist in theIServiceProviderand multiple calls will all contribute to its configuration, in order. This makes it possible to detach from theTracerProviderBuilder. This enables a scenario like this...Design details
TracerProviderBuilderBasehas 3 ctors which drive its 2 phases.Phase one: Service configuration
The first phase is used to register services before the
IServiceProvideris ready. Everything done on the builder in this phase is queued up into theIServiceCollection. Previously the builder had some state and certain calls likeAddProcessorwould modify that state. Other things likeConfigure/Builderwould be queued up for later. Moving everything to the queue is how we are now able to callAddOpenTelemetryTracingmultiple times. Previously that would result in state needing to be managed for each builder, now everything just dumps intoIServiceCollection. Also now everything will happen in the order of the builder invocation. Previously some stuff would happen instantly via the state, some things would happen later on. The order now should be predictable.The two ctors:
Phase two: Build-up
At some point the
IServiceProvideris ready and services are closed. In the case ofSdk.CreateTracerProviderBuilder()this happens whenBuild()is called. In the case ofAddOpenTelemetryTracingthe host controls that and we don't build until something asks for aTracerProviderfrom theIServiceProvider. In either case phase two is executed here:opentelemetry-dotnet/src/OpenTelemetry/Trace/TracerProviderSdk.cs
Lines 52 to 56 in 9e02797
We create a state object (
TracerProviderBuilderState) and then execute all the queued configuration actions against that state.We use this ctor:
Which tells the builder to modify the state instead of queuing things up.
The
TracerProvideris created using the state after all the configuration is applied.I'm sure this all feels very non-standard but actually it is very close to the built-in patterns. For example,
AddLoggingimmediately invokes the configuration callback on aLoggingBuilderwhich only modifies theIServiceCollection. Nothing actually happens until theLoggerFactoryis created at which point everything is retrieved from theIServiceProvider.Our case is more complicated because we weren't disciplined about using interfaces registered for all the
TracerProvider"services"/features, and we have a callback for configuring the builder onceIServiceProvideris available, but the underlying mechanisms are essentially the same.TODOs
CHANGELOG.mdupdated for non-trivial changes