diff --git a/Splitio.Redis/Splitio.Redis.csproj b/Splitio.Redis/Splitio.Redis.csproj index bda6e1e7..a72862ac 100644 --- a/Splitio.Redis/Splitio.Redis.csproj +++ b/Splitio.Redis/Splitio.Redis.csproj @@ -7,7 +7,7 @@ false false false - 7.13.0 + 7.14.0-rc1 true SplitioRedis.snk Apache-2.0 diff --git a/src/Splitio/Services/Logger/SplitLoggerFactoryExtensions.cs b/src/Splitio/Services/Logger/SplitLoggerFactoryExtensions.cs index e926ac64..ab9f1e05 100644 --- a/src/Splitio/Services/Logger/SplitLoggerFactoryExtensions.cs +++ b/src/Splitio/Services/Logger/SplitLoggerFactoryExtensions.cs @@ -1,4 +1,6 @@ -#if NET_LATEST +#if NET_LATEST +using Microsoft.Extensions.DependencyInjection; + namespace Microsoft.Extensions.Logging { public static class SplitLoggerFactoryExtensions @@ -18,6 +20,70 @@ public static ILoggerFactory GetLoggerFactory() } public static bool LoggerFactoryHasValue => _loggerFactory != null; + + /// + /// Configures Split logs for ASP.NET Core applications. + /// The logger factory will be captured when logging is first used in the application. + /// + public static ILoggingBuilder AddSplitLogs(this ILoggingBuilder builder) + { + // Replace the ILoggerFactory registration with a capturing wrapper + var descriptor = new ServiceDescriptor( + typeof(ILoggerFactory), + sp => + { + // Get the original logger factory + var loggerFactory = LoggerFactory.Create(loggingBuilder => + { + // Copy all the logging configuration from the builder + foreach (var service in sp.GetServices()) + { + // Logger providers are already registered, LoggerFactory will pick them up + } + }); + + // This is a simpler approach - just capture when resolved + loggerFactory.AddSplitLogs(); + + return loggerFactory; + }, + ServiceLifetime.Singleton); + + // Remove existing ILoggerFactory registration and add ours + for (int i = builder.Services.Count - 1; i >= 0; i--) + { + if (builder.Services[i].ServiceType == typeof(ILoggerFactory)) + { + var existing = builder.Services[i]; + builder.Services[i] = new ServiceDescriptor( + typeof(ILoggerFactory), + sp => + { + ILoggerFactory factory; + if (existing.ImplementationFactory != null) + { + factory = (ILoggerFactory)existing.ImplementationFactory(sp); + } + else if (existing.ImplementationInstance != null) + { + factory = (ILoggerFactory)existing.ImplementationInstance; + } + else + { + factory = (ILoggerFactory)ActivatorUtilities.CreateInstance(sp, existing.ImplementationType); + } + + // Capture the factory + factory.AddSplitLogs(); + return factory; + }, + existing.Lifetime); + break; + } + } + + return builder; + } } } -#endif \ No newline at end of file +#endif diff --git a/src/Splitio/Splitio.csproj b/src/Splitio/Splitio.csproj index d826f49a..858fd83f 100644 --- a/src/Splitio/Splitio.csproj +++ b/src/Splitio/Splitio.csproj @@ -7,7 +7,7 @@ false false false - 7.13.0 + 7.14.0-rc1 Splitio true Splitio.snk @@ -29,6 +29,7 @@ + diff --git a/tests/Splitio-tests/Unit Tests/Logger/SplitLoggerFactoryExtensionsAspNetTests.cs b/tests/Splitio-tests/Unit Tests/Logger/SplitLoggerFactoryExtensionsAspNetTests.cs new file mode 100644 index 00000000..fae77659 --- /dev/null +++ b/tests/Splitio-tests/Unit Tests/Logger/SplitLoggerFactoryExtensionsAspNetTests.cs @@ -0,0 +1,117 @@ +#if NETCORE +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Splitio_Tests.Unit_Tests.Logger +{ + [TestClass] + public class SplitLoggerFactoryExtensionsAspNetTests + { + [TestCleanup] + public void Cleanup() + { + // Reset static state between tests + var emptyFactory = LoggerFactory.Create(builder => { }); + Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.AddSplitLogs(emptyFactory); + } + + [TestMethod] + public void AddSplitLogs_WithHostBuilder_ShouldSetLoggerFactoryBeforeHostRuns() + { + // Arrange - Simulate ASP.NET Core Host Builder pattern + var hostBuilder = Host.CreateDefaultBuilder() + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddSplitLogs(); + }); + + // Act - Build the host (this should trigger DI container creation) + using var host = hostBuilder.Build(); + + // Assert - Check if logger factory is set BEFORE calling host.Run() + // This is critical because logging might be used during startup + var hasValue = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue; + var factory = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + + // The current implementation may fail this because the hosted service + // factory is only called when resolving IHostedService instances + Assert.IsNotNull(factory, "Logger factory should be set after Build() but before Run()"); + Assert.IsTrue(hasValue, "LoggerFactoryHasValue should be true after Build()"); + } + + [TestMethod] + public void AddSplitLogs_WithHostBuilder_LoggerFactoryAvailableAfterStart() + { + // Arrange + var hostBuilder = Host.CreateDefaultBuilder() + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddSplitLogs(); + }); + + // Act - Build and start the host + using var host = hostBuilder.Build(); + + // Force hosted services to be resolved + var hostedServices = host.Services.GetServices(); + foreach (var _ in hostedServices) { } // Force enumeration + + // Assert + var hasValue = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue; + var factory = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + + Assert.IsNotNull(factory, "Logger factory should be set after hosted services are resolved"); + Assert.IsTrue(hasValue); + } + + [TestMethod] + public void AddSplitLogs_WithServiceProvider_LoggerFactoryAvailableImmediately() + { + // Arrange - Test eager initialization approach + var services = new ServiceCollection(); + services.AddLogging(logging => + { + logging.AddSplitLogs(); + }); + + // Act - Build service provider + using var serviceProvider = services.BuildServiceProvider(); + + // Force hosted service to be registered by resolving it + var hostedService = serviceProvider.GetServices(); + var _ = hostedService.GetEnumerator(); // Start enumeration + + // Assert - This might not work until hosted services are fully resolved + var hasValue = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue; + + // If this fails, it means the timing issue exists + Assert.IsTrue(hasValue, "Logger factory should be available after service resolution"); + } + + [TestMethod] + public void AddSplitLogs_ManualInitialization_WorksImmediately() + { + // Arrange - Alternative approach: manual initialization + var services = new ServiceCollection(); + services.AddLogging(); + + using var serviceProvider = services.BuildServiceProvider(); + var loggerFactory = serviceProvider.GetRequiredService(); + + // Act - Manually call AddSplitLogs on the factory + loggerFactory.AddSplitLogs(); + + // Assert + var hasValue = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue; + var retrievedFactory = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + + Assert.IsTrue(hasValue); + Assert.AreSame(loggerFactory, retrievedFactory); + } + } +} +#endif diff --git a/tests/Splitio-tests/Unit Tests/Logger/SplitLoggerFactoryExtensionsTests.cs b/tests/Splitio-tests/Unit Tests/Logger/SplitLoggerFactoryExtensionsTests.cs new file mode 100644 index 00000000..5608a09e --- /dev/null +++ b/tests/Splitio-tests/Unit Tests/Logger/SplitLoggerFactoryExtensionsTests.cs @@ -0,0 +1,246 @@ +#if NETCORE +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; + +namespace Splitio_Tests.Unit_Tests.Logger +{ + [TestClass] + public class SplitLoggerFactoryExtensionsTests + { + [TestCleanup] + public void Cleanup() + { + // Reset static state between tests + var factory = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + if (factory != null) + { + // Create a new empty factory to reset + var emptyFactory = LoggerFactory.Create(builder => { }); + Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.AddSplitLogs(emptyFactory); + } + } + + #region AddSplitLogs(ILoggerFactory) Tests + + [TestMethod] + public void AddSplitLogs_WithLoggerFactory_ShouldSetStaticFactory() + { + // Arrange + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + + // Act + var result = loggerFactory.AddSplitLogs(); + + // Assert + Assert.IsNotNull(result); + Assert.AreSame(loggerFactory, result); + Assert.IsTrue(Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue); + } + + [TestMethod] + public void AddSplitLogs_WithLoggerFactory_ShouldReturnSameFactory() + { + // Arrange + var loggerFactory = LoggerFactory.Create(builder => { }); + + // Act + var result = loggerFactory.AddSplitLogs(); + + // Assert + Assert.AreSame(loggerFactory, result); + } + + [TestMethod] + public void AddSplitLogs_WithNullFactory_ShouldNotThrow() + { + // Arrange + ILoggerFactory loggerFactory = null; + + // Act & Assert - this will set the static field to null + var result = loggerFactory.AddSplitLogs(); + Assert.IsNull(result); + } + + #endregion + + #region GetLoggerFactory Tests + + [TestMethod] + public void GetLoggerFactory_AfterAddSplitLogs_ShouldReturnSameFactory() + { + // Arrange + var loggerFactory = LoggerFactory.Create(builder => { }); + loggerFactory.AddSplitLogs(); + + // Act + var retrievedFactory = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + + // Assert + Assert.IsNotNull(retrievedFactory); + Assert.AreSame(loggerFactory, retrievedFactory); + } + + [TestMethod] + public void GetLoggerFactory_BeforeAddSplitLogs_ShouldReturnNull() + { + // Act + var retrievedFactory = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + + // Assert - may be null if not set previously + // This test depends on test execution order, so we just verify it doesn't throw + Assert.IsTrue(true); + } + + #endregion + + #region LoggerFactoryHasValue Tests + + [TestMethod] + public void LoggerFactoryHasValue_AfterAddSplitLogs_ShouldBeTrue() + { + // Arrange + var loggerFactory = LoggerFactory.Create(builder => { }); + loggerFactory.AddSplitLogs(); + + // Act + var hasValue = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue; + + // Assert + Assert.IsTrue(hasValue); + } + + [TestMethod] + public void LoggerFactoryHasValue_WithNullFactory_ShouldBeFalse() + { + // Arrange + ILoggerFactory loggerFactory = null; + loggerFactory.AddSplitLogs(); + + // Act + var hasValue = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue; + + // Assert + Assert.IsFalse(hasValue); + } + + #endregion + + #region AddSplitLogs(ILoggingBuilder) Tests + + [TestMethod] + public void AddSplitLogs_WithLoggingBuilder_ShouldWrapLoggerFactory() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddLogging(builder => + { + builder.AddSplitLogs(); + }); + + // Assert - Verify that ILoggerFactory can be resolved + var serviceProvider = services.BuildServiceProvider(); + var loggerFactory = serviceProvider.GetService(); + + Assert.IsNotNull(loggerFactory, "ILoggerFactory should be registered"); + } + + [TestMethod] + public void AddSplitLogs_WithLoggingBuilder_ShouldSetStaticLoggerFactory() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddLogging(builder => + { + builder.AddSplitLogs(); + }); + var serviceProvider = services.BuildServiceProvider(); + + // Trigger the logger factory to be resolved (this sets the static field) + var loggerFactory = serviceProvider.GetRequiredService(); + + // Assert + Assert.IsTrue(Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.LoggerFactoryHasValue); + var retrievedFactory = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + Assert.IsNotNull(retrievedFactory); + } + + [TestMethod] + public void AddSplitLogs_WithLoggingBuilder_ShouldReturnBuilder() + { + // Arrange + var services = new ServiceCollection(); + ILoggingBuilder capturedBuilder = null; + + // Act + services.AddLogging(builder => + { + capturedBuilder = builder; + var result = builder.AddSplitLogs(); + Assert.AreSame(builder, result); + }); + + // Assert + Assert.IsNotNull(capturedBuilder); + } + + [TestMethod] + public void AddSplitLogs_WithLoggingBuilder_ShouldAllowChaining() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddLogging(builder => + { + builder.AddConsole() + .AddSplitLogs() + .AddDebug(); + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var loggerFactory = serviceProvider.GetRequiredService(); + Assert.IsNotNull(loggerFactory); + } + + #endregion + + #region Integration Tests + + [TestMethod] + public void AddSplitLogs_BothOverloads_ShouldWorkTogether() + { + // Arrange + var loggerFactory1 = LoggerFactory.Create(builder => { }); + + // Act - Use ILoggerFactory extension + loggerFactory1.AddSplitLogs(); + var retrieved1 = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + + // Arrange - Use ILoggingBuilder extension + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddSplitLogs()); + var serviceProvider = services.BuildServiceProvider(); + var _ = serviceProvider.GetServices().ToList(); + + var retrieved2 = Microsoft.Extensions.Logging.SplitLoggerFactoryExtensions.GetLoggerFactory(); + + // Assert + Assert.IsNotNull(retrieved1); + Assert.IsNotNull(retrieved2); + } + + #endregion + } +} +#endif