File: Logging\ExtendedLoggerTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Telemetry.Tests\Microsoft.Extensions.Telemetry.Tests.csproj (Microsoft.Extensions.Telemetry.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Enrichment;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
 
namespace Microsoft.Extensions.Logging.Test;
 
public static class ExtendedLoggerTests
{
    [Theory]
    [CombinatorialData]
    public static void FeatureEnablement(bool enableRedaction, bool enableEnrichment)
    {
        const string Category = "C1";
 
        using var provider = new Provider();
        var enricher = new ForcedEnricher(
            new[]
            {
                new KeyValuePair<string, object?>("EK1", "EV1"),
            });
 
        var staticEnricher = new ForcedEnricher(
            new[]
            {
                new KeyValuePair<string, object?>("SEK1", "SEV1"),
            });
 
        var redactorProvider = new FakeRedactorProvider(new FakeRedactorOptions
        {
            RedactionFormat = "REDACTED<{0}>",
        });
 
        var enrichmentOptions = enableEnrichment ? new StaticOptionsMonitor<LoggerEnrichmentOptions>(new()) : null;
        var redactionOptions = enableRedaction ? new StaticOptionsMonitor<LoggerRedactionOptions>(new() { ApplyDiscriminator = false }) : null;
 
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(new()),
            enrichmentOptions: enrichmentOptions,
            redactionOptions: redactionOptions,
            enrichers: new[] { enricher },
            staticEnrichers: new[] { staticEnricher },
            redactorProvider: redactorProvider,
            scopeProvider: null,
            factoryOptions: null);
 
        var logger = lf.CreateLogger(Category);
        logger.LogWarning("MSG0");
 
        var lms = LoggerMessageHelper.ThreadLocalState;
        var index = lms.ReserveTagSpace(1);
        lms.TagArray[index] = new("PK2", "PV2");
 
        index = lms.ReserveClassifiedTagSpace(2);
        lms.ClassifiedTagArray[index] = new("PK3", "PV3", FakeTaxonomy.PrivateData);
        lms.ClassifiedTagArray[index + 1] = new("PK4", null, FakeTaxonomy.PrivateData);
 
        logger.Log(LogLevel.Warning, new EventId(2, "ID2"), lms, null, (_, _) => "MSG2");
 
        var sink = provider.Logger!;
        var collector = sink.Collector;
        Assert.Equal(Category, sink.Category);
        Assert.Equal(2, collector.Count);
 
        var snap = collector.GetSnapshot();
 
        Assert.Equal(Category, snap[0].Category);
        Assert.Null(snap[0].Exception);
        Assert.Equal(new EventId(0), snap[0].Id);
        Assert.Equal("MSG0", snap[0].Message);
 
        if (enableEnrichment)
        {
            Assert.Equal("SEV1", snap[0].GetStructuredStateValue("SEK1"));
            Assert.Equal("EV1", snap[0].GetStructuredStateValue("EK1"));
        }
        else
        {
            Assert.Null(snap[0].GetStructuredStateValue("SEK1"));
            Assert.Null(snap[0].GetStructuredStateValue("EK1"));
        }
 
        Assert.Equal(Category, snap[1].Category);
        Assert.Null(snap[1].Exception);
        Assert.Equal(new EventId(2, "ID2"), snap[1].Id);
        Assert.Equal("MSG2", snap[1].Message);
        Assert.Equal("PV2", snap[1].GetStructuredStateValue("PK2"));
 
        if (enableRedaction)
        {
            Assert.Equal("REDACTED<PV3>", snap[1].GetStructuredStateValue("PK3"));
        }
        else
        {
            Assert.Equal("PV3", snap[1].GetStructuredStateValue("PK3"));
        }
 
        Assert.Null(snap[1].GetStructuredStateValue("PK4"));
 
        if (enableEnrichment)
        {
            Assert.Equal("SEV1", snap[1].GetStructuredStateValue("SEK1"));
            Assert.Equal("EV1", snap[1].GetStructuredStateValue("EK1"));
        }
        else
        {
            Assert.Null(snap[1].GetStructuredStateValue("SEK1"));
            Assert.Null(snap[1].GetStructuredStateValue("EK1"));
        }
    }
 
    [Theory]
    [CombinatorialData]
    public static void BagAndJoiner(bool objectVersion)
    {
        const string Category = "C1";
 
        using var provider = new Provider();
        var enricher = new FancyEnricher(
            new[]
            {
                new KeyValuePair<string, object?>("EK1", "EV1"),
                new KeyValuePair<string, object?>("EK2", "EV2"),
            }, objectVersion);
 
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(new()),
            enrichmentOptions: new StaticOptionsMonitor<LoggerEnrichmentOptions>(new()),
            redactionOptions: new StaticOptionsMonitor<LoggerRedactionOptions>(new()),
            enrichers: new[] { enricher },
            staticEnrichers: Array.Empty<IStaticLogEnricher>(),
            redactorProvider: null,
            scopeProvider: null,
            factoryOptions: null);
 
        var logger = lf.CreateLogger(Category);
        logger.LogWarning("MSG0");
 
        var lms = LoggerMessageHelper.ThreadLocalState;
        var index = lms.ReserveTagSpace(1);
        lms.TagArray[index] = new("PK2", "PV2");
        logger.Log(LogLevel.Warning, new EventId(2, "ID2"), lms, null, (_, _) => "MSG2");
 
        var sink = provider.Logger!;
        var collector = sink.Collector;
        Assert.Equal(Category, sink.Category);
        Assert.Equal(2, collector.Count);
 
        var snap = collector.GetSnapshot();
 
        Assert.Equal(Category, snap[0].Category);
        Assert.Null(snap[0].Exception);
        Assert.Equal(new EventId(0), snap[0].Id);
        Assert.Equal("MSG0", snap[0].Message);
        Assert.Equal("EV1", snap[0].GetStructuredStateValue("EK1"));
        Assert.Equal("EV2", snap[0].GetStructuredStateValue("EK2"));
 
        Assert.Equal(Category, snap[1].Category);
        Assert.Null(snap[1].Exception);
        Assert.Equal(new EventId(2, "ID2"), snap[1].Id);
        Assert.Equal("MSG2", snap[1].Message);
        Assert.Equal("PV2", snap[1].GetStructuredStateValue("PK2"));
        Assert.Equal("EV1", snap[1].GetStructuredStateValue("EK1"));
        Assert.Equal("EV2", snap[1].GetStructuredStateValue("EK2"));
    }
 
    [Fact]
    public static void NullStateObject()
    {
        const string Category = "C1";
 
        using var provider = new Provider();
        var enricher = new ForcedEnricher(
            new[]
            {
                new KeyValuePair<string, object?>("EK1", "EV1"),
            });
 
        var staticEnricher = new ForcedEnricher(
            new[]
            {
                new KeyValuePair<string, object?>("SEK1", "SEV1"),
            });
 
        var redactorProvider = new FakeRedactorProvider(new FakeRedactorOptions
        {
            RedactionFormat = "REDACTED<{0}>",
        });
 
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(new()),
            enrichmentOptions: new StaticOptionsMonitor<LoggerEnrichmentOptions>(new()),
            redactionOptions: new StaticOptionsMonitor<LoggerRedactionOptions>(new()),
            enrichers: new[] { enricher },
            staticEnrichers: new[] { staticEnricher },
            redactorProvider: redactorProvider,
            scopeProvider: null,
            factoryOptions: null);
 
        var logger = lf.CreateLogger(Category);
        logger.Log<object?>(LogLevel.Error, new EventId(0, "ID0"), null, null, (_, _) => "MSG0");
        logger.Log<object?>(LogLevel.Error, new EventId(0, "ID0b"), null, null, (_, _) => "MSG0b");
 
        var lms = LoggerMessageHelper.ThreadLocalState;
        logger.Log(LogLevel.Warning, new EventId(2, "ID2"), (LoggerMessageState?)null, null, (_, _) => "MSG2");
        logger.Log(LogLevel.Warning, new EventId(2, "ID2b"), (LoggerMessageState?)null, null, (_, _) => "MSG2b");
 
        var sink = provider.Logger!;
        var collector = sink.Collector;
        Assert.Equal(Category, sink.Category);
        Assert.Equal(4, collector.Count);
 
        var snap = collector.GetSnapshot();
 
        Assert.Equal(Category, snap[0].Category);
        Assert.Null(snap[0].Exception);
        Assert.Equal(new EventId(0, "ID0"), snap[0].Id);
        Assert.Equal("MSG0", snap[0].Message);
        Assert.Equal("EV1", snap[0].GetStructuredStateValue("EK1"));
        Assert.Equal("SEV1", snap[0].GetStructuredStateValue("SEK1"));
 
        Assert.Equal(Category, snap[1].Category);
        Assert.Null(snap[1].Exception);
        Assert.Equal(new EventId(0, "ID0b"), snap[1].Id);
        Assert.Equal("MSG0b", snap[1].Message);
        Assert.Equal("EV1", snap[1].GetStructuredStateValue("EK1"));
        Assert.Equal("SEV1", snap[1].GetStructuredStateValue("SEK1"));
 
        Assert.Equal(Category, snap[2].Category);
        Assert.Null(snap[2].Exception);
        Assert.Equal(new EventId(2, "ID2"), snap[2].Id);
        Assert.Equal("MSG2", snap[2].Message);
        Assert.Equal("EV1", snap[2].GetStructuredStateValue("EK1"));
        Assert.Equal("SEV1", snap[2].GetStructuredStateValue("SEK1"));
 
        Assert.Equal(Category, snap[3].Category);
        Assert.Null(snap[3].Exception);
        Assert.Equal(new EventId(2, "ID2b"), snap[3].Id);
        Assert.Equal("MSG2b", snap[3].Message);
        Assert.Equal("EV1", snap[3].GetStructuredStateValue("EK1"));
        Assert.Equal("SEV1", snap[3].GetStructuredStateValue("SEK1"));
    }
 
    [Fact]
    public static void EnumerableStateObject()
    {
        const string Category = "C1";
 
        using var provider = new Provider();
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(new()),
            enrichmentOptions: new StaticOptionsMonitor<LoggerEnrichmentOptions>(new()),
            redactionOptions: new StaticOptionsMonitor<LoggerRedactionOptions>(new()),
            enrichers: Array.Empty<ILogEnricher>(),
            staticEnrichers: Array.Empty<IStaticLogEnricher>(),
            redactorProvider: null,
            scopeProvider: null,
            factoryOptions: null);
 
        var logger = lf.CreateLogger(Category);
 
        var a = new[] { new KeyValuePair<string, object?>("K1", "V1") };
        var e = a.Where(_ => true);
 
        logger.Log(LogLevel.Warning, new EventId(0, "ID0"), e, null, (_, _) => "MSG0");
 
        var sink = provider.Logger!;
        var collector = sink.Collector;
        Assert.Equal(Category, sink.Category);
        Assert.Equal(1, collector.Count);
 
        var snap = collector.GetSnapshot();
 
        Assert.Equal(Category, snap[0].Category);
        Assert.Null(snap[0].Exception);
        Assert.Equal(new EventId(0, "ID0"), snap[0].Id);
        Assert.Equal("MSG0", snap[0].Message);
        Assert.Equal("V1", snap[0].GetStructuredStateValue("K1"));
    }
 
    [Fact]
    public static void Filtering()
    {
        const string FilteredCategory = "C1";
        const string UnfilteredCategory = "C2";
 
        var filterOptions = new LoggerFilterOptions();
        filterOptions.Rules.Add(new LoggerFilterRule(null, FilteredCategory, null, (_, _, _) => false));
 
        using var provider = new Provider();
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(filterOptions),
            enrichmentOptions: new StaticOptionsMonitor<LoggerEnrichmentOptions>(new()),
            redactionOptions: new StaticOptionsMonitor<LoggerRedactionOptions>(new()),
            enrichers: Array.Empty<ILogEnricher>(),
            staticEnrichers: Array.Empty<IStaticLogEnricher>(),
            redactorProvider: null,
            scopeProvider: null,
            factoryOptions: null);
 
        var filteredLogger = lf.CreateLogger(FilteredCategory);
        var unfilteredLogger = lf.CreateLogger(UnfilteredCategory);
 
        Assert.False(filteredLogger.IsEnabled(LogLevel.Warning));
        Assert.True(unfilteredLogger.IsEnabled(LogLevel.Warning));
 
        var fake = provider.Logger!;
        fake.ControlLevel(LogLevel.Warning, false);
 
        Assert.False(filteredLogger.IsEnabled(LogLevel.Warning));
        Assert.False(unfilteredLogger.IsEnabled(LogLevel.Warning));
    }
 
    [Fact]
    public static void StringStateObject()
    {
        const string Category = "C1";
 
        using var provider = new Provider();
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(new()),
            enrichmentOptions: new StaticOptionsMonitor<LoggerEnrichmentOptions>(new()),
            redactionOptions: new StaticOptionsMonitor<LoggerRedactionOptions>(new()),
            enrichers: Array.Empty<ILogEnricher>(),
            staticEnrichers: Array.Empty<IStaticLogEnricher>(),
            redactorProvider: null,
            scopeProvider: null,
            factoryOptions: null);
 
        var logger = lf.CreateLogger(Category);
 
        logger.Log(LogLevel.Warning, new EventId(0, "ID0"), "PAYLOAD", null, (_, _) => "MSG0");
 
        var sink = provider.Logger!;
        var collector = sink.Collector;
        Assert.Equal(Category, sink.Category);
        Assert.Equal(1, collector.Count);
 
        var snap = collector.GetSnapshot();
 
        Assert.Equal(Category, snap[0].Category);
        Assert.Null(snap[0].Exception);
        Assert.Equal(new EventId(0, "ID0"), snap[0].Id);
        Assert.Equal("MSG0", snap[0].Message);
        Assert.Equal("PAYLOAD", snap[0].GetStructuredStateValue("{OriginalFormat}"));
    }
 
    [Fact]
    public static void StateToStringWorks()
    {
        using var provider = new CapturingProvider();
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(new()),
            enrichmentOptions: new StaticOptionsMonitor<LoggerEnrichmentOptions>(new()),
            redactionOptions: new StaticOptionsMonitor<LoggerRedactionOptions>(new()),
            enrichers: Array.Empty<ILogEnricher>(),
            staticEnrichers: Array.Empty<IStaticLogEnricher>(),
            redactorProvider: null,
            scopeProvider: null,
            factoryOptions: null);
 
        var logger = lf.CreateLogger("FOO");
 
        logger.Log(LogLevel.Information, new EventId(0, "ID0"), "PAYLOAD", null, (_, _) => "MSG0");
 
        Assert.Equal("PAYLOAD", provider.State!);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public static void Exceptions(bool includeExceptionMessage)
    {
        const string Category = "C1";
 
        using var provider = new Provider();
        var stackTraceOptions = new LoggerEnrichmentOptions
        {
            CaptureStackTraces = true,
            UseFileInfoForStackTraces = true,
            MaxStackTraceLength = 4096,
            IncludeExceptionMessage = includeExceptionMessage,
        };
 
        using var lf = new ExtendedLoggerFactory(
            providers: new[] { provider },
            filterOptions: new StaticOptionsMonitor<LoggerFilterOptions>(new()),
            enrichmentOptions: new StaticOptionsMonitor<LoggerEnrichmentOptions>(stackTraceOptions),
            redactionOptions: new StaticOptionsMonitor<LoggerRedactionOptions>(new()),
            enrichers: Array.Empty<ILogEnricher>(),
            staticEnrichers: Array.Empty<IStaticLogEnricher>(),
            redactorProvider: new FakeRedactorProvider(),
            scopeProvider: null,
            factoryOptions: null);
 
        Exception ex;
        try
        {
            List<Exception> exceptions = [];
            try
            {
                throw new ArgumentNullException("EM1", (Exception?)null);
            }
            catch (ArgumentNullException e)
            {
                exceptions.Add(e);
            }
 
            try
            {
                throw new ArgumentOutOfRangeException("EM2", (Exception?)null);
            }
            catch (ArgumentOutOfRangeException e)
            {
                exceptions.Add(e);
            }
 
            try
            {
                throw new InvalidOperationException("EM3");
            }
            catch (InvalidOperationException e)
            {
                exceptions.Add(e);
            }
 
            throw new AggregateException("EM4", exceptions);
        }
        catch (AggregateException e)
        {
            ex = e;
        }
 
        var logger = lf.CreateLogger(Category);
        logger.Log<object?>(LogLevel.Error, new EventId(0, "ID0"), null, null, (_, _) => "MSG0");
        logger.Log<object?>(LogLevel.Error, new EventId(0, "ID0b"), null, ex, (_, _) => "MSG0b");
 
        var lms = LoggerMessageHelper.ThreadLocalState;
        logger.Log(LogLevel.Warning, new EventId(2, "ID2"), lms, null, (_, _) => "MSG2");
        logger.Log(LogLevel.Warning, new EventId(2, "ID2b"), lms, ex, (_, _) => "MSG2b");
 
        var sink = provider.Logger!;
        var collector = sink.Collector;
        Assert.Equal(Category, sink.Category);
        Assert.Equal(4, collector.Count);
 
        var snap = collector.GetSnapshot();
 
        Assert.Equal(Category, snap[0].Category);
        Assert.Null(snap[0].Exception);
        Assert.Equal(new EventId(0, "ID0"), snap[0].Id);
        Assert.Equal("MSG0", snap[0].Message);
 
        Assert.Equal(Category, snap[1].Category);
        Assert.NotNull(snap[1].Exception);
        Assert.Equal(new EventId(0, "ID0b"), snap[1].Id);
        Assert.Equal("MSG0b", snap[1].Message);
 
        Assert.Equal(Category, snap[2].Category);
        Assert.Null(snap[2].Exception);
        Assert.Equal(new EventId(2, "ID2"), snap[2].Id);
        Assert.Equal("MSG2", snap[2].Message);
 
        Assert.Equal(Category, snap[3].Category);
        Assert.NotNull(snap[3].Exception);
        Assert.Equal(new EventId(2, "ID2b"), snap[3].Id);
        Assert.Equal("MSG2b", snap[3].Message);
 
        var exceptionType = snap[3].GetStructuredStateValue("exception.type")!;
        Assert.Equal("System.AggregateException", exceptionType);
 
        var stackTrace = snap[3].GetStructuredStateValue("exception.stacktrace")!;
        Assert.Contains("AggregateException", stackTrace);
        Assert.Contains("ArgumentNullException", stackTrace);
        Assert.Contains("ArgumentOutOfRangeException", stackTrace);
        Assert.Contains("InvalidOperationException", stackTrace);
 
        if (includeExceptionMessage)
        {
            var exceptionMessage = snap[3].GetStructuredStateValue("exception.message");
            Assert.Equal("EM4 (EM1) (EM2) (EM3)", exceptionMessage);
 
            Assert.Contains("EM1", stackTrace);
            Assert.Contains("EM2", stackTrace);
            Assert.Contains("EM3", stackTrace);
            Assert.Contains("EM4", stackTrace);
        }
        else
        {
            var state = snap[3].StructuredState;
            Assert.DoesNotContain(state!, x => x.Key == "exception.message");
 
            Assert.DoesNotContain("EM1", stackTrace);
            Assert.DoesNotContain("EM2", stackTrace);
            Assert.DoesNotContain("EM3", stackTrace);
            Assert.DoesNotContain("EM4", stackTrace);
        }
    }
 
