[logging] UseOpenTelemetry extension & WithLogging default behavior (#5072)

This commit is contained in:
Mikel Blanchard 2023-11-29 15:15:46 -08:00 committed by GitHub
parent 39c77d40f8
commit a47b222101
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 304 additions and 37 deletions

View File

@ -1,2 +1,3 @@
OpenTelemetry.OpenTelemetryBuilder.WithLogging() -> OpenTelemetry.OpenTelemetryBuilder!
OpenTelemetry.OpenTelemetryBuilder.WithLogging(System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>! configure) -> OpenTelemetry.OpenTelemetryBuilder!
OpenTelemetry.OpenTelemetryBuilder.WithLogging(System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>? configureBuilder, System.Action<OpenTelemetry.Logs.OpenTelemetryLoggerOptions!>? configureOptions) -> OpenTelemetry.OpenTelemetryBuilder!

View File

@ -12,6 +12,11 @@
APIs.
([#4958](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4958))
* The `OpenTelemetryBuilder.WithLogging` experimental API method will now
register an `ILoggerProvider` named 'OpenTelemetry' into the
`IServiceCollection` to enable `ILoggerFactory` integration.
([#5072](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5072))
## 1.7.0-alpha.1
Released 2023-Oct-16

View File

@ -16,6 +16,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Internal;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
@ -146,10 +147,17 @@ public sealed class OpenTelemetryBuilder
/// Adds logging services into the builder.
/// </summary>
/// <remarks>
/// <para><b>WARNING</b>: This is an experimental API which might change or be removed in the future. Use at your own risk.</para>
/// Note: This is safe to be called multiple times and by library authors.
/// <para><b>WARNING</b>: This is an experimental API which might change or
/// be removed in the future. Use at your own risk.</para>
/// Notes:
/// <list type="bullet">
/// <item>This is safe to be called multiple times and by library authors.
/// Only a single <see cref="LoggerProvider"/> will be created for a given
/// <see cref="IServiceCollection"/>.
/// <see cref="IServiceCollection"/>.</item>
/// <item>This method automatically registers an <see
/// cref="ILoggerProvider"/> named 'OpenTelemetry' into the <see
/// cref="IServiceCollection"/>.</item>
/// </list>
/// </remarks>
/// <returns>The supplied <see cref="OpenTelemetryBuilder"/> for chaining
/// calls.</returns>
@ -159,16 +167,22 @@ public sealed class OpenTelemetryBuilder
/// Adds logging services into the builder.
/// </summary>
/// <remarks>
/// Note: This is safe to be called multiple times and by library authors.
/// Notes:
/// <list type="bullet">
/// <item>This is safe to be called multiple times and by library authors.
/// Only a single <see cref="LoggerProvider"/> will be created for a given
/// <see cref="IServiceCollection"/>.
/// <see cref="IServiceCollection"/>.</item>
/// <item>This method automatically registers an <see
/// cref="ILoggerProvider"/> named 'OpenTelemetry' into the <see
/// cref="IServiceCollection"/>.</item>
/// </list>
/// </remarks>
/// <returns>The supplied <see cref="OpenTelemetryBuilder"/> for chaining
/// calls.</returns>
internal
#endif
OpenTelemetryBuilder WithLogging()
=> this.WithLogging(b => { });
=> this.WithLogging(configureBuilder: null, configureOptions: null);
#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
@ -195,9 +209,40 @@ public sealed class OpenTelemetryBuilder
{
Guard.ThrowIfNull(configure);
var builder = new LoggerProviderBuilderBase(this.Services);
return this.WithLogging(configureBuilder: configure, configureOptions: null);
}
configure(builder);
#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
/// Adds logging services into the builder.
/// </summary>
/// <remarks><inheritdoc cref="WithLogging()" path="/remarks"/></remarks>
/// <param name="configureBuilder">Optional <see
/// cref="LoggerProviderBuilder"/> configuration callback.</param>
/// <param name="configureOptions">Optional <see
/// cref="OpenTelemetryLoggerOptions"/> configuration callback.</param>
/// <returns>The supplied <see cref="OpenTelemetryBuilder"/> for chaining
/// calls.</returns>
public
#else
/// <summary>
/// Adds logging services into the builder.
/// </summary>
/// <remarks><inheritdoc cref="WithLogging()" path="/remarks"/></remarks>
/// <param name="configureBuilder">Optional <see
/// cref="LoggerProviderBuilder"/> configuration callback.</param>
/// <param name="configureOptions">Optional <see
/// cref="OpenTelemetryLoggerOptions"/> configuration callback.</param>
/// <returns>The supplied <see cref="OpenTelemetryBuilder"/> for chaining
/// calls.</returns>
internal
#endif
OpenTelemetryBuilder WithLogging(
Action<LoggerProviderBuilder>? configureBuilder,
Action<OpenTelemetryLoggerOptions>? configureOptions)
{
this.Services.AddLogging(
logging => logging.UseOpenTelemetry(configureBuilder, configureOptions));
return this;
}

View File

@ -40,3 +40,6 @@ override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value,
override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder!
static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder!
static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>? configureBuilder, System.Action<OpenTelemetry.Logs.OpenTelemetryLoggerOptions!>? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder!

View File

@ -64,6 +64,18 @@
[#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563).
([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089))
* Added the `ILoggingBuilder.UseOpenTelemetry` experimental API extension for
registering OpenTelemetry `ILogger` integration using `LoggerProviderBuilder`
which supports the full DI (`IServiceCollection` \ `IServiceProvider`) API
surface (mirrors tracing & metrics).
([#5072](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5072))
* Changed the `ILoggingBuilder` registration extensions (`AddOpenTelemetry` &
`UseOpenTelemetry`) to fire the optional `OpenTelemetryLoggerOptions`
configuration delegate AFTER the "Logging:OpenTelemetry" `IConfiguration`
section has been applied.
([#5072](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5072))
## 1.7.0-alpha.1
Released 2023-Oct-16

View File

@ -17,6 +17,9 @@
#if NET6_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
#if EXPOSE_EXPERIMENTAL_FEATURES
using System.ComponentModel;
#endif
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
@ -48,6 +51,11 @@ public static class OpenTelemetryLoggingExtensions
/// </remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
#if EXPOSE_EXPERIMENTAL_FEATURES
// todo: [Obsolete("Call UseOpenTelemetry instead this method will be removed in a future version.")]
// Note: We hide AddOpenTelemetry from IDEs using EditorBrowsable when UseOpenTelemetry is present to reduce confusion.
[EditorBrowsable(EditorBrowsableState.Never)]
#endif
public static ILoggingBuilder AddOpenTelemetry(
this ILoggingBuilder builder)
=> AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null);
@ -59,11 +67,101 @@ public static class OpenTelemetryLoggingExtensions
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Optional configuration action.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
#if EXPOSE_EXPERIMENTAL_FEATURES
// todo: [Obsolete("Call UseOpenTelemetry instead this method will be removed in a future version.")]
// Note: We hide AddOpenTelemetry from IDEs using EditorBrowsable when UseOpenTelemetry is present to reduce confusion.
[EditorBrowsable(EditorBrowsableState.Never)]
#endif
public static ILoggingBuilder AddOpenTelemetry(
this ILoggingBuilder builder,
Action<OpenTelemetryLoggerOptions>? configure)
=> AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: configure);
#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks>
/// <para><b>WARNING</b>: This is an experimental API which might change or be removed in the future. Use at your own risk.</para>
/// Note: This is safe to be called multiple times and by library authors.
/// Only a single <see cref="OpenTelemetryLoggerProvider"/> will be created
/// for a given <see cref="IServiceCollection"/>.
/// </remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
public
#else
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks>
/// Note: This is safe to be called multiple times and by library authors.
/// Only a single <see cref="OpenTelemetryLoggerProvider"/> will be created
/// for a given <see cref="IServiceCollection"/>.
/// </remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
internal
#endif
static ILoggingBuilder UseOpenTelemetry(
this ILoggingBuilder builder)
=> AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null);
#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks><inheritdoc cref="UseOpenTelemetry(ILoggingBuilder)" path="/remarks"/></remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Optional <see cref="LoggerProviderBuilder"/> configuration action.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
public
#else
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks><inheritdoc cref="UseOpenTelemetry(ILoggingBuilder)" path="/remarks"/></remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure"><see cref="LoggerProviderBuilder"/> configuration action.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
internal
#endif
static ILoggingBuilder UseOpenTelemetry(
this ILoggingBuilder builder,
Action<LoggerProviderBuilder> configure)
{
Guard.ThrowIfNull(configure);
return AddOpenTelemetryInternal(builder, configureBuilder: configure, configureOptions: null);
}
#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks><inheritdoc cref="UseOpenTelemetry(ILoggingBuilder)" path="/remarks"/></remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configureBuilder">Optional <see cref="LoggerProviderBuilder"/> configuration action.</param>
/// <param name="configureOptions">Optional <see cref="OpenTelemetryLoggerOptions"/> configuration action.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
public
#else
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks><inheritdoc cref="UseOpenTelemetry(ILoggingBuilder)" path="/remarks"/></remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configureBuilder">Optional <see cref="LoggerProviderBuilder"/> configuration action.</param>
/// <param name="configureOptions">Optional <see cref="OpenTelemetryLoggerOptions"/> configuration action.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
internal
#endif
static ILoggingBuilder UseOpenTelemetry(
this ILoggingBuilder builder,
Action<LoggerProviderBuilder>? configureBuilder,
Action<OpenTelemetryLoggerOptions>? configureOptions)
=> AddOpenTelemetryInternal(builder, configureBuilder, configureOptions);
private static ILoggingBuilder AddOpenTelemetryInternal(
ILoggingBuilder builder,
Action<LoggerProviderBuilder>? configureBuilder,
@ -75,19 +173,19 @@ public static class OpenTelemetryLoggingExtensions
var services = builder.Services;
if (configureOptions != null)
{
// TODO: Move this below the RegisterLoggerProviderOptions call so
// that user-supplied delegate fires AFTER the options are bound to
// Logging:OpenTelemetry configuration.
services.Configure(configureOptions);
}
// Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions
RegisterLoggerProviderOptions(services);
services.AddOpenTelemetrySharedProviderBuilderServices();
if (configureOptions != null)
{
// Note: Order is important here so that user-supplied delegate
// fires AFTER the options are bound to Logging:OpenTelemetry
// configuration.
services.Configure(configureOptions);
}
var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder(
(sp, logging) =>
{

View File

@ -381,7 +381,7 @@ public class OpenTelemetryServicesExtensionsTests
}
[Fact]
public async Task AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest()
public void AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest()
{
bool configureBuilderCalled = false;
@ -416,14 +416,8 @@ public class OpenTelemetryServicesExtensionsTests
var host = builder.Build();
Assert.False(configureBuilderCalled);
await host.StartAsync();
Assert.True(configureBuilderCalled);
await host.StopAsync();
host.Dispose();
}

View File

@ -17,6 +17,7 @@
#nullable enable
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
@ -25,16 +26,25 @@ namespace OpenTelemetry.Logs.Tests;
public sealed class OpenTelemetryLoggingExtensionsTests
{
[Fact]
public void ServiceCollectionAddOpenTelemetryNoParametersTest()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ServiceCollectionAddOpenTelemetryNoParametersTest(bool callUseExtension)
{
bool optionsCallbackInvoked = false;
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(configure =>
serviceCollection.AddLogging(logging =>
{
configure.AddOpenTelemetry();
if (callUseExtension)
{
logging.UseOpenTelemetry();
}
else
{
logging.AddOpenTelemetry();
}
});
serviceCollection.Configure<OpenTelemetryLoggerOptions>(options =>
@ -52,10 +62,16 @@ public sealed class OpenTelemetryLoggingExtensionsTests
}
[Theory]
[InlineData(1, 0)]
[InlineData(1, 1)]
[InlineData(5, 5)]
public void ServiceCollectionAddOpenTelemetryConfigureActionTests(int numberOfBuilderRegistrations, int numberOfOptionsRegistrations)
[InlineData(false, 1, 0)]
[InlineData(false, 1, 1)]
[InlineData(false, 5, 5)]
[InlineData(true, 1, 0)]
[InlineData(true, 1, 1)]
[InlineData(true, 5, 5)]
public void ServiceCollectionAddOpenTelemetryConfigureActionTests(
bool callUseExtension,
int numberOfBuilderRegistrations,
int numberOfOptionsRegistrations)
{
int configureCallbackInvocations = 0;
int optionsCallbackInvocations = 0;
@ -63,11 +79,18 @@ public sealed class OpenTelemetryLoggingExtensionsTests
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(configure =>
serviceCollection.AddLogging(logging =>
{
for (int i = 0; i < numberOfBuilderRegistrations; i++)
{
configure.AddOpenTelemetry(ConfigureCallback);
if (callUseExtension)
{
logging.UseOpenTelemetry(configureBuilder: null, configureOptions: ConfigureCallback);
}
else
{
logging.AddOpenTelemetry(ConfigureCallback);
}
}
});
@ -116,6 +139,92 @@ public sealed class OpenTelemetryLoggingExtensionsTests
}
}
[Fact]
public void UseOpenTelemetryDependencyInjectionTest()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(logging =>
{
logging.UseOpenTelemetry(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton<TestLogProcessor>();
});
builder.ConfigureBuilder((sp, builder) =>
{
builder.AddProcessor(
sp.GetRequiredService<TestLogProcessor>());
});
});
});
using var sp = serviceCollection.BuildServiceProvider();
var loggerProvider = sp.GetRequiredService<LoggerProvider>() as LoggerProviderSdk;
Assert.NotNull(loggerProvider);
Assert.NotNull(loggerProvider.Processor);
Assert.True(loggerProvider.Processor is TestLogProcessor);
}
[Fact]
public void UseOpenTelemetryOptionsOrderingTest()
{
int currentIndex = -1;
int beforeDelegateIndex = -1;
int extensionDelegateIndex = -1;
int afterDelegateIndex = -1;
var serviceCollection = new ServiceCollection();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { ["Logging:OpenTelemetry:IncludeFormattedMessage"] = "true" })
.Build();
serviceCollection.Configure<OpenTelemetryLoggerOptions>(o =>
{
// Verify this fires BEFORE options are bound
Assert.False(o.IncludeFormattedMessage);
beforeDelegateIndex = ++currentIndex;
});
serviceCollection.AddLogging(logging =>
{
// Note: Typically the host binds logging configuration to the
// "Logging" section but since we aren't using a host we do this
// manually.
logging.AddConfiguration(config.GetSection("Logging"));
logging.UseOpenTelemetry(
configureBuilder: null,
configureOptions: o =>
{
// Verify this fires AFTER options are bound
Assert.True(o.IncludeFormattedMessage);
extensionDelegateIndex = ++currentIndex;
});
});
serviceCollection.Configure<OpenTelemetryLoggerOptions>(o => afterDelegateIndex = ++currentIndex);
using var sp = serviceCollection.BuildServiceProvider();
var loggerProvider = sp.GetRequiredService<LoggerProvider>() as LoggerProviderSdk;
Assert.NotNull(loggerProvider);
Assert.Equal(0, beforeDelegateIndex);
Assert.Equal(1, extensionDelegateIndex);
Assert.Equal(2, afterDelegateIndex);
}
// This test validates that the OpenTelemetryLoggerOptions contains only primitive type properties.
// This is necessary to ensure trim correctness since that class is effectively deserialized from
// configuration. The top level properties are ensured via annotation on the RegisterProviderOptions API
@ -136,11 +245,11 @@ public sealed class OpenTelemetryLoggingExtensionsTests
// arrange
var services = new ServiceCollection();
services.AddSingleton<MyProcessor>();
services.AddSingleton<TestLogProcessor>();
services.AddLogging(logging =>
logging.AddOpenTelemetry(
o => o.AddProcessor(sp => sp.GetRequiredService<MyProcessor>())));
o => o.AddProcessor(sp => sp.GetRequiredService<TestLogProcessor>())));
// act
using var sp = services.BuildServiceProvider();
@ -150,7 +259,7 @@ public sealed class OpenTelemetryLoggingExtensionsTests
// assert
Assert.NotNull(loggerProvider);
Assert.NotNull(loggerProvider.Processor);
Assert.True(loggerProvider.Processor is MyProcessor);
Assert.True(loggerProvider.Processor is TestLogProcessor);
}
[Fact]
@ -170,7 +279,7 @@ public sealed class OpenTelemetryLoggingExtensionsTests
Assert.Throws<ArgumentNullException>(() => sp.GetRequiredService<LoggerProvider>() as LoggerProviderSdk);
}
private class MyProcessor : BaseProcessor<LogRecord>
private class TestLogProcessor : BaseProcessor<LogRecord>
{
}
}