opentelemetry-dotnet/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensi...

416 lines
13 KiB
C#

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
#nullable enable
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
namespace OpenTelemetry.Logs.Tests;
public sealed class OpenTelemetryLoggingExtensionsTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ServiceCollectionAddOpenTelemetryNoParametersTest(bool callUseExtension)
{
bool optionsCallbackInvoked = false;
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(logging =>
{
if (callUseExtension)
{
logging.UseOpenTelemetry();
}
else
{
logging.AddOpenTelemetry();
}
});
serviceCollection.Configure<OpenTelemetryLoggerOptions>(options =>
{
optionsCallbackInvoked = true;
});
using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
ILoggerFactory? loggerFactory = serviceProvider.GetService<ILoggerFactory>();
Assert.NotNull(loggerFactory);
Assert.True(optionsCallbackInvoked);
}
[Theory]
[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;
OpenTelemetryLoggerOptions? optionsInstance = null;
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(logging =>
{
for (int i = 0; i < numberOfBuilderRegistrations; i++)
{
if (callUseExtension)
{
logging.UseOpenTelemetry(configureBuilder: null, configureOptions: ConfigureCallback);
}
else
{
logging.AddOpenTelemetry(ConfigureCallback);
}
}
});
for (int i = 0; i < numberOfOptionsRegistrations; i++)
{
serviceCollection.Configure<OpenTelemetryLoggerOptions>(OptionsCallback);
}
using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
ILoggerFactory? loggerFactory = serviceProvider.GetService<ILoggerFactory>();
Assert.NotNull(loggerFactory);
Assert.NotNull(optionsInstance);
Assert.Equal(numberOfBuilderRegistrations, configureCallbackInvocations);
Assert.Equal(numberOfOptionsRegistrations, optionsCallbackInvocations);
void ConfigureCallback(OpenTelemetryLoggerOptions options)
{
if (optionsInstance == null)
{
optionsInstance = options;
}
else
{
Assert.Equal(optionsInstance, options);
}
configureCallbackInvocations++;
}
void OptionsCallback(OpenTelemetryLoggerOptions options)
{
if (optionsInstance == null)
{
optionsInstance = options;
}
else
{
Assert.Equal(optionsInstance, options);
}
optionsCallbackInvocations++;
}
}
[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
// but if there was any complex type property, members of the complex type would not be preserved
// and could lead to incompatibilities with trimming.
[Fact]
public void TestTrimmingCorrectnessOfOpenTelemetryLoggerOptions()
{
foreach (var prop in typeof(OpenTelemetryLoggerOptions).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
Assert.True(prop.PropertyType.IsPrimitive, $"Property OpenTelemetryLoggerOptions.{prop.Name} doesn't have a primitive type. This is potentially a trim compatibility issue.");
}
}
[Fact]
public void VerifyAddProcessorOverloadWithImplementationFactory()
{
// arrange
var services = new ServiceCollection();
services.AddSingleton<TestLogProcessor>();
services.AddLogging(logging =>
logging.AddOpenTelemetry(
o => o.AddProcessor(sp => sp.GetRequiredService<TestLogProcessor>())));
// act
using var sp = services.BuildServiceProvider();
var loggerProvider = sp.GetRequiredService<LoggerProvider>() as LoggerProviderSdk;
// assert
Assert.NotNull(loggerProvider);
Assert.NotNull(loggerProvider.Processor);
Assert.True(loggerProvider.Processor is TestLogProcessor);
}
[Fact]
public void VerifyExceptionIsThrownWhenImplementationFactoryIsNull()
{
// arrange
var services = new ServiceCollection();
services.AddLogging(logging =>
logging.AddOpenTelemetry(
o => o.AddProcessor(implementationFactory: null!)));
// act
using var sp = services.BuildServiceProvider();
// assert
Assert.Throws<ArgumentNullException>(() => sp.GetRequiredService<LoggerProvider>() as LoggerProviderSdk);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CircularReferenceTest(bool requestLoggerProviderDirectly)
{
var services = new ServiceCollection();
services.AddLogging(logging => logging.AddOpenTelemetry());
services.ConfigureOpenTelemetryLoggerProvider(builder => builder.AddProcessor<TestLogProcessorWithILoggerFactoryDependency>());
using var sp = services.BuildServiceProvider();
if (requestLoggerProviderDirectly)
{
var provider = sp.GetRequiredService<LoggerProvider>();
Assert.NotNull(provider);
}
else
{
var factory = sp.GetRequiredService<ILoggerFactory>();
Assert.NotNull(factory);
}
var loggerProvider = sp.GetRequiredService<LoggerProvider>() as LoggerProviderSdk;
Assert.NotNull(loggerProvider);
Assert.True(loggerProvider.Processor is TestLogProcessorWithILoggerFactoryDependency);
}
[Theory]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void OptionReloadingTest(bool useOptionsMonitor, bool useOptionsSnapshot)
{
var delegateInvocationCount = 0;
var root = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(root);
services.AddLogging(logging => logging
.AddConfiguration(root.GetSection("logging"))
.AddOpenTelemetry(options =>
{
delegateInvocationCount++;
options.AddProcessor(new TestLogProcessor());
}));
using var sp = services.BuildServiceProvider();
if (useOptionsMonitor)
{
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<OpenTelemetryLoggerOptions>>();
Assert.NotNull(optionsMonitor.CurrentValue);
}
if (useOptionsSnapshot)
{
using var scope = sp.CreateScope();
var optionsSnapshot = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<OpenTelemetryLoggerOptions>>();
Assert.NotNull(optionsSnapshot.Value);
}
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
Assert.Equal(1, delegateInvocationCount);
root.Reload();
Assert.Equal(1, delegateInvocationCount);
}
[Fact]
public void MixedOptionsUsageTest()
{
var root = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(root);
services.AddLogging(logging => logging
.AddConfiguration(root.GetSection("logging"))
.AddOpenTelemetry(options =>
{
options.AddProcessor(new TestLogProcessor());
}));
using var sp = services.BuildServiceProvider();
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<OpenTelemetryLoggerOptions>>().CurrentValue;
var options = sp.GetRequiredService<IOptions<OpenTelemetryLoggerOptions>>().Value;
Assert.True(ReferenceEquals(options, optionsMonitor));
using var scope = sp.CreateScope();
var optionsSnapshot = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<OpenTelemetryLoggerOptions>>().Value;
Assert.True(ReferenceEquals(options, optionsSnapshot));
}
private sealed class TestLogProcessor : BaseProcessor<LogRecord>
{
public bool Disposed;
protected override void Dispose(bool disposing)
{
this.Disposed = true;
base.Dispose(disposing);
}
}
private sealed class TestLogProcessorWithILoggerFactoryDependency : BaseProcessor<LogRecord>
{
private readonly ILogger logger;
public TestLogProcessorWithILoggerFactoryDependency(ILoggerFactory loggerFactory)
{
// Note: It is NOT recommended to log from inside a processor. This
// test is meant to mirror someone injecting IHttpClientFactory
// (which itself uses ILoggerFactory) as part of an exporter. That
// is a more realistic scenario but needs a dependency to do that so
// here we approximate the graph.
this.logger = loggerFactory.CreateLogger("MyLogger");
}
protected override void Dispose(bool disposing)
{
this.logger.LogInformation("Dispose called");
base.Dispose(disposing);
}
}
}