Skip to content

Commit ba8fd47

Browse files
authored
[di] Expose a detached MeterProviderBuilder extension on IServiceCollection which may modify services (#4517)
1 parent d89af85 commit ba8fd47

File tree

10 files changed

+252
-78
lines changed

10 files changed

+252
-78
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.ConfigureOpenTelemetryMeterProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Metrics.MeterProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
12
static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Trace.TracerProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.ConfigureOpenTelemetryMeterProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Metrics.MeterProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
12
static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<OpenTelemetry.Trace.TracerProviderBuilder!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ Released 2023-May-25
1818
created).
1919
([#4508](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4508))
2020

21+
* Added an `IServiceCollection.ConfigureOpenTelemetryMeterProvider` overload
22+
which may be used to configure `MeterProviderBuilder`s while the
23+
`IServiceCollection` is modifiable (before the `IServiceProvider` has been
24+
created).
25+
([#4517](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4517))
26+
2127
## 1.5.0-alpha.2
2228

2329
Released 2023-Mar-31
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// <copyright file="MeterProviderServiceCollectionBuilder.cs" company="OpenTelemetry Authors">
2+
// Copyright The OpenTelemetry Authors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
// </copyright>
16+
17+
using Microsoft.Extensions.DependencyInjection;
18+
using OpenTelemetry.Internal;
19+
20+
namespace OpenTelemetry.Metrics;
21+
22+
internal sealed class MeterProviderServiceCollectionBuilder : MeterProviderBuilder, IMeterProviderBuilder
23+
{
24+
public MeterProviderServiceCollectionBuilder(IServiceCollection services)
25+
{
26+
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => this.Services = null);
27+
28+
this.Services = services;
29+
}
30+
31+
public IServiceCollection? Services { get; set; }
32+
33+
public MeterProvider? Provider => null;
34+
35+
/// <inheritdoc />
36+
public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory)
37+
{
38+
Guard.ThrowIfNull(instrumentationFactory);
39+
40+
this.ConfigureBuilderInternal((sp, builder) =>
41+
{
42+
builder.AddInstrumentation(instrumentationFactory);
43+
});
44+
45+
return this;
46+
}
47+
48+
/// <inheritdoc />
49+
public override MeterProviderBuilder AddMeter(params string[] names)
50+
{
51+
Guard.ThrowIfNull(names);
52+
53+
this.ConfigureBuilderInternal((sp, builder) =>
54+
{
55+
builder.AddMeter(names);
56+
});
57+
58+
return this;
59+
}
60+
61+
/// <inheritdoc />
62+
public MeterProviderBuilder ConfigureServices(Action<IServiceCollection> configure)
63+
=> this.ConfigureServicesInternal(configure);
64+
65+
/// <inheritdoc cref="IDeferredMeterProviderBuilder.Configure" />
66+
public MeterProviderBuilder ConfigureBuilder(Action<IServiceProvider, MeterProviderBuilder> configure)
67+
=> this.ConfigureBuilderInternal(configure);
68+
69+
/// <inheritdoc />
70+
MeterProviderBuilder IDeferredMeterProviderBuilder.Configure(Action<IServiceProvider, MeterProviderBuilder> configure)
71+
=> this.ConfigureBuilderInternal(configure);
72+
73+
private MeterProviderBuilder ConfigureBuilderInternal(Action<IServiceProvider, MeterProviderBuilder> configure)
74+
{
75+
var services = this.Services
76+
?? throw new NotSupportedException("Builder cannot be configured during MeterProvider construction.");
77+
78+
services.ConfigureOpenTelemetryMeterProvider(configure);
79+
80+
return this;
81+
}
82+
83+
private MeterProviderBuilder ConfigureServicesInternal(Action<IServiceCollection> configure)
84+
{
85+
Guard.ThrowIfNull(configure);
86+
87+
var services = this.Services
88+
?? throw new NotSupportedException("Services cannot be configured during MeterProvider construction.");
89+
90+
configure(services);
91+
92+
return this;
93+
}
94+
}

src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.cs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,44 +26,79 @@ public static class OpenTelemetryDependencyInjectionMetricsServiceCollectionExte
2626
{
2727
/// <summary>
2828
/// Registers an action used to configure the OpenTelemetry <see
29-
/// cref="MeterProviderBuilder"/> used to create the <see
30-
/// cref="MeterProvider"/> for the <see cref="IServiceCollection"/> being
31-
/// configured.
29+
/// cref="MeterProviderBuilder"/>.
3230
/// </summary>
3331
/// <remarks>
3432
/// Notes:
3533
/// <list type="bullet">
3634
/// <item>This is safe to be called multiple times and by library authors.
3735
/// Each registered configuration action will be applied
3836
/// sequentially.</item>
39-
/// <item>A <see cref="MeterProvider"/> will not be created automatically
37+
/// <item>A <see cref="MeterProvider"/> will NOT be created automatically
4038
/// using this method. To begin collecting metrics use the
4139
/// <c>IServiceCollection.AddOpenTelemetry</c> extension in the
4240
/// <c>OpenTelemetry.Extensions.Hosting</c> package.</item>
4341
/// </list>
4442
/// </remarks>
45-
/// <param name="services">The <see cref="IServiceCollection" /> to add
46-
/// services to.</param>
43+
/// <param name="services"><see cref="IServiceCollection" />.</param>
4744
/// <param name="configure">Callback action to configure the <see
4845
/// cref="MeterProviderBuilder"/>.</param>
4946
/// <returns>The <see cref="IServiceCollection"/> so that additional calls
5047
/// can be chained.</returns>
5148
public static IServiceCollection ConfigureOpenTelemetryMeterProvider(
5249
this IServiceCollection services,
53-
Action<IServiceProvider, MeterProviderBuilder> configure)
50+
Action<MeterProviderBuilder> configure)
5451
{
55-
RegisterBuildAction(services, configure);
52+
Guard.ThrowIfNull(services);
53+
Guard.ThrowIfNull(configure);
54+
55+
configure(new MeterProviderServiceCollectionBuilder(services));
5656

5757
return services;
5858
}
5959

60-
private static void RegisterBuildAction(IServiceCollection services, Action<IServiceProvider, MeterProviderBuilder> configure)
60+
/// <summary>
61+
/// Registers an action used to configure the OpenTelemetry <see
62+
/// cref="MeterProviderBuilder"/> once the <see cref="IServiceProvider"/>
63+
/// is available.
64+
/// </summary>
65+
/// <remarks>
66+
/// Notes:
67+
/// <list type="bullet">
68+
/// <item>This is safe to be called multiple times and by library authors.
69+
/// Each registered configuration action will be applied
70+
/// sequentially.</item>
71+
/// <item>A <see cref="MeterProvider"/> will NOT be created automatically
72+
/// using this method. To begin collecting metrics use the
73+
/// <c>IServiceCollection.AddOpenTelemetry</c> extension in the
74+
/// <c>OpenTelemetry.Extensions.Hosting</c> package.</item>
75+
/// <item>The supplied configuration delegate is called once the <see
76+
/// cref="IServiceProvider"/> is available. Services may NOT be added to a
77+
/// <see cref="MeterProviderBuilder"/> once the <see
78+
/// cref="IServiceProvider"/> has been created. Many helper extensions
79+
/// register services and may throw if invoked inside the configuration
80+
/// delegate. If you don't need access to the <see cref="IServiceProvider"/>
81+
/// call <see cref="ConfigureOpenTelemetryMeterProvider(IServiceCollection,
82+
/// Action{MeterProviderBuilder})"/> instead which is safe to be used with
83+
/// helper extensions.</item>
84+
/// </list>
85+
/// </remarks>
86+
/// <param name="services"><see cref="IServiceCollection" />.</param>
87+
/// <param name="configure">Callback action to configure the <see
88+
/// cref="MeterProviderBuilder"/>.</param>
89+
/// <returns>The <see cref="IServiceCollection"/> so that additional calls
90+
/// can be chained.</returns>
91+
public static IServiceCollection ConfigureOpenTelemetryMeterProvider(
92+
this IServiceCollection services,
93+
Action<IServiceProvider, MeterProviderBuilder> configure)
6194
{
6295
Guard.ThrowIfNull(services);
6396
Guard.ThrowIfNull(configure);
6497

6598
services.AddSingleton<IConfigureMeterProviderBuilder>(
6699
new ConfigureMeterProviderBuilderCallbackWrapper(configure));
100+
101+
return services;
67102
}
68103

69104
private sealed class ConfigureMeterProviderBuilderCallbackWrapper : IConfigureMeterProviderBuilder

src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ public static IServiceCollection ConfigureOpenTelemetryTracerProvider(
7777
/// <see cref="TracerProviderBuilder"/> once the <see
7878
/// cref="IServiceProvider"/> has been created. Many helper extensions
7979
/// register services and may throw if invoked inside the configuration
80-
/// delegate.</item>
80+
/// delegate. If you don't need access to the <see cref="IServiceProvider"/>
81+
/// call <see cref="ConfigureOpenTelemetryTracerProvider(IServiceCollection,
82+
/// Action{TracerProviderBuilder})"/> instead which is safe to be used with
83+
/// helper extensions.</item>
8184
/// </list>
8285
/// </remarks>
8386
/// <param name="services"><see cref="IServiceCollection" />.</param>

src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs

Lines changed: 18 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ namespace OpenTelemetry.Metrics;
2828
public class MeterProviderBuilderBase : MeterProviderBuilder, IMeterProviderBuilder
2929
{
3030
private readonly bool allowBuild;
31-
private IServiceCollection? services;
31+
private readonly MeterProviderServiceCollectionBuilder innerBuilder;
3232

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

43-
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => this.services = null);
44-
45-
this.services = services;
43+
this.innerBuilder = new MeterProviderServiceCollectionBuilder(services);
4644

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

58-
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => this.services = null);
59-
60-
this.services = services;
56+
this.innerBuilder = new MeterProviderServiceCollectionBuilder(services);
6157

6258
this.allowBuild = false;
6359
}
@@ -68,36 +64,34 @@ internal MeterProviderBuilderBase(IServiceCollection services)
6864
/// <inheritdoc />
6965
public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory)
7066
{
71-
Guard.ThrowIfNull(instrumentationFactory);
72-
73-
this.ConfigureBuilderInternal((sp, builder) =>
74-
{
75-
builder.AddInstrumentation(instrumentationFactory);
76-
});
67+
this.innerBuilder.AddInstrumentation(instrumentationFactory);
7768

7869
return this;
7970
}
8071