#if false
    [Fact]
    public void Log_IgnoresExceptionInIntermediateLoggersAndThrowsAggregateException()
    {
        // Arrange
        var store = new List<string>();
        var loggerFactory = TestLoggerBuilder.Create(builder => builder
            .AddProvider(new CustomLoggerProvider("provider1", ThrowExceptionAt.None, store))
            .AddProvider(new CustomLoggerProvider("provider2", ThrowExceptionAt.Log, store))
            .AddProvider(new CustomLoggerProvider("provider3", ThrowExceptionAt.None, store)));
 
        var logger = loggerFactory.CreateLogger("Test");
 
        // Act
        var aggregateException = Assert.Throws<AggregateException>(() => logger.LogInformation("Hello!"));
 
        // Assert
        Assert.Equal(new[] { "provider1.Test-Hello!", "provider3.Test-Hello!" }, store);
        Assert.NotNull(aggregateException);
        Assert.StartsWith("An error occurred while writing to logger(s).", aggregateException.Message);
        Assert.Single(aggregateException.InnerExceptions);
        var exception = aggregateException.InnerExceptions[0];
        Assert.Equal("provider2.Test-Error occurred while logging data.", exception.Message);
    }
 
    [Fact]
    public static void BeginScope_IgnoresExceptionInIntermediateLoggersAndThrowsAggregateException()
    {
        // Arrange
        var store = new List<string>();
        var loggerFactory = TestLoggerBuilder.Create(builder => builder
            .AddProvider(new CustomLoggerProvider("provider1", ThrowExceptionAt.None, store))
            .AddProvider(new CustomLoggerProvider("provider2", ThrowExceptionAt.BeginScope, store))
            .AddProvider(new CustomLoggerProvider("provider3", ThrowExceptionAt.None, store)));
 
        var logger = loggerFactory.CreateLogger("Test");
 
        // Act
        var aggregateException = Assert.Throws<AggregateException>(() => logger.BeginScope("Scope1"));
 
        // Assert
        Assert.Equal(new[] { "provider1.Test-Scope1", "provider3.Test-Scope1" }, store);
        Assert.NotNull(aggregateException);
        Assert.StartsWith("An error occurred while writing to logger(s).", aggregateException.Message);
        Assert.Single(aggregateException.InnerExceptions);
        var exception = aggregateException.InnerExceptions[0];
        Assert.Equal("provider2.Test-Error occurred while creating scope.", exception.Message);
    }
 
    [Fact]
    public static void IsEnabled_IgnoresExceptionInIntermediateLoggers()
    {
        // Arrange
        var store = new List<string>();
        var loggerFactory = TestLoggerBuilder.Create(builder => builder
            .AddProvider(new CustomLoggerProvider("provider1", ThrowExceptionAt.None, store))
            .AddProvider(new CustomLoggerProvider("provider2", ThrowExceptionAt.IsEnabled, store))
            .AddProvider(new CustomLoggerProvider("provider3", ThrowExceptionAt.None, store)));
 
        var logger = loggerFactory.CreateLogger("Test");
 
        // Act
        var aggregateException = Assert.Throws<AggregateException>(() => logger.LogInformation("Hello!"));
 
        // Assert
        Assert.Equal(new[] { "provider1.Test-Hello!", "provider3.Test-Hello!" }, store);
        Assert.NotNull(aggregateException);
        Assert.StartsWith("An error occurred while writing to logger(s).", aggregateException.Message);
        Assert.Single(aggregateException.InnerExceptions);
        var exception = aggregateException.InnerExceptions[0];
        Assert.Equal("provider2.Test-Error occurred while checking if logger is enabled.", exception.Message);
    }
 
    [Fact]
    public static void Log_AggregatesExceptionsFromMultipleLoggers()
    {
        // Arrange
        var store = new List<string>();
        var loggerFactory = TestLoggerBuilder.Create(builder => builder
            .AddProvider(new CustomLoggerProvider("provider1", ThrowExceptionAt.Log, store))
            .AddProvider(new CustomLoggerProvider("provider2", ThrowExceptionAt.Log, store)));
 
        var logger = loggerFactory.CreateLogger("Test");
 
        // Act
        var aggregateException = Assert.Throws<AggregateException>(() => logger.LogInformation("Hello!"));
 
        // Assert
        Assert.Empty(store);
        Assert.NotNull(aggregateException);
        Assert.StartsWith("An error occurred while writing to logger(s).", aggregateException.Message);
        var exceptions = aggregateException.InnerExceptions;
        Assert.Equal(2, exceptions.Count);
        Assert.Equal("provider1.Test-Error occurred while logging data.", exceptions[0].Message);
        Assert.Equal("provider2.Test-Error occurred while logging data.", exceptions[1].Message);
    }
