diff --git a/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj b/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj index 89d295439..791cf75a6 100644 --- a/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj +++ b/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj @@ -13,10 +13,16 @@ + + + + + + @@ -36,6 +42,21 @@ + + + Always + + + Always + + + Always + + + Always + + + full True diff --git a/Snowflake.Data.Tests/TestLog4Net.config b/Snowflake.Data.Tests/TestLog4Net.config new file mode 100644 index 000000000..72de5b815 --- /dev/null +++ b/Snowflake.Data.Tests/TestLog4Net.config @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Snowflake.Data.Tests/TestNLog.config b/Snowflake.Data.Tests/TestNLog.config new file mode 100644 index 000000000..6dd1b6ce8 --- /dev/null +++ b/Snowflake.Data.Tests/TestNLog.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/Snowflake.Data.Tests/TestSerilog.config b/Snowflake.Data.Tests/TestSerilog.config new file mode 100644 index 000000000..65bf5c82c --- /dev/null +++ b/Snowflake.Data.Tests/TestSerilog.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs index 847d57c51..60a3ab272 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs @@ -33,7 +33,7 @@ public static void AfterAll() { Directory.Delete(s_workingDirectory, true); } - + [Test] public void TestThatParsesConfigFile() { @@ -59,12 +59,12 @@ public void TestThatParsesConfigFileWithNullValues(string filePath) // act var config = parser.Parse(filePath); - + // assert Assert.IsNotNull(config); Assert.IsNotNull(config.CommonProps); Assert.IsNull(config.CommonProps.LogLevel); - Assert.IsNull(config.CommonProps.LogPath); + Assert.IsNull(config.CommonProps.LogPath); } [Test] @@ -77,20 +77,20 @@ public void TestThatReturnsNullWhenNothingToParse(string noFilePath) // act var config = parser.Parse(noFilePath); - + // assert Assert.IsNull(config); } - + [Test] public void TestThatFailsWhenTheFileDoesNotExist() { // arrange var parser = new EasyLoggingConfigParser(); - + // act var thrown = Assert.Throws(() => parser.Parse(NotExistingFilePath)); - + // assert Assert.IsNotNull(thrown); Assert.AreEqual("Finding easy logging configuration failed", thrown.Message); diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs index f61d51503..6e971ff9e 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs @@ -13,7 +13,7 @@ public class EasyLoggingConfigProviderTest { private const string FilePathFromConnectionString = "/Users/dotnet/config.json"; private const string FilePathToUse = "/home/config.json"; - + [Test] public void TestThatProvidesConfiguration() { @@ -31,7 +31,7 @@ public void TestThatProvidesConfiguration() // act var result = configProvider.ProvideConfig(FilePathFromConnectionString); - + // assert Assert.AreSame(config, result); } @@ -45,11 +45,11 @@ public void TestThatReturnsNullWhenNoConfigurationFound() var configProvider = new EasyLoggingConfigProvider(configFinder.Object, configParser.Object); configFinder .Setup(finder => finder.FindConfigFilePath(FilePathFromConnectionString)) - .Returns((string) null); - + .Returns((string)null); + // act var result = configProvider.ProvideConfig(FilePathFromConnectionString); - + // assert Assert.IsNull(result); configParser.Verify(parser => parser.Parse(It.IsAny()), Times.Never); diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs index d61cfaeec..7b6e827db 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs @@ -19,7 +19,7 @@ public void TestThatGetsLogLevelValueIgnoringLetterCase(string loglevelString, E { // act var logLevel = EasyLoggingLogLevelExtensions.From(loglevelString); - + // assert Assert.AreEqual(expectedLogLevel, logLevel); } @@ -29,7 +29,7 @@ public void TestThatFailsToParseLogLevelFromUnknownValue() { // act var thrown = Assert.Throws(() => EasyLoggingLogLevelExtensions.From("unknown")); - + // assert Assert.IsNotNull(thrown); Assert.AreEqual("Requested value 'unknown' was not found.", thrown.Message); diff --git a/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs b/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs index 4dfba8c83..b3aaab92e 100644 --- a/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs @@ -16,7 +16,7 @@ namespace Snowflake.Data.Tests.UnitTests.Logger [TestFixture, NonParallelizable] public class EasyLoggerManagerTest { - + private const string InfoMessage = "Easy logging Info message"; private const string DebugMessage = "Easy logging Debug message"; private const string WarnMessage = "Easy logging Warn message"; @@ -26,7 +26,7 @@ public class EasyLoggerManagerTest [ThreadStatic] private static string t_directoryLogPath; - + [OneTimeTearDown] public static void CleanUp() { @@ -44,14 +44,46 @@ public void AfterEach() { EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Warn, t_directoryLogPath); } - + [Test] public void TestThatChangesLogLevel() { // arrange - var logger = SFLoggerFactory.GetLogger(); + var logger = SFLoggerFactory.GetSFLogger(); + + // act + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Off, t_directoryLogPath); + + // assert + Assert.IsFalse(logger.IsDebugEnabled()); + Assert.IsFalse(logger.IsInfoEnabled()); + Assert.IsFalse(logger.IsWarnEnabled()); + Assert.IsFalse(logger.IsErrorEnabled()); + Assert.IsFalse(logger.IsFatalEnabled()); + + // act + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Fatal, t_directoryLogPath); + + // assert + Assert.IsFalse(logger.IsDebugEnabled()); + Assert.IsFalse(logger.IsInfoEnabled()); + Assert.IsFalse(logger.IsWarnEnabled()); + Assert.IsFalse(logger.IsErrorEnabled()); + Assert.IsTrue(logger.IsFatalEnabled()); + + // act + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Error, t_directoryLogPath); + + // assert + Assert.IsFalse(logger.IsDebugEnabled()); + Assert.IsFalse(logger.IsInfoEnabled()); + Assert.IsFalse(logger.IsWarnEnabled()); + Assert.IsTrue(logger.IsErrorEnabled()); + Assert.IsTrue(logger.IsFatalEnabled()); + + // act EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Warn, t_directoryLogPath); - + // assert Assert.IsFalse(logger.IsDebugEnabled()); Assert.IsFalse(logger.IsInfoEnabled()); @@ -74,9 +106,9 @@ public void TestThatChangesLogLevel() public void TestThatLogsToProperFileWithProperLogLevelOnly() { // arrange - var logger = SFLoggerFactory.GetLogger(); + var logger = SFLoggerFactory.GetSFLogger(); EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Info, t_directoryLogPath); - + // act logger.Debug(DebugMessage); logger.Info(InfoMessage); @@ -91,8 +123,52 @@ public void TestThatLogsToProperFileWithProperLogLevelOnly() Assert.That(logLines, Has.Exactly(1).Matches(s => s.Contains(WarnMessage))); Assert.That(logLines, Has.Exactly(1).Matches(s => s.Contains(ErrorMessage))); Assert.That(logLines, Has.Exactly(1).Matches(s => s.Contains(FatalMessage))); + + // arrange + File.Delete(FindLogFilePath(t_directoryLogPath)); + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Debug, t_directoryLogPath); + + // act + logger.Debug(DebugMessage); + + // assert + logLines = File.ReadLines(FindLogFilePath(t_directoryLogPath)); + Assert.That(logLines, Has.Exactly(1).Matches(s => s.Contains(DebugMessage))); } - + + [Test] + public void TestThatRollsLogIfSizeIsTooBig() + { + // arrange + const int expecetedBackupLogCount = 2; + var logger = SFLoggerFactory.GetSFLogger(); + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Trace, t_directoryLogPath); + + var appenders = SFLogRepository.s_rootLogger.GetAppenders(); + appenders.Remove(appenders[0]); + var randomFileName = $"snowflake_dotnet_{Path.GetRandomFileName()}"; + var logFileName = randomFileName.Substring(0, randomFileName.Length - 4) + ".log"; + appenders.Add(new SFRollingFileAppender() + { + _name = "RollingFileAppender", + _logFilePath = Path.Combine(t_directoryLogPath, logFileName), + _maximumFileSizeInBytes = 1, + _maxSizeRollBackups = expecetedBackupLogCount, + _patternLayout = EasyLoggerManager.PatternLayout() + }); + + // act + for (int i = 0; i < 5; i++) + { + logger.Debug(DebugMessage); + System.Threading.Thread.Sleep(1000); + } + var backupLogs = Directory.GetFiles(t_directoryLogPath, $"{logFileName}.*.bak"); + + // assert + Assert.AreEqual(expecetedBackupLogCount, backupLogs.Length); + } + [Test] public void TestThatOnlyUnknownFieldsAreLogged() { @@ -138,7 +214,7 @@ private static string FindLogFilePath(string directoryLogPath) Assert.AreEqual(1, files.Length); return files.First(); } - + private static void RemoveEasyLoggingLogFiles() { Directory.GetFiles(s_logsDirectory) diff --git a/Snowflake.Data.Tests/UnitTests/Logger/ILoggerTest.cs b/Snowflake.Data.Tests/UnitTests/Logger/ILoggerTest.cs new file mode 100644 index 000000000..d0e3959b5 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Logger/ILoggerTest.cs @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using NUnit.Framework; +using Snowflake.Data.Log; +using Microsoft.Extensions.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using NLog.Extensions.Logging; +using Serilog; +using Serilog.Extensions.Logging; +using System.IO; +using System; + +namespace Snowflake.Data.Tests.UnitTests +{ + [TestFixture, NonParallelizable] + class ILoggerTest + { + private const string InfoMessage = "Info message"; + private const string DebugMessage = "Debug message"; + private const string WarnMessage = "Warn message"; + private const string ErrorMessage = "Error message"; + private const string CriticalMessage = "critical message"; + + private const string Log4NetFileName = "test_log4net.log"; + private const string SerilogFileName = "test_serilog.log"; + private const string NlogFileName = "test_nlog.log"; + + public abstract class ILoggerBaseTest + { + protected ILogger _logger; + protected string _logFile; + + [OneTimeSetUp] + public void BeforeTest() + { + SFLoggerFactory.EnableCustomLogger(); + } + + [OneTimeTearDown] + public void AfterTest() + { + // Return to default setting + SFLoggerFactory.UseDefaultLogger(); + SFLoggerFactory.DisableCustomLogger(); + if (_logFile != null) + { + File.Delete(_logFile); + _logFile = null; + } + } + + [Test] + public void TestUsingDefaultLogger() + { + var originalLogger = SFLoggerFactory.GetCustomLogger(); + SFLoggerFactory.UseDefaultLogger(); + _logger = SFLoggerFactory.GetCustomLogger(); + Assert.IsInstanceOf(_logger); + SFLoggerFactory.SetCustomLogger(originalLogger); + } + + [Test] + public void TestSettingCustomLogger() + { + var originalLogger = SFLoggerFactory.GetCustomLogger(); + SFLoggerFactory.SetCustomLogger(new ILoggerEmptyImpl()); + _logger = SFLoggerFactory.GetCustomLogger(); + Assert.IsInstanceOf(_logger); + SFLoggerFactory.SetCustomLogger(originalLogger); + } + + [Test] + public void TestBeginScope( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + + if (_logger is ILoggerEmptyImpl) + { + Assert.Throws(() => _logger.BeginScope("Test")); + } + } + + [Test] + public void TestIsDebugEnabled( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + Assert.AreEqual(isEnabled, _logger.IsEnabled(LogLevel.Debug)); + } + + [Test] + public void TestIsInfoEnabled( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + Assert.AreEqual(isEnabled, _logger.IsEnabled(LogLevel.Information)); + } + + [Test] + public void TestIsWarnEnabled( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + Assert.AreEqual(isEnabled, _logger.IsEnabled(LogLevel.Warning)); + } + + [Test] + public void TestIsErrorEnabled( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + Assert.AreEqual(isEnabled, _logger.IsEnabled(LogLevel.Error)); + } + + [Test] + public void TestIsFatalEnabled( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + Assert.AreEqual(isEnabled, _logger.IsEnabled(LogLevel.Critical)); + } + + private ILogger GetLogger(bool isEnabled) + { + if (isEnabled) + { + SFLoggerFactory.EnableCustomLogger(); + } + else + { + SFLoggerFactory.DisableCustomLogger(); + } + + return SFLoggerFactory.GetCustomLogger(); + } + + [Test] + public void TestThatLogsToProperFileWithProperLogLevelOnly() + { + _logger = SFLoggerFactory.GetCustomLogger(); + + // act + _logger.LogDebug(DebugMessage); + _logger.LogInformation(InfoMessage); + _logger.LogWarning(WarnMessage); + _logger.LogError(ErrorMessage); + _logger.LogCritical(CriticalMessage); + + // assert + using (FileStream logFileStream = new FileStream(_logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + using (StreamReader logFileReader = new StreamReader(logFileStream)) + { + string logLines = logFileReader.ReadToEnd(); + Assert.IsTrue(logLines.Contains(DebugMessage)); + Assert.IsTrue(logLines.Contains(InfoMessage)); + Assert.IsTrue(logLines.Contains(WarnMessage)); + Assert.IsTrue(logLines.Contains(ErrorMessage)); + Assert.IsTrue(logLines.Contains(CriticalMessage)); + } + } + } + } + + [TestFixture] + public class Log4NetTest : ILoggerBaseTest + { + [OneTimeSetUp] + public void SetUp() + { + Environment.SetEnvironmentVariable("TEST_LOG4NET_FILE_NAME", Log4NetFileName); + var factory = LoggerFactory.Create( + builder => builder + .AddLog4Net("TestLog4Net.config") + .SetMinimumLevel(LogLevel.Trace)); + + var log4netLogger = factory.CreateLogger("Log4NetTest"); + SFLoggerFactory.SetCustomLogger(log4netLogger); + _logFile = Log4NetFileName; + } + } + + [TestFixture] + public class SerilogTest : ILoggerBaseTest + { + [OneTimeSetUp] + public void SetUp() + { + var loggerSerilog = new LoggerConfiguration() + //.ReadFrom.Xml("TestSerilog.Config") + .MinimumLevel.Verbose() + .WriteTo.File(SerilogFileName) + .CreateLogger(); + + var serilogLogger = new SerilogLoggerFactory(loggerSerilog).CreateLogger("SerilogTest"); + SFLoggerFactory.SetCustomLogger(serilogLogger); + _logFile = SerilogFileName; + } + } + + [TestFixture] + public class NlogTest : ILoggerBaseTest + { + [OneTimeSetUp] + public void SetUp() + { + Environment.SetEnvironmentVariable("TEST_NLOG_FILE_NAME", NlogFileName); + var factory = LoggerFactory.Create( + builder => builder + .AddNLog("TestNLog.config") + .SetMinimumLevel(LogLevel.Trace)); + + var nlogLogger = factory.CreateLogger("NlogTest"); + SFLoggerFactory.SetCustomLogger(nlogLogger); + _logFile = NlogFileName; + } + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerPairTest.cs b/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerPairTest.cs new file mode 100644 index 000000000..8555b0fcd --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerPairTest.cs @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using NUnit.Framework; +using Snowflake.Data.Configuration; +using Snowflake.Data.Log; +using System; + +namespace Snowflake.Data.Tests.UnitTests +{ + [TestFixture, NonParallelizable] + class SFLoggerPairTest + { + SFLogger _loggerPair; + + [OneTimeSetUp] + public static void BeforeAll() + { + // Log level defaults to Warn on net6.0 builds in github actions + // Set the root level to Debug + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Debug, "STDOUT"); + } + + [OneTimeTearDown] + public static void AfterAll() + { + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Warn, "STDOUT"); + } + + [SetUp] + public void BeforeTest() + { + _loggerPair = SFLoggerFactory.GetLogger(); + } + + [TearDown] + public void AfterTest() + { + // Return to default setting + SFLoggerFactory.UseDefaultSFLogger(); + } + + [Test] + public void TestUsingSFLogger() + { + SFLoggerFactory.UseDefaultSFLogger(); + _loggerPair = SFLoggerFactory.GetLogger(); + Assert.IsInstanceOf(_loggerPair); + } + + [Test] + public void TestIsDebugEnabled( + [Values(false, true)] bool isEnabled) + { + _loggerPair = GetLogger(isEnabled); + + Assert.AreEqual(isEnabled, _loggerPair.IsDebugEnabled()); + _loggerPair.Debug("debug log message", new Exception("test exception")); + } + + [Test] + public void TestIsInfoEnabled( + [Values(false, true)] bool isEnabled) + { + _loggerPair = GetLogger(isEnabled); + + Assert.AreEqual(isEnabled, _loggerPair.IsInfoEnabled()); + _loggerPair.Info("info log message", new Exception("test exception")); + } + + [Test] + public void TestIsWarnEnabled( + [Values(false, true)] bool isEnabled) + { + _loggerPair = GetLogger(isEnabled); + + Assert.AreEqual(isEnabled, _loggerPair.IsWarnEnabled()); + _loggerPair.Warn("warn log message", new Exception("test exception")); + } + + [Test] + public void TestIsErrorEnabled( + [Values(false, true)] bool isEnabled) + { + _loggerPair = GetLogger(isEnabled); + + Assert.AreEqual(isEnabled, _loggerPair.IsErrorEnabled()); + _loggerPair.Error("error log message", new Exception("test exception")); + } + + [Test] + public void TestIsFatalEnabled( + [Values(false, true)] bool isEnabled) + { + _loggerPair = GetLogger(isEnabled); + + Assert.AreEqual(isEnabled, _loggerPair.IsFatalEnabled()); + _loggerPair.Fatal("fatal log message", new Exception("test exception")); + } + + [Test] + public void TestGetAppenders() + { + Assert.Throws(() => _loggerPair.GetAppenders()); + } + + [Test] + public void TestAddAppender() + { + Assert.Throws(() => _loggerPair.AddAppender(new SFConsoleAppender())); + } + + [Test] + public void TestRemoveAppender() + { + Assert.Throws(() => _loggerPair.RemoveAppender(new SFConsoleAppender())); + } + + [Test] + public void TestSetLevel() + { + Assert.Throws(() => _loggerPair.SetLevel(LoggingEvent.DEBUG)); + } + + private SFLogger GetLogger(bool isEnabled) + { + if (isEnabled) + { + SFLoggerFactory.UseDefaultSFLogger(); + } + else + { + SFLoggerFactory.UseEmptySFLogger(); + } + + return SFLoggerFactory.GetLogger(); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerTest.cs b/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerTest.cs index 710a8d645..f0a2da54a 100644 --- a/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerTest.cs @@ -1,14 +1,14 @@ -/* - * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using NUnit.Framework; using Snowflake.Data.Configuration; +using Snowflake.Data.Log; +using System; namespace Snowflake.Data.Tests.UnitTests { - using NUnit.Framework; - using Snowflake.Data.Log; - [TestFixture, NonParallelizable] class SFLoggerTest { @@ -27,27 +27,27 @@ public static void AfterAll() { EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Warn, "STDOUT"); } - - [TearDown] public void AfterTest() + + [TearDown] + public void AfterTest() { // Return to default setting - SFLoggerFactory.useDefaultLogger(); - SFLoggerFactory.enableLogger(); + SFLoggerFactory.UseDefaultSFLogger(); } [Test] - public void TestUsingDefaultLogger() + public void TestUsingSFLogger() { - SFLoggerFactory.useDefaultLogger(); - _logger = SFLoggerFactory.GetLogger(); - Assert.IsInstanceOf(_logger); + SFLoggerFactory.UseDefaultSFLogger(); + _logger = SFLoggerFactory.GetSFLogger(); + Assert.IsInstanceOf(_logger); } [Test] - public void TestSettingCustomLogger() + public void TestUsingEmptyLogger() { - SFLoggerFactory.Instance(new SFLoggerEmptyImpl()); - _logger = SFLoggerFactory.GetLogger(); + SFLoggerFactory.UseEmptySFLogger(); + _logger = SFLoggerFactory.GetSFLogger(); Assert.IsInstanceOf(_logger); } @@ -58,6 +58,7 @@ public void TestIsDebugEnabled( _logger = GetLogger(isEnabled); Assert.AreEqual(isEnabled, _logger.IsDebugEnabled()); + _logger.Debug("debug log message", new Exception("test exception")); } [Test] @@ -67,6 +68,7 @@ public void TestIsInfoEnabled( _logger = GetLogger(isEnabled); Assert.AreEqual(isEnabled, _logger.IsInfoEnabled()); + _logger.Info("info log message", new Exception("test exception")); } [Test] @@ -76,6 +78,7 @@ public void TestIsWarnEnabled( _logger = GetLogger(isEnabled); Assert.AreEqual(isEnabled, _logger.IsWarnEnabled()); + _logger.Warn("warn log message", new Exception("test exception")); } [Test] @@ -85,6 +88,7 @@ public void TestIsErrorEnabled( _logger = GetLogger(isEnabled); Assert.AreEqual(isEnabled, _logger.IsErrorEnabled()); + _logger.Error("error log message", new Exception("test exception")); } [Test] @@ -94,20 +98,89 @@ public void TestIsFatalEnabled( _logger = GetLogger(isEnabled); Assert.AreEqual(isEnabled, _logger.IsFatalEnabled()); + _logger.Fatal("fatal log message", new Exception("test exception")); + } + + [Test] + public void TestGetAppenders( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + if (isEnabled) + { + var appenders = _logger.GetAppenders(); + Assert.IsInstanceOf(appenders[0]); + } + else + { + Assert.Throws(() => _logger.GetAppenders()); + } + } + + [Test] + public void TestAddAppender( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + if (isEnabled) + { + var appenders = _logger.GetAppenders(); + Assert.AreEqual(1, appenders.Count); + _logger.AddAppender(new SFConsoleAppender()); + Assert.AreEqual(2, appenders.Count); + } + else + { + Assert.Throws(() => _logger.AddAppender(new SFConsoleAppender())); + } + } + + [Test] + public void TestRemoveAppender( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + if (isEnabled) + { + var appenders = _logger.GetAppenders(); + Assert.AreEqual(1, appenders.Count); + _logger.RemoveAppender(appenders[0]); + Assert.AreEqual(0, appenders.Count); + } + else + { + Assert.Throws(() => _logger.RemoveAppender(new SFConsoleAppender())); + } + } + + [Test] + public void TestSetLevel( + [Values(false, true)] bool isEnabled) + { + _logger = GetLogger(isEnabled); + if (isEnabled) + { + _logger.SetLevel(LoggingEvent.DEBUG); + Assert.AreEqual(LoggingEvent.DEBUG, ((SFLoggerImpl)_logger)._level); + } + else + { + Assert.Throws(() => _logger.SetLevel(LoggingEvent.DEBUG)); + } } private SFLogger GetLogger(bool isEnabled) { if (isEnabled) { - SFLoggerFactory.enableLogger(); + SFLoggerFactory.UseDefaultSFLogger(); } else { - SFLoggerFactory.disableLogger(); + SFLoggerFactory.UseEmptySFLogger(); } - return SFLoggerFactory.GetLogger(); + return SFLoggerFactory.GetSFLogger(false); } } } diff --git a/Snowflake.Data.Tests/log4net.config b/Snowflake.Data.Tests/log4net.config new file mode 100644 index 000000000..e461d54b5 --- /dev/null +++ b/Snowflake.Data.Tests/log4net.config @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs b/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs index d417eb4d8..d42c2573a 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs @@ -13,15 +13,15 @@ namespace Snowflake.Data.Configuration { internal class EasyLoggingConfigFinder { - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - + private static readonly SFLogger s_logger = SFLoggerFactory.GetSFLogger(); + internal const string ClientConfigFileName = "sf_client_config.json"; internal const string ClientConfigEnvironmentName = "SF_CLIENT_CONFIG_FILE"; private readonly FileOperations _fileOperations; private readonly UnixOperations _unixOperations; private readonly EnvironmentOperations _environmentOperations; - + public static readonly EasyLoggingConfigFinder Instance = new EasyLoggingConfigFinder(FileOperations.Instance, UnixOperations.Instance, EnvironmentOperations.Instance); internal EasyLoggingConfigFinder(FileOperations fileOperations, UnixOperations unixFileOperations, EnvironmentOperations environmentOperations) @@ -47,7 +47,7 @@ public virtual string FindConfigFilePath(string configFilePathFromConnectionStri } return configFilePath; } - + private string GetFilePathEnvironmentVariable() { var filePath = _environmentOperations.GetEnvironmentVariable(ClientConfigEnvironmentName); @@ -69,7 +69,7 @@ private string GetFilePathFromInputParameter(string filePath, string inputDescri private string GetHomeDirectory() => HomeDirectoryProvider.HomeDirectory(_environmentOperations); private string GetFilePathFromDriverLocation() => SearchForConfigInDirectory(() => _environmentOperations.GetExecutionDirectory(), "driver"); - + private string SearchForConfigInDirectory(Func directoryProvider, string directoryDescription) { try diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs b/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs index dbc6820b4..48b244b57 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs @@ -15,7 +15,7 @@ namespace Snowflake.Data.Configuration { internal class EasyLoggingConfigParser { - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private static readonly SFLogger s_logger = SFLoggerFactory.GetSFLogger(); public static readonly EasyLoggingConfigParser Instance = new EasyLoggingConfigParser(); @@ -45,7 +45,8 @@ private string TryToReadFile(string filePath) private ClientConfig TryToParseFile(string fileContent) { - try { + try + { var config = JsonConvert.DeserializeObject(fileContent); Validate(config); CheckForUnknownFields(fileContent); diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs b/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs index 32788fae0..027d6b695 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs @@ -15,7 +15,7 @@ internal class EasyLoggingConfigProvider internal EasyLoggingConfigProvider() : this(EasyLoggingConfigFinder.Instance, EasyLoggingConfigParser.Instance) { } - + internal EasyLoggingConfigProvider(EasyLoggingConfigFinder finder, EasyLoggingConfigParser configParser) { _finder = finder; diff --git a/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs b/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs index 450a9bcf1..0b6ebe31c 100644 --- a/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs +++ b/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs @@ -6,9 +6,10 @@ namespace Snowflake.Data.Configuration { - internal enum EasyLoggingLogLevel + public enum EasyLoggingLogLevel { Off, + Fatal, Error, Warn, Info, diff --git a/Snowflake.Data/Core/ArrowResultSet.cs b/Snowflake.Data/Core/ArrowResultSet.cs old mode 100755 new mode 100644 diff --git a/Snowflake.Data/Core/HttpUtil.cs b/Snowflake.Data/Core/HttpUtil.cs old mode 100755 new mode 100644 diff --git a/Snowflake.Data/Core/SFBlockingChunkDownloaderV3.cs b/Snowflake.Data/Core/SFBlockingChunkDownloaderV3.cs index 2e19146aa..955af16a4 100755 --- a/Snowflake.Data/Core/SFBlockingChunkDownloaderV3.cs +++ b/Snowflake.Data/Core/SFBlockingChunkDownloaderV3.cs @@ -1,239 +1,239 @@ -/* - * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. - */ - -using System; -using System.IO.Compression; -using System.IO; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Net.Http; -using Newtonsoft.Json; -using System.Diagnostics; -using Newtonsoft.Json.Serialization; -using Snowflake.Data.Log; - -namespace Snowflake.Data.Core -{ - class SFBlockingChunkDownloaderV3 : IChunkDownloader - { - static private SFLogger logger = SFLoggerFactory.GetLogger(); - - private List chunkDatas = new List(); - - private string qrmk; - - private int nextChunkToDownloadIndex; - - private int nextChunkToConsumeIndex; - - // External cancellation token, used to stop donwload - private CancellationToken externalCancellationToken; - - private readonly int prefetchSlot; - - private readonly IRestRequester _RestRequester; - - private readonly SFSessionProperties sessionProperies; - - private Dictionary chunkHeaders; - - private readonly SFBaseResultSet ResultSet; - - private readonly List chunkInfos; - - private readonly List> taskQueues; - - public SFBlockingChunkDownloaderV3(int colCount, - List chunkInfos, string qrmk, - Dictionary chunkHeaders, - CancellationToken cancellationToken, - SFBaseResultSet ResultSet, - ResultFormat resultFormat) - { - this.qrmk = qrmk; - this.chunkHeaders = chunkHeaders; - this.nextChunkToDownloadIndex = 0; - this.ResultSet = ResultSet; - this._RestRequester = ResultSet.sfStatement.SfSession.restRequester; - this.sessionProperies = ResultSet.sfStatement.SfSession.properties; - this.prefetchSlot = Math.Min(chunkInfos.Count, GetPrefetchThreads(ResultSet)); - this.chunkInfos = chunkInfos; - this.nextChunkToConsumeIndex = 0; - this.taskQueues = new List>(); - externalCancellationToken = cancellationToken; - - for (int i=0; i sessionParameters = resultSet.sfStatement.SfSession.ParameterMap; - String val = (String)sessionParameters[SFSessionParameter.CLIENT_PREFETCH_THREADS]; - return Int32.Parse(val); - } - - public async Task GetNextChunkAsync() - { - logger.Info($"NextChunkToConsume: {nextChunkToConsumeIndex}, NextChunkToDownload: {nextChunkToDownloadIndex}"); - if (nextChunkToConsumeIndex < chunkInfos.Count) - { - Task chunk = taskQueues[nextChunkToConsumeIndex % prefetchSlot]; - - if (nextChunkToDownloadIndex < chunkInfos.Count && nextChunkToConsumeIndex > 0) - { - BaseResultChunk reusableChunk = chunkDatas[nextChunkToDownloadIndex % prefetchSlot]; - reusableChunk.Reset(chunkInfos[nextChunkToDownloadIndex], nextChunkToDownloadIndex); - - taskQueues[nextChunkToDownloadIndex % prefetchSlot] = DownloadChunkAsync(new DownloadContextV3() - { - chunk = reusableChunk, - qrmk = this.qrmk, - chunkHeaders = this.chunkHeaders, - cancellationToken = externalCancellationToken - }); - nextChunkToDownloadIndex++; - - // in case of one slot we need to return the chunk already downloaded - if (prefetchSlot == 1) - { - chunk = taskQueues[0]; - } - } - nextChunkToConsumeIndex++; - return await chunk; - } - else - { - return await Task.FromResult(null); - } - } - - private async Task DownloadChunkAsync(DownloadContextV3 downloadContext) - { - BaseResultChunk chunk = downloadContext.chunk; - int backOffInSec = 1; - bool retry = false; - int retryCount = 0; - int maxRetry = int.Parse(sessionProperies[SFSessionProperty.MAXHTTPRETRIES]); - - do - { - retry = false; - - S3DownloadRequest downloadRequest = - new S3DownloadRequest() - { - Url = new UriBuilder(chunk.Url).Uri, - qrmk = downloadContext.qrmk, - // s3 download request timeout to one hour - RestTimeout = TimeSpan.FromHours(1), - HttpTimeout = Timeout.InfiniteTimeSpan, // Disable timeout for each request - chunkHeaders = downloadContext.chunkHeaders, - sid = ResultSet.sfStatement.SfSession.sessionId - }; - - using (var httpResponse = await _RestRequester.GetAsync(downloadRequest, downloadContext.cancellationToken) - .ConfigureAwait(continueOnCapturedContext: false)) - using (Stream stream = await httpResponse.Content.ReadAsStreamAsync() - .ConfigureAwait(continueOnCapturedContext: false)) - { - // retry on chunk downloading since the retry logic in HttpClient.RetryHandler - // doesn't cover this. The GET request could be succeeded but network error - // still could happen during reading chunk data from stream and that needs - // retry as well. - try - { - IEnumerable encoding; - if (httpResponse.Content.Headers.TryGetValues("Content-Encoding", out encoding)) - { - if (String.Compare(encoding.First(), "gzip", true) == 0) - { - using (Stream streamGzip = new GZipStream(stream, CompressionMode.Decompress)) - { - await ParseStreamIntoChunk(streamGzip, chunk).ConfigureAwait(false); - } - } - else - { - await ParseStreamIntoChunk(stream, chunk).ConfigureAwait(false); - } - } - else - { - await ParseStreamIntoChunk(stream, chunk).ConfigureAwait(false); - } - } - catch (Exception e) - { - if ((maxRetry <= 0) || (retryCount < maxRetry)) - { - logger.Debug($"Retry {retryCount}/{maxRetry} of parse stream to chunk error: " + e.Message); - retry = true; - // reset the chunk before retry in case there could be garbage - // data left from last attempt - chunk.ResetForRetry(); - await Task.Delay(TimeSpan.FromSeconds(backOffInSec), downloadContext.cancellationToken).ConfigureAwait(false); - ++retryCount; - // Set next backoff time - backOffInSec = backOffInSec * 2; - if (backOffInSec > HttpUtil.MAX_BACKOFF) - { - backOffInSec = HttpUtil.MAX_BACKOFF; - } - } - else - { - //parse error - logger.Error("Failed retries of parse stream to chunk error: " + e.Message); - throw new Exception("Parse stream to chunk error: " + e.Message); - } - } - } - } while (retry); - logger.Info($"Succeed downloading chunk #{chunk.ChunkIndex}"); - return chunk; - } - - private async Task ParseStreamIntoChunk(Stream content, BaseResultChunk resultChunk) - { - IChunkParser parser = ChunkParserFactory.Instance.GetParser(resultChunk.ResultFormat, content); - await parser.ParseChunk(resultChunk); - } - } - - class DownloadContextV3 - { - public BaseResultChunk chunk { get; set; } - - public string qrmk { get; set; } - - public Dictionary chunkHeaders { get; set; } - - public CancellationToken cancellationToken { get; set; } - } -} +/* + * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO.Compression; +using System.IO; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http; +using Newtonsoft.Json; +using System.Diagnostics; +using Newtonsoft.Json.Serialization; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Core +{ + class SFBlockingChunkDownloaderV3 : IChunkDownloader + { + static private SFLogger logger = SFLoggerFactory.GetLogger(); + + private List chunkDatas = new List(); + + private string qrmk; + + private int nextChunkToDownloadIndex; + + private int nextChunkToConsumeIndex; + + // External cancellation token, used to stop donwload + private CancellationToken externalCancellationToken; + + private readonly int prefetchSlot; + + private readonly IRestRequester _RestRequester; + + private readonly SFSessionProperties sessionProperies; + + private Dictionary chunkHeaders; + + private readonly SFBaseResultSet ResultSet; + + private readonly List chunkInfos; + + private readonly List> taskQueues; + + public SFBlockingChunkDownloaderV3(int colCount, + List chunkInfos, string qrmk, + Dictionary chunkHeaders, + CancellationToken cancellationToken, + SFBaseResultSet ResultSet, + ResultFormat resultFormat) + { + this.qrmk = qrmk; + this.chunkHeaders = chunkHeaders; + this.nextChunkToDownloadIndex = 0; + this.ResultSet = ResultSet; + this._RestRequester = ResultSet.sfStatement.SfSession.restRequester; + this.sessionProperies = ResultSet.sfStatement.SfSession.properties; + this.prefetchSlot = Math.Min(chunkInfos.Count, GetPrefetchThreads(ResultSet)); + this.chunkInfos = chunkInfos; + this.nextChunkToConsumeIndex = 0; + this.taskQueues = new List>(); + externalCancellationToken = cancellationToken; + + for (int i=0; i sessionParameters = resultSet.sfStatement.SfSession.ParameterMap; + String val = (String)sessionParameters[SFSessionParameter.CLIENT_PREFETCH_THREADS]; + return Int32.Parse(val); + } + + public async Task GetNextChunkAsync() + { + logger.Info($"NextChunkToConsume: {nextChunkToConsumeIndex}, NextChunkToDownload: {nextChunkToDownloadIndex}"); + if (nextChunkToConsumeIndex < chunkInfos.Count) + { + Task chunk = taskQueues[nextChunkToConsumeIndex % prefetchSlot]; + + if (nextChunkToDownloadIndex < chunkInfos.Count && nextChunkToConsumeIndex > 0) + { + BaseResultChunk reusableChunk = chunkDatas[nextChunkToDownloadIndex % prefetchSlot]; + reusableChunk.Reset(chunkInfos[nextChunkToDownloadIndex], nextChunkToDownloadIndex); + + taskQueues[nextChunkToDownloadIndex % prefetchSlot] = DownloadChunkAsync(new DownloadContextV3() + { + chunk = reusableChunk, + qrmk = this.qrmk, + chunkHeaders = this.chunkHeaders, + cancellationToken = externalCancellationToken + }); + nextChunkToDownloadIndex++; + + // in case of one slot we need to return the chunk already downloaded + if (prefetchSlot == 1) + { + chunk = taskQueues[0]; + } + } + nextChunkToConsumeIndex++; + return await chunk; + } + else + { + return await Task.FromResult(null); + } + } + + private async Task DownloadChunkAsync(DownloadContextV3 downloadContext) + { + BaseResultChunk chunk = downloadContext.chunk; + int backOffInSec = 1; + bool retry = false; + int retryCount = 0; + int maxRetry = int.Parse(sessionProperies[SFSessionProperty.MAXHTTPRETRIES]); + + do + { + retry = false; + + S3DownloadRequest downloadRequest = + new S3DownloadRequest() + { + Url = new UriBuilder(chunk.Url).Uri, + qrmk = downloadContext.qrmk, + // s3 download request timeout to one hour + RestTimeout = TimeSpan.FromHours(1), + HttpTimeout = Timeout.InfiniteTimeSpan, // Disable timeout for each request + chunkHeaders = downloadContext.chunkHeaders, + sid = ResultSet.sfStatement.SfSession.sessionId + }; + + using (var httpResponse = await _RestRequester.GetAsync(downloadRequest, downloadContext.cancellationToken) + .ConfigureAwait(continueOnCapturedContext: false)) + using (Stream stream = await httpResponse.Content.ReadAsStreamAsync() + .ConfigureAwait(continueOnCapturedContext: false)) + { + // retry on chunk downloading since the retry logic in HttpClient.RetryHandler + // doesn't cover this. The GET request could be succeeded but network error + // still could happen during reading chunk data from stream and that needs + // retry as well. + try + { + IEnumerable encoding; + if (httpResponse.Content.Headers.TryGetValues("Content-Encoding", out encoding)) + { + if (String.Compare(encoding.First(), "gzip", true) == 0) + { + using (Stream streamGzip = new GZipStream(stream, CompressionMode.Decompress)) + { + await ParseStreamIntoChunk(streamGzip, chunk).ConfigureAwait(false); + } + } + else + { + await ParseStreamIntoChunk(stream, chunk).ConfigureAwait(false); + } + } + else + { + await ParseStreamIntoChunk(stream, chunk).ConfigureAwait(false); + } + } + catch (Exception e) + { + if ((maxRetry <= 0) || (retryCount < maxRetry)) + { + logger.Debug($"Retry {retryCount}/{maxRetry} of parse stream to chunk error: " + e.Message); + retry = true; + // reset the chunk before retry in case there could be garbage + // data left from last attempt + chunk.ResetForRetry(); + await Task.Delay(TimeSpan.FromSeconds(backOffInSec), downloadContext.cancellationToken).ConfigureAwait(false); + ++retryCount; + // Set next backoff time + backOffInSec = backOffInSec * 2; + if (backOffInSec > HttpUtil.MAX_BACKOFF) + { + backOffInSec = HttpUtil.MAX_BACKOFF; + } + } + else + { + //parse error + logger.Error("Failed retries of parse stream to chunk error: " + e.Message); + throw new Exception("Parse stream to chunk error: " + e.Message); + } + } + } + } while (retry); + logger.Info($"Succeed downloading chunk #{chunk.ChunkIndex}"); + return chunk; + } + + private async Task ParseStreamIntoChunk(Stream content, BaseResultChunk resultChunk) + { + IChunkParser parser = ChunkParserFactory.Instance.GetParser(resultChunk.ResultFormat, content); + await parser.ParseChunk(resultChunk); + } + } + + class DownloadContextV3 + { + public BaseResultChunk chunk { get; set; } + + public string qrmk { get; set; } + + public Dictionary chunkHeaders { get; set; } + + public CancellationToken cancellationToken { get; set; } + } +} diff --git a/Snowflake.Data/Core/Session/EasyLoggingStarter.cs b/Snowflake.Data/Core/Session/EasyLoggingStarter.cs index 3b07d349c..2358d8441 100644 --- a/Snowflake.Data/Core/Session/EasyLoggingStarter.cs +++ b/Snowflake.Data/Core/Session/EasyLoggingStarter.cs @@ -14,7 +14,7 @@ namespace Snowflake.Data.Core { internal class EasyLoggingStarter { - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private static readonly SFLogger s_logger = SFLoggerFactory.GetSFLogger(); private readonly EasyLoggingConfigProvider _easyLoggingConfigProvider; diff --git a/Snowflake.Data/Logger/EasyLoggerManager.cs b/Snowflake.Data/Logger/EasyLoggerManager.cs index 18ffe818b..8e364fc73 100644 --- a/Snowflake.Data/Logger/EasyLoggerManager.cs +++ b/Snowflake.Data/Logger/EasyLoggerManager.cs @@ -5,9 +5,6 @@ using System; using System.IO; using System.Linq; -using log4net; -using log4net.Appender; -using log4net.Layout; using Snowflake.Data.Configuration; namespace Snowflake.Data.Log @@ -18,23 +15,21 @@ internal class EasyLoggerManager private readonly object _lockForExclusiveConfigure = new object(); - private const string AppenderPrefix = "SFEasyLogging"; + internal const string AppenderPrefix = "SFEasyLogging"; private readonly EasyLoggingLevelMapper _levelMapper = EasyLoggingLevelMapper.Instance; public virtual void ReconfigureEasyLogging(EasyLoggingLogLevel easyLoggingLogLevel, string logsPath) { - var log4netLevel = _levelMapper.ToLog4NetLevel(easyLoggingLogLevel); + var sfLoggerLevel = _levelMapper.ToLoggingEventLevel(easyLoggingLogLevel); lock (_lockForExclusiveConfigure) { - var repository = (log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository(); - var rootLogger = (log4net.Repository.Hierarchy.Logger)repository.GetLogger("Snowflake.Data"); - rootLogger.Level = log4netLevel; + var rootLogger = SFLogRepository.GetRootLogger(); + rootLogger.SetLevel(sfLoggerLevel); var appender = IsStdout(logsPath) ? AddConsoleAppender(rootLogger) : AddRollingFileAppender(rootLogger, logsPath); RemoveOtherEasyLoggingAppenders(rootLogger, appender); - repository.RaiseConfigurationChanged(EventArgs.Empty); } } @@ -45,27 +40,24 @@ internal static bool IsStdout(string logsPath) internal void ResetEasyLogging(EasyLoggingLogLevel easyLoggingLogLevel) { - var log4netLevel = _levelMapper.ToLog4NetLevel(easyLoggingLogLevel); + var sfLoggerLevel = _levelMapper.ToLoggingEventLevel(easyLoggingLogLevel); lock (_lockForExclusiveConfigure) { - var repository = (log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository(); - var rootLogger = (log4net.Repository.Hierarchy.Logger)repository.GetLogger("Snowflake.Data"); - rootLogger.Level = log4netLevel; + var rootLogger = SFLogRepository.GetRootLogger(); + rootLogger.SetLevel(sfLoggerLevel); RemoveOtherEasyLoggingAppenders(rootLogger, null); - repository.RaiseConfigurationChanged(EventArgs.Empty); } } internal static bool HasEasyLoggingAppender() { - var repository = (log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository(); - var rootLogger = (log4net.Repository.Hierarchy.Logger)repository.GetLogger("Snowflake.Data"); - return rootLogger.Appenders.ToArray().Any(IsEasyLoggingAppender); + var rootLogger = SFLogRepository.GetRootLogger(); + return rootLogger.GetAppenders().ToArray().Any(IsEasyLoggingAppender); } - private static void RemoveOtherEasyLoggingAppenders(log4net.Repository.Hierarchy.Logger logger, IAppender appender) + private static void RemoveOtherEasyLoggingAppenders(SFLogger logger, SFAppender appender) { - var existingAppenders = logger.Appenders.ToArray(); + var existingAppenders = logger.GetAppenders().ToArray(); foreach (var existingAppender in existingAppenders) { if (IsEasyLoggingAppender(existingAppender) && existingAppender != appender) @@ -75,67 +67,60 @@ private static void RemoveOtherEasyLoggingAppenders(log4net.Repository.Hierarchy } } - private static IAppender AddRollingFileAppender(log4net.Repository.Hierarchy.Logger logger, + private static SFAppender AddRollingFileAppender(SFLogger logger, string directoryPath) { var patternLayout = PatternLayout(); var randomFileName = $"snowflake_dotnet_{Path.GetRandomFileName()}"; var logFileName = randomFileName.Substring(0, randomFileName.Length - 4) + ".log"; - var appender = new RollingFileAppender + var appender = new SFRollingFileAppender { - Layout = patternLayout, - AppendToFile = true, - File = Path.Combine(directoryPath, logFileName), - Name = $"{AppenderPrefix}RollingFileAppender", - StaticLogFileName = true, - RollingStyle = RollingFileAppender.RollingMode.Size, - MaximumFileSize = "1GB", - MaxSizeRollBackups = 2, - PreserveLogFileNameExtension = true, - LockingModel = new FileAppender.MinimalLock() + _patternLayout = patternLayout, + _logFilePath = Path.Combine(directoryPath, logFileName), + _name = $"{AppenderPrefix}RollingFileAppender", + _maximumFileSizeInBytes = 1000000000, // "1GB" + _maxSizeRollBackups = 2, }; appender.ActivateOptions(); logger.AddAppender(appender); return appender; } - private static bool IsEasyLoggingAppender(IAppender appender) + private static bool IsEasyLoggingAppender(SFAppender appender) { - if (appender.GetType() == typeof(ConsoleAppender)) + if (appender.GetType() == typeof(SFConsoleAppender)) { - var consoleAppender = (ConsoleAppender)appender; - return consoleAppender.Name != null && consoleAppender.Name.StartsWith(AppenderPrefix); + var consoleAppender = (SFConsoleAppender)appender; + return consoleAppender._name != null && consoleAppender._name.StartsWith(AppenderPrefix); } - if (appender.GetType() == typeof(RollingFileAppender)) + if (appender.GetType() == typeof(SFRollingFileAppender)) { - var rollingFileAppender = (RollingFileAppender)appender; - return rollingFileAppender.Name != null && rollingFileAppender.Name.StartsWith(AppenderPrefix); + var rollingFileAppender = (SFRollingFileAppender)appender; + return rollingFileAppender._name != null && rollingFileAppender._name.StartsWith(AppenderPrefix); } return false; } - private static IAppender AddConsoleAppender(log4net.Repository.Hierarchy.Logger logger) + private static SFAppender AddConsoleAppender(SFLogger logger) { var patternLayout = PatternLayout(); - var appender = new ConsoleAppender() + var appender = new SFConsoleAppender() { - Layout = patternLayout, - Name = $"{AppenderPrefix}ConsoleAppender" + _patternLayout = patternLayout, + _name = $"{AppenderPrefix}ConsoleAppender" }; - appender.ActivateOptions(); logger.AddAppender(appender); return appender; } - private static PatternLayout PatternLayout() + internal static PatternLayout PatternLayout() { var patternLayout = new PatternLayout { - ConversionPattern = "[%date] [%t] [%-5level] [%logger] %message%newline" + _conversionPattern = "[%date] [%t] [%-5level] [%logger] %message%newline" }; - patternLayout.ActivateOptions(); return patternLayout; } } diff --git a/Snowflake.Data/Logger/EasyLoggingLevelMapper.cs b/Snowflake.Data/Logger/EasyLoggingLevelMapper.cs index 609265e30..6364321f2 100644 --- a/Snowflake.Data/Logger/EasyLoggingLevelMapper.cs +++ b/Snowflake.Data/Logger/EasyLoggingLevelMapper.cs @@ -3,7 +3,6 @@ */ using System; -using log4net.Core; using Snowflake.Data.Configuration; namespace Snowflake.Data.Log @@ -12,18 +11,19 @@ internal class EasyLoggingLevelMapper { public static readonly EasyLoggingLevelMapper Instance = new EasyLoggingLevelMapper(); - public Level ToLog4NetLevel(EasyLoggingLogLevel level) + public LoggingEvent ToLoggingEventLevel(EasyLoggingLogLevel level) { switch (level) { - case EasyLoggingLogLevel.Off: return Level.Off; - case EasyLoggingLogLevel.Error: return Level.Error; - case EasyLoggingLogLevel.Warn: return Level.Warn; - case EasyLoggingLogLevel.Info: return Level.Info; - case EasyLoggingLogLevel.Debug: return Level.Debug; - case EasyLoggingLogLevel.Trace: return Level.Trace; + case EasyLoggingLogLevel.Off: return LoggingEvent.OFF; + case EasyLoggingLogLevel.Fatal: return LoggingEvent.FATAL; + case EasyLoggingLogLevel.Error: return LoggingEvent.ERROR; + case EasyLoggingLogLevel.Warn: return LoggingEvent.WARN; + case EasyLoggingLogLevel.Info: return LoggingEvent.INFO; + case EasyLoggingLogLevel.Debug: return LoggingEvent.DEBUG; + case EasyLoggingLogLevel.Trace: return LoggingEvent.TRACE; default: throw new Exception("Unknown log level"); } } } -} \ No newline at end of file +} diff --git a/Snowflake.Data/Logger/ILoggerEmptyImpl.cs b/Snowflake.Data/Logger/ILoggerEmptyImpl.cs new file mode 100644 index 000000000..4a0d83374 --- /dev/null +++ b/Snowflake.Data/Logger/ILoggerEmptyImpl.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. + */ + +using Microsoft.Extensions.Logging; +using System; + +namespace Snowflake.Data.Log +{ + // Empty implementation of SFLogger + // Used when SFLoggerFactory.disableLogger() is called. + + class ILoggerEmptyImpl : ILogger + { + IDisposable ILogger.BeginScope(TState state) + { + throw new NotImplementedException(); + } + + bool ILogger.IsEnabled(LogLevel logLevel) + { + return false; + } + + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + return; + } + } + +} diff --git a/Snowflake.Data/Logger/Log4netImpl.cs b/Snowflake.Data/Logger/Log4netImpl.cs deleted file mode 100755 index 4a9ec00d6..000000000 --- a/Snowflake.Data/Logger/Log4netImpl.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. - */ - -using log4net; -using System; - -namespace Snowflake.Data.Log -{ - // Default implementation for SFLogger - - class Log4NetImpl : SFLogger - { - private readonly ILog logger; - - public Log4NetImpl(ILog logger) - { - this.logger = logger; - } - - public bool IsDebugEnabled() - { - return logger.IsDebugEnabled; - } - - public bool IsInfoEnabled() - { - return logger.IsInfoEnabled; - } - - public bool IsWarnEnabled() - { - return logger.IsWarnEnabled; - } - - public bool IsErrorEnabled() - { - return logger.IsErrorEnabled; - } - - public bool IsFatalEnabled() - { - return logger.IsFatalEnabled; - } - - public void Debug(string msg, Exception ex = null) - { - msg = SecretDetector.MaskSecrets(msg).maskedText; - logger.Debug(msg, ex); - } - - public void Info(string msg, Exception ex = null) - { - msg = SecretDetector.MaskSecrets(msg).maskedText; - logger.Info(msg, ex); - } - - public void Warn(string msg, Exception ex = null) - { - msg = SecretDetector.MaskSecrets(msg).maskedText; - logger.Warn(msg, ex); - } - - - public void Error(string msg, Exception ex = null) - { - msg = SecretDetector.MaskSecrets(msg).maskedText; - logger.Error(msg, ex); - } - - public void Fatal(string msg, Exception ex = null) - { - msg = SecretDetector.MaskSecrets(msg).maskedText; - logger.Fatal(msg, ex); - } - } - -} diff --git a/Snowflake.Data/Logger/SFAppender.cs b/Snowflake.Data/Logger/SFAppender.cs new file mode 100644 index 000000000..becfebc7f --- /dev/null +++ b/Snowflake.Data/Logger/SFAppender.cs @@ -0,0 +1,13 @@ +/* + //* Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; + +namespace Snowflake.Data.Log +{ + public interface SFAppender + { + void Append(string logLevel, string message, Type type, Exception ex = null); + } +} diff --git a/Snowflake.Data/Logger/SFAppenderImpl.cs b/Snowflake.Data/Logger/SFAppenderImpl.cs new file mode 100644 index 000000000..38ee5089b --- /dev/null +++ b/Snowflake.Data/Logger/SFAppenderImpl.cs @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Snowflake.Data.Log; +using System; +using System.IO; +using System.Linq; +using System.Threading; + +internal class PatternLayout +{ + internal string _conversionPattern; + + public PatternLayout() { } + + public string Format(string logLevel, string message, Type type) + { + var formattedMessage = _conversionPattern + .Replace("%date", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")) + .Replace("%t", Thread.CurrentThread.ManagedThreadId.ToString()) + .Replace("%-5level", logLevel) + .Replace("%logger", type.ToString()) + .Replace("%message", message) + .Replace("%newline", "\n"); + + return formattedMessage; + } +} + +internal class SFConsoleAppender : SFAppender +{ + internal string _name; + internal PatternLayout _patternLayout; + + public SFConsoleAppender() { } + + public void Append(string logLevel, string message, Type type, Exception ex = null) + { + var formattedMessage = _patternLayout.Format(logLevel, message, type); + Console.Write(formattedMessage); + if (ex != null) + { + Console.WriteLine(ex.Message); + } + } +} + +internal class SFRollingFileAppender : SFAppender +{ + internal string _name; + internal string _logFilePath; + internal long _maximumFileSizeInBytes; + internal int _maxSizeRollBackups; + internal PatternLayout _patternLayout; + + public SFRollingFileAppender() { } + + public void Append(string logLevel, string message, Type type, Exception ex = null) + { + var formattedMessage = _patternLayout.Format(logLevel, message, type); + try + { + if (LogFileIsTooLarge()) + { + RollLogFile(); + } + + using (FileStream fs = new FileStream(_logFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) + { + using (var writer = new StreamWriter(fs)) + { + writer.Write(formattedMessage); + if (ex != null) + { + writer.WriteLine(ex.Message); + } + } + } + } + catch (Exception logEx) + { + Console.WriteLine($"Unable to log the following message:\n{formattedMessage}" + + $"Due to the error: {logEx.Message}\n"); + } + } + + public void ActivateOptions() + { + var logDir = Path.GetDirectoryName(_logFilePath); + if (!Directory.Exists(logDir)) + { + Directory.CreateDirectory(logDir); + } + if (!File.Exists(_logFilePath)) + { + File.Create(_logFilePath).Dispose(); + } + } + + private bool LogFileIsTooLarge() + { + FileInfo fileInfo = new FileInfo(_logFilePath); + return fileInfo.Exists && fileInfo.Length > _maximumFileSizeInBytes; + } + + private void RollLogFile() + { + string rollFilePath = $"{_logFilePath}.{DateTime.Now:yyyyMMddHHmmss}.bak"; + File.Move(_logFilePath, rollFilePath); + + var logDirectory = Path.GetDirectoryName(_logFilePath); + var logFileName = Path.GetFileName(_logFilePath); + var rollFiles = Directory.GetFiles(logDirectory, $"{logFileName}.*.bak") + .OrderByDescending(f => f) + .Skip(_maxSizeRollBackups); + foreach (var oldRollFile in rollFiles) + { + File.Delete(oldRollFile); + } + } +} diff --git a/Snowflake.Data/Logger/SFLogger.cs b/Snowflake.Data/Logger/SFLogger.cs old mode 100755 new mode 100644 index 28a8a5b3c..167a171f7 --- a/Snowflake.Data/Logger/SFLogger.cs +++ b/Snowflake.Data/Logger/SFLogger.cs @@ -1,39 +1,45 @@ -/* - * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. - */ - -using System; -using System.Collections.Generic; -using System.Text; - -namespace Snowflake.Data.Log -{ - interface SFLogger - { - bool IsDebugEnabled(); - - bool IsInfoEnabled(); - - bool IsWarnEnabled(); - - bool IsErrorEnabled(); - - bool IsFatalEnabled(); - - void Debug(string msg, Exception ex = null); - - void Info(string msg, Exception ex = null); - - void Warn(string msg, Exception ex = null); - - void Error(string msg, Exception ex = null); - - void Fatal(string msg, Exception ex = null); - } - - enum LoggingEvent - { - DEBUG, INFO, WARN, ERROR, FATAL - } - -} \ No newline at end of file +/* + * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.Collections.Generic; + +namespace Snowflake.Data.Log +{ + public interface SFLogger + { + bool IsDebugEnabled(); + + bool IsInfoEnabled(); + + bool IsWarnEnabled(); + + bool IsErrorEnabled(); + + bool IsFatalEnabled(); + + void Debug(string msg, Exception ex = null); + + void Info(string msg, Exception ex = null); + + void Warn(string msg, Exception ex = null); + + void Error(string msg, Exception ex = null); + + void Fatal(string msg, Exception ex = null); + + List GetAppenders(); + + void AddAppender(SFAppender appender); + + void RemoveAppender(SFAppender appender); + + void SetLevel(LoggingEvent level); + } + + public enum LoggingEvent + { + OFF, TRACE, DEBUG, INFO, WARN, ERROR, FATAL + } +} diff --git a/Snowflake.Data/Logger/SFLoggerEmptyImpl.cs b/Snowflake.Data/Logger/SFLoggerEmptyImpl.cs index 2508c4309..1e6eb2e68 100644 --- a/Snowflake.Data/Logger/SFLoggerEmptyImpl.cs +++ b/Snowflake.Data/Logger/SFLoggerEmptyImpl.cs @@ -1,8 +1,9 @@ -/* +/* * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ - - using System; + +using System; +using System.Collections.Generic; namespace Snowflake.Data.Log { @@ -60,6 +61,26 @@ public void Fatal(string msg, Exception ex) { return; } + + List SFLogger.GetAppenders() + { + throw new NotImplementedException(); + } + + void SFLogger.AddAppender(SFAppender appender) + { + throw new NotImplementedException(); + } + + void SFLogger.RemoveAppender(SFAppender appender) + { + throw new NotImplementedException(); + } + + void SFLogger.SetLevel(LoggingEvent level) + { + throw new NotImplementedException(); + } } } diff --git a/Snowflake.Data/Logger/SFLoggerFactory.cs b/Snowflake.Data/Logger/SFLoggerFactory.cs index e38440167..4e212ab7e 100755 --- a/Snowflake.Data/Logger/SFLoggerFactory.cs +++ b/Snowflake.Data/Logger/SFLoggerFactory.cs @@ -1,51 +1,88 @@ -/* +/* * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ -using log4net; +using Microsoft.Extensions.Logging; +using System.IO; namespace Snowflake.Data.Log { - class SFLoggerFactory + public class SFLoggerFactory { - private static bool isLoggerEnabled = true; + private static bool s_isCustomLoggerEnabled = false; - private static SFLogger logger = null; + private static bool s_isSFLoggerEnabled = false; - private SFLoggerFactory() + private static bool s_useDefaultSFLogger = true; + + private static ILogger s_customLogger = null; + + public static void UseEmptySFLogger() + { + s_useDefaultSFLogger = false; + } + + public static void UseDefaultSFLogger() + { + s_useDefaultSFLogger = true; + } + + public static void DisableCustomLogger() { + s_isCustomLoggerEnabled = false; } - public static void disableLogger() + public static void EnableCustomLogger() { - isLoggerEnabled = false; + s_isCustomLoggerEnabled = true; } - public static void enableLogger() + public static void UseDefaultLogger() { - isLoggerEnabled = true; + s_customLogger = null; } - public static void useDefaultLogger() + public static void SetCustomLogger(ILogger customLogger) { - logger = null; + s_customLogger = customLogger; + SFLoggerPair.s_customLogger = s_customLogger; } - public static void Instance(SFLogger customLogger) - { - logger = customLogger; + internal static SFLogger GetLogger() + { + return new SFLoggerPair(GetSFLogger(), GetCustomLogger()); } - public static SFLogger GetLogger() + internal static SFLogger GetSFLogger(bool useFileAppender = true) { // If true, return the default/specified logger - if (isLoggerEnabled) + if (s_useDefaultSFLogger) { - // If no logger specified, use the default logger: log4net - if (logger == null) + var logger = new SFLoggerImpl(typeof(T)); + if (!s_isSFLoggerEnabled) + { + logger.SetLevel(LoggingEvent.OFF); // Logger is disabled by default and can be enabled by the EasyLogging feature + } + if (useFileAppender) { - ILog loggerL = LogManager.GetLogger(typeof(T)); - return new Log4NetImpl(loggerL); + var fileAppender = new SFRollingFileAppender() + { + _name = "RollingFileAppender", + _logFilePath = Path.Combine(Directory.GetCurrentDirectory(), "test_snowflake_log.log"), + _maximumFileSizeInBytes = 1000000000, // "1GB" + _maxSizeRollBackups = 0, + _patternLayout = EasyLoggerManager.PatternLayout() + }; + logger.AddAppender(fileAppender); + } + else + { + var consoleAppender = new SFConsoleAppender() + { + _name = "ConsoleAppender", + _patternLayout = EasyLoggerManager.PatternLayout() + }; + logger.AddAppender(consoleAppender); } return logger; } @@ -55,6 +92,30 @@ public static SFLogger GetLogger() return new SFLoggerEmptyImpl(); } } - } + internal static ILogger GetCustomLogger() + { + // If true, return the default/specified logger + if (s_isCustomLoggerEnabled) + { + // If no logger specified, use the default logger: Microsoft's console logger + if (s_customLogger == null) + { + ILoggerFactory factory = LoggerFactory.Create( + builder => builder + .AddConsole() + .SetMinimumLevel(LogLevel.Trace) + ); + + return factory.CreateLogger(); + } + return s_customLogger; + } + // Else, return the empty logger implementation which outputs nothing + else + { + return new ILoggerEmptyImpl(); + } + } + } } diff --git a/Snowflake.Data/Logger/SFLoggerImpl.cs b/Snowflake.Data/Logger/SFLoggerImpl.cs new file mode 100644 index 000000000..0c09dfdc3 --- /dev/null +++ b/Snowflake.Data/Logger/SFLoggerImpl.cs @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Snowflake.Data.Log; +using System; +using System.Collections.Generic; + +public static class SFLogRepository +{ + internal static SFLogger s_rootLogger = new SFLoggerImpl(typeof(SFLogRepository)); + + internal static SFLogger GetRootLogger() + { + return s_rootLogger; + } +} + +public class SFLoggerImpl : SFLogger +{ + private readonly Type _type; + internal readonly List _appenders; + internal LoggingEvent _level; + + private bool _isDebugEnabled; + private bool _isInfoEnabled; + private bool _isWarnEnabled; + private bool _isErrorEnabled; + private bool _isFatalEnabled; + + internal SFLoggerImpl(Type type, LoggingEvent level = LoggingEvent.DEBUG) + { + _appenders = new List(); + _type = type; + SetLevel(level); + } + + public void SetLevel(LoggingEvent level) + { + _level = level; + SetEnableValues(); + } + + private void SetEnableValues() + { + var enabled = _level != LoggingEvent.OFF; + _isDebugEnabled = enabled; + _isInfoEnabled = enabled; + _isWarnEnabled = enabled; + _isErrorEnabled = enabled; + _isFatalEnabled = enabled; + + if (enabled) + { + switch (_level) + { + case LoggingEvent.TRACE: + case LoggingEvent.DEBUG: + break; + case LoggingEvent.FATAL: + _isErrorEnabled = false; + goto case LoggingEvent.ERROR; + case LoggingEvent.ERROR: + _isWarnEnabled = false; + goto case LoggingEvent.WARN; + case LoggingEvent.WARN: + _isInfoEnabled = false; + goto case LoggingEvent.INFO; + case LoggingEvent.INFO: + _isDebugEnabled = false; + break; + } + } + } + + public List GetAppenders() + { + return _appenders; + } + + public void AddAppender(SFAppender appender) + { + _appenders.Add(appender); + } + + public void RemoveAppender(SFAppender appender) + { + _appenders.Remove(appender); + } + + public bool IsDebugEnabled() + { + return SFLogRepository.s_rootLogger == this ? + _isDebugEnabled : + SFLogRepository.s_rootLogger.IsDebugEnabled(); + } + + public bool IsInfoEnabled() + { + return SFLogRepository.s_rootLogger == this ? + _isInfoEnabled : + SFLogRepository.s_rootLogger.IsInfoEnabled(); + } + + public bool IsWarnEnabled() + { + return SFLogRepository.s_rootLogger == this ? + _isWarnEnabled : + SFLogRepository.s_rootLogger.IsWarnEnabled(); + } + + public bool IsErrorEnabled() + { + return SFLogRepository.s_rootLogger == this ? + _isErrorEnabled : + SFLogRepository.s_rootLogger.IsErrorEnabled(); + } + + public bool IsFatalEnabled() + { + return SFLogRepository.s_rootLogger == this ? + _isFatalEnabled : + SFLogRepository.s_rootLogger.IsFatalEnabled(); + } + + public void Debug(string msg, Exception ex = null) + { + if (IsDebugEnabled()) + { + Log(LoggingEvent.DEBUG.ToString(), msg, ex); + } + } + + public void Info(string msg, Exception ex = null) + { + if (IsInfoEnabled()) + { + Log(LoggingEvent.INFO.ToString(), msg, ex); + } + } + + public void Warn(string msg, Exception ex = null) + { + if (IsWarnEnabled()) + { + Log(LoggingEvent.WARN.ToString(), msg, ex); + } + } + + + public void Error(string msg, Exception ex = null) + { + if (IsErrorEnabled()) + { + Log(LoggingEvent.ERROR.ToString(), msg, ex); + } + } + + public void Fatal(string msg, Exception ex = null) + { + if (IsFatalEnabled()) + { + Log(LoggingEvent.FATAL.ToString(), msg, ex); + } + } + + private void Log(string logLevel, string logMessage, Exception ex = null) + { + var rootAppenders = SFLogRepository.s_rootLogger.GetAppenders(); + var appenders = rootAppenders.Count > 0 ? rootAppenders : _appenders; + foreach (var appender in appenders) + { + appender.Append(logLevel, logMessage, _type, ex); + } + } +} diff --git a/Snowflake.Data/Logger/SFLoggerPair.cs b/Snowflake.Data/Logger/SFLoggerPair.cs new file mode 100644 index 000000000..92b176e00 --- /dev/null +++ b/Snowflake.Data/Logger/SFLoggerPair.cs @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Snowflake.Data.Log +{ + public class SFLoggerPair : SFLogger + { + private static SFLogger s_snowflakeLogger; + internal static ILogger s_customLogger; + + public SFLoggerPair(SFLogger snowflakeLogger, ILogger customLogger) + { + s_snowflakeLogger = snowflakeLogger; + s_customLogger = customLogger; + } + + public void Debug(string message, Exception ex = null) + { + message = SecretDetector.MaskSecrets(message).maskedText; + s_snowflakeLogger.Debug(message, ex); + s_customLogger.LogDebug(FormatBrackets(message), ex); + } + + public void Info(string message, Exception ex = null) + { + message = SecretDetector.MaskSecrets(message).maskedText; + s_snowflakeLogger.Info(message, ex); + s_customLogger.LogInformation(FormatBrackets(message), ex); + } + + public void Warn(string message, Exception ex = null) + { + message = SecretDetector.MaskSecrets(message).maskedText; + s_snowflakeLogger.Warn(message, ex); + s_customLogger.LogWarning(FormatBrackets(message), ex); + } + + public void Error(string message, Exception ex = null) + { + message = SecretDetector.MaskSecrets(message).maskedText; + s_snowflakeLogger.Error(message, ex); + s_customLogger.LogError(FormatBrackets(message), ex); + } + + public void Fatal(string message, Exception ex = null) + { + message = SecretDetector.MaskSecrets(message).maskedText; + s_snowflakeLogger.Fatal(message, ex); + s_customLogger.LogCritical(FormatBrackets(message), ex); + } + + public bool IsDebugEnabled() + { + return s_snowflakeLogger.IsDebugEnabled() || + s_customLogger.IsEnabled(LogLevel.Debug); + } + + public bool IsInfoEnabled() + { + return s_snowflakeLogger.IsInfoEnabled() || + s_customLogger.IsEnabled(LogLevel.Information); + } + + public bool IsWarnEnabled() + { + return s_snowflakeLogger.IsWarnEnabled() || + s_customLogger.IsEnabled(LogLevel.Warning); + } + + public bool IsErrorEnabled() + { + return s_snowflakeLogger.IsErrorEnabled() || + s_customLogger.IsEnabled(LogLevel.Error); + } + + public bool IsFatalEnabled() + { + return s_snowflakeLogger.IsFatalEnabled() || + s_customLogger.IsEnabled(LogLevel.Critical); + } + + public List GetAppenders() + { + throw new NotImplementedException(); + } + + public void AddAppender(SFAppender appender) + { + throw new NotImplementedException(); + } + + public void RemoveAppender(SFAppender appender) + { + throw new NotImplementedException(); + } + + public void SetLevel(LoggingEvent level) + { + throw new NotImplementedException(); + } + + private string FormatBrackets(string message) + { + var sb = new StringBuilder(message).Replace("{", "{{").Replace("}", "}}"); + return sb.ToString(); + } + } +} diff --git a/Snowflake.Data/Snowflake.Data.csproj b/Snowflake.Data/Snowflake.Data.csproj index d38c0b0e1..79e1c0899 100644 --- a/Snowflake.Data/Snowflake.Data.csproj +++ b/Snowflake.Data/Snowflake.Data.csproj @@ -22,10 +22,12 @@ + + - +