[di] Expose a detached TracerProviderBuilder extension on IServiceCollection which may modify services. (#4508)

This commit is contained in:
Mikel Blanchard 2023-05-25 10:31:27 -07:00 committed by GitHub
parent eb12f9b386
commit 107dd264c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 212 additions and 80 deletions

View File

@ -0,0 +1 @@
static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Trace.TracerProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

View File

@ -0,0 +1 @@
static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Trace.TracerProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

View File

@ -8,6 +8,12 @@
`TracerProvider`. `TracerProvider`.
([#4468](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4468)) ([#4468](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4468))
* Added an `IServiceCollection.ConfigureOpenTelemetryTracerProvider` overload
which may be used to configure `TracerProviderBuilder`s while the
`IServiceCollection` is modifiable (before the `IServiceProvider` has been
created).
([#4508](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4508))
## 1.5.0-alpha.2 ## 1.5.0-alpha.2
Released 2023-Mar-31 Released 2023-Mar-31

View File

@ -26,9 +26,7 @@ public static class OpenTelemetryDependencyInjectionTracingServiceCollectionExte
{ {
/// <summary> /// <summary>
/// Registers an action used to configure the OpenTelemetry <see /// Registers an action used to configure the OpenTelemetry <see
/// cref="TracerProviderBuilder"/> used to create the <see /// cref="TracerProviderBuilder"/>.
/// cref="TracerProvider"/> for the <see cref="IServiceCollection"/> being
/// configured.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Notes: /// Notes:
@ -36,14 +34,53 @@ public static class OpenTelemetryDependencyInjectionTracingServiceCollectionExte
/// <item>This is safe to be called multiple times and by library authors. /// <item>This is safe to be called multiple times and by library authors.
/// Each registered configuration action will be applied /// Each registered configuration action will be applied
/// sequentially.</item> /// sequentially.</item>
/// <item>A <see cref="TracerProvider"/> will not be created automatically /// <item>A <see cref="TracerProvider"/> will NOT be created automatically
/// using this method. To begin collecting metrics use the /// using this method. To begin collecting traces use the
/// <c>IServiceCollection.AddOpenTelemetry</c> extension in the /// <c>IServiceCollection.AddOpenTelemetry</c> extension in the
/// <c>OpenTelemetry.Extensions.Hosting</c> package.</item> /// <c>OpenTelemetry.Extensions.Hosting</c> package.</item>
/// </list> /// </list>
/// </remarks> /// </remarks>
/// <param name="services">The <see cref="IServiceCollection" /> to add /// <param name="services"><see cref="IServiceCollection" />.</param>
/// services to.</param> /// <param name="configure">Callback action to configure the <see
/// cref="TracerProviderBuilder"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls
/// can be chained.</returns>
public static IServiceCollection ConfigureOpenTelemetryTracerProvider(
this IServiceCollection services,
Action<TracerProviderBuilder> configure)
{
Guard.ThrowIfNull(services);
Guard.ThrowIfNull(configure);
configure(new TracerProviderServiceCollectionBuilder(services));
return services;
}
/// <summary>
/// Registers an action used to configure the OpenTelemetry <see
/// cref="TracerProviderBuilder"/> once the <see cref="IServiceProvider"/>
/// is available.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>This is safe to be called multiple times and by library authors.
/// Each registered configuration action will be applied
/// sequentially.</item>
/// <item>A <see cref="TracerProvider"/> will NOT be created automatically
/// using this method. To begin collecting traces use the
/// <c>IServiceCollection.AddOpenTelemetry</c> extension in the
/// <c>OpenTelemetry.Extensions.Hosting</c> package.</item>
/// <item>The supplied configuration delegate is called once the <see
/// cref="IServiceProvider"/> is available. Services may NOT be added to a
/// <see cref="TracerProviderBuilder"/> once the <see
/// cref="IServiceProvider"/> has been created. Many helper extensions
/// register services and may throw if invoked inside the configuration
/// delegate.</item>
/// </list>
/// </remarks>
/// <param name="services"><see cref="IServiceCollection" />.</param>
/// <param name="configure">Callback action to configure the <see /// <param name="configure">Callback action to configure the <see
/// cref="TracerProviderBuilder"/>.</param> /// cref="TracerProviderBuilder"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls /// <returns>The <see cref="IServiceCollection"/> so that additional calls
@ -51,19 +88,14 @@ public static class OpenTelemetryDependencyInjectionTracingServiceCollectionExte
public static IServiceCollection ConfigureOpenTelemetryTracerProvider( public static IServiceCollection ConfigureOpenTelemetryTracerProvider(
this IServiceCollection services, this IServiceCollection services,
Action<IServiceProvider, TracerProviderBuilder> configure) Action<IServiceProvider, TracerProviderBuilder> configure)
{
RegisterBuildAction(services, configure);
return services;
}
private static void RegisterBuildAction(IServiceCollection services, Action<IServiceProvider, TracerProviderBuilder> configure)
{ {
Guard.ThrowIfNull(services); Guard.ThrowIfNull(services);
Guard.ThrowIfNull(configure); Guard.ThrowIfNull(configure);
services.AddSingleton<IConfigureTracerProviderBuilder>( services.AddSingleton<IConfigureTracerProviderBuilder>(
new ConfigureTracerProviderBuilderCallbackWrapper(configure)); new ConfigureTracerProviderBuilderCallbackWrapper(configure));
return services;
} }
private sealed class ConfigureTracerProviderBuilderCallbackWrapper : IConfigureTracerProviderBuilder private sealed class ConfigureTracerProviderBuilderCallbackWrapper : IConfigureTracerProviderBuilder

View File

@ -0,0 +1,107 @@
// <copyright file="TracerProviderServiceCollectionBuilder.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Trace;
internal sealed class TracerProviderServiceCollectionBuilder : TracerProviderBuilder, ITracerProviderBuilder
{
public TracerProviderServiceCollectionBuilder(IServiceCollection services)
{
services.ConfigureOpenTelemetryTracerProvider((sp, builder) => this.Services = null);
this.Services = services;
}
public IServiceCollection? Services { get; set; }
public TracerProvider? Provider => null;
/// <inheritdoc />
public override TracerProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory)
{
Guard.ThrowIfNull(instrumentationFactory);
this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddInstrumentation(instrumentationFactory);
});
return this;
}
/// <inheritdoc />
public override TracerProviderBuilder AddSource(params string[] names)
{
Guard.ThrowIfNull(names);
this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddSource(names);
});
return this;
}
/// <inheritdoc />
public override TracerProviderBuilder AddLegacySource(string operationName)
{
Guard.ThrowIfNullOrWhitespace(operationName);
this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddLegacySource(operationName);
});
return this;
}
/// <inheritdoc />
public TracerProviderBuilder ConfigureServices(Action<IServiceCollection> configure)
=> this.ConfigureServicesInternal(configure);
/// <inheritdoc cref="IDeferredTracerProviderBuilder.Configure" />
public TracerProviderBuilder ConfigureBuilder(Action<IServiceProvider, TracerProviderBuilder> configure)
=> this.ConfigureBuilderInternal(configure);
/// <inheritdoc />
TracerProviderBuilder IDeferredTracerProviderBuilder.Configure(Action<IServiceProvider, TracerProviderBuilder> configure)
=> this.ConfigureBuilderInternal(configure);
private TracerProviderBuilder ConfigureBuilderInternal(Action<IServiceProvider, TracerProviderBuilder> configure)
{
var services = this.Services
?? throw new NotSupportedException("Builder cannot be configured during TracerProvider construction.");
services.ConfigureOpenTelemetryTracerProvider(configure);
return this;
}
private TracerProviderBuilder ConfigureServicesInternal(Action<IServiceCollection> configure)
{
Guard.ThrowIfNull(configure);
var services = this.Services
?? throw new NotSupportedException("Services cannot be configured during TracerProvider construction.");
configure(services);
return this;
}
}

