diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/IConfigureLoggerProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/IConfigureLoggerProviderBuilder.cs new file mode 100644 index 00000000000..91a43a3ae9e --- /dev/null +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/IConfigureLoggerProviderBuilder.cs @@ -0,0 +1,31 @@ +// +// 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. +// + +namespace OpenTelemetry.Logs; + +/// +/// Represents something that configures the type. +/// +// Note: This API may be made public if there is a need for it. +internal interface IConfigureLoggerProviderBuilder +{ + /// + /// Invoked to configure a instance. + /// + /// . + /// . + void ConfigureBuilder(IServiceProvider serviceProvider, LoggerProviderBuilder loggerProviderBuilder); +} diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/ILoggerProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/ILoggerProviderBuilder.cs new file mode 100644 index 00000000000..0142659bbca --- /dev/null +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/ILoggerProviderBuilder.cs @@ -0,0 +1,50 @@ +// +// 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. +// + +using Microsoft.Extensions.DependencyInjection; + +namespace OpenTelemetry.Logs; + +/// +/// Describes a backed by an . +/// +// Note: This API may be made public if there is a need for it. +internal interface ILoggerProviderBuilder : IDeferredLoggerProviderBuilder +{ + /// + /// Gets the being constructed by the builder. + /// + /// + /// Note: should return until + /// construction has started and the has + /// closed. + /// + LoggerProvider? Provider { get; } + + /// + /// Register a callback action to configure the where logging services are configured. + /// + /// + /// Note: Logging services are only available during the application + /// configuration phase. This method should throw a if services are configured after the + /// application has been created. + /// + /// Configuration callback. + /// The supplied for chaining. + LoggerProviderBuilder ConfigureServices(Action configure); +} diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.cs new file mode 100644 index 00000000000..cd28371830a --- /dev/null +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.cs @@ -0,0 +1,181 @@ +// +// 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. +// + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Logs; + +/// +/// Contains extension methods for the class. +/// +internal static class OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions +{ + /// + /// Adds instrumentation to the provider. + /// + /// + /// Note: The type specified by will be + /// registered as a singleton service into application services. + /// + /// Instrumentation type. + /// . + /// The supplied for chaining. + public static LoggerProviderBuilder AddInstrumentation(this LoggerProviderBuilder loggerProviderBuilder) + where T : class + { + loggerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); + + loggerProviderBuilder.ConfigureBuilder((sp, builder) => + { + builder.AddInstrumentation(() => sp.GetRequiredService()); + }); + + return loggerProviderBuilder; + } + + /// + /// Adds instrumentation to the provider. + /// + /// Instrumentation type. + /// . + /// Instrumentation instance. + /// The supplied for chaining. + public static LoggerProviderBuilder AddInstrumentation(this LoggerProviderBuilder loggerProviderBuilder, T instrumentation) + where T : class + { + Guard.ThrowIfNull(instrumentation); + + loggerProviderBuilder.ConfigureBuilder((sp, builder) => + { + builder.AddInstrumentation(() => instrumentation); + }); + + return loggerProviderBuilder; + } + + /// + /// Adds instrumentation to the provider. + /// + /// Instrumentation type. + /// . + /// Instrumentation factory. + /// The supplied for chaining. + public static LoggerProviderBuilder AddInstrumentation( + this LoggerProviderBuilder loggerProviderBuilder, + Func instrumentationFactory) + where T : class + { + Guard.ThrowIfNull(instrumentationFactory); + + loggerProviderBuilder.ConfigureBuilder((sp, builder) => + { + builder.AddInstrumentation(() => instrumentationFactory(sp)); + }); + + return loggerProviderBuilder; + } + + /// + /// Adds instrumentation to the provider. + /// + /// Instrumentation type. + /// . + /// Instrumentation factory. + /// The supplied for chaining. + public static LoggerProviderBuilder AddInstrumentation( + this LoggerProviderBuilder loggerProviderBuilder, + Func instrumentationFactory) + where T : class + { + Guard.ThrowIfNull(instrumentationFactory); + + loggerProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (loggerProviderBuilder is ILoggerProviderBuilder iLoggerProviderBuilder + && iLoggerProviderBuilder.Provider != null) + { + builder.AddInstrumentation(() => instrumentationFactory(sp, iLoggerProviderBuilder.Provider)); + } + }); + + return loggerProviderBuilder; + } + + /// + /// Register a callback action to configure the where logging services are configured. + /// + /// + /// Note: Logging services are only available during the application + /// configuration phase. + /// + /// . + /// Configuration callback. + /// The supplied for chaining. + public static LoggerProviderBuilder ConfigureServices( + this LoggerProviderBuilder loggerProviderBuilder, + Action configure) + { + if (loggerProviderBuilder is ILoggerProviderBuilder iLoggerProviderBuilder) + { + iLoggerProviderBuilder.ConfigureServices(configure); + } + + return loggerProviderBuilder; + } + + /// + /// Register a callback action to configure the once the application is available. + /// + /// + /// is an advanced API and is expected + /// to be used primarily by library authors. + /// Notes: + /// + /// Services may NOT be added to the + /// (via ) inside because the has + /// already been created. A will be + /// thrown if services are accessed. + /// Library extension methods (for example AddOtlpExporter + /// inside OpenTelemetry.Exporter.OpenTelemetryProtocol) may depend + /// on services being available today or at any point in the future. It is + /// NOT recommend to call library extension methods from inside . + /// + /// For more information see: Dependency + /// injection support. + /// + /// . + /// Configuration callback. + /// The supplied for chaining. + internal static LoggerProviderBuilder ConfigureBuilder( + this LoggerProviderBuilder loggerProviderBuilder, + Action configure) + { + if (loggerProviderBuilder is IDeferredLoggerProviderBuilder deferredLoggerProviderBuilder) + { + deferredLoggerProviderBuilder.Configure(configure); + } + + return loggerProviderBuilder; + } +} diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.cs new file mode 100644 index 00000000000..d0542ddcc32 --- /dev/null +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.cs @@ -0,0 +1,85 @@ +// +// 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. +// + +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Logs; + +/// +/// Extension methods for setting up OpenTelemetry logging services in an . +/// +internal static class OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions +{ + /// + /// Registers an action used to configure the OpenTelemetry used to create the for the being + /// configured. + /// + /// + /// Notes: + /// + /// This is safe to be called multiple times and by library authors. + /// Each registered configuration action will be applied + /// sequentially. + /// A will not be created automatically + /// using this method. To begin collecting metrics use the + /// IServiceCollection.AddOpenTelemetry extension in the + /// OpenTelemetry.Extensions.Hosting package. + /// + /// + /// The to add + /// services to. + /// Callback action to configure the . + /// The so that additional calls + /// can be chained. + public static IServiceCollection ConfigureOpenTelemetryLoggerProvider( + this IServiceCollection services, + Action configure) + { + RegisterBuildAction(services, configure); + + return services; + } + + private static void RegisterBuildAction(IServiceCollection services, Action configure) + { + Guard.ThrowIfNull(services); + Guard.ThrowIfNull(configure); + + services.AddSingleton( + new ConfigureLoggerProviderBuilderCallbackWrapper(configure)); + } + + private sealed class ConfigureLoggerProviderBuilderCallbackWrapper : IConfigureLoggerProviderBuilder + { + private readonly Action configure; + + public ConfigureLoggerProviderBuilderCallbackWrapper(Action configure) + { + Guard.ThrowIfNull(configure); + + this.configure = configure; + } + + public void ConfigureBuilder(IServiceProvider serviceProvider, LoggerProviderBuilder loggerProviderBuilder) + { + this.configure(serviceProvider, loggerProviderBuilder); + } + } +} diff --git a/src/OpenTelemetry.Api/AssemblyInfo.cs b/src/OpenTelemetry.Api/AssemblyInfo.cs index 09359852148..661ecc7563b 100644 --- a/src/OpenTelemetry.Api/AssemblyInfo.cs +++ b/src/OpenTelemetry.Api/AssemblyInfo.cs @@ -18,6 +18,7 @@ [assembly: InternalsVisibleTo("OpenTelemetry" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Api.ProviderBuilderExtensions" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Api.ProviderBuilderExtensions.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Api.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Shims.OpenTracing.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs new file mode 100644 index 00000000000..cbe42fba156 --- /dev/null +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs @@ -0,0 +1,146 @@ +// +// 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. +// + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenTelemetry.Logs; +using Xunit; + +namespace OpenTelemetry.Api.ProviderBuilderExtensions.Tests; + +public class LoggerProviderBuilderExtensionsTests +{ + [Fact] + public void AddInstrumentationFromServiceProviderTest() + { + using var builder = new TestLoggerProviderBuilder(); + + builder.AddInstrumentation(); + + var serviceProvider = builder.BuildServiceProvider(); + + var instrumentation = serviceProvider.GetRequiredService(); + + Assert.NotNull(instrumentation); + + var registrationCount = builder.InvokeRegistrations(); + + Assert.Equal(1, registrationCount); + Assert.Single(builder.Instrumentation); + Assert.Equal(instrumentation, builder.Instrumentation[0]); + } + + [Fact] + public void AddInstrumentationUsingInstanceTest() + { + using var builder = new TestLoggerProviderBuilder(); + + var instrumentation = new TestInstrumentation(); + + builder.AddInstrumentation(instrumentation); + + var serviceProvider = builder.BuildServiceProvider(); + var registrationCount = builder.InvokeRegistrations(); + + Assert.Equal(1, registrationCount); + Assert.Single(builder.Instrumentation); + Assert.Equal(instrumentation, builder.Instrumentation[0]); + } + + [Fact] + public void AddInstrumentationUsingFactoryTest() + { + using var builder = new TestLoggerProviderBuilder(); + + var instrumentation = new TestInstrumentation(); + + builder.AddInstrumentation(sp => + { + Assert.NotNull(sp); + + return instrumentation; + }); + + var serviceProvider = builder.BuildServiceProvider(); + var registrationCount = builder.InvokeRegistrations(); + + Assert.Equal(1, registrationCount); + Assert.Single(builder.Instrumentation); + Assert.Equal(instrumentation, builder.Instrumentation[0]); + } + + [Fact] + public void AddInstrumentationUsingFactoryAndProviderTest() + { + using var builder = new TestLoggerProviderBuilder(); + + var instrumentation = new TestInstrumentation(); + + builder.AddInstrumentation((sp, provider) => + { + Assert.NotNull(sp); + Assert.NotNull(provider); + + return instrumentation; + }); + + var serviceProvider = builder.BuildServiceProvider(); + var registrationCount = builder.InvokeRegistrations(); + + Assert.Equal(1, registrationCount); + Assert.Single(builder.Instrumentation); + Assert.Equal(instrumentation, builder.Instrumentation[0]); + } + + [Fact] + public void ConfigureServicesTest() + { + using var builder = new TestLoggerProviderBuilder(); + + builder.ConfigureServices(services => services.TryAddSingleton()); + + var serviceProvider = builder.BuildServiceProvider(); + + serviceProvider.GetRequiredService(); + } + + [Fact] + public void ConfigureBuilderTest() + { + var instrumentation = new object(); + + using var builder = new TestLoggerProviderBuilder(); + + builder.ConfigureBuilder((sp, builder) => + { + Assert.NotNull(sp); + Assert.NotNull(builder); + + builder.AddInstrumentation(instrumentation); + }); + + var serviceProvider = builder.BuildServiceProvider(); + var registrationCount = builder.InvokeRegistrations(); + + Assert.Equal(1, registrationCount); + Assert.Single(builder.Instrumentation); + Assert.True(ReferenceEquals(instrumentation, builder.Instrumentation[0])); + } + + private sealed class TestInstrumentation + { + } +} diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/TestLoggerProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/TestLoggerProviderBuilder.cs new file mode 100644 index 00000000000..61b0c411e11 --- /dev/null +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/TestLoggerProviderBuilder.cs @@ -0,0 +1,121 @@ +// +// 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. +// + +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Logs; + +namespace OpenTelemetry.Api.ProviderBuilderExtensions.Tests; + +internal sealed class TestLoggerProviderBuilder : LoggerProviderBuilder, ILoggerProviderBuilder, IDisposable +{ + public TestLoggerProviderBuilder() + { + this.Services = new ServiceCollection(); + } + + public IServiceCollection? Services { get; private set; } + + public ServiceProvider? ServiceProvider { get; private set; } + + public List Instrumentation { get; } = new(); + + public LoggerProvider? Provider { get; private set; } + + public override LoggerProviderBuilder AddInstrumentation(Func instrumentationFactory) + { + if (this.Services != null) + { + this.ConfigureBuilder((sp, builder) => builder.AddInstrumentation(instrumentationFactory)); + } + else + { + this.Instrumentation.Add(instrumentationFactory()); + } + + return this; + } + + public LoggerProviderBuilder ConfigureBuilder(Action configure) + { + var services = this.Services; + if (services != null) + { + services.ConfigureOpenTelemetryLoggerProvider(configure); + } + else + { + var serviceProvider = this.ServiceProvider ?? throw new InvalidOperationException("Test failure"); + configure(serviceProvider, this); + } + + return this; + } + + public LoggerProviderBuilder ConfigureServices(Action configure) + { + var services = this.Services; + if (services != null) + { + configure(services); + } + else + { + throw new NotSupportedException("Services cannot be configured after the ServiceProvider has been created."); + } + + return this; + } + + public IServiceProvider BuildServiceProvider() + { + var services = this.Services ?? throw new InvalidOperationException(); + + this.Services = null; + + this.Provider = new NoopLoggerProvider(); + + return this.ServiceProvider = services.BuildServiceProvider(); + } + + public int InvokeRegistrations() + { + var serviceProvider = this.ServiceProvider ?? throw new InvalidOperationException(); + + var registrations = serviceProvider.GetServices(); + + var count = 0; + + foreach (var registration in registrations) + { + registration.ConfigureBuilder(serviceProvider, this); + count++; + } + + return count; + } + + public void Dispose() + { + this.ServiceProvider?.Dispose(); + } + + LoggerProviderBuilder IDeferredLoggerProviderBuilder.Configure(Action configure) + => this.ConfigureBuilder(configure); + + private sealed class NoopLoggerProvider : LoggerProvider + { + } +} diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/MeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs similarity index 100% rename from test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/MeterProviderBuilderExtensionsTests.cs rename to test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/TestMeterProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs similarity index 100% rename from test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/TestMeterProviderBuilder.cs rename to test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs index 990335a3a09..16fee70d1d4 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs @@ -15,6 +15,7 @@ // using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Xunit; @@ -78,4 +79,32 @@ public void ConfigureOpenTelemetryMeterProvider(int numberOfCalls) Assert.Equal(invocations, registrations.Count()); Assert.Equal(numberOfCalls, registrations.Count()); } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(3)] + public void ConfigureOpenTelemetryLoggerProvider(int numberOfCalls) + { + var invocations = 0; + + var services = new ServiceCollection(); + + for (int i = 0; i < numberOfCalls; i++) + { + services.ConfigureOpenTelemetryLoggerProvider((sp, builder) => invocations++); + } + + using var serviceProvider = services.BuildServiceProvider(); + + var registrations = serviceProvider.GetServices(); + + foreach (var registration in registrations) + { + registration.ConfigureBuilder(serviceProvider, null!); + } + + Assert.Equal(invocations, registrations.Count()); + Assert.Equal(numberOfCalls, registrations.Count()); + } } diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/TestTracerProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs similarity index 100% rename from test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/TestTracerProviderBuilder.cs rename to test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/TracerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TracerProviderBuilderExtensionsTests.cs similarity index 100% rename from test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/TracerProviderBuilderExtensionsTests.cs rename to test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TracerProviderBuilderExtensionsTests.cs