8172
/// <inheritdoc />
8273
public override MeterProviderBuilder AddMeter(params string[] names)
8374
{
84-
Guard.ThrowIfNull(names);
85-
86-
this.ConfigureBuilderInternal((sp, builder) =>
87-
{
88-
builder.AddMeter(names);
89-
});
75+
this.innerBuilder.AddMeter(names);
9076

9177
return this;
9278
}
9379

9480
/// <inheritdoc />
9581
MeterProviderBuilder IMeterProviderBuilder.ConfigureServices(Action<IServiceCollection> configure)
96-
=> this.ConfigureServicesInternal(configure);
82+
{
83+
this.innerBuilder.ConfigureServices(configure);
84+
85+
return this;
86+
}
9787

9888
/// <inheritdoc />
9989
MeterProviderBuilder IDeferredMeterProviderBuilder.Configure(Action<IServiceProvider, MeterProviderBuilder> configure)
100-
=> this.ConfigureBuilderInternal(configure);
90+
{
91+
this.innerBuilder.ConfigureBuilder(configure);
92+
93+
return this;
94+
}
10195

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

116-
var services = this.services;
110+
var services = this.innerBuilder.Services
111+
?? throw new NotSupportedException("MeterProviderBuilder build method cannot be called multiple times.");
117112