View File

@ -28,7 +28,7 @@ namespace OpenTelemetry.Trace;
public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderBuilder public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderBuilder
{ {
private readonly bool allowBuild; private readonly bool allowBuild;
private IServiceCollection? services; private readonly TracerProviderServiceCollectionBuilder innerBuilder;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TracerProviderBuilderBase"/> class. /// Initializes a new instance of the <see cref="TracerProviderBuilderBase"/> class.
@ -43,9 +43,7 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
.TryAddSingleton<TracerProvider>( .TryAddSingleton<TracerProvider>(
sp => throw new NotSupportedException("Self-contained TracerProvider cannot be accessed using the application IServiceProvider call Build instead.")); sp => throw new NotSupportedException("Self-contained TracerProvider cannot be accessed using the application IServiceProvider call Build instead."));
services.ConfigureOpenTelemetryTracerProvider((sp, builder) => this.services = null); this.innerBuilder = new TracerProviderServiceCollectionBuilder(services);
this.services = services;
this.allowBuild = true; this.allowBuild = true;
} }
@ -58,9 +56,7 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
.AddOpenTelemetryTracerProviderBuilderServices() .AddOpenTelemetryTracerProviderBuilderServices()
.TryAddSingleton<TracerProvider>(sp => new TracerProviderSdk(sp, ownsServiceProvider: false)); .TryAddSingleton<TracerProvider>(sp => new TracerProviderSdk(sp, ownsServiceProvider: false));
services.ConfigureOpenTelemetryTracerProvider((sp, builder) => this.services = null); this.innerBuilder = new TracerProviderServiceCollectionBuilder(services);
this.services = services;
this.allowBuild = false; this.allowBuild = false;
} }
@ -71,12 +67,7 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
/// <inheritdoc /> /// <inheritdoc />
public override TracerProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory) public override TracerProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory)
{ {
Guard.ThrowIfNull(instrumentationFactory); this.innerBuilder.AddInstrumentation(instrumentationFactory);
this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddInstrumentation(instrumentationFactory);
});
return this; return this;
} }
@ -84,12 +75,7 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
/// <inheritdoc /> /// <inheritdoc />
public override TracerProviderBuilder AddSource(params string[] names) public override TracerProviderBuilder AddSource(params string[] names)
{ {
Guard.ThrowIfNull(names); this.innerBuilder.AddSource(names);
this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddSource(names);
});
return this; return this;
} }
@ -97,23 +83,18 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
/// <inheritdoc /> /// <inheritdoc />
public override TracerProviderBuilder AddLegacySource(string operationName) public override TracerProviderBuilder AddLegacySource(string operationName)
{ {
Guard.ThrowIfNullOrWhitespace(operationName); this.innerBuilder.AddLegacySource(operationName);
this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddLegacySource(operationName);
});
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
TracerProviderBuilder ITracerProviderBuilder.ConfigureServices(Action<IServiceCollection> configure) TracerProviderBuilder ITracerProviderBuilder.ConfigureServices(Action<IServiceCollection> configure)
=> this.ConfigureServicesInternal(configure); => this.innerBuilder.ConfigureServices(configure);
/// <inheritdoc /> /// <inheritdoc />
TracerProviderBuilder IDeferredTracerProviderBuilder.Configure(Action<IServiceProvider, TracerProviderBuilder> configure) TracerProviderBuilder IDeferredTracerProviderBuilder.Configure(Action<IServiceProvider, TracerProviderBuilder> configure)
=> this.ConfigureBuilderInternal(configure); => this.innerBuilder.ConfigureBuilder(configure);
internal TracerProvider InvokeBuild() internal TracerProvider InvokeBuild()
=> this.Build(); => this.Build();
@ -134,7 +115,7 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
Guard.ThrowIfNullOrWhitespace(instrumentationVersion); Guard.ThrowIfNullOrWhitespace(instrumentationVersion);
Guard.ThrowIfNull(instrumentationFactory); Guard.ThrowIfNull(instrumentationFactory);
return this.ConfigureBuilderInternal((sp, builder) => return this.innerBuilder.ConfigureBuilder((sp, builder) =>
{ {
if (builder is TracerProviderBuilderSdk tracerProviderBuilderState) if (builder is TracerProviderBuilderSdk tracerProviderBuilderState)
{ {
@ -157,14 +138,14 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
throw new NotSupportedException("A TracerProviderBuilder bound to external service cannot be built directly. Access the TracerProvider using the application IServiceProvider instead."); throw new NotSupportedException("A TracerProviderBuilder bound to external service cannot be built directly. Access the TracerProvider using the application IServiceProvider instead.");
} }
var services = this.services; var services = this.innerBuilder.Services;
if (services == null) if (services == null)
{ {
throw new NotSupportedException("TracerProviderBuilder build method cannot be called multiple times."); throw new NotSupportedException("TracerProviderBuilder build method cannot be called multiple times.");
} }
this.services = null; this.innerBuilder.Services = null;
#if DEBUG #if DEBUG
bool validateScopes = true; bool validateScopes = true;
@ -175,34 +156,4 @@ public class TracerProviderBuilderBase : TracerProviderBuilder, ITracerProviderB
return new TracerProviderSdk(serviceProvider, ownsServiceProvider: true); return new TracerProviderSdk(serviceProvider, ownsServiceProvider: true);
} }
private TracerProviderBuilder ConfigureBuilderInternal(Action<IServiceProvider, TracerProviderBuilder> configure)
{
var services = this.services;
if (services == null)
{
throw new NotSupportedException("Builder cannot be configured during TracerProvider construction.");
}
services.ConfigureOpenTelemetryTracerProvider(configure);
return this;
}
private TracerProviderBuilder ConfigureServicesInternal(Action<IServiceCollection> configure)
{
Guard.ThrowIfNull(configure);
var services = this.services;
if (services == null)
{
throw new NotSupportedException("Services cannot be configured during TracerProvider construction.");
}
configure(services);
return this;
}
} }

View File

@ -30,26 +30,33 @@ public class ServiceCollectionExtensionsTests
[InlineData(3)] [InlineData(3)]
public void ConfigureOpenTelemetryTracerProvider(int numberOfCalls) public void ConfigureOpenTelemetryTracerProvider(int numberOfCalls)
{ {
var invocations = 0; var beforeServiceProviderInvocations = 0;
var afterServiceProviderInvocations = 0;
var services = new ServiceCollection(); var services = new ServiceCollection();
for (int i = 0; i < numberOfCalls; i++) for (int i = 0; i < numberOfCalls; i++)
{ {
services.ConfigureOpenTelemetryTracerProvider((sp, builder) => invocations++); services.ConfigureOpenTelemetryTracerProvider(builder => beforeServiceProviderInvocations++);
services.ConfigureOpenTelemetryTracerProvider((sp, builder) => afterServiceProviderInvocations++);
} }
using var serviceProvider = services.BuildServiceProvider(); using var serviceProvider = services.BuildServiceProvider();
var registrations = serviceProvider.GetServices<IConfigureTracerProviderBuilder>(); var registrations = serviceProvider.GetServices<IConfigureTracerProviderBuilder>();
Assert.Equal(numberOfCalls, beforeServiceProviderInvocations);
Assert.Equal(0, afterServiceProviderInvocations);
foreach (var registration in registrations) foreach (var registration in registrations)
{ {
registration.ConfigureBuilder(serviceProvider, null!); registration.ConfigureBuilder(serviceProvider, null!);
} }
Assert.Equal(invocations, registrations.Count()); Assert.Equal(numberOfCalls, beforeServiceProviderInvocations);
Assert.Equal(numberOfCalls, registrations.Count()); Assert.Equal(numberOfCalls, afterServiceProviderInvocations);
Assert.Equal(numberOfCalls * 2, registrations.Count());
} }
[Theory] [Theory]
@ -107,4 +114,8 @@ public class ServiceCollectionExtensionsTests
Assert.Equal(invocations, registrations.Count()); Assert.Equal(invocations, registrations.Count());
Assert.Equal(numberOfCalls, registrations.Count()); Assert.Equal(numberOfCalls, registrations.Count());
} }
private sealed class CustomType
{
}
} }

