Skip to content

Commit

Permalink
[di] Expose a detached MeterProviderBuilder extension on IServiceColl…
Browse files Browse the repository at this point in the history
…ection which may modify services (#4517)
  • Loading branch information
CodeBlanch authored May 31, 2023
1 parent d89af85 commit ba8fd47
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 78 deletions.
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!
6 changes: 6 additions & 0 deletions src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md
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(
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

0 comments on commit ba8fd47

Please sign in to comment.