#endif
 
    [Fact]
    public static void LoggerCanGetProviderAfterItIsCreated()
    {
        // Arrange
        var store = new List<string>();
        using var loggerFactory = new LoggerFactory();
        var logger = loggerFactory.CreateLogger("Test");
        using var provider = new CustomLoggerProvider("provider1", ThrowExceptionAt.None, store);
 
        loggerFactory.AddProvider(provider);
 
        // Act
        logger.LogInformation("Hello");
 
        // Assert
        Assert.Equal(new[] { "provider1.Test-Hello" }, store);
    }
 
    [Fact]
    public static void ScopesAreNotCreatedForDisabledLoggers()
    {
        var provider = new Mock<ILoggerProvider>();
        var logger = new Mock<ILogger>();
 
        provider.Setup(loggerProvider => loggerProvider.CreateLogger(It.IsAny<string>()))
            .Returns(logger.Object);
 
        using var factory = Utils.CreateLoggerFactory(
            builder =>
            {
                builder.AddProvider(provider.Object);
 
                // Disable all logs
                builder.AddFilter(null, LogLevel.None);
            });
 
        var newLogger = factory.CreateLogger("Logger");
        using (newLogger.BeginScope("Scope"))
        {
            // nop
        }
 
        provider.Verify(p => p.CreateLogger("Logger"), Times.Once);
        logger.Verify(l => l.BeginScope(It.IsAny<object>()), Times.Never);
    }
 
    [Fact]
    public static void ScopesAreNotCreatedWhenScopesAreDisabled()
    {
        var provider = new Mock<ILoggerProvider>();
        var logger = new Mock<ILogger>();
 
        provider.Setup(loggerProvider => loggerProvider.CreateLogger(It.IsAny<string>()))
            .Returns(logger.Object);
 
        using var factory = Utils.CreateLoggerFactory(
            builder =>
            {
                builder.AddProvider(provider.Object);
                builder.Services.Configure<LoggerFilterOptions>(options => options.CaptureScopes = false);
            });
 
        var newLogger = factory.CreateLogger("Logger");
        using (newLogger.BeginScope("Scope"))
        {
            // nop
        }
 
        provider.Verify(p => p.CreateLogger("Logger"), Times.Once);
        logger.Verify(l => l.BeginScope(It.IsAny<object>()), Times.Never);
    }
 
    [Fact]
    public static void ScopesAreNotCreatedInIScopeProviderWhenScopesAreDisabled()
    {
        var provider = new Mock<ILoggerProvider>();
        var logger = new Mock<ILogger>();
 
        IExternalScopeProvider? externalScopeProvider = null;
 
        provider.Setup(loggerProvider => loggerProvider.CreateLogger(It.IsAny<string>()))
            .Returns(logger.Object);
        provider.As<ISupportExternalScope>().Setup(scope => scope.SetScopeProvider(It.IsAny<IExternalScopeProvider>()))
            .Callback((IExternalScopeProvider scopeProvider) => externalScopeProvider = scopeProvider);
 
        using var factory = Utils.CreateLoggerFactory(
            builder =>
            {
                builder.AddProvider(provider.Object);
                builder.Services.Configure<LoggerFilterOptions>(options => options.CaptureScopes = false);
            });
 
        var newLogger = factory.CreateLogger("Logger");
        int scopeCount = 0;
 
        using (newLogger.BeginScope("Scope"))
        {
            externalScopeProvider!.ForEachScope<object>((_, _) => scopeCount++, null!);
        }
 
        provider.Verify(p => p.CreateLogger("Logger"), Times.Once);
        logger.Verify(l => l.BeginScope(It.IsAny<object>()), Times.Never);
        Assert.Equal(0, scopeCount);
    }
 
    [Fact]
    public static void CaptureScopesIsReadFromConfiguration()
    {
        var provider = new Mock<ILoggerProvider>();
        var logger = new Mock<ILogger>();
        var json = @"{ ""CaptureScopes"": ""false"" }"{ ""CaptureScopes"": ""false"" }";
 
        using var config = TestConfiguration.Create(() => json);
        IExternalScopeProvider? externalScopeProvider = null;
 
        provider.Setup(loggerProvider => loggerProvider.CreateLogger(It.IsAny<string>()))
            .Returns(logger.Object);
        provider.As<ISupportExternalScope>().Setup(scope => scope.SetScopeProvider(It.IsAny<IExternalScopeProvider>()))
            .Callback((IExternalScopeProvider scopeProvider) => externalScopeProvider = scopeProvider);
 
        using var factory = Utils.CreateLoggerFactory(
            builder =>
            {
                builder.AddProvider(provider.Object);
                builder.AddConfiguration(config);
            });
 
        var newLogger = factory.CreateLogger("Logger");
        int scopeCount = 0;
 
        using (newLogger.BeginScope("Scope"))
        {
            externalScopeProvider!.ForEachScope<object>((_, _) => scopeCount++, null!);
            Assert.Equal(0, scopeCount);
        }
 
        json = @"{ ""CaptureScopes"": ""true"" }"{ ""CaptureScopes"": ""true"" }";
        config.Reload();
 
        scopeCount = 0;
        using (newLogger.BeginScope("Scope"))
        {
            externalScopeProvider.ForEachScope<object>((_, _) => scopeCount++, null!);
            Assert.Equal(1, scopeCount);
        }
    }
 
    private sealed class CustomLoggerProvider : ILoggerProvider
    {
        private readonly string _providerName;
        private readonly ThrowExceptionAt _throwExceptionAt;
        private readonly List<string> _store;
 
        public CustomLoggerProvider(string providerName, ThrowExceptionAt throwExceptionAt, List<string> store)
        {
            _providerName = providerName;
            _throwExceptionAt = throwExceptionAt;
            _store = store;
        }
 
        public ILogger CreateLogger(string name)
        {
            return new CustomLogger($"{_providerName}.{name}", _throwExceptionAt, _store);
        }
 
        public void Dispose()
        {
            // nop[
        }
    }
 
    private sealed class CustomLogger : ILogger
    {
        private readonly string _name;
        private readonly ThrowExceptionAt _throwExceptionAt;
        private readonly List<string> _store;
 
        public CustomLogger(string name, ThrowExceptionAt throwExceptionAt, List<string> store)
        {
            _name = name;
            _throwExceptionAt = throwExceptionAt;
            _store = store;
        }
 
        public IDisposable? BeginScope<TState>(TState state)
            where TState : notnull
        {
            if (_throwExceptionAt == ThrowExceptionAt.BeginScope)
            {
                throw new InvalidOperationException($"{_name}-Error occurred while creating scope.");
            }
 
            _store.Add($"{_name}-{state}");
 
            return null;
        }
 
        public bool IsEnabled(LogLevel logLevel)
        {
            if (_throwExceptionAt == ThrowExceptionAt.IsEnabled)
            {
                throw new InvalidOperationException($"{_name}-Error occurred while checking if logger is enabled.");
            }
 
            return true;
        }
 
        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception? exception,
            Func<TState, Exception?, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }
 
            if (_throwExceptionAt == ThrowExceptionAt.Log)
            {
                throw new InvalidOperationException($"{_name}-Error occurred while logging data.");
            }
 
            _store.Add($"{_name}-{state}");
        }
    }
 
    private enum ThrowExceptionAt
    {
        None,
        BeginScope,
        Log,
        IsEnabled
    }
 
    private sealed class Provider : ILoggerProvider
    {
        public FakeLogger? Logger { get; private set; }
 
        public ILogger CreateLogger(string categoryName)
        {
            Logger = new FakeLogger((FakeLogCollector?)null, categoryName);
            return Logger;
        }
 
        public void Dispose()
        {
            // nothing to do
        }
    }
 
    private sealed class CapturingProvider : ILoggerProvider
    {
        public object? State { get; private set; }
        public ILogger CreateLogger(string categoryName) => new Logger(this);
 
        public void Dispose()
        {
            // nothing to do
        }
 
        private sealed class Logger : ILogger
        {
            private readonly CapturingProvider _provider;
 
            public Logger(CapturingProvider provider)
            {
                _provider = provider;
            }
 
            public IDisposable? BeginScope<TState>(TState state)
                where TState : notnull => throw new NotSupportedException();
 
            public bool IsEnabled(LogLevel logLevel) => true;
            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) => _provider.State = state?.ToString();
        }
    }
 
    private sealed class ForcedEnricher : ILogEnricher, IStaticLogEnricher
    {
        private readonly KeyValuePair<string, object?>[] _values;
 
        public ForcedEnricher(KeyValuePair<string, object?>[] values)
        {
            _values = values;
        }
 
        public void Enrich(IEnrichmentTagCollector enrichmentPropertyBag)
        {
            foreach (var kvp in _values)
            {
                enrichmentPropertyBag.Add(kvp.Key, kvp.Value!);
            }
        }
    }
 
    private sealed class FancyEnricher : ILogEnricher
    {
        private readonly KeyValuePair<string, object?>[] _values;
        private readonly bool _objectVersion;
 
        public FancyEnricher(KeyValuePair<string, object?>[] values, bool objectVersion)
        {
            _values = values;
            _objectVersion = objectVersion;
        }
 
        public void Enrich(IEnrichmentTagCollector collector)
        {
            if (_objectVersion)
            {
                var p = (KeyValuePair<string, object>[])(object)_values;
                foreach (var kvp in p)
                {
                    collector.Add(kvp.Key, kvp.Value);
                }
            }
            else
            {
                var a = new KeyValuePair<string, string>[_values.Length];
                int i = 0;
                foreach (var kvp in _values)
                {
                    a[i++] = new(kvp.Key, (string)kvp.Value!);
                }
 
                foreach (var kvp in a)
                {
                    collector.Add(kvp.Key, kvp.Value);
                }
            }
        }
    }
 
    private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
    {
        public StaticOptionsMonitor(T currentValue)
        {
            CurrentValue = currentValue;
        }
 
        public IDisposable? OnChange(Action<T, string> listener) => null;
        public T Get(string? name) => CurrentValue;
        public T CurrentValue { get; }
    }
}