diff --git a/src/SchematicHQ.Client.Test/Datastream/DatastreamCacheTests.cs b/src/SchematicHQ.Client.Test/Datastream/DatastreamCacheTests.cs index b4dec908..e7493637 100644 --- a/src/SchematicHQ.Client.Test/Datastream/DatastreamCacheTests.cs +++ b/src/SchematicHQ.Client.Test/Datastream/DatastreamCacheTests.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Microsoft.Extensions.Logging.Testing; using System.Text.Json.Serialization; using NUnit.Framework; using SchematicHQ.Client.Datastream; @@ -10,7 +11,7 @@ namespace SchematicHQ.Client.Test.Datastream public class DatastreamCacheTests { private MockWebSocket _mockWebSocket; - private MockSchematicLogger _mockLogger; + private FakeLogger _mockLogger; private DatastreamClient _client; [SetUp] diff --git a/src/SchematicHQ.Client.Test/Datastream/DatastreamClientAdapterTests.cs b/src/SchematicHQ.Client.Test/Datastream/DatastreamClientAdapterTests.cs index b3e19b96..6e9e0715 100644 --- a/src/SchematicHQ.Client.Test/Datastream/DatastreamClientAdapterTests.cs +++ b/src/SchematicHQ.Client.Test/Datastream/DatastreamClientAdapterTests.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging.Testing; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ public class DatastreamClientAdapterTests { private DatastreamClientAdapter _adapter; private MockWebSocket _mockWebSocket; - private MockSchematicLogger _mockLogger; + private FakeLogger _mockLogger; private Action _connectionCallback; [SetUp] @@ -21,7 +22,7 @@ public void Setup() { // We need to capture the connection callback to trigger connection state changes in tests _connectionCallback = null; - _mockLogger = new MockSchematicLogger(); + _mockLogger = new FakeLogger(); _mockWebSocket = new MockWebSocket(); _mockWebSocket.SetState(WebSocketState.Open); diff --git a/src/SchematicHQ.Client.Test/Datastream/DatastreamClientTests.cs b/src/SchematicHQ.Client.Test/Datastream/DatastreamClientTests.cs index 21bc17d1..a6edd4cc 100644 --- a/src/SchematicHQ.Client.Test/Datastream/DatastreamClientTests.cs +++ b/src/SchematicHQ.Client.Test/Datastream/DatastreamClientTests.cs @@ -1,6 +1,8 @@ using System.Text; +using Microsoft.Extensions.Logging.Testing; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; using NUnit.Framework; using SchematicHQ.Client.RulesEngine.Utils; using SchematicHQ.Client.Datastream; @@ -12,7 +14,7 @@ namespace SchematicHQ.Client.Test.Datastream public class DatastreamClientTests { private MockWebSocket _mockWebSocket; - private MockSchematicLogger _mockLogger; + private FakeLogger _mockLogger; private DatastreamClient _client; private JsonSerializerOptions _jsonOptions; @@ -228,7 +230,7 @@ public void Dispose_ClosesWebSocketConnection() _client.Dispose(); // Assert - Assert.That(_mockLogger.HasLogEntry(LogLevel.Info, "Connected to Schematic WebSocket"), Is.False); + Assert.That(_mockLogger.HasLogEntry(LogLevel.Information, "Connected to Schematic WebSocket"), Is.False); } private void SetupFlagsResponse() diff --git a/src/SchematicHQ.Client.Test/Datastream/DatastreamConcurrencyTests.cs b/src/SchematicHQ.Client.Test/Datastream/DatastreamConcurrencyTests.cs index b40d6527..956368d6 100644 --- a/src/SchematicHQ.Client.Test/Datastream/DatastreamConcurrencyTests.cs +++ b/src/SchematicHQ.Client.Test/Datastream/DatastreamConcurrencyTests.cs @@ -1,5 +1,7 @@ using System.Text; +using Microsoft.Extensions.Logging.Testing; using System.Text.Json; +using Microsoft.Extensions.Logging; using NUnit.Framework; using SchematicHQ.Client.RulesEngine; using SchematicHQ.Client.RulesEngine.Utils; @@ -12,7 +14,7 @@ namespace SchematicHQ.Client.Test.Datastream public class DatastreamConcurrencyTests { private MockWebSocket _mockWebSocket; - private MockSchematicLogger _mockLogger; + private FakeLogger _mockLogger; private DatastreamClient _client; [SetUp] @@ -112,7 +114,7 @@ public async Task EmptyResponse_LogsWarning() await Task.Delay(100); // Assert - Assert.That(_mockLogger.HasLogEntry(LogLevel.Warn, "Received empty company data"), Is.True); + Assert.That(_mockLogger.HasLogEntry(LogLevel.Warning, "Received empty company data"), Is.True); } [Test] diff --git a/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionMonitoringTests.cs b/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionMonitoringTests.cs index 6c11ece9..9501becc 100644 --- a/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionMonitoringTests.cs +++ b/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionMonitoringTests.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging.Testing; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; @@ -11,12 +12,12 @@ namespace SchematicHQ.Client.Test.Datastream [TestFixture] public class DatastreamConnectionMonitoringTests { - private MockSchematicLogger _logger; + private FakeLogger _logger; [SetUp] public void Setup() { - _logger = new MockSchematicLogger(); + _logger = new FakeLogger(); } [Test] diff --git a/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionTests.cs b/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionTests.cs index 9d2ddd3a..21e5fa81 100644 --- a/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionTests.cs +++ b/src/SchematicHQ.Client.Test/Datastream/DatastreamConnectionTests.cs @@ -1,6 +1,8 @@ using System.Net.WebSockets; +using Microsoft.Extensions.Logging.Testing; using System.Text; using System.Text.Json; +using Microsoft.Extensions.Logging; using NUnit.Framework; using SchematicHQ.Client.Datastream; using SchematicHQ.Client.Test.Datastream.Mocks; @@ -11,7 +13,7 @@ namespace SchematicHQ.Client.Test.Datastream public class DatastreamConnectionTests { private MockWebSocket _mockWebSocket; - private MockSchematicLogger _mockLogger; + private FakeLogger _mockLogger; private DatastreamClient _client; private Action _connectionCallback; diff --git a/src/SchematicHQ.Client.Test/Datastream/Mocks/DatastreamClientTestFactory.cs b/src/SchematicHQ.Client.Test/Datastream/Mocks/DatastreamClientTestFactory.cs index 63f72687..b67cb974 100644 --- a/src/SchematicHQ.Client.Test/Datastream/Mocks/DatastreamClientTestFactory.cs +++ b/src/SchematicHQ.Client.Test/Datastream/Mocks/DatastreamClientTestFactory.cs @@ -1,4 +1,5 @@ using System.Net.WebSockets; +using Microsoft.Extensions.Logging.Testing; using System.Reflection; using SchematicHQ.Client.Datastream; @@ -9,10 +10,10 @@ namespace SchematicHQ.Client.Test.Datastream.Mocks /// public class DatastreamClientTestFactory { - public static (DatastreamClient Client, MockWebSocket WebSocket, MockSchematicLogger Logger, Action ConnectionCallback) + public static (DatastreamClient Client, MockWebSocket WebSocket, FakeLogger Logger, Action ConnectionCallback) CreateClientWithMocks(string apiKey = "test-api-key", TimeSpan? cacheTtl = null, Action? connectionCallback = null) { - var logger = new MockSchematicLogger(); + var logger = new FakeLogger(); var mockWebSocket = new MockWebSocket(); mockWebSocket.SetState(WebSocketState.Open); diff --git a/src/SchematicHQ.Client.Test/Datastream/Mocks/FakeLoggerExtensions.cs b/src/SchematicHQ.Client.Test/Datastream/Mocks/FakeLoggerExtensions.cs new file mode 100644 index 00000000..c7e0aaa6 --- /dev/null +++ b/src/SchematicHQ.Client.Test/Datastream/Mocks/FakeLoggerExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace SchematicHQ.Client.Test.Datastream.Mocks +{ + public static class FakeLoggerExtensions + { + public static bool HasLogEntry(this FakeLogger logger, LogLevel level, string messageContains) + { + return logger.Collector.GetSnapshot().Any(r => r.Level == level && r.Message.Contains(messageContains)); + } + + public static ILoggerFactory ToLoggerFactory(this FakeLogger logger) + { + return LoggerFactory.Create(b => b + .SetMinimumLevel(LogLevel.Trace) + .AddProvider(new FakeLoggerProvider(logger.Collector))); + } + } +} diff --git a/src/SchematicHQ.Client.Test/Datastream/Mocks/MockSchematicLogger.cs b/src/SchematicHQ.Client.Test/Datastream/Mocks/MockSchematicLogger.cs deleted file mode 100644 index 4bcf96dc..00000000 --- a/src/SchematicHQ.Client.Test/Datastream/Mocks/MockSchematicLogger.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace SchematicHQ.Client.Test.Datastream.Mocks -{ - /// - /// Mock implementation of ISchematicLogger for testing - /// - public class MockSchematicLogger : ISchematicLogger - { - public List LogEntries { get; } = new(); - - public void Debug(string message, params object[] args) - { - LogEntries.Add(new LogEntry(LogLevel.Debug, message, args)); - } - - public void Error(string message, params object[] args) - { - LogEntries.Add(new LogEntry(LogLevel.Error, message, args)); - } - - public void Info(string message, params object[] args) - { - LogEntries.Add(new LogEntry(LogLevel.Info, message, args)); - } - - public void Warn(string message, params object[] args) - { - LogEntries.Add(new LogEntry(LogLevel.Warn, message, args)); - } - - public bool HasLogEntry(LogLevel level, string messageContains) - { - return LogEntries.Any(e => e.Level == level && e.Message.Contains(messageContains)); - } - } - - public enum LogLevel - { - Debug, - Info, - Warn, - Error - } - - public class LogEntry - { - public LogEntry(LogLevel level, string message, object[] args) - { - Level = level; - Message = message; - Args = args; - } - - public LogLevel Level { get; } - public string Message { get; } - public object[] Args { get; } - - public string FormattedMessage => string.Format(Message, Args); - } -} diff --git a/src/SchematicHQ.Client.Test/Datastream/ReplicatorHealthServiceTests.cs b/src/SchematicHQ.Client.Test/Datastream/ReplicatorHealthServiceTests.cs index 67ce4380..97f504aa 100644 --- a/src/SchematicHQ.Client.Test/Datastream/ReplicatorHealthServiceTests.cs +++ b/src/SchematicHQ.Client.Test/Datastream/ReplicatorHealthServiceTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Microsoft.Extensions.Logging.Testing; using Moq; using Moq.Protected; using System; @@ -20,7 +21,7 @@ public async Task PerformHealthCheck_WhenReplicatorReady_SetsHealthyToTrue() // Arrange var mockHttpMessageHandler = new Mock(); var httpClient = new HttpClient(mockHttpMessageHandler.Object); - var logger = new MockSchematicLogger(); + var logger = new FakeLogger(); var healthResponse = """ { @@ -66,7 +67,7 @@ public async Task PerformHealthCheck_WhenReplicatorNotReady_SetsHealthyToFalse() // Arrange var mockHttpMessageHandler = new Mock(); var httpClient = new HttpClient(mockHttpMessageHandler.Object); - var logger = new MockSchematicLogger(); + var logger = new FakeLogger(); var healthResponse = """ { @@ -113,7 +114,7 @@ public async Task CacheVersionChanged_WhenVersionChanges_FiresEvent() // Arrange var mockHttpMessageHandler = new Mock(); var httpClient = new HttpClient(mockHttpMessageHandler.Object); - var logger = new MockSchematicLogger(); + var logger = new FakeLogger(); var eventCallCount = 0; string? firstEventOldVersion = null; @@ -225,7 +226,7 @@ public void Dispose_CleansUpResources() { // Arrange var httpClient = new HttpClient(); - var logger = new MockSchematicLogger(); + var logger = new FakeLogger(); var service = new ReplicatorHealthService(httpClient, "http://test/ready", logger); // Act & Assert - should not throw diff --git a/src/SchematicHQ.Client.Test/SchematicHQ.Client.Test.Custom.props b/src/SchematicHQ.Client.Test/SchematicHQ.Client.Test.Custom.props index f9fd6a30..96aaee2c 100644 --- a/src/SchematicHQ.Client.Test/SchematicHQ.Client.Test.Custom.props +++ b/src/SchematicHQ.Client.Test/SchematicHQ.Client.Test.Custom.props @@ -9,6 +9,7 @@ Configure additional MSBuild properties for your project in this file: false + diff --git a/src/SchematicHQ.Client.Test/TestClient.cs b/src/SchematicHQ.Client.Test/TestClient.cs index b3ebd6ca..72be70e3 100644 --- a/src/SchematicHQ.Client.Test/TestClient.cs +++ b/src/SchematicHQ.Client.Test/TestClient.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; using Moq; using NUnit.Framework; using System.Net; @@ -8,6 +10,7 @@ using SchematicHQ.Client.Core; using SchematicHQ.Client.Cache; using SchematicHQ.Client.RulesEngine; +using SchematicHQ.Client.Test.Datastream.Mocks; namespace SchematicHQ.Client.Test { @@ -16,7 +19,7 @@ public class SchematicTests { private Schematic _schematic; private ClientOptions _options; - private Mock _logger; + private FakeLogger _logger; private int _defaultEventBufferPeriod = 3; // seconds private HttpResponseMessage CreateCheckFlagResponse(HttpStatusCode code, bool flagValue) @@ -101,7 +104,7 @@ private void SetupSchematicTestClient(bool isOffline, HttpResponseMessage respon HttpClient testClient = new HttpClient(new OfflineHttpMessageHandler()); _options = new ClientOptions { - Logger = _logger.Object, + LoggerFactory = _logger.ToLoggerFactory(), Offline = isOffline, FlagDefaults = flagDefaults ?? new Dictionary(), DefaultEventBufferPeriod = TimeSpan.FromSeconds(_defaultEventBufferPeriod), @@ -122,7 +125,7 @@ private void SetupSchematicTestClient(bool isOffline, HttpResponseMessage respon [SetUp] public void Setup() { - _logger = new Mock(); + _logger = new FakeLogger(); } [TearDown] @@ -207,7 +210,7 @@ public async Task CheckFlag_LogsErrorAndReturnsDefaultOnException() // Assert Assert.That(result, Is.True); // default is true for the test flag - _logger.Verify(logger => logger.Error("Error checking flag via API: {0}", It.IsAny()), Times.Once); + Assert.That(_logger.HasLogEntry(LogLevel.Error, "Error checking flag via API"), Is.True); } [Test] @@ -450,7 +453,7 @@ public async Task CheckFlag_WithCacheDisabled_AlwaysCallsAPI() var testClient = handler.CreateClient(); _options = new ClientOptions { - Logger = _logger.Object, + LoggerFactory = _logger.ToLoggerFactory(), Offline = false, FlagDefaults = new Dictionary(), DefaultEventBufferPeriod = TimeSpan.FromSeconds(_defaultEventBufferPeriod), @@ -486,7 +489,7 @@ public async Task CheckFlag_ReturnsFalseOnErrorWithNoDefault() // Assert Assert.That(result, Is.False); - _logger.Verify(logger => logger.Error("Error checking flag via API: {0}", It.IsAny()), Times.Once); + Assert.That(_logger.HasLogEntry(LogLevel.Error, "Error checking flag via API"), Is.True); } [Test] @@ -500,7 +503,7 @@ public async Task CheckFlag_DifferentContextsProduceDifferentCacheKeys() var testClient = handler.CreateClient(); _options = new ClientOptions { - Logger = _logger.Object, + LoggerFactory = _logger.ToLoggerFactory(), Offline = false, FlagDefaults = new Dictionary(), DefaultEventBufferPeriod = TimeSpan.FromSeconds(_defaultEventBufferPeriod), diff --git a/src/SchematicHQ.Client.Test/TestCompaniesClient.cs b/src/SchematicHQ.Client.Test/TestCompaniesClient.cs index f19b037a..3ff2e8b1 100644 --- a/src/SchematicHQ.Client.Test/TestCompaniesClient.cs +++ b/src/SchematicHQ.Client.Test/TestCompaniesClient.cs @@ -1,3 +1,6 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; using Moq; using NUnit.Framework; using System.Net; @@ -13,7 +16,6 @@ public class CompaniesClientTests { private Schematic _schematic; private ClientOptions _options; - private Mock _logger; private HttpResponseMessage CreateUpsertCompanyResponse(HttpStatusCode code) { @@ -58,10 +60,9 @@ private HttpResponseMessage CreateUpsertUserResponse(HttpStatusCode code) private void SetupSchematicTestClient(HttpResponseMessage response) { - _logger = new Mock(); _options = new ClientOptions { - Logger = _logger.Object + LoggerFactory = NullLoggerFactory.Instance }; var handler = new Mock(MockBehavior.Strict); diff --git a/src/SchematicHQ.Client.Test/TestEventBuffer.cs b/src/SchematicHQ.Client.Test/TestEventBuffer.cs index 310a1472..0dca68e3 100644 --- a/src/SchematicHQ.Client.Test/TestEventBuffer.cs +++ b/src/SchematicHQ.Client.Test/TestEventBuffer.cs @@ -1,4 +1,5 @@ -using Moq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; namespace SchematicHQ.Client.Tests @@ -6,14 +7,14 @@ namespace SchematicHQ.Client.Tests [TestFixture] public class EventBufferTests { - private Mock _mockLogger; + private ILogger _mockLogger; private EventBuffer _buffer; private List _processedItems; [SetUp] public void SetUp() { - _mockLogger = new Mock(); + _mockLogger = NullLogger.Instance; _processedItems = new List(); _buffer = new EventBuffer(async items => { @@ -22,7 +23,7 @@ public void SetUp() _processedItems.AddRange(items); } await Task.CompletedTask; // Simulate async operation - }, _mockLogger.Object); + }, _mockLogger); } [TearDown] @@ -85,7 +86,7 @@ public void PeriodicFlush_FlushesAtInterval() _processedItems.AddRange(items); autoResetEvent.Set(); await Task.CompletedTask; // Simulate async operation - }, _mockLogger.Object, 100, TimeSpan.FromMilliseconds(500)); + }, _mockLogger, 100, TimeSpan.FromMilliseconds(500)); _buffer.Start(); _buffer.Push(1); @@ -121,7 +122,7 @@ public async Task Flush_DoesNotInterfereWithPeriodicFlush() manualFlushes++; } await Task.CompletedTask; - }, _mockLogger.Object, 100, TimeSpan.FromMilliseconds(500)); + }, _mockLogger, 100, TimeSpan.FromMilliseconds(500)); _buffer.Start(); @@ -155,7 +156,7 @@ public async Task StartAndStop_ConcurrencyTest() processedItems.AddRange(items); } await Task.CompletedTask; - }, _mockLogger.Object); + }, _mockLogger); buffer.Start(); await Task.Delay(10); @@ -220,7 +221,7 @@ public void SizeBasedFlush_TriggersWhenMaxSizeReached() } autoResetEvent.Set(); await Task.CompletedTask; - }, _mockLogger.Object, maxSize, TimeSpan.FromSeconds(60)); + }, _mockLogger, maxSize, TimeSpan.FromSeconds(60)); _buffer.Start(); @@ -253,7 +254,7 @@ public async Task Flush_RetriesOnTransientFailure() flushedItems.AddRange(items); } await Task.CompletedTask; - }, _mockLogger.Object); + }, _mockLogger); _buffer.Start(); _buffer.Push(1); @@ -276,7 +277,7 @@ public async Task Flush_DiscardsAfterMaxRetries() Interlocked.Increment(ref callCount); await Task.CompletedTask; throw new Exception("Persistent failure"); - }, _mockLogger.Object); + }, _mockLogger); _buffer.Start(); _buffer.Push(1); @@ -300,7 +301,7 @@ public async Task Stop_FlushesRemainingEvents() flushedItems.AddRange(items); } await Task.CompletedTask; - }, _mockLogger.Object, 100, TimeSpan.FromSeconds(60)); + }, _mockLogger, 100, TimeSpan.FromSeconds(60)); _buffer.Start(); _buffer.Push(1); diff --git a/src/SchematicHQ.Client.Test/TestEventCaptureClient.cs b/src/SchematicHQ.Client.Test/TestEventCaptureClient.cs index 132c7856..dd67f82c 100644 --- a/src/SchematicHQ.Client.Test/TestEventCaptureClient.cs +++ b/src/SchematicHQ.Client.Test/TestEventCaptureClient.cs @@ -1,5 +1,7 @@ using System.Net; using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using Moq.Protected; using NUnit.Framework; @@ -10,14 +12,14 @@ namespace SchematicHQ.Client.Tests [TestFixture] public class EventCaptureClientTests { - private Mock _mockLogger; + private ILogger _mockLogger; private Mock _mockHandler; private HttpClient _httpClient; [SetUp] public void SetUp() { - _mockLogger = new Mock(); + _mockLogger = NullLogger.Instance; _mockHandler = new Mock(); _httpClient = new HttpClient(_mockHandler.Object); } @@ -42,7 +44,7 @@ public async Task SendBatchAsync_SendsToCorrectEndpoint() .Callback((req, _) => capturedRequest = req) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - var client = new EventCaptureClient(_httpClient, "test_api_key", _mockLogger.Object); + var client = new EventCaptureClient(_httpClient, "test_api_key", _mockLogger); var events = new List { new CreateEventRequestBody @@ -73,7 +75,7 @@ public async Task SendBatchAsync_UsesCustomBaseUrl() .Callback((req, _) => capturedRequest = req) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - var client = new EventCaptureClient(_httpClient, "test_api_key", _mockLogger.Object, "https://custom.capture.com"); + var client = new EventCaptureClient(_httpClient, "test_api_key", _mockLogger, "https://custom.capture.com"); var events = new List { new CreateEventRequestBody @@ -105,7 +107,7 @@ public async Task SendBatchAsync_IncludesApiKeyInPayload() }) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - var client = new EventCaptureClient(_httpClient, "test_api_key_123", _mockLogger.Object); + var client = new EventCaptureClient(_httpClient, "test_api_key_123", _mockLogger); var events = new List { new CreateEventRequestBody @@ -144,7 +146,7 @@ public async Task SendBatchAsync_TransformsMultipleEvents() }) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger.Object); + var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger); var events = new List { new CreateEventRequestBody @@ -192,7 +194,7 @@ public async Task SendBatchAsync_SkipsEmptyList() ItExpr.IsAny()) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger.Object); + var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger); await client.SendBatchAsync(new List()); _mockHandler @@ -218,7 +220,7 @@ public void SendBatchAsync_ThrowsOnHttpError() Content = new StringContent("Internal Server Error") }); - var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger.Object); + var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger); var events = new List { new CreateEventRequestBody @@ -249,7 +251,7 @@ public async Task SendBatchAsync_IncludesSentAtWhenPresent() }) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger.Object); + var client = new EventCaptureClient(_httpClient, "test_key", _mockLogger); var events = new List { new CreateEventRequestBody diff --git a/src/SchematicHQ.Client.Test/TestLogger.cs b/src/SchematicHQ.Client.Test/TestLogger.cs deleted file mode 100644 index c9a6ad9a..00000000 --- a/src/SchematicHQ.Client.Test/TestLogger.cs +++ /dev/null @@ -1,152 +0,0 @@ -using NUnit.Framework; - -namespace SchematicHQ.Client.Tests -{ - [TestFixture] - public class LoggerTests - { - private StringWriter _stringWriter; - private ConsoleLogger _logger; - - [SetUp] - public void SetUp() - { - //redirect console output - _stringWriter = new StringWriter(); - Console.SetOut(_stringWriter); - _logger = new ConsoleLogger(); - } - - [TearDown] - public void TearDown() - { - _stringWriter.Dispose(); - // reset console output writer - Console.SetOut(Console.Out); - } - - [Test] - public void Error_ShouldLogErrorMessage() - { - // Arrange - var message = "This is an error message"; - - // Act - _logger.Error(message); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[ERROR]"), Is.True); - Assert.That(output.Contains(message), Is.True); - } - - [Test] - public void Test_Warn_ShouldLogWarnMessage() - { - // Arrange - var message = "This is a warning message"; - - // Act - _logger.Warn(message); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[WARN]"), Is.True); - Assert.That(output.Contains(message), Is.True); - } - - [Test] - public void Test_Info_ShouldLogInfoMessage() - { - // Arrange - var message = "This is an info message"; - - // Act - _logger.Info(message); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[INFO]"), Is.True); - Assert.That(output.Contains(message), Is.True); - } - - [Test] - public void Test_Debug_ShouldLogDebugMessage() - { - // Arrange - var message = "This is a debug message"; - - // Act - _logger.Debug(message); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[DEBUG]"), Is.True); - Assert.That(output.Contains(message), Is.True); - } - - [Test] - public void Test_Error_ShouldFormatMessageWithArgs() - { - // Arrange - var message = "Error {0}"; - var arg = "123"; - - // Act - _logger.Error(message, arg); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[ERROR]"), Is.True); - Assert.That(output.Contains("Error 123"), Is.True); - } - - [Test] - public void Test_Warn_ShouldFormatMessageWithArgs() - { - // Arrange - var message = "Warning {0}"; - var arg = "123"; - - // Act - _logger.Warn(message, arg); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[WARN]"), Is.True); - Assert.That(output.Contains("Warning 123"), Is.True); - } - - [Test] - public void Test_Info_ShouldFormatMessageWithArgs() - { - // Arrange - var message = "Info {0}"; - var arg = "123"; - - // Act - _logger.Info(message, arg); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[INFO]"), Is.True); - Assert.That(output.Contains("Info 123"), Is.True); - } - - [Test] - public void Test_Debug_ShouldFormatMessageWithArgs() - { - // Arrange - var message = "Debug {0}"; - var arg = "123"; - - // Act - _logger.Debug(message, arg); - - // Assert - var output = _stringWriter.ToString().Trim(); - Assert.That(output.Contains("[DEBUG]"), Is.True); - Assert.That(output.Contains("Debug 123"), Is.True); - } - } -} \ No newline at end of file diff --git a/src/SchematicHQ.Client/Core/Public/ClientOptionsCustom.cs b/src/SchematicHQ.Client/Core/Public/ClientOptionsCustom.cs index ac0261a9..3b795608 100644 --- a/src/SchematicHQ.Client/Core/Public/ClientOptionsCustom.cs +++ b/src/SchematicHQ.Client/Core/Public/ClientOptionsCustom.cs @@ -1,4 +1,5 @@ -using System.Net.Http; + +using Microsoft.Extensions.Logging; using SchematicHQ.Client.Core; using SchematicHQ.Client.Cache; using SchematicHQ.Client.RulesEngine; @@ -9,8 +10,11 @@ namespace SchematicHQ.Client; public partial class ClientOptions { + private static readonly ILoggerFactory DefaultLoggerFactory = + Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddSimpleConsole()); + public Dictionary FlagDefaults { get; set; } = new Dictionary(); - public ISchematicLogger Logger { get; set; } = new ConsoleLogger(); + public ILoggerFactory LoggerFactory { get; set; } = DefaultLoggerFactory; public List> CacheProviders { get; set; } = new List>(); public CacheConfiguration? CacheConfiguration { get; set; } public bool Offline { get; set; } @@ -52,7 +56,7 @@ public static ClientOptions WithHttpClient(this ClientOptions options, HttpClien FlagDefaults = options.FlagDefaults, Headers = new Headers(new Dictionary(options.Headers)), HttpClient = httpClient, - Logger = options.Logger, + LoggerFactory = options.LoggerFactory, MaxRetries = options.MaxRetries, Offline = options.Offline, ReplicatorMode = options.ReplicatorMode, diff --git a/src/SchematicHQ.Client/Datastream/Client.cs b/src/SchematicHQ.Client/Datastream/Client.cs index c43eb5cb..5fa4d7a5 100644 --- a/src/SchematicHQ.Client/Datastream/Client.cs +++ b/src/SchematicHQ.Client/Datastream/Client.cs @@ -17,7 +17,7 @@ namespace SchematicHQ.Client.Datastream { public class DatastreamClient : IDisposable { - private readonly ISchematicLogger _logger; + private readonly ILogger _logger; private readonly string _apiKey; private readonly Uri _baseUrl; private readonly TimeSpan _cacheTtl; @@ -73,7 +73,7 @@ public class DatastreamClient : IDisposable public DatastreamClient( string baseUrl, - ISchematicLogger logger, + ILogger logger, string apiKey, Action connectionStateCallback, TimeSpan? cacheTtl = null, @@ -112,7 +112,7 @@ public DatastreamClient( { try { - _logger.Info("Initializing Redis cache for Datastream company, user and flag data"); + _logger.LogInformation("Initializing Redis cache for Datastream company, user and flag data"); // We need to use the Cache namespace version, but cast it to the Client namespace interface _companyCache = new RedisCache(options.RedisConfig); _userCache = new RedisCache(options.RedisConfig); @@ -124,7 +124,7 @@ public DatastreamClient( } catch (Exception ex) { - _logger.Error("Failed to initialize Redis cache: {0}. Falling back to local cache.", ex.Message); + _logger.LogError(ex, "Failed to initialize Redis cache. Falling back to local cache."); _companyCache = new LocalCache(options.LocalCacheCapacity, _cacheTtl); _userCache = new LocalCache(options.LocalCacheCapacity, _cacheTtl); _companyLookupCache = new LocalCache(options.LocalCacheCapacity, _cacheTtl); @@ -214,7 +214,7 @@ private async Task ConnectAndReadAsync() { await _webSocket.ConnectAsync(_baseUrl, _cancellationTokenSource.Token); - _logger.Info("Connected to Schematic WebSocket"); + _logger.LogInformation("Connected to Schematic WebSocket"); attempts = 0; // Signal connection state @@ -254,7 +254,7 @@ private async Task ConnectAndReadAsync() catch (Exception connectEx) { // Handle connection errors specifically - _logger.Error("Failed to connect to WebSocket: {0}", connectEx.Message); + _logger.LogError(connectEx, "Failed to connect to WebSocket"); // Don't rethrow - allow the outer exception handler to handle retries throw; } @@ -273,7 +273,7 @@ await _webSocket.CloseAsync( } catch (Exception ex) { - _logger.Error("Error closing WebSocket: {0}", ex.Message); + _logger.LogError(ex, "Error closing WebSocket"); _webSocket.Abort(); } } @@ -282,14 +282,14 @@ await _webSocket.CloseAsync( catch (WebSocketAuthenticationException authEx) { _reconnectSemaphore.Release(); - _logger.Error(authEx.Message); + _logger.LogError(authEx, "WebSocket authentication failed (close code {CloseCode})", authEx.CloseCode); _connectionStateCallback(false); return; } catch (Exception connectionEx) { _reconnectSemaphore.Release(); - _logger.Error("WebSocket connection error: {0}", connectionEx.Message); + _logger.LogError(connectionEx, "WebSocket connection error"); attempts++; _connectionStateCallback(false); @@ -301,7 +301,7 @@ await _webSocket.CloseAsync( if (attempts >= MaxReconnectAttempts) { - _logger.Error("Unable to connect to server after {0} attempts", MaxReconnectAttempts); + _logger.LogError("Unable to connect to server after {MaxReconnectAttempts} attempts", MaxReconnectAttempts); return; } @@ -336,7 +336,7 @@ private async Task ReadMessagesAsync() { var buffer = new byte[4096]; var receiveBuffer = new List(); - _logger.Info("Starting to read messages from WebSocket"); + _logger.LogInformation("Starting to read messages from WebSocket"); try { while (_webSocket.State == WebSocketState.Open && !_cancellationTokenSource.Token.IsCancellationRequested) @@ -374,7 +374,7 @@ private async Task ReadMessagesAsync() if (string.IsNullOrEmpty(message)) { - _logger.Debug("Received empty message from WebSocket"); + _logger.LogDebug("Received empty message from WebSocket"); return; // Trigger reconnection } @@ -385,13 +385,13 @@ private async Task ReadMessagesAsync() } catch (Exception ex) { - _logger.Error("Failed to process WebSocket message: {0}", ex.Message); + _logger.LogError(ex, "Failed to process WebSocket message"); } } } catch (OperationCanceledException) { - _logger.Info("WebSocket read operation was cancelled"); + _logger.LogInformation("WebSocket read operation was cancelled"); return; } catch (WebSocketAuthenticationException) @@ -400,7 +400,7 @@ private async Task ReadMessagesAsync() } catch (Exception ex) { - _logger.Error("Error reading from WebSocket: {0}", ex.Message); + _logger.LogError(ex, "Error reading from WebSocket"); return; // Exit and trigger reconnection } } @@ -411,14 +411,14 @@ private async Task ReadMessagesAsync() } catch (Exception ex) { - _logger.Error("Fatal error in ReadMessagesAsync: {0}", ex.Message); + _logger.LogError(ex, "Fatal error in ReadMessagesAsync"); } finally { // Signal that reconnection should happen if (!_cancellationTokenSource.IsCancellationRequested) { - _logger.Info("Signaling for WebSocket reconnection"); + _logger.LogInformation("Signaling for WebSocket reconnection"); _readCancellationSource.Cancel(); } } @@ -452,7 +452,7 @@ private void HandleMessageResponse(DataStreamResponse? message) HandleUserMessage(message); break; default: - _logger.Error("Received unknown entity type: {0}", message.EntityType); + _logger.LogError("Received unknown entity type: {EntityType}", message.EntityType); break; } } @@ -463,7 +463,7 @@ private void HandleFlagsMessage(DataStreamResponse response) { if (response.Data == null) { - _logger.Warn("Received empty flags data"); + _logger.LogWarning("Received empty flags data"); return; } @@ -480,7 +480,7 @@ private void HandleFlagsMessage(DataStreamResponse response) if (flags == null || flags.Count == 0) { - _logger.Warn("Received empty or null flags list"); + _logger.LogWarning("Received empty or null flags list"); return; } @@ -488,7 +488,7 @@ private void HandleFlagsMessage(DataStreamResponse response) { if (string.IsNullOrEmpty(flag.Key)) { - _logger.Debug("Flag key is null, skipping flag: {0}", flag.Id); + _logger.LogDebug("Flag key is null, skipping flag: {FlagId}", flag.Id); continue; } var cacheKey = FlagCacheKey(flag.Key); @@ -508,7 +508,7 @@ private void HandleFlagsMessage(DataStreamResponse response) } catch (Exception ex) { - _logger.Error("Failed to handle flags message: {0}", ex.Message); + _logger.LogError(ex, "Failed to handle flags message"); } } @@ -518,7 +518,7 @@ private void HandleFlagMessage(DataStreamResponse response) { if (response.Data == null) { - _logger.Warn("Received empty flag data"); + _logger.LogWarning("Received empty flag data"); return; } @@ -558,11 +558,11 @@ private void HandleFlagMessage(DataStreamResponse response) { var deleteCacheKey = FlagCacheKey(flagKey); _flagsCache.Delete(deleteCacheKey); - _logger.Debug("Deleted single flag from cache: {0}", flagKey); + _logger.LogDebug("Deleted single flag from cache: {FlagKey}", flagKey); } else { - _logger.Warn("Could not extract flag key from delete message"); + _logger.LogWarning("Could not extract flag key from delete message"); } return; @@ -573,25 +573,25 @@ private void HandleFlagMessage(DataStreamResponse response) if (flag == null) { - _logger.Warn("Received null flag data"); + _logger.LogWarning("Received null flag data"); return; } if (string.IsNullOrEmpty(flag.Key)) { - _logger.Debug("Flag key is null, skipping flag: {0}", flag.Id); + _logger.LogDebug("Flag key is null, skipping flag: {FlagId}", flag.Id); return; } var cacheKey = FlagCacheKey(flag.Key); _flagsCache.Set(cacheKey, flag); - _logger.Debug("Cached single flag: {0}", flag.Key); + _logger.LogDebug("Cached single flag: {FlagKey}", flag.Key); // Note: Unlike bulk flags processing, we do NOT call DeleteMissing for single flag updates } catch (Exception ex) { - _logger.Error("Failed to handle single flag message: {0}", ex.Message); + _logger.LogError(ex, "Failed to handle single flag message"); } } @@ -629,7 +629,7 @@ private async void HandleCompanyMessage(DataStreamResponse response) { if (response.Data == null) { - _logger.Warn("Received empty company data"); + _logger.LogWarning("Received empty company data"); return; } @@ -650,7 +650,7 @@ private async void HandleCompanyMessage(DataStreamResponse response) { if (string.IsNullOrEmpty(response.EntityId)) { - _logger.Error("Partial company message missing entity_id"); + _logger.LogError("Partial company message missing entity_id"); return; } var id = response.EntityId; @@ -659,7 +659,7 @@ private async void HandleCompanyMessage(DataStreamResponse response) var existing = _companyCache.Get(existingIdKey); if (existing == null) { - _logger.Warn("Cache miss for partial company '{0}', skipping", id); + _logger.LogWarning("Cache miss for partial company '{CompanyId}', skipping", id); return; } @@ -669,7 +669,7 @@ private async void HandleCompanyMessage(DataStreamResponse response) } catch (Exception ex) { - _logger.Error("Failed to merge partial company: {0}", ex.Message); + _logger.LogError(ex, "Failed to merge partial company"); return; } } @@ -680,7 +680,7 @@ private async void HandleCompanyMessage(DataStreamResponse response) if (company == null) { - _logger.Warn("Received null company data"); + _logger.LogWarning("Received null company data"); return; } @@ -692,7 +692,7 @@ private async void HandleCompanyMessage(DataStreamResponse response) // Wait for the lock with a reasonable timeout if (!await companyLock.WaitAsync(TimeSpan.FromSeconds(5))) { - _logger.Warn("Timeout waiting for company lock during WebSocket update"); + _logger.LogWarning("Timeout waiting for company lock during WebSocket update"); return; } @@ -726,12 +726,12 @@ private async void HandleCompanyMessage(DataStreamResponse response) } catch (Exception lockEx) { - _logger.Error($"Error during locked company update: {lockEx.Message}"); + _logger.LogError(lockEx, "Error during locked company update"); } } catch (Exception ex) { - _logger.Error("Failed to handle company message: {0}", ex.Message); + _logger.LogError(ex, "Failed to handle company message"); } } @@ -741,7 +741,7 @@ private void HandleUserMessage(DataStreamResponse response) { if (response.Data == null) { - _logger.Warn("Received empty user data"); + _logger.LogWarning("Received empty user data"); return; } @@ -761,7 +761,7 @@ private void HandleUserMessage(DataStreamResponse response) { if (string.IsNullOrEmpty(response.EntityId)) { - _logger.Error("Partial user message missing entity_id"); + _logger.LogError("Partial user message missing entity_id"); return; } var id = response.EntityId; @@ -770,7 +770,7 @@ private void HandleUserMessage(DataStreamResponse response) var existing = _userCache.Get(existingIdKey); if (existing == null) { - _logger.Warn("Cache miss for partial user '{0}', skipping", id); + _logger.LogWarning("Cache miss for partial user '{UserId}', skipping", id); return; } @@ -780,7 +780,7 @@ private void HandleUserMessage(DataStreamResponse response) } catch (Exception ex) { - _logger.Error("Failed to merge partial user: {0}", ex.Message); + _logger.LogError(ex, "Failed to merge partial user"); return; } } @@ -791,7 +791,7 @@ private void HandleUserMessage(DataStreamResponse response) if (user == null) { - _logger.Warn("Received null user data"); + _logger.LogWarning("Received null user data"); return; } @@ -804,7 +804,7 @@ private void HandleUserMessage(DataStreamResponse response) { var resourceKey = ResourceKeyToCacheKey(CacheKeyPrefixUser, key.Key, key.Value); _userLookupCache.Delete(resourceKey); - _logger.Debug("Deleted user from cache with key: {0}", resourceKey); + _logger.LogDebug("Deleted user from cache with key: {ResourceKey}", resourceKey); } return; } @@ -817,7 +817,7 @@ private void HandleUserMessage(DataStreamResponse response) } catch (Exception ex) { - _logger.Error("Failed to handle user message: {0}", ex.Message); + _logger.LogError(ex, "Failed to handle user message"); } } @@ -827,7 +827,7 @@ private void HandleErrorMessage(DataStreamResponse response) { if (response.Data == null) { - _logger.Warn("Received empty error data"); + _logger.LogWarning("Received empty error data"); return; } @@ -838,7 +838,7 @@ private void HandleErrorMessage(DataStreamResponse response) { if (!string.IsNullOrEmpty(error.Error)) { - _logger.Error("Received error from server: {0}", error.Error); + _logger.LogError("Received error from server: {ServerError}", error.Error); } // Check if we have keys and entity type in the error response @@ -853,7 +853,7 @@ private void HandleErrorMessage(DataStreamResponse response) NotifyPendingRequests(null, error.Keys, CacheKeyPrefixUser, _pendingUserRequests); break; default: - _logger.Warn("Received error for unsupported entity type: {0}", error.EntityType.Value); + _logger.LogWarning("Received error for unsupported entity type: {EntityType}", error.EntityType.Value); break; } } @@ -861,7 +861,7 @@ private void HandleErrorMessage(DataStreamResponse response) } catch (Exception ex) { - _logger.Error("Failed to deserialize error message: {0}", ex.Message); + _logger.LogError(ex, "Failed to deserialize error message"); } } @@ -874,7 +874,7 @@ internal async Task CheckFlag(RulesengineCompany? company, Rule } catch (Exception ex) { - _logger.Error("Error checking flag {0}: {1}", flag.Key, ex.Message); + _logger.LogError(ex, "Error checking flag {FlagKey}", flag.Key); return new CheckFlagResult { Reason = "Error", @@ -1024,7 +1024,7 @@ private async Task GetAllFlagsAsync(CancellationToken cancellationToken) } else if (_webSocket.State != WebSocketState.Open) { - _logger.Warn("WebSocket is not open, cannot request flags data"); + _logger.LogWarning("WebSocket is not open, cannot request flags data"); return; } @@ -1120,13 +1120,13 @@ public async Task UpdateCompanyMetricsAsync(EventBodyTrack eventBody) { if (eventBody == null) { - _logger.Error("Event body cannot be null"); + _logger.LogError("Event body cannot be null"); return false; } if (eventBody.Company == null || eventBody.Company.Count == 0) { - _logger.Error("No keys provided for company lookup"); + _logger.LogError("No keys provided for company lookup"); return false; } @@ -1138,7 +1138,7 @@ public async Task UpdateCompanyMetricsAsync(EventBodyTrack eventBody) // Wait for the lock with a reasonable timeout to prevent deadlocks if (!await companyLock.WaitAsync(TimeSpan.FromSeconds(5))) { - _logger.Warn("Timeout waiting for company lock during metrics update"); + _logger.LogWarning("Timeout waiting for company lock during metrics update"); return false; } @@ -1155,7 +1155,7 @@ public async Task UpdateCompanyMetricsAsync(EventBodyTrack eventBody) var companyCopy = DeepCopyCompany(company); if (companyCopy == null) { - _logger.Error("Failed to create deep copy of company"); + _logger.LogError("Failed to create deep copy of company"); return false; } @@ -1173,7 +1173,7 @@ public async Task UpdateCompanyMetricsAsync(EventBodyTrack eventBody) if (!metricUpdated) { - _logger.Debug($"No matching metric found for event {eventBody.Event}"); + _logger.LogDebug("No matching metric found for event {EventName}", eventBody.Event); return false; } @@ -1184,7 +1184,7 @@ public async Task UpdateCompanyMetricsAsync(EventBodyTrack eventBody) } catch (Exception ex) { - _logger.Warn($"Failed to cache company metrics: {ex.Message}"); + _logger.LogWarning(ex, "Failed to cache company metrics"); return false; } @@ -1198,7 +1198,7 @@ public async Task UpdateCompanyMetricsAsync(EventBodyTrack eventBody) } catch (Exception ex) { - _logger.Error($"Error during company metrics update: {ex.Message}"); + _logger.LogError(ex, "Error during company metrics update"); return false; } } @@ -1217,7 +1217,7 @@ public bool UpdateCompanyMetrics(EventBodyTrack eventBody) } catch (Exception ex) { - _logger.Error($"Error in synchronous company metrics update: {ex.Message}"); + _logger.LogError(ex, "Error in synchronous company metrics update"); return false; } } /// @@ -1307,16 +1307,16 @@ await _webSocket.SendAsync( private string GetCacheVersion() { var replicatorVersion = _cacheVersionProvider?.Invoke(); - _logger.Debug("Cache version provider returned: {0}", replicatorVersion ?? "(null)"); + _logger.LogDebug("Cache version provider returned: {ReplicatorVersion}", replicatorVersion ?? "(null)"); if (!string.IsNullOrEmpty(replicatorVersion)) { - _logger.Info("Using replicator cache version: {0}", replicatorVersion); + _logger.LogInformation("Using replicator cache version: {ReplicatorVersion}", replicatorVersion); return replicatorVersion; } var sdkVersion = SchemaVersionGenerator.GetGlobalSchemaVersion(); - _logger.Info("Using SDK cache version: {0}", sdkVersion); + _logger.LogInformation("Using SDK cache version: {SdkVersion}", sdkVersion); return sdkVersion; } @@ -1467,7 +1467,7 @@ private void CleanupUnusedCompanyLocks() if (locksToRemove.Count > 0) { - _logger.Debug($"Cleaned up {locksToRemove.Count} unused company locks"); + _logger.LogDebug("Cleaned up {LockCount} unused company locks", locksToRemove.Count); } } } @@ -1495,29 +1495,29 @@ public void Dispose() // we need to eventually block here, but with a clean timeout if (!closeTask.Wait(TimeSpan.FromSeconds(5))) { - _logger.Warn("WebSocket close handshake timed out"); + _logger.LogWarning("WebSocket close handshake timed out"); } } catch (OperationCanceledException) { - _logger.Warn("WebSocket close handshake was cancelled"); + _logger.LogWarning("WebSocket close handshake was cancelled"); } catch (AggregateException ex) when (ex.InnerExceptions.Any(e => e is OperationCanceledException)) { - _logger.Warn("WebSocket close handshake was cancelled"); + _logger.LogWarning("WebSocket close handshake was cancelled"); } // If it didn't close gracefully, abort if (_webSocket.State != WebSocketState.Closed) { - _logger.Warn("WebSocket didn't close gracefully, aborting"); + _logger.LogWarning("WebSocket didn't close gracefully, aborting"); _webSocket.Abort(); } } } catch (Exception ex) { - _logger.Error("Error closing WebSocket connection: {0}", ex.Message); + _logger.LogError(ex, "Error closing WebSocket connection"); // Ensure we abort in case of errors try { _webSocket.Abort(); } catch { /* Ignore any errors during abort */ } diff --git a/src/SchematicHQ.Client/Datastream/DatastreamClientAdapter.cs b/src/SchematicHQ.Client/Datastream/DatastreamClientAdapter.cs index 477986dc..db679c97 100644 --- a/src/SchematicHQ.Client/Datastream/DatastreamClientAdapter.cs +++ b/src/SchematicHQ.Client/Datastream/DatastreamClientAdapter.cs @@ -16,7 +16,7 @@ namespace SchematicHQ.Client.Datastream public class DatastreamClientAdapter { private readonly DatastreamClient _client; - private readonly ISchematicLogger _logger; + private readonly ILogger _logger; private readonly bool _replicatorMode; private readonly IReplicatorHealthService? _replicatorHealthService; @@ -26,27 +26,27 @@ public class DatastreamClientAdapter /// /// Creates a new datastream client adapter /// - public DatastreamClientAdapter(string baseUrl, ISchematicLogger logger, string apiKey, DatastreamOptions options, bool replicatorMode = false, string? replicatorHealthUrl = null) + public DatastreamClientAdapter(string baseUrl, ILogger logger, string apiKey, DatastreamOptions options, bool replicatorMode = false, string? replicatorHealthUrl = null) { _logger = logger; _replicatorMode = replicatorMode; - + // Initialize replicator health service if in replicator mode if (_replicatorMode && !string.IsNullOrWhiteSpace(replicatorHealthUrl)) { // Create a simple HTTP client for health checks var httpClient = new System.Net.Http.HttpClient(); _replicatorHealthService = new ReplicatorHealthService(httpClient, replicatorHealthUrl, logger); - + // Subscribe to cache version changes for logging and potential cache invalidation _replicatorHealthService.CacheVersionChanged += OnCacheVersionChanged; - + _replicatorHealthService.Start(); } - + _client = new DatastreamClient( baseUrl, - _logger, + logger, apiKey, _connectionTracker.UpdateConnectionState, // callback to update connection state options.CacheTTL, @@ -128,13 +128,13 @@ public bool IsReplicatorMode() } else { - _logger.Debug("Timed out waiting for replicator cache version"); + _logger.LogDebug("Timed out waiting for replicator cache version"); return null; } } catch (Exception ex) { - _logger.Debug("Failed to get replicator cache version: {0}", ex.Message); + _logger.LogDebug(ex, "Failed to get replicator cache version"); return null; } } @@ -144,7 +144,7 @@ public bool IsReplicatorMode() /// private void OnCacheVersionChanged(string? oldVersion, string? newVersion) { - _logger.Info("Cache version changed from {0} to {1} - new cache keys will use updated version", + _logger.LogInformation("Cache version changed from {OldVersion} to {NewVersion} - new cache keys will use updated version", oldVersion ?? "(null)", newVersion ?? "(null)"); // Note: Cache invalidation is automatic because cache keys include the version. @@ -180,7 +180,7 @@ public async Task IsConnectedAsync(TimeSpan? timeout = null) } catch (Exception ex) { - _logger.Error($"Error checking datastream connection: {ex.Message}"); + _logger.LogError(ex, "Error checking datastream connection"); return false; } } @@ -229,13 +229,13 @@ public async Task CheckFlag(CheckFlagRequestBody request, strin if (allRequiredResourcesInCache) { // All required resources in cache - evaluate flag - _logger.Debug("Replicator mode: All required resources in cache, evaluating flag '{0}'", flagKey); + _logger.LogDebug("Replicator mode: All required resources in cache, evaluating flag '{FlagKey}'", flagKey); return await _client.CheckFlag(cachedCompany, cachedUser, cachedFlag!); } else if (!flagInCache) { // Flag missing from cache - replicator should have populated it, so flag doesn't exist - _logger.Debug("Replicator mode: Flag '{0}' missing from cache, replicator is healthy so flag doesn't exist", flagKey); + _logger.LogDebug("Replicator mode: Flag '{FlagKey}' missing from cache, replicator is healthy so flag doesn't exist", flagKey); return new CheckFlagResult { Reason = "FlagNotFound", @@ -247,7 +247,7 @@ public async Task CheckFlag(CheckFlagRequestBody request, strin else { // Some company/user resources missing - evaluate with available data - _logger.Warn("Replicator mode: Some required resources missing from cache for flag '{0}', evaluating with available data", flagKey); + _logger.LogWarning("Replicator mode: Some required resources missing from cache for flag '{FlagKey}', evaluating with available data", flagKey); return await _client.CheckFlag(cachedCompany, cachedUser, cachedFlag!); } } @@ -257,13 +257,13 @@ public async Task CheckFlag(CheckFlagRequestBody request, strin if (allRequiredResourcesInCache) { // All required resources in cache - evaluate flag even though replicator is unhealthy - _logger.Warn("Replicator mode: Replicator unhealthy but all required resources in cache, evaluating flag '{0}'", flagKey); + _logger.LogWarning("Replicator mode: Replicator unhealthy but all required resources in cache, evaluating flag '{FlagKey}'", flagKey); return await _client.CheckFlag(cachedCompany, cachedUser, cachedFlag!); } else { // Not all resources in cache and replicator unhealthy - fallback to API - _logger.Warn("Replicator mode: Replicator unhealthy and missing required resources for flag '{0}', falling back to API", flagKey); + _logger.LogWarning("Replicator mode: Replicator unhealthy and missing required resources for flag '{FlagKey}', falling back to API", flagKey); throw new InvalidOperationException($"Replicator unhealthy and required resources missing for flag '{flagKey}' - API fallback required"); } } @@ -315,7 +315,7 @@ public async Task CheckFlag(CheckFlagRequestBody request, strin } catch (Exception ex) { - _logger.Error("Error fetching missing resources for flag {0}: {1}", flagKey, ex.Message); + _logger.LogError(ex, "Error fetching missing resources for flag {FlagKey}", flagKey); return new CheckFlagResult { Reason = "Error", @@ -423,57 +423,5 @@ public async Task WaitForConnectionAsync(TimeSpan timeout) } } - // Adapter to convert ISchematicLogger to ILogger for the DatastreamClient - private class LoggerAdapter : ILogger - { - private readonly ISchematicLogger _logger; - - public LoggerAdapter(ConsoleLogger logger) - { - _logger = logger; - } - - public IDisposable BeginScope(TState state) - { - return new NoopDisposable(); - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - var message = formatter(state, exception); - - switch (logLevel) - { - case LogLevel.Critical: - case LogLevel.Error: - _logger.Error(message); - break; - case LogLevel.Warning: - _logger.Warn(message); - break; - case LogLevel.Information: - _logger.Info(message); - break; - case LogLevel.Debug: - case LogLevel.Trace: - _logger.Debug(message); - break; - default: - _logger.Info(message); - break; - } - } - - // A no-operation disposable class - private class NoopDisposable : IDisposable - { - public void Dispose() { } - } - } } } diff --git a/src/SchematicHQ.Client/Datastream/ReplicatorHealthService.cs b/src/SchematicHQ.Client/Datastream/ReplicatorHealthService.cs index 46f3de5e..edfed68c 100644 --- a/src/SchematicHQ.Client/Datastream/ReplicatorHealthService.cs +++ b/src/SchematicHQ.Client/Datastream/ReplicatorHealthService.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using SchematicHQ.Client.Core; namespace SchematicHQ.Client.Datastream @@ -50,7 +51,7 @@ internal class ReplicatorHealthService : IReplicatorHealthService { private readonly HttpClient _httpClient; private readonly string _healthUrl; - private readonly ISchematicLogger _logger; + private readonly ILogger _logger; private readonly TimeSpan _checkInterval; private readonly CancellationTokenSource _cancellationTokenSource; private Task? _healthCheckTask; @@ -71,7 +72,7 @@ internal class ReplicatorHealthService : IReplicatorHealthService public ReplicatorHealthService( HttpClient httpClient, string healthUrl, - ISchematicLogger logger, + ILogger logger, TimeSpan? checkInterval = null) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); @@ -107,7 +108,7 @@ public ReplicatorHealthService( } catch (Exception ex) { - _logger.Debug("Failed to get cache version immediately: {0}", ex.Message); + _logger.LogDebug(ex, "Failed to get cache version immediately"); return null; } } @@ -121,7 +122,7 @@ public void Start() if (_healthCheckTask != null) return; // Already started - _logger.Info("Starting replicator health check service for URL: {0}", _healthUrl); + _logger.LogInformation("Starting replicator health check service for URL: {HealthUrl}", _healthUrl); // Perform an immediate health check to get the cache version right away _ = Task.Run(async () => @@ -129,11 +130,11 @@ public void Start() try { await PerformHealthCheck(); - _logger.Debug("Initial health check completed"); + _logger.LogDebug("Initial health check completed"); } catch (Exception ex) { - _logger.Debug("Initial health check failed: {0}", ex.Message); + _logger.LogDebug(ex, "Initial health check failed"); } }); @@ -146,7 +147,7 @@ public void Stop() if (_disposed || _healthCheckTask == null) return; - _logger.Info("Stopping replicator health check service"); + _logger.LogInformation("Stopping replicator health check service"); _cancellationTokenSource.Cancel(); try @@ -159,7 +160,7 @@ public void Stop() } catch (Exception ex) { - _logger.Error("Error stopping health check service: {0}", ex.Message); + _logger.LogError(ex, "Error stopping health check service"); } _healthCheckTask = null; @@ -176,7 +177,7 @@ private async Task HealthCheckLoop() } catch (Exception ex) { - _logger.Error("Error during health check: {0}", ex.Message); + _logger.LogError(ex, "Error during health check"); _isHealthy = false; } @@ -199,14 +200,14 @@ private async Task PerformHealthCheck() // Read and parse the JSON response first var responseBody = await response.Content.ReadAsStringAsync(_cancellationTokenSource.Token); - _logger.Debug("Raw health response: {0}", responseBody); + _logger.LogDebug("Raw health response: {ResponseBody}", responseBody); var healthResponse = System.Text.Json.JsonSerializer.Deserialize(responseBody, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - _logger.Debug("Deserialized health response - Ready: {0}, CacheVersion: '{1}'", + _logger.LogDebug("Deserialized health response - Ready: {Ready}, CacheVersion: '{CacheVersion}'", healthResponse?.Ready ?? false, healthResponse?.CacheVersion ?? "NULL"); bool wasHealthy = _isHealthy; @@ -219,29 +220,29 @@ private async Task PerformHealthCheck() _isHealthy = response.IsSuccessStatusCode && healthResponse?.Ready == true; _cacheVersion = healthResponse?.CacheVersion; - _logger.Debug("Updated cache version from '{0}' to '{1}'", + _logger.LogDebug("Updated cache version from '{PreviousCacheVersion}' to '{CacheVersion}'", previousCacheVersion ?? "NULL", _cacheVersion ?? "NULL"); // Log state changes - matching Go client exactly if (_isHealthy && !wasHealthy) { - _logger.Info("External replicator is now ready"); + _logger.LogInformation("External replicator is now ready"); } else if (!_isHealthy && wasHealthy) { - _logger.Info("External replicator is no longer ready"); + _logger.LogInformation("External replicator is no longer ready"); } // Log readiness status for debugging if (!response.IsSuccessStatusCode) { - _logger.Debug("Replicator readiness check: HTTP {0}, Ready={1}", response.StatusCode, healthResponse?.Ready ?? false); + _logger.LogDebug("Replicator readiness check: HTTP {StatusCode}, Ready={Ready}", response.StatusCode, healthResponse?.Ready ?? false); } // Log cache version changes and notify subscribers if (_cacheVersion != previousCacheVersion) { - _logger.Info("Replicator cache version updated: {0} -> {1}", previousCacheVersion ?? "(null)", _cacheVersion ?? "(null)"); + _logger.LogInformation("Replicator cache version updated: {PreviousCacheVersion} -> {CacheVersion}", previousCacheVersion ?? "(null)", _cacheVersion ?? "(null)"); // Notify subscribers of the cache version change CacheVersionChanged?.Invoke(previousCacheVersion, _cacheVersion); @@ -259,7 +260,7 @@ private async Task PerformHealthCheck() if (wasHealthyBefore) { - _logger.Warn("Failed to parse replicator health response: {0}", ex.Message); + _logger.LogWarning(ex, "Failed to parse replicator health response"); } } catch (Exception ex) @@ -269,7 +270,7 @@ private async Task PerformHealthCheck() if (wasHealthyBefore) { - _logger.Debug("Replicator health check failed: {0}", ex.Message); + _logger.LogDebug(ex, "Replicator health check failed"); } } } diff --git a/src/SchematicHQ.Client/EventBuffer.cs b/src/SchematicHQ.Client/EventBuffer.cs index 95f17932..5c4c54bb 100644 --- a/src/SchematicHQ.Client/EventBuffer.cs +++ b/src/SchematicHQ.Client/EventBuffer.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; #nullable enable @@ -24,7 +25,7 @@ public class EventBuffer : IEventBuffer private readonly int _maxSize; private readonly TimeSpan _flushPeriod; private readonly Func, Task> _action; - private readonly ISchematicLogger _logger; + private readonly ILogger _logger; private readonly ConcurrentQueue _queue; private readonly SemaphoreSlim _semaphore; private CancellationTokenSource _cts; @@ -34,7 +35,7 @@ public class EventBuffer : IEventBuffer private readonly object _taskLock = new object(); private readonly object _runningLock = new object(); - public EventBuffer(Func, Task> action, ISchematicLogger logger, int maxSize = DefaultMaxSize, TimeSpan? flushPeriod = null) + public EventBuffer(Func, Task> action, ILogger logger, int maxSize = DefaultMaxSize, TimeSpan? flushPeriod = null) { _maxSize = maxSize; _flushPeriod = flushPeriod ?? DefaultFlushPeriod; @@ -45,7 +46,7 @@ public EventBuffer(Func, Task> action, ISchematicLogger logger, int maxS _cts = new CancellationTokenSource(); _isRunning = false; - _logger.Debug("EventBuffer initialized with maxSize: {0}, flushPeriod: {1}", _maxSize, _flushPeriod); + _logger.LogDebug("EventBuffer initialized with maxSize: {MaxSize}, flushPeriod: {FlushPeriod}", _maxSize, _flushPeriod); AppDomain.CurrentDomain.ProcessExit += async (s, e) => { await EmergencyFlush(); @@ -61,7 +62,7 @@ private async Task EmergencyFlush() { try { - _logger.Debug("Emergency flush triggered by program termination"); + _logger.LogDebug("Emergency flush triggered by program termination"); // Don't check _isRunning here since we're in an emergency shutdown var items = new List(); while (_queue.TryDequeue(out var item)) @@ -71,14 +72,14 @@ private async Task EmergencyFlush() if (items.Count > 0) { - _logger.Info("Emergency flushing {0} items", items.Count); + _logger.LogInformation("Emergency flushing {ItemCount} items", items.Count); await _action(items); } - _logger.Info("Emergency flush completed"); + _logger.LogInformation("Emergency flush completed"); } catch (Exception ex) { - _logger.Error("Error during emergency flush: {0}", ex.Message); + _logger.LogError(ex, "Error during emergency flush"); } } @@ -90,7 +91,7 @@ public void Push(T item) } _queue.Enqueue(item); - _logger.Debug("Item added to buffer. Current size: {0}", _queue.Count); + _logger.LogDebug("Item added to buffer. Current size: {QueueSize}", _queue.Count); if (_queue.Count >= _maxSize) { _semaphore.Release(); @@ -119,7 +120,7 @@ public void Start() } } - _logger.Info("EventBuffer started."); + _logger.LogInformation("EventBuffer started."); } public async Task Flush() @@ -133,7 +134,7 @@ public async Task Flush() { await FlushBufferAsync(); } - _logger.Info("Buffer flushed manually."); + _logger.LogInformation("Buffer flushed manually."); } private async Task ProcessBufferAsync(CancellationToken token) @@ -147,7 +148,7 @@ private async Task ProcessBufferAsync(CancellationToken token) } catch (OperationCanceledException) { - _logger.Warn("Process buffer task was canceled."); + _logger.LogWarning("Process buffer task was canceled."); } } } @@ -167,11 +168,11 @@ private async Task PeriodicFlushAsync(CancellationToken token) } catch (OperationCanceledException) { - _logger.Warn("Periodic flush task was canceled."); + _logger.LogWarning("Periodic flush task was canceled."); } catch (Exception ex) { - _logger.Error("An error occurred during periodic flush: {0}", ex.Message); + _logger.LogError(ex, "An error occurred during periodic flush"); } } } @@ -186,7 +187,7 @@ private async Task FlushBufferAsync() if (items.Count > 0) { - _logger.Info("Flushing buffer with {0} items.", items.Count); + _logger.LogInformation("Flushing buffer with {ItemCount} items.", items.Count); // Initialize retry counter and success flag int retryCount = 0; @@ -202,7 +203,7 @@ private async Task FlushBufferAsync() if (retryCount > 0) { // Log retry attempt - _logger.Info("Retrying event batch submission (attempt {0} of {1})", retryCount, MaxRetries); + _logger.LogInformation("Retrying event batch submission (attempt {Attempt} of {MaxRetries})", retryCount, MaxRetries); } // Attempt to send events @@ -222,10 +223,9 @@ private async Task FlushBufferAsync() double jitter = random.NextDouble() * 0.1 * delay; // 10% jitter TimeSpan waitTime = TimeSpan.FromSeconds(delay + jitter); - _logger.Warn( - string.Format("Event batch submission failed: {0}. Retrying in {1:0.##} seconds...", - ex.Message, waitTime.TotalSeconds) - ); + _logger.LogWarning(ex, + "Event batch submission failed. Retrying in {WaitSeconds:0.##} seconds...", + waitTime.TotalSeconds); // Wait before retry await Task.Delay(waitTime); @@ -236,11 +236,11 @@ private async Task FlushBufferAsync() // After all retries, if still not successful, log the error if (!success) { - _logger.Error("Event batch submission failed after {0} retries: {1}", MaxRetries, lastException?.Message ?? "Unknown error"); + _logger.LogError(lastException, "Event batch submission failed after {MaxRetries} retries", MaxRetries); } else if (retryCount > 0) { - _logger.Info("Event batch submission succeeded after {0} retries", retryCount); + _logger.LogInformation("Event batch submission succeeded after {RetryCount} retries", retryCount); } } } @@ -291,11 +291,11 @@ public async Task Stop() _semaphore.Dispose(); _cts.Dispose(); - _logger.Info("EventBuffer shut down cleanly."); + _logger.LogInformation("EventBuffer shut down cleanly."); } catch (Exception ex) { - _logger.Error("Error during shutdown: {0}", ex.Message); + _logger.LogError(ex, "Error during shutdown"); throw; } } diff --git a/src/SchematicHQ.Client/EventCaptureClient.cs b/src/SchematicHQ.Client/EventCaptureClient.cs index 04efbc64..cc320916 100644 --- a/src/SchematicHQ.Client/EventCaptureClient.cs +++ b/src/SchematicHQ.Client/EventCaptureClient.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; using OneOf; using SchematicHQ.Client.Core; @@ -46,9 +47,9 @@ internal class EventCaptureClient private readonly HttpClient _httpClient; private readonly string _apiKey; private readonly string _baseUrl; - private readonly ISchematicLogger _logger; + private readonly ILogger _logger; - public EventCaptureClient(HttpClient httpClient, string apiKey, ISchematicLogger logger, string? baseUrl = null) + public EventCaptureClient(HttpClient httpClient, string apiKey, ILogger logger, string? baseUrl = null) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); @@ -87,7 +88,7 @@ public async Task SendBatchAsync(List events) var json = JsonSerializer.Serialize(batchPayload, JsonOptions.JsonSerializerOptions); var content = new StringContent(json, Encoding.UTF8, "application/json"); - _logger.Debug("Sending {0} events to capture service at {1}", events.Count, endpoint); + _logger.LogDebug("Sending {EventCount} events to capture service at {Endpoint}", events.Count, endpoint); using var response = await _httpClient.PostAsync(endpoint, content); diff --git a/src/SchematicHQ.Client/Logger.cs b/src/SchematicHQ.Client/Logger.cs deleted file mode 100644 index 8f9e6fdb..00000000 --- a/src/SchematicHQ.Client/Logger.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable enable - -namespace SchematicHQ.Client; - -public interface ISchematicLogger -{ - void Error(string message, params object[] args); - void Warn(string message, params object[] args); - void Info(string message, params object[] args); - void Debug(string message, params object[] args); -} - -public class ConsoleLogger : ISchematicLogger -{ - public void Error(string message, params object[] args) - { - Console.WriteLine($"[ERROR] {string.Format(message, args)}"); - } - - public void Warn(string message, params object[] args) - { - Console.WriteLine($"[WARN] {string.Format(message, args)}"); - } - - public void Info(string message, params object[] args) - { - Console.WriteLine($"[INFO] {string.Format(message, args)}"); - } - - public void Debug(string message, params object[] args) - { - Console.WriteLine($"[DEBUG] {string.Format(message, args)}"); - } -} diff --git a/src/SchematicHQ.Client/OpenFeature/SchematicProvider.cs b/src/SchematicHQ.Client/OpenFeature/SchematicProvider.cs index 5787d7b1..2cb3358d 100644 --- a/src/SchematicHQ.Client/OpenFeature/SchematicProvider.cs +++ b/src/SchematicHQ.Client/OpenFeature/SchematicProvider.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using OpenFeature; using OpenFeature.Constant; using OpenFeature.Error; @@ -18,6 +19,7 @@ public class SchematicProvider : FeatureProvider private static readonly Metadata _metadata = new Metadata("schematic-provider"); private readonly Schematic _schematic; private readonly ClientOptions _options; + private readonly ILogger _logger; /// /// Creates a new instance of the SchematicProvider. @@ -32,6 +34,7 @@ public SchematicProvider(string apiKey, ClientOptions? options = null) } _options = options ?? new ClientOptions(); + _logger = _options.LoggerFactory.CreateLogger(); _schematic = new Schematic(apiKey, _options); } @@ -44,6 +47,7 @@ public SchematicProvider(Schematic schematic, ClientOptions? options = null) { _schematic = schematic ?? throw new ArgumentNullException(nameof(schematic)); _options = options ?? new ClientOptions(); + _logger = _options.LoggerFactory.CreateLogger(); } /// @@ -78,7 +82,7 @@ public override async Task> ResolveBooleanValueAsync( } catch (Exception ex) { - _options.Logger?.Error("Error evaluating boolean flag '{0}': {1}", flagKey, ex.Message); + _logger.LogError(ex, "Error evaluating boolean flag '{FlagKey}'", flagKey); var errorType = MapExceptionToErrorType(ex); return new ResolutionDetails( @@ -159,14 +163,14 @@ public override Task> ResolveStructureValueAsync( /// public override Task InitializeAsync(EvaluationContext? context, CancellationToken cancellationToken = default) { - _options.Logger?.Info("Schematic provider initialized"); + _logger.LogInformation("Schematic provider initialized"); return Task.CompletedTask; } /// public override async Task ShutdownAsync(CancellationToken cancellationToken = default) { - _options.Logger?.Info("Schematic provider shutting down"); + _logger.LogInformation("Schematic provider shutting down"); await _schematic.Shutdown().ConfigureAwait(false); } diff --git a/src/SchematicHQ.Client/Schematic.cs b/src/SchematicHQ.Client/Schematic.cs index 525da6a5..e9f7dd17 100644 --- a/src/SchematicHQ.Client/Schematic.cs +++ b/src/SchematicHQ.Client/Schematic.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using SchematicHQ.Client.Datastream; using SchematicHQ.Client.Cache; using SchematicHQ.Client.Core; @@ -17,7 +18,7 @@ public partial class Schematic { private readonly ClientOptions _options; private readonly IEventBuffer _eventBuffer; - private readonly ISchematicLogger _logger; + private readonly ILogger _logger; private readonly List> _flagCheckCacheProviders; private readonly bool _offline; private readonly DatastreamClientAdapter? _datastreamClient; @@ -50,7 +51,7 @@ public Schematic(string apiKey, ClientOptions? options = null) _options = options ?? new ClientOptions(); _offline = _options.Offline; _replicatorMode = _options.ReplicatorMode; - _logger = _options.Logger ?? new ConsoleLogger(); + _logger = _options.LoggerFactory.CreateLogger("SchematicHQ.Client"); // Validate replicator mode configuration if (_replicatorMode && string.IsNullOrWhiteSpace(_options.ReplicatorHealthUrl)) @@ -147,7 +148,7 @@ public Schematic(string apiKey, ClientOptions? options = null) case CacheProviderType.Redis: if (_options.CacheConfiguration.RedisConfig == null) { - _logger.Warn("Redis configuration not provided, falling back to local cache"); + _logger.LogWarning("Redis configuration not provided, falling back to local cache"); _flagCheckCacheProviders.Add(new LocalCache()); } else @@ -229,7 +230,7 @@ public Schematic(string apiKey, ClientOptions? options = null) else { _datastreamConnected = false; - _logger.Info("Replicator mode enabled - datastream client created for cache access only"); + _logger.LogInformation("Replicator mode enabled - datastream client created for cache access only"); } } } @@ -255,17 +256,17 @@ private void StartConnectionMonitoring() _datastreamConnected = isConnected; if (isConnected) { - _logger.Info("Datastream connection established"); + _logger.LogInformation("Datastream connection established"); } else { - _logger.Warn("Datastream connection lost, falling back to API"); + _logger.LogWarning("Datastream connection lost, falling back to API"); } } } catch (Exception ex) { - _logger.Error($"Error monitoring datastream connection: {ex.Message}"); + _logger.LogError(ex, "Error monitoring datastream connection"); } await Task.Delay(TimeSpan.FromSeconds(5)); @@ -273,7 +274,7 @@ private void StartConnectionMonitoring() } catch (Exception ex) { - _logger.Error($"Connection monitoring stopped: {ex.Message}"); + _logger.LogError(ex, "Connection monitoring stopped"); } }); } @@ -345,7 +346,7 @@ public async Task CheckFlagWithEntitlement(str catch (Exception ex) { // Fall back to API if datastream fails - _logger.Debug("Datastream flag check failed ({0}), falling back to API", ex.Message); + _logger.LogDebug(ex, "Datastream flag check failed, falling back to API"); return await CheckFlagWithEntitlementApi(flagKey, company, user); } } @@ -404,7 +405,7 @@ private async Task CheckFlagWithEntitlementApi } catch (Exception cacheEx) { - _logger.Error("Error caching flag result: {0}", cacheEx.Message); + _logger.LogError(cacheEx, "Error caching flag result"); } } @@ -412,7 +413,7 @@ private async Task CheckFlagWithEntitlementApi } catch (Exception ex) { - _logger.Error("Error checking flag via API: {0}", ex.Message); + _logger.LogError(ex, "Error checking flag via API"); return new CheckFlagWithEntitlementResponse { FlagKey = flagKey, @@ -431,7 +432,7 @@ public async Task> CheckFlags( if (_offline) { - _logger.Debug("Offline mode enabled, returning default flag values"); + _logger.LogDebug("Offline mode enabled, returning default flag values"); if (keyList == null || keyList.Count == 0) { return new List(); @@ -463,7 +464,7 @@ public async Task> CheckFlags( if (keyList == null || keyList.Count == 0) { - _logger.Debug("No specific flag keys provided, calling CheckFlags API"); + _logger.LogDebug("No specific flag keys provided, calling CheckFlags API"); var apiResp = await API.Features.CheckFlagsAsync(requestBody); return apiResp.Data.Flags.ToList(); } @@ -496,11 +497,11 @@ public async Task> CheckFlags( if (allCached) { - _logger.Debug("All {0} flags found in cache", keyList.Count); + _logger.LogDebug("All {KeyCount} flags found in cache", keyList.Count); return keyList.Select(k => cachedResults[k]).ToList(); } - _logger.Debug("Cache miss for some flags, calling API for all {0} keys", keyList.Count); + _logger.LogDebug("Cache miss for some flags, calling API for all {KeyCount} keys", keyList.Count); var freshResp = await API.Features.CheckFlagsAsync(requestBody); var apiResults = freshResp.Data.Flags.ToDictionary(f => f.Flag); @@ -516,7 +517,7 @@ public async Task> CheckFlags( } catch (Exception cacheEx) { - _logger.Error("Error caching flag result: {0}", cacheEx.Message); + _logger.LogError(cacheEx, "Error caching flag result"); } } } @@ -537,7 +538,7 @@ public async Task> CheckFlags( } catch (Exception ex) { - _logger.Error("Error checking flags: {0}", ex.Message); + _logger.LogError(ex, "Error checking flags"); return (keyList ?? new List()).Select(k => new CheckFlagResponseData { Flag = k, @@ -569,12 +570,12 @@ public async Task> CheckFlags( UserId = result.UserId }); } - _logger.Debug("All {0} flags evaluated via Datastream", keys.Count); + _logger.LogDebug("All {KeyCount} flags evaluated via Datastream", keys.Count); return results; } catch (Exception ex) { - _logger.Debug("Datastream CheckFlags failed ({0}), falling back to API", ex.Message); + _logger.LogDebug(ex, "Datastream CheckFlags failed, falling back to API"); return null; } } @@ -629,12 +630,12 @@ public void Track(string eventName, Dictionary? company = null, var success = _datastreamClient.UpdateCompanyMetrics(eventBody); if (!success) { - _logger.Error("Failed to update company metrics: datastream update failed"); + _logger.LogError("Failed to update company metrics: datastream update failed"); } } catch (Exception ex) { - _logger.Error("Failed to update company metrics: {0}", ex.Message); + _logger.LogError(ex, "Failed to update company metrics"); } } } @@ -657,7 +658,7 @@ private void EnqueueEvent(EventType eventType, OneOfMIT + + + + + + + + + + $(MSBuildThisFileDirectory)RulesEngine/Utils/GeneratedModelHash.cs