118-
if (services == null)
119-
{
120-
throw new NotSupportedException("MeterProviderBuilder build method cannot be called multiple times.");
121-
}
122-
123-
this.services = null;
113+
this.innerBuilder.Services = null;
124114

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

132122
return new MeterProviderSdk(serviceProvider, ownsServiceProvider: true);
133123
}
134-
135-
private MeterProviderBuilder ConfigureBuilderInternal(Action<IServiceProvider, MeterProviderBuilder> configure)
136-
{
137-
var services = this.services;
138-
139-
if (services == null)
140-
{
141-
throw new NotSupportedException("Builder cannot be configured during MeterProvider construction.");
142-
}
143-
144-
services.ConfigureOpenTelemetryMeterProvider(configure);
145-
146-
return this;
147-
}
148-
149-
private MeterProviderBuilder ConfigureServicesInternal(Action<IServiceCollection> configure)
150-
{
151-
Guard.ThrowIfNull(configure);
152-
153-
var services = this.services;
154-
155-
if (services == null)
156-
{
157-
throw new NotSupportedException("Services cannot be configured during MeterProvider construction.");
158-
}
159-
160-
configure(services);
161-
162-
return this;
163-
}
164124
}

test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,26 +65,33 @@ public void ConfigureOpenTelemetryTracerProvider(int numberOfCalls)
6565
[InlineData(3)]
6666
public void ConfigureOpenTelemetryMeterProvider(int numberOfCalls)
6767
{
68-
var invocations = 0;
68+
var beforeServiceProviderInvocations = 0;
69+
var afterServiceProviderInvocations = 0;
6970

7071
var services = new ServiceCollection();
7172

7273
for (int i = 0; i < numberOfCalls; i++)
7374
{
74-
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => invocations++);
75+
services.ConfigureOpenTelemetryMeterProvider(builder => beforeServiceProviderInvocations++);
76+
services.ConfigureOpenTelemetryMeterProvider((sp, builder) => afterServiceProviderInvocations++);
7577
}
7678

7779
using var serviceProvider = services.BuildServiceProvider();
7880

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

83+
Assert.Equal(numberOfCalls, beforeServiceProviderInvocations);
84+
Assert.Equal(0, afterServiceProviderInvocations);
85+
8186
foreach (var registration in registrations)
8287
{
8388
registration.ConfigureBuilder(serviceProvider, null!);
8489
}
8590

86-
Assert.Equal(invocations, registrations.Count());
87-
Assert.Equal(numberOfCalls, registrations.Count());
91+
Assert.Equal(numberOfCalls, beforeServiceProviderInvocations);
92+
Assert.Equal(numberOfCalls, afterServiceProviderInvocations);
93+
94+
Assert.Equal(numberOfCalls * 2, registrations.Count());
8895
}
8996

9097
[Theory]
@@ -114,8 +121,4 @@ public void ConfigureOpenTelemetryLoggerProvider(int numberOfCalls)
114121
Assert.Equal(invocations, registrations.Count());
115122
Assert.Equal(numberOfCalls, registrations.Count());
116123
}
117-
118-
private sealed class CustomType
119-
{
120-
}
121124
}

0 commit comments

Comments
 (0)