View File

@ -311,6 +311,7 @@ namespace OpenTelemetry.Trace.Tests
{ {
bool innerConfigureBuilderTestExecuted = false; bool innerConfigureBuilderTestExecuted = false;
bool innerConfigureOpenTelemetryLoggerProviderTestExecuted = false; bool innerConfigureOpenTelemetryLoggerProviderTestExecuted = false;
bool innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = false;
using var provider = Sdk.CreateTracerProviderBuilder() using var provider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services => .ConfigureServices(services =>
@ -318,7 +319,17 @@ namespace OpenTelemetry.Trace.Tests
if (callNestedConfigure) if (callNestedConfigure)
{ {
services.ConfigureOpenTelemetryTracerProvider( services.ConfigureOpenTelemetryTracerProvider(
(sp, builder) => innerConfigureOpenTelemetryLoggerProviderTestExecuted = true); builder =>
{
innerConfigureOpenTelemetryLoggerProviderTestExecuted = true;
builder.AddInstrumentation<MyInstrumentation>();
});
services.ConfigureOpenTelemetryTracerProvider(
(sp, builder) =>
{
innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = true;
Assert.Throws<NotSupportedException>(() => builder.AddInstrumentation<MyInstrumentation>());
});
} }
}) })
.ConfigureBuilder((sp, builder) => .ConfigureBuilder((sp, builder) =>
@ -326,10 +337,22 @@ namespace OpenTelemetry.Trace.Tests
innerConfigureBuilderTestExecuted = true; innerConfigureBuilderTestExecuted = true;
Assert.Throws<NotSupportedException>(() => sp.GetService<TracerProvider>()); Assert.Throws<NotSupportedException>(() => sp.GetService<TracerProvider>());
}) })
.Build(); .Build() as TracerProviderSdk;
Assert.NotNull(provider);
Assert.True(innerConfigureBuilderTestExecuted); Assert.True(innerConfigureBuilderTestExecuted);
Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestExecuted); Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestExecuted);
Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted);
if (callNestedConfigure)
{
Assert.Single(provider.Instrumentations);
}
else
{
Assert.Empty(provider.Instrumentations);
}
Assert.Throws<NotSupportedException>(() => provider.GetServiceProvider()?.GetService<TracerProvider>()); Assert.Throws<NotSupportedException>(() => provider.GetServiceProvider()?.GetService<TracerProvider>());
} }