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

[di] Expose a detached MeterProviderBuilder extension on IServiceCollection which may modify services #4517

Merged
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.ConfigureOpenTelemetryMeterProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Metrics.MeterProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Trace.TracerProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.ConfigureOpenTelemetryMeterProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Metrics.MeterProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Trace.TracerProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ Released 2023-May-25
created).
([#4508](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4508))

* Added an `IServiceCollection.ConfigureOpenTelemetryMeterProvider` overload
which may be used to configure `MeterProviderBuilder`s while the
`IServiceCollection` is modifiable (before the `IServiceProvider` has been
created).
([#4517](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4517))

## 1.5.0-alpha.2

Released 2023-Mar-31
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// <copyright file="MeterProviderServiceCollectionBuilder.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.Metrics;

internal sealed class MeterProviderServiceCollectionBuilder : MeterProviderBuilder, IMeterProviderBuilder
{
public MeterProviderServiceCollectionBuilder(IServiceCollection services)
{
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => this.Services = null);

this.Services = services;
}

public IServiceCollection? Services { get; set; }

public MeterProvider? Provider => null;

/// <inheritdoc />
public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory)
{
Guard.ThrowIfNull(instrumentationFactory);

this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddInstrumentation(instrumentationFactory);
});

return this;
}

/// <inheritdoc />
public override MeterProviderBuilder AddMeter(params string[] names)
{
Guard.ThrowIfNull(names);

this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddMeter(names);
});

return this;
}

/// <inheritdoc />
public MeterProviderBuilder ConfigureServices(Action<IServiceCollection> configure)
=> this.ConfigureServicesInternal(configure);

/// <inheritdoc cref="IDeferredMeterProviderBuilder.Configure" />
public MeterProviderBuilder ConfigureBuilder(Action<IServiceProvider, MeterProviderBuilder> configure)
=> this.ConfigureBuilderInternal(configure);

/// <inheritdoc />
MeterProviderBuilder IDeferredMeterProviderBuilder.Configure(Action<IServiceProvider, MeterProviderBuilder> configure)
=> this.ConfigureBuilderInternal(configure);

private MeterProviderBuilder ConfigureBuilderInternal(Action<IServiceProvider, MeterProviderBuilder> configure)
{
var services = this.Services
?? throw new NotSupportedException("Builder cannot be configured during MeterProvider construction.");

services.ConfigureOpenTelemetryMeterProvider(configure);

return this;
}

private MeterProviderBuilder ConfigureServicesInternal(Action<IServiceCollection> configure)
{
Guard.ThrowIfNull(configure);

var services = this.Services
?? throw new NotSupportedException("Services cannot be configured during MeterProvider construction.");

configure(services);

return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,44 +26,79 @@ public static class OpenTelemetryDependencyInjectionMetricsServiceCollectionExte
{
/// <summary>
/// Registers an action used to configure the OpenTelemetry <see
/// cref="MeterProviderBuilder"/> used to create the <see
/// cref="MeterProvider"/> for the <see cref="IServiceCollection"/> being
/// configured.
/// cref="MeterProviderBuilder"/>.
/// </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="MeterProvider"/> will not be created automatically
/// <item>A <see cref="MeterProvider"/> will NOT be created automatically
/// using this method. To begin collecting metrics use the
/// <c>IServiceCollection.AddOpenTelemetry</c> extension in the
/// <c>OpenTelemetry.Extensions.Hosting</c> package.</item>
/// </list>
/// </remarks>
/// <param name="services">The <see cref="IServiceCollection" /> to add
/// services to.</param>
/// <param name="services"><see cref="IServiceCollection" />.</param>
/// <param name="configure">Callback action to configure the <see
/// cref="MeterProviderBuilder"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls
/// can be chained.</returns>
public static IServiceCollection ConfigureOpenTelemetryMeterProvider(
this IServiceCollection services,
Action<IServiceProvider, MeterProviderBuilder> configure)
Action<MeterProviderBuilder> configure)
{
RegisterBuildAction(services, configure);
Guard.ThrowIfNull(services);
Guard.ThrowIfNull(configure);

configure(new MeterProviderServiceCollectionBuilder(services));

return services;
}

private static void RegisterBuildAction(IServiceCollection services, Action<IServiceProvider, MeterProviderBuilder> configure)
/// <summary>
/// Registers an action used to configure the OpenTelemetry <see
/// cref="MeterProviderBuilder"/> 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="MeterProvider"/> will NOT be created automatically
/// using this method. To begin collecting metrics 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="MeterProviderBuilder"/> once the <see
/// cref="IServiceProvider"/> has been created. Many helper extensions
/// register services and may throw if invoked inside the configuration
/// delegate. If you don't need access to the <see cref="IServiceProvider"/>
/// call <see cref="ConfigureOpenTelemetryMeterProvider(IServiceCollection,
/// Action{MeterProviderBuilder})"/> instead which is safe to be used with
/// helper extensions.</item>
/// </list>
/// </remarks>
/// <param name="services"><see cref="IServiceCollection" />.</param>
/// <param name="configure">Callback action to configure the <see
/// cref="MeterProviderBuilder"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls
/// can be chained.</returns>
public static IServiceCollection ConfigureOpenTelemetryMeterProvider(
CodeBlanch marked this conversation as resolved.
Show resolved Hide resolved
this IServiceCollection services,
Action<IServiceProvider, MeterProviderBuilder> configure)
{
Guard.ThrowIfNull(services);
Guard.ThrowIfNull(configure);

services.AddSingleton<IConfigureMeterProviderBuilder>(
new ConfigureMeterProviderBuilderCallbackWrapper(configure));

return services;
}

private sealed class ConfigureMeterProviderBuilderCallbackWrapper : IConfigureMeterProviderBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ public static IServiceCollection ConfigureOpenTelemetryTracerProvider(
/// <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>
/// delegate. If you don't need access to the <see cref="IServiceProvider"/>
/// call <see cref="ConfigureOpenTelemetryTracerProvider(IServiceCollection,
/// Action{TracerProviderBuilder})"/> instead which is safe to be used with
/// helper extensions.</item>
/// </list>
/// </remarks>
/// <param name="services"><see cref="IServiceCollection" />.</param>
Expand Down
76 changes: 18 additions & 58 deletions src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace OpenTelemetry.Metrics;
public class MeterProviderBuilderBase : MeterProviderBuilder, IMeterProviderBuilder
{
private readonly bool allowBuild;
private IServiceCollection? services;
private readonly MeterProviderServiceCollectionBuilder innerBuilder;

public MeterProviderBuilderBase()
{
Expand All @@ -40,9 +40,7 @@ public MeterProviderBuilderBase()
.TryAddSingleton<MeterProvider>(
sp => throw new NotSupportedException("Self-contained MeterProvider cannot be accessed using the application IServiceProvider call Build instead."));

services.ConfigureOpenTelemetryMeterProvider((sp, builder) => this.services = null);

this.services = services;
this.innerBuilder = new MeterProviderServiceCollectionBuilder(services);

this.allowBuild = true;
}
Expand All @@ -55,9 +53,7 @@ internal MeterProviderBuilderBase(IServiceCollection services)
.AddOpenTelemetryMeterProviderBuilderServices()
.TryAddSingleton<MeterProvider>(sp => new MeterProviderSdk(sp, ownsServiceProvider: false));

services.ConfigureOpenTelemetryMeterProvider((sp, builder) => this.services = null);

this.services = services;
this.innerBuilder = new MeterProviderServiceCollectionBuilder(services);

this.allowBuild = false;
}
Expand All @@ -68,36 +64,34 @@ internal MeterProviderBuilderBase(IServiceCollection services)
/// <inheritdoc />
public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory)
{
Guard.ThrowIfNull(instrumentationFactory);

this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddInstrumentation(instrumentationFactory);
});
this.innerBuilder.AddInstrumentation(instrumentationFactory);

return this;
}

/// <inheritdoc />
public override MeterProviderBuilder AddMeter(params string[] names)
{
Guard.ThrowIfNull(names);

this.ConfigureBuilderInternal((sp, builder) =>
{
builder.AddMeter(names);
});
this.innerBuilder.AddMeter(names);

return this;
}

/// <inheritdoc />
MeterProviderBuilder IMeterProviderBuilder.ConfigureServices(Action<IServiceCollection> configure)
=> this.ConfigureServicesInternal(configure);
{
this.innerBuilder.ConfigureServices(configure);

return this;
}

/// <inheritdoc />
MeterProviderBuilder IDeferredMeterProviderBuilder.Configure(Action<IServiceProvider, MeterProviderBuilder> configure)
=> this.ConfigureBuilderInternal(configure);
{
this.innerBuilder.ConfigureBuilder(configure);

return this;
}

internal MeterProvider InvokeBuild()
=> this.Build();
Expand All @@ -113,14 +107,10 @@ protected MeterProvider Build()
throw new NotSupportedException("A MeterProviderBuilder bound to external service cannot be built directly. Access the MeterProvider using the application IServiceProvider instead.");
}

var services = this.services;
var services = this.innerBuilder.Services
?? throw new NotSupportedException("MeterProviderBuilder build method cannot be called multiple times.");

if (services == null)
{
throw new NotSupportedException("MeterProviderBuilder build method cannot be called multiple times.");
}

this.services = null;
this.innerBuilder.Services = null;

#if DEBUG
bool validateScopes = true;
Expand All @@ -131,34 +121,4 @@ protected MeterProvider Build()

return new MeterProviderSdk(serviceProvider, ownsServiceProvider: true);
}

private MeterProviderBuilder ConfigureBuilderInternal(Action<IServiceProvider, MeterProviderBuilder> configure)
{
var services = this.services;

if (services == null)
{
throw new NotSupportedException("Builder cannot be configured during MeterProvider construction.");
}

services.ConfigureOpenTelemetryMeterProvider(configure);

return this;
}

private MeterProviderBuilder ConfigureServicesInternal(Action<IServiceCollection> configure)
{
Guard.ThrowIfNull(configure);

var services = this.services;

if (services == null)
{
throw new NotSupportedException("Services cannot be configured during MeterProvider construction.");
}

configure(services);

return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,33 @@ public void ConfigureOpenTelemetryTracerProvider(int numberOfCalls)
[InlineData(3)]
public void ConfigureOpenTelemetryMeterProvider(int numberOfCalls)
{
var invocations = 0;
var beforeServiceProviderInvocations = 0;
var afterServiceProviderInvocations = 0;

var services = new ServiceCollection();

for (int i = 0; i < numberOfCalls; i++)
{
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => invocations++);
services.ConfigureOpenTelemetryMeterProvider(builder => beforeServiceProviderInvocations++);
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => afterServiceProviderInvocations++);
}

using var serviceProvider = services.BuildServiceProvider();

var registrations = serviceProvider.GetServices<IConfigureMeterProviderBuilder>();

Assert.Equal(numberOfCalls, beforeServiceProviderInvocations);
Assert.Equal(0, afterServiceProviderInvocations);

foreach (var registration in registrations)
{
registration.ConfigureBuilder(serviceProvider, null!);
}

Assert.Equal(invocations, registrations.Count());
Assert.Equal(numberOfCalls, registrations.Count());
Assert.Equal(numberOfCalls, beforeServiceProviderInvocations);
Assert.Equal(numberOfCalls, afterServiceProviderInvocations);

Assert.Equal(numberOfCalls * 2, registrations.Count());
}

[Theory]
Expand Down Expand Up @@ -114,8 +121,4 @@ public void ConfigureOpenTelemetryLoggerProvider(int numberOfCalls)
Assert.Equal(invocations, registrations.Count());
Assert.Equal(numberOfCalls, registrations.Count());
}

private sealed class CustomType
{
}
}
Loading