From 6fceac19acf5f4e0ab68c512d0e56558fd0ed874 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Tue, 19 Sep 2023 10:15:26 +0200 Subject: [PATCH] SNOW-856231 Enable easy logging (#774) ### Description Enable easy logging feature. ### Checklist - [x] Code compiles correctly - [x] Code is formatted according to [Coding Conventions](../CodingConventions.md) - [x] Created tests which fail without the change (if possible) - [x] All tests passing (`dotnet test`) - [x] Extended the README / documentation, if necessary - [x] Provide JIRA issue id (if possible) or GitHub issue id in PR name --- README.md | 93 ++++++++---- .../EasyLoggingConfigFinderTest.cs | 5 + .../EasyLoggingConfigGenerator.cs | 4 + .../EasyLoggingConfigParserTest.cs | 68 +++++++-- .../EasyLoggingConfigProviderTest.cs | 6 +- .../Configuration/EasyLoggingLogLevelTest.cs | 4 + .../UnitTests/Logger/EasyLoggerManagerTest.cs | 117 +++++++++++++++ .../UnitTests/{ => Logger}/SFLoggerTest.cs | 21 +-- .../UnitTests/SFSessionTest.cs | 22 +++ .../Session/EasyLoggingStarterTest.cs | 137 ++++++++++++++++++ Snowflake.Data/Configuration/ClientConfig.cs | 14 ++ .../Configuration/ClientConfigCommonProps.cs | 17 +++ .../Configuration/EasyLoggingCommonProps.cs | 13 -- .../Configuration/EasyLoggingConfig.cs | 10 -- .../Configuration/EasyLoggingConfigFinder.cs | 9 +- .../Configuration/EasyLoggingConfigParser.cs | 33 +++-- .../EasyLoggingConfigProvider.cs | 14 +- .../Configuration/EasyLoggingLogLevel.cs | 4 + .../Configuration/FileOperations.cs | 12 -- .../Core/Session/EasyLoggingStarter.cs | 128 ++++++++++++++++ Snowflake.Data/Core/Session/SFSession.cs | 20 ++- .../Core/Session/SFSessionProperty.cs | 2 + .../Core/Tools/DirectoryOperations.cs | 17 +++ .../Tools}/EnvironmentOperations.cs | 8 +- Snowflake.Data/Core/Tools/FileOperations.cs | 18 +++ Snowflake.Data/Logger/EasyLoggerManager.cs | 116 +++++++++++++++ .../Logger/EasyLoggingLevelMapper.cs | 29 ++++ 27 files changed, 837 insertions(+), 104 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs rename Snowflake.Data.Tests/UnitTests/{ => Logger}/SFLoggerTest.cs (86%) create mode 100644 Snowflake.Data.Tests/UnitTests/Session/EasyLoggingStarterTest.cs create mode 100644 Snowflake.Data/Configuration/ClientConfig.cs create mode 100644 Snowflake.Data/Configuration/ClientConfigCommonProps.cs delete mode 100644 Snowflake.Data/Configuration/EasyLoggingCommonProps.cs delete mode 100644 Snowflake.Data/Configuration/EasyLoggingConfig.cs delete mode 100644 Snowflake.Data/Configuration/FileOperations.cs create mode 100644 Snowflake.Data/Core/Session/EasyLoggingStarter.cs create mode 100644 Snowflake.Data/Core/Tools/DirectoryOperations.cs rename Snowflake.Data/{Configuration => Core/Tools}/EnvironmentOperations.cs (64%) create mode 100644 Snowflake.Data/Core/Tools/FileOperations.cs create mode 100644 Snowflake.Data/Logger/EasyLoggerManager.cs create mode 100644 Snowflake.Data/Logger/EasyLoggingLevelMapper.cs diff --git a/README.md b/README.md index 97f276b5c..440cd0e0a 100644 --- a/README.md +++ b/README.md @@ -133,35 +133,36 @@ i.e "\=\;\=\...". The following table lists all valid connection properties:
-| Connection Property | Required | Comment | -|------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ACCOUNT | Yes | Your full account name might include additional segments that identify the region and cloud platform where your account is hosted | -| APPLICATION | No | **_Snowflake partner use only_**: Specifies the name of a partner application to connect through .NET. The name must match the following pattern: ^\[A-Za-z](\[A-Za-z0-9.-]){1,50}$ (one letter followed by 1 to 50 letter, digit, .,- or, \_ characters). | -| DB | No | | -| HOST | No | Specifies the hostname for your account in the following format: \.snowflakecomputing.com.
If no value is specified, the driver uses \.snowflakecomputing.com. | -| PASSWORD | Depends | Required if AUTHENTICATOR is set to `snowflake` (the default value) or the URL for native SSO through Okta. Ignored for all the other authentication types. | -| ROLE | No | | -| SCHEMA | No | | -| USER | Depends | If AUTHENTICATOR is set to `externalbrowser` this is optional. For native SSO through Okta, set this to the login name for your identity provider (IdP). | -| WAREHOUSE | No | | -| CONNECTION_TIMEOUT | No | Total timeout in seconds when connecting to Snowflake. The default is 120 seconds | -| MAXHTTPRETRIES | No | Maximum number of times to retry failed HTTP requests (default: 7). You can set `MAXHTTPRETRIES=0` to remove the retry limit, but doing so runs the risk of the .NET driver infinitely retrying failed HTTP calls. | -| CLIENT_SESSION_KEEP_ALIVE | No | Whether to keep the current session active after a period of inactivity, or to force the user to login again. If the value is `true`, Snowflake keeps the session active indefinitely, even if there is no activity from the user. If the value is `false`, the user must log in again after four hours of inactivity. The default is `false`. Setting this value overrides the server session property for the current session. | -| DISABLERETRY | No | Set this property to `true` to prevent the driver from reconnecting automatically when the connection fails or drops. The default value is `false`. | -| AUTHENTICATOR | No | The method of authentication. Currently supports the following values:
- snowflake (default): You must also set USER and PASSWORD.
- [the URL for native SSO through Okta](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#native-sso-okta-only): You must also set USER and PASSWORD.
- [externalbrowser](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#browser-based-sso): You must also set USER.
- [snowflake_jwt](https://docs.snowflake.com/en/user-guide/key-pair-auth.html): You must also set PRIVATE_KEY_FILE or PRIVATE_KEY.
- [oauth](https://docs.snowflake.com/en/user-guide/oauth.html): You must also set TOKEN. | BROWSER_RESPONSE_TIMEOUT | No | Number to seconds to wait for authentication in an external browser (default: 120). | -| VALIDATE_DEFAULT_PARAMETERS | No | Whether DB, SCHEMA and WAREHOUSE should be verified when making connection. Default to be true. | -| PRIVATE_KEY_FILE | Depends | The path to the private key file to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt | -| PRIVATE_KEY_PWD | No | The passphrase to use for decrypting the private key, if the key is encrypted. | -| PRIVATE_KEY | Depends | The private key to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt.
If the private key value includes any equal signs (=), make sure to replace each equal sign with two signs (==) to ensure that the connection string is parsed correctly. | -| TOKEN | Depends | The OAuth token to use for OAuth authentication. Must be used in combination with AUTHENTICATOR=oauth. | -| INSECUREMODE | No | Set to true to disable the certificate revocation list check. Default is false. | -| USEPROXY | No | Set to true if you need to use a proxy server. The default value is false.

This parameter was introduced in v2.0.4. | -| PROXYHOST | Depends | The hostname of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | -| PROXYPORT | Depends | The port number of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | -| PROXYUSER | No | The username for authenticating to the proxy server.

This parameter was introduced in v2.0.4. | -| PROXYPASSWORD | Depends | The password for authenticating to the proxy server.

If USEPROXY is `true` and PROXYUSER is set, you must set this parameter.

This parameter was introduced in v2.0.4. | -| NONPROXYHOSTS | No | The list of hosts that the driver should connect to directly, bypassing the proxy server. Separate the hostnames with a pipe symbol (\|). You can also use an asterisk (`*`) as a wildcard.

This parameter was introduced in v2.0.4. | -| FILE_TRANSFER_MEMORY_THRESHOLD | No | The maximum number of bytes to store in memory used in order to provide a file encryption. If encrypting/decrypting file size exeeds provided value a temporary file will be created and the work will be continued in the temporary file instead of memory.
If no value provided 1MB will be used as a default value (that is 1048576 bytes).
It is possible to configure any integer value bigger than zero representing maximal number of bytes to reside in memory. | +| Connection Property | Required | Comment | +|--------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ACCOUNT | Yes | Your full account name might include additional segments that identify the region and cloud platform where your account is hosted | +| APPLICATION | No | **_Snowflake partner use only_**: Specifies the name of a partner application to connect through .NET. The name must match the following pattern: ^\[A-Za-z](\[A-Za-z0-9.-]){1,50}$ (one letter followed by 1 to 50 letter, digit, .,- or, \_ characters). | +| DB | No | | +| HOST | No | Specifies the hostname for your account in the following format: \.snowflakecomputing.com.
If no value is specified, the driver uses \.snowflakecomputing.com. | +| PASSWORD | Depends | Required if AUTHENTICATOR is set to `snowflake` (the default value) or the URL for native SSO through Okta. Ignored for all the other authentication types. | +| ROLE | No | | +| SCHEMA | No | | +| USER | Depends | If AUTHENTICATOR is set to `externalbrowser` this is optional. For native SSO through Okta, set this to the login name for your identity provider (IdP). | +| WAREHOUSE | No | | +| CONNECTION_TIMEOUT | No | Total timeout in seconds when connecting to Snowflake. The default is 120 seconds | +| MAXHTTPRETRIES | No | Maximum number of times to retry failed HTTP requests (default: 7). You can set `MAXHTTPRETRIES=0` to remove the retry limit, but doing so runs the risk of the .NET driver infinitely retrying failed HTTP calls. | +| CLIENT_SESSION_KEEP_ALIVE | No | Whether to keep the current session active after a period of inactivity, or to force the user to login again. If the value is `true`, Snowflake keeps the session active indefinitely, even if there is no activity from the user. If the value is `false`, the user must log in again after four hours of inactivity. The default is `false`. Setting this value overrides the server session property for the current session. | +| DISABLERETRY | No | Set this property to `true` to prevent the driver from reconnecting automatically when the connection fails or drops. The default value is `false`. | +| AUTHENTICATOR | No | The method of authentication. Currently supports the following values:
- snowflake (default): You must also set USER and PASSWORD.
- [the URL for native SSO through Okta](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#native-sso-okta-only): You must also set USER and PASSWORD.
- [externalbrowser](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#browser-based-sso): You must also set USER.
- [snowflake_jwt](https://docs.snowflake.com/en/user-guide/key-pair-auth.html): You must also set PRIVATE_KEY_FILE or PRIVATE_KEY.
- [oauth](https://docs.snowflake.com/en/user-guide/oauth.html): You must also set TOKEN. | BROWSER_RESPONSE_TIMEOUT | No | Number to seconds to wait for authentication in an external browser (default: 120). | +| VALIDATE_DEFAULT_PARAMETERS | No | Whether DB, SCHEMA and WAREHOUSE should be verified when making connection. Default to be true. | +| PRIVATE_KEY_FILE | Depends | The path to the private key file to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt | +| PRIVATE_KEY_PWD | No | The passphrase to use for decrypting the private key, if the key is encrypted. | +| PRIVATE_KEY | Depends | The private key to use for key-pair authentication. Must be used in combination with AUTHENTICATOR=snowflake_jwt.
If the private key value includes any equal signs (=), make sure to replace each equal sign with two signs (==) to ensure that the connection string is parsed correctly. | +| TOKEN | Depends | The OAuth token to use for OAuth authentication. Must be used in combination with AUTHENTICATOR=oauth. | +| INSECUREMODE | No | Set to true to disable the certificate revocation list check. Default is false. | +| USEPROXY | No | Set to true if you need to use a proxy server. The default value is false.

This parameter was introduced in v2.0.4. | +| PROXYHOST | Depends | The hostname of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | +| PROXYPORT | Depends | The port number of the proxy server.

If USEPROXY is set to `true`, you must set this parameter.

This parameter was introduced in v2.0.4. | +| PROXYUSER | No | The username for authenticating to the proxy server.

This parameter was introduced in v2.0.4. | +| PROXYPASSWORD | Depends | The password for authenticating to the proxy server.

If USEPROXY is `true` and PROXYUSER is set, you must set this parameter.

This parameter was introduced in v2.0.4. | +| NONPROXYHOSTS | No | The list of hosts that the driver should connect to directly, bypassing the proxy server. Separate the hostnames with a pipe symbol (\|). You can also use an asterisk (`*`) as a wildcard.

This parameter was introduced in v2.0.4. | +| FILE_TRANSFER_MEMORY_THRESHOLD | No | The maximum number of bytes to store in memory used in order to provide a file encryption. If encrypting/decrypting file size exeeds provided value a temporary file will be created and the work will be continued in the temporary file instead of memory.
If no value provided 1MB will be used as a default value (that is 1048576 bytes).
It is possible to configure any integer value bigger than zero representing maximal number of bytes to reside in memory. | +| CLIENT_CONFIG_FILE | No | The location of the client configuration json file. In this file you can configure easy logging feature. |
@@ -690,6 +691,40 @@ Here is a sample app.config file that uses [log4net](http://logging.apache.org/l ``` +Easy logging +------------ + +The Easy logging feature allows you to change log level of all driver's classes and add extra file appender for logs from driver's classes in runtime. +This feature was introduced to make tracing of driver's logs easier. +The feature is activated by a config file which can be: +1. provided in connection string as `CLIENT_CONFIG_FILE` parameter (eg. `"ACCOUNT=test;USER=test;PASSWORD=test;CLIENT_CONFIG_FILE=C:\\some-path\\client_config.json;"`) +2. provided as environmental variable called `SF_CLIENT_CONFIG_FILE` (eg. `SET SF_CLIENT_CONFIG_FILE=C:\some-path\client_config.json`) +3. found in the driver location by searching for `sf_client_config.json` file +4. found in the home location by searching for `sf_client_config.json` file +5. found in temp directory location by searching for `sf_client_config.json` file + +The search for a config file is executed in the order listed above. + +To minimize the number of searches for a configuration file it is executed only: +- for the first connection +- for the first connection with `CLIENT_CONFIG_FILE` parameter. + +The example of the configuration file is: +```json +{ + "common": { + "log_level": "INFO", + "log_path": "c:\\some-path\\some-directory" + } +} +``` +Available log levels are: `OFF`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`. The log levels are case insensitive. + +The extra logs land into `dotnet` subfolder of given directory `C:\some-path\some-directory` so in this example: `C:\some-path\some-directory\dotnet`. + +If the client uses log4net library for logging in their application enabling easy logging affect the log level in their logs as well. + + Getting the code coverage ---------------- diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs index 952e4773c..07260fea9 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs @@ -1,8 +1,13 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using System; using System.IO; using Moq; using NUnit.Framework; using Snowflake.Data.Configuration; +using Snowflake.Data.Core.Tools; namespace Snowflake.Data.Tests.UnitTests.Configuration { diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigGenerator.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigGenerator.cs index 8f4ec5d62..a1fa1f06d 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigGenerator.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigGenerator.cs @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using System.IO; namespace Snowflake.Data.Tests.UnitTests.Configuration diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs index 692042032..e5d1f3b90 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigParserTest.cs @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System; using System.Collections.Generic; using System.IO; using NUnit.Framework; @@ -6,7 +11,7 @@ namespace Snowflake.Data.Tests.UnitTests.Configuration { - [TestFixture] + [TestFixture, NonParallelizable] public class EasyLoggingConfigParserTest { private const string NotExistingFilePath = "../../../Resources/EasyLogging/not_existing_config.json"; @@ -45,32 +50,72 @@ public void TestThatParsesConfigFile() Assert.AreEqual(LogPath, config.CommonProps.LogPath); } - [Test] - public void TestThatReturnsNullIfFileDoesNotExist( - [Values(null, "", NotExistingFilePath)] - string notExistingFilePath) + [Test, TestCaseSource(nameof(ConfigFilesWithoutValues))] + public void TestThatParsesConfigFileWithNullValues(string filePath) { // arrange var parser = new EasyLoggingConfigParser(); // act - var config = parser.Parse(notExistingFilePath); + var config = parser.Parse(filePath); + + // assert + Assert.IsNotNull(config); + Assert.IsNotNull(config.CommonProps); + Assert.IsNull(config.CommonProps.LogLevel); + Assert.IsNull(config.CommonProps.LogPath); + } + [Test] + [TestCase(null)] + [TestCase("")] + public void TestThatReturnsNullWhenNothingToParse(string noFilePath) + { + // arrange + var parser = new EasyLoggingConfigParser(); + + // 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); + } [Test, TestCaseSource(nameof(WrongConfigFiles))] - public void TestThatReturnsNullIfMissingOrInvalidRequiredFields(string filePath) + public void TestThatFailsIfMissingOrInvalidRequiredFields(string filePath) { // arrange var parser = new EasyLoggingConfigParser(); // act - var config = parser.Parse(filePath); - + var thrown = Assert.Throws(() => parser.Parse(filePath)); // assert - Assert.IsNull(config); + Assert.IsNotNull(thrown); + Assert.IsTrue(thrown.Message == "Parsing easy logging configuration failed"); + } + + public static IEnumerable ConfigFilesWithoutValues() + { + BeforeAll(); + return new[] + { + CreateConfigTempFile(EmptyCommonConfig), + CreateConfigTempFile(Config(null, null)) + }; } public static IEnumerable WrongConfigFiles() @@ -79,9 +124,6 @@ public static IEnumerable WrongConfigFiles() return new[] { CreateConfigTempFile(EmptyConfig), - CreateConfigTempFile(EmptyCommonConfig), - CreateConfigTempFile(Config(null, LogPath)), - CreateConfigTempFile(Config(LogLevel, null)), CreateConfigTempFile(Config("unknown", LogPath)), }; } diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs index 64bc15ac9..f61d51503 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigProviderTest.cs @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using Moq; using NUnit.Framework; using Snowflake.Data.Configuration; @@ -17,7 +21,7 @@ public void TestThatProvidesConfiguration() var configFinder = new Mock(); var configParser = new Mock(); var configProvider = new EasyLoggingConfigProvider(configFinder.Object, configParser.Object); - var config = new EasyLoggingConfig(); + var config = new ClientConfig(); configFinder .Setup(finder => finder.FindConfigFilePath(FilePathFromConnectionString)) .Returns(FilePathToUse); diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs index 68b9b593f..d61cfaeec 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingLogLevelTest.cs @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using System; using NUnit.Framework; using Snowflake.Data.Configuration; diff --git a/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs b/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs new file mode 100644 index 000000000..feeb728b9 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using System.Linq; +using NUnit.Framework; +using Snowflake.Data.Configuration; +using Snowflake.Data.Core; +using Snowflake.Data.Log; + +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"; + private const string ErrorMessage = "Easy logging Error message"; + private const string FatalMessage = "Easy logging Fatal message"; + private static readonly string s_logsDirectory = Path.GetTempPath(); + + [ThreadStatic] + private static string t_directoryLogPath; + + [OneTimeTearDown] + public static void CleanUp() + { + RemoveEasyLoggingLogFiles(); + } + + [SetUp] + public void BeforeEach() + { + t_directoryLogPath = RandomLogsDirectoryPath(); + } + + [TearDown] + public void AfterEach() + { + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Warn, t_directoryLogPath); + } + + [Test] + public void TestThatChangesLogLevel() + { + // arrange + var logger = SFLoggerFactory.GetLogger(); + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Warn, t_directoryLogPath); + + // assert + Assert.IsFalse(logger.IsDebugEnabled()); + Assert.IsFalse(logger.IsInfoEnabled()); + Assert.IsTrue(logger.IsWarnEnabled()); + Assert.IsTrue(logger.IsErrorEnabled()); + Assert.IsTrue(logger.IsFatalEnabled()); + + // act + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Debug, t_directoryLogPath); + + // assert + Assert.IsTrue(logger.IsDebugEnabled()); + Assert.IsTrue(logger.IsInfoEnabled()); + Assert.IsTrue(logger.IsWarnEnabled()); + Assert.IsTrue(logger.IsErrorEnabled()); + Assert.IsTrue(logger.IsFatalEnabled()); + } + + [Test] + public void TestThatLogsToProperFileWithProperLogLevelOnly() + { + // arrange + var logger = SFLoggerFactory.GetLogger(); + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Info, t_directoryLogPath); + + // act + logger.Debug(DebugMessage); + logger.Info(InfoMessage); + logger.Warn(WarnMessage); + logger.Error(ErrorMessage); + logger.Fatal(FatalMessage); + + // assert + var logLines = File.ReadLines(FindLogFilePath(t_directoryLogPath)); + Assert.That(logLines, Has.Exactly(0).Matches(s => s.Contains(DebugMessage))); + Assert.That(logLines, Has.Exactly(1).Matches(s => s.Contains(InfoMessage))); + 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))); + } + + private static string RandomLogsDirectoryPath() + { + var randomName = Path.GetRandomFileName(); + return Path.Combine(s_logsDirectory, $"easy_logging_logs_{randomName}", "dotnet"); + } + + private static string FindLogFilePath(string directoryLogPath) + { + Assert.IsTrue(Directory.Exists(directoryLogPath)); + var files = Directory.GetFiles(directoryLogPath); + Assert.AreEqual(1, files.Length); + return files.First(); + } + + private static void RemoveEasyLoggingLogFiles() + { + Directory.GetFiles(s_logsDirectory) + .Where(filePath => filePath.StartsWith(Path.Combine(s_logsDirectory, "easy_logging_logs"))) + .AsParallel() + .ForAll(filePath => File.Delete(filePath)); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/SFLoggerTest.cs b/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerTest.cs similarity index 86% rename from Snowflake.Data.Tests/UnitTests/SFLoggerTest.cs rename to Snowflake.Data.Tests/UnitTests/Logger/SFLoggerTest.cs index 18e5dd876..710a8d645 100644 --- a/Snowflake.Data.Tests/UnitTests/SFLoggerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Logger/SFLoggerTest.cs @@ -2,29 +2,32 @@ * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. */ +using Snowflake.Data.Configuration; + namespace Snowflake.Data.Tests.UnitTests { - using log4net.Repository.Hierarchy; - using log4net; using NUnit.Framework; using Snowflake.Data.Log; - using log4net.Core; - using System; - - [TestFixture] + + [TestFixture, NonParallelizable] class SFLoggerTest { SFLogger _logger; [OneTimeSetUp] - public void BeforeTest() + public static void BeforeTest() { // Log level defaults to Warn on net6.0 builds in github actions // Set the root level to Debug - ((Hierarchy)LogManager.GetRepository()).Root.Level = Level.Debug; - ((Hierarchy)LogManager.GetRepository()).RaiseConfigurationChanged(EventArgs.Empty); + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Debug, "STDOUT"); } + [OneTimeTearDown] + public static void AfterAll() + { + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Warn, "STDOUT"); + } + [TearDown] public void AfterTest() { // Return to default setting diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 236c7a43b..51635d662 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -2,6 +2,9 @@ * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ +using Snowflake.Data.Configuration; +using Snowflake.Data.Log; + namespace Snowflake.Data.Tests.UnitTests { using Snowflake.Data.Core; @@ -33,5 +36,24 @@ public void TestUpdateDatabaseAndSchema() Assert.AreEqual(schemaName, sfSession.schema); } + [Test] + [TestCase(null)] + [TestCase("/some-path/config.json")] + [TestCase("C:\\some-path\\config.json")] + public void TestThatConfiguresEasyLogging(string configPath) + { + // arrange + var easyLoggingStarter = new Moq.Mock(); + var simpleConnectionString = "account=test;user=test;password=test;"; + var connectionString = configPath == null + ? simpleConnectionString + : $"{simpleConnectionString}client_config_file={configPath};"; + + // act + new SFSession(connectionString, null, easyLoggingStarter.Object); + + // assert + easyLoggingStarter.Verify(starter => starter.Init(configPath)); + } } } diff --git a/Snowflake.Data.Tests/UnitTests/Session/EasyLoggingStarterTest.cs b/Snowflake.Data.Tests/UnitTests/Session/EasyLoggingStarterTest.cs new file mode 100644 index 000000000..00a2ed9b7 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Session/EasyLoggingStarterTest.cs @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using Moq; +using NUnit.Framework; +using Snowflake.Data.Configuration; +using Snowflake.Data.Core; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Tests.UnitTests.Session +{ + [TestFixture] + public class EasyLoggingStarterTest + { + + private const string LogPath = "/some-logs-path/some-folder"; + private const string ConfigPath = "/some-path/config.json"; + private const string AnotherConfigPath = "/another/path"; + private static readonly string s_expectedLogPath = Path.Combine(LogPath, "dotnet"); + + private static readonly ClientConfig s_configWithErrorLevel = new ClientConfig + { + CommonProps = new ClientConfigCommonProps + { + LogLevel = "Error", + LogPath = LogPath + } + }; + + private static readonly ClientConfig s_configWithInfoLevel = new ClientConfig + { + CommonProps = new ClientConfigCommonProps + { + LogLevel = "Info", + LogPath = LogPath + } + }; + + [ThreadStatic] + private static Mock t_easyLoggingProvider; + + [ThreadStatic] + private static Mock t_easyLoggerManager; + + [ThreadStatic] + private static Mock t_directoryOperations; + + [ThreadStatic] + private static EasyLoggingStarter t_easyLoggerStarter; + + [SetUp] + public void BeforeEach() + { + t_easyLoggingProvider = new Mock(); + t_easyLoggerManager = new Mock(); + t_directoryOperations = new Mock(); + t_easyLoggerStarter = new EasyLoggingStarter(t_easyLoggingProvider.Object, t_easyLoggerManager.Object, t_directoryOperations.Object); + } + + [Test] + public void TestThatConfiguresEasyLoggingOnlyOnceWhenInitializedWithConfigPath() + { + // arrange + t_easyLoggingProvider + .Setup(provider => provider.ProvideConfig(ConfigPath)) + .Returns(s_configWithErrorLevel); + t_easyLoggingProvider + .Setup(provider => provider.ProvideConfig(null)) + .Returns(s_configWithInfoLevel); + t_easyLoggingProvider + .Setup(provider => provider.ProvideConfig(AnotherConfigPath)) + .Returns(s_configWithInfoLevel); + + // act + t_easyLoggerStarter.Init(ConfigPath); + + // assert + t_directoryOperations.Verify(d => d.CreateDirectory(s_expectedLogPath), Times.Once); + t_easyLoggerManager.Verify(manager => manager.ReconfigureEasyLogging(EasyLoggingLogLevel.Error, s_expectedLogPath), Times.Once); + + // act + t_easyLoggerStarter.Init(null); + t_easyLoggerStarter.Init(ConfigPath); + t_easyLoggerStarter.Init(AnotherConfigPath); + + // assert + t_easyLoggerManager.VerifyNoOtherCalls(); + } + + [Test] + public void TestThatConfiguresEasyLoggingOnlyOnceForInitializationsWithoutConfigPath() + { + // arrange + t_easyLoggingProvider + .Setup(provider => provider.ProvideConfig(null)) + .Returns(s_configWithErrorLevel); + + // act + t_easyLoggerStarter.Init(null); + t_easyLoggerStarter.Init(null); + + // assert + t_directoryOperations.Verify(d => d.CreateDirectory(s_expectedLogPath), Times.Once); + t_easyLoggerManager.Verify(manager => manager.ReconfigureEasyLogging(EasyLoggingLogLevel.Error, s_expectedLogPath), Times.Once); + } + + [Test] + public void TestThatReconfiguresEasyLoggingWithConfigPathIfNotGivenForTheFirstTime() + { + // arrange + t_easyLoggingProvider + .Setup(provider => provider.ProvideConfig(null)) + .Returns(s_configWithErrorLevel); + t_easyLoggingProvider + .Setup(provider => provider.ProvideConfig(ConfigPath)) + .Returns(s_configWithInfoLevel); + + // act + t_easyLoggerStarter.Init(null); + + // assert + t_directoryOperations.Verify(d => d.CreateDirectory(s_expectedLogPath), Times.Once); + t_easyLoggerManager.Verify(manager => manager.ReconfigureEasyLogging(EasyLoggingLogLevel.Error, s_expectedLogPath), Times.Once); + + // act + t_easyLoggerStarter.Init(ConfigPath); + + // assert + t_easyLoggerManager.Verify(manager => manager.ReconfigureEasyLogging(EasyLoggingLogLevel.Info, s_expectedLogPath), Times.Once); + t_easyLoggerManager.VerifyNoOtherCalls(); + } + } +} diff --git a/Snowflake.Data/Configuration/ClientConfig.cs b/Snowflake.Data/Configuration/ClientConfig.cs new file mode 100644 index 000000000..d67aac3b3 --- /dev/null +++ b/Snowflake.Data/Configuration/ClientConfig.cs @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using Newtonsoft.Json; + +namespace Snowflake.Data.Configuration +{ + internal class ClientConfig + { + [JsonProperty(Required = Required.Always, PropertyName = "common")] + public ClientConfigCommonProps CommonProps { get; set; } + } +} diff --git a/Snowflake.Data/Configuration/ClientConfigCommonProps.cs b/Snowflake.Data/Configuration/ClientConfigCommonProps.cs new file mode 100644 index 000000000..ca6268719 --- /dev/null +++ b/Snowflake.Data/Configuration/ClientConfigCommonProps.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using Newtonsoft.Json; + +namespace Snowflake.Data.Configuration +{ + internal class ClientConfigCommonProps + { + [JsonProperty(PropertyName = "log_level")] + public string LogLevel { get; set; } + + [JsonProperty(PropertyName = "log_path")] + public string LogPath { get; set; } + } +} diff --git a/Snowflake.Data/Configuration/EasyLoggingCommonProps.cs b/Snowflake.Data/Configuration/EasyLoggingCommonProps.cs deleted file mode 100644 index de671fa4e..000000000 --- a/Snowflake.Data/Configuration/EasyLoggingCommonProps.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Newtonsoft.Json; - -namespace Snowflake.Data.Configuration -{ - internal class EasyLoggingCommonProps - { - [JsonProperty(Required = Required.Always, PropertyName = "log_level")] - public string LogLevel { get; set; } - - [JsonProperty(Required = Required.Always, PropertyName = "log_path")] - public string LogPath { get; set; } - } -} diff --git a/Snowflake.Data/Configuration/EasyLoggingConfig.cs b/Snowflake.Data/Configuration/EasyLoggingConfig.cs deleted file mode 100644 index 0ac018731..000000000 --- a/Snowflake.Data/Configuration/EasyLoggingConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace Snowflake.Data.Configuration -{ - internal class EasyLoggingConfig - { - [JsonProperty(Required = Required.Always, PropertyName = "common")] - public EasyLoggingCommonProps CommonProps { get; set; } - } -} diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs b/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs index 9dd43d52e..625da5e06 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs @@ -1,5 +1,10 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using System; using System.IO; +using Snowflake.Data.Core.Tools; namespace Snowflake.Data.Configuration { @@ -10,8 +15,10 @@ internal class EasyLoggingConfigFinder private readonly FileOperations _fileOperations; private readonly EnvironmentOperations _environmentOperations; + + public static readonly EasyLoggingConfigFinder Instance = new EasyLoggingConfigFinder(FileOperations.Instance, EnvironmentOperations.Instance); - public EasyLoggingConfigFinder(FileOperations fileOperations, EnvironmentOperations environmentOperations) + internal EasyLoggingConfigFinder(FileOperations fileOperations, EnvironmentOperations environmentOperations) { _fileOperations = fileOperations; _environmentOperations = environmentOperations; diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs b/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs index b8125c599..f21af2302 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using System; using System.IO; using Newtonsoft.Json; @@ -8,8 +12,10 @@ namespace Snowflake.Data.Configuration internal class EasyLoggingConfigParser { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - - public virtual EasyLoggingConfig Parse(string filePath) + + public static readonly EasyLoggingConfigParser Instance = new EasyLoggingConfigParser(); + + public virtual ClientConfig Parse(string filePath) { var configFile = TryToReadFile(filePath); return configFile == null ? null : TryToParseFile(configFile); @@ -27,28 +33,33 @@ private string TryToReadFile(string filePath) } catch (Exception e) { - s_logger.Error("Finding easy logging configuration failed"); - return null; - } + var errorMessage = "Finding easy logging configuration failed"; + s_logger.Error(errorMessage, e); + throw new Exception(errorMessage); + } } - private EasyLoggingConfig TryToParseFile(string fileContent) + private ClientConfig TryToParseFile(string fileContent) { try { - var config = JsonConvert.DeserializeObject(fileContent); + var config = JsonConvert.DeserializeObject(fileContent); Validate(config); return config; } catch (Exception e) { - s_logger.Error("Parsing easy logging configuration failed"); - return null; + var errorMessage = "Parsing easy logging configuration failed"; + s_logger.Error(errorMessage, e); + throw new Exception(errorMessage); } } - private void Validate(EasyLoggingConfig config) + private void Validate(ClientConfig config) { - EasyLoggingLogLevelExtensions.From(config.CommonProps.LogLevel); + if (config.CommonProps.LogLevel != null) + { + EasyLoggingLogLevelExtensions.From(config.CommonProps.LogLevel); + } } } } diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs b/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs index dbc8490d9..32788fae0 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigProvider.cs @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + namespace Snowflake.Data.Configuration { internal class EasyLoggingConfigProvider @@ -6,13 +10,19 @@ internal class EasyLoggingConfigProvider private readonly EasyLoggingConfigParser _configParser; - public EasyLoggingConfigProvider(EasyLoggingConfigFinder finder, EasyLoggingConfigParser configParser) + public static readonly EasyLoggingConfigProvider Instance = new EasyLoggingConfigProvider(); + + internal EasyLoggingConfigProvider() : this(EasyLoggingConfigFinder.Instance, EasyLoggingConfigParser.Instance) + { + } + + internal EasyLoggingConfigProvider(EasyLoggingConfigFinder finder, EasyLoggingConfigParser configParser) { _finder = finder; _configParser = configParser; } - public EasyLoggingConfig ProvideConfig(string configFilePathFromConnectionString) + public virtual ClientConfig ProvideConfig(string configFilePathFromConnectionString) { var filePath = _finder.FindConfigFilePath(configFilePathFromConnectionString); return filePath == null ? null : _configParser.Parse(filePath); diff --git a/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs b/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs index 8e903180a..450a9bcf1 100644 --- a/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs +++ b/Snowflake.Data/Configuration/EasyLoggingLogLevel.cs @@ -1,3 +1,7 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using System; namespace Snowflake.Data.Configuration diff --git a/Snowflake.Data/Configuration/FileOperations.cs b/Snowflake.Data/Configuration/FileOperations.cs deleted file mode 100644 index d95bb83b1..000000000 --- a/Snowflake.Data/Configuration/FileOperations.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.IO; - -namespace Snowflake.Data.Configuration -{ - internal class FileOperations - { - public virtual bool Exists(string path) - { - return File.Exists(path); - } - } -} diff --git a/Snowflake.Data/Core/Session/EasyLoggingStarter.cs b/Snowflake.Data/Core/Session/EasyLoggingStarter.cs new file mode 100644 index 000000000..9800e5186 --- /dev/null +++ b/Snowflake.Data/Core/Session/EasyLoggingStarter.cs @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System.IO; +using Snowflake.Data.Configuration; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Core +{ + internal class EasyLoggingStarter + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + private readonly EasyLoggingConfigProvider _easyLoggingConfigProvider; + + private readonly EasyLoggerManager _easyLoggerManager; + + private readonly DirectoryOperations _directoryOperations; + + private readonly object _lockForExclusiveInit = new object(); + + private EasyLoggingInitTrialParameters _initTrialParameters = null; + + public static readonly EasyLoggingStarter Instance = new EasyLoggingStarter(EasyLoggingConfigProvider.Instance, + EasyLoggerManager.Instance, DirectoryOperations.Instance); + + internal EasyLoggingStarter( + EasyLoggingConfigProvider easyLoggingConfigProvider, + EasyLoggerManager easyLoggerManager, + DirectoryOperations directoryOperations) + { + _easyLoggingConfigProvider = easyLoggingConfigProvider; + _easyLoggerManager = easyLoggerManager; + _directoryOperations = directoryOperations; + } + + internal EasyLoggingStarter() + { + } + + public virtual void Init(string configFilePathFromConnectionString) + { + lock (_lockForExclusiveInit) + { + if (!AllowedToInitialize(configFilePathFromConnectionString)) + { + return; + } + var config = _easyLoggingConfigProvider.ProvideConfig(configFilePathFromConnectionString); + if (config == null) + { + _initTrialParameters = new EasyLoggingInitTrialParameters(configFilePathFromConnectionString); + return; + } + var logLevel = GetLogLevel(config.CommonProps.LogLevel); + var logPath = GetLogPath(config.CommonProps.LogPath); + _easyLoggerManager.ReconfigureEasyLogging(logLevel, logPath); + _initTrialParameters = new EasyLoggingInitTrialParameters(configFilePathFromConnectionString); + } + } + + private bool AllowedToInitialize(string configFilePathFromConnectionString) + { + var everTriedToInitialize = _initTrialParameters != null; + var triedToInitializeWithoutConfigFile = everTriedToInitialize && !_initTrialParameters.IsConfigFilePathGiven(); + var isGivenConfigFilePath = !string.IsNullOrEmpty(configFilePathFromConnectionString); + var isAllowedToInitialize = !everTriedToInitialize || (triedToInitializeWithoutConfigFile && isGivenConfigFilePath); + if (!isAllowedToInitialize && _initTrialParameters.HasDifferentConfigPath(configFilePathFromConnectionString)) + { + s_logger.Warn($"Easy logging will not be configured for CLIENT_CONFIG_FILE={configFilePathFromConnectionString} because it was previously configured for a different client config"); + } + + return isAllowedToInitialize; + } + + private EasyLoggingLogLevel GetLogLevel(string logLevel) + { + if (string.IsNullOrEmpty(logLevel)) + { + s_logger.Warn("LogLevel in client config not found. Using default value: OFF"); + return EasyLoggingLogLevel.Off; + } + return EasyLoggingLogLevelExtensions.From(logLevel); + } + + private string GetLogPath(string logPath) + { + var logPathOrDefault = logPath; + if (string.IsNullOrEmpty(logPath)) + { + s_logger.Warn("LogPath in client config not found. Using temporary directory as a default value"); + logPathOrDefault = Path.GetTempPath(); + } + var pathWithDotnetSubdirectory = Path.Combine(logPathOrDefault, "dotnet"); + if (!_directoryOperations.Exists(pathWithDotnetSubdirectory)) + { + _directoryOperations.CreateDirectory(pathWithDotnetSubdirectory); + } + + return pathWithDotnetSubdirectory; + } + } + + internal class EasyLoggingInitTrialParameters + { + private readonly string _configFilePathFromConnectionString; + + public EasyLoggingInitTrialParameters( + string configFilePathFromConnectionString) + { + _configFilePathFromConnectionString = configFilePathFromConnectionString; + } + + public bool IsConfigFilePathGiven() + { + return _configFilePathFromConnectionString != null; + } + + public bool HasDifferentConfigPath(string configFilePath) + { + return IsConfigFilePathGiven() + && configFilePath != null + && _configFilePathFromConnectionString != configFilePath; + } + } +} diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index f371369be..6d91aa094 100755 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Security; using System.Web; @@ -14,6 +15,7 @@ using System.Threading.Tasks; using System.Net.Http; using System.Text.RegularExpressions; +using Snowflake.Data.Configuration; namespace Snowflake.Data.Core { @@ -64,6 +66,8 @@ public class SFSession internal static readonly SFSessionHttpClientProperties.Extractor propertiesExtractor = new SFSessionHttpClientProperties.Extractor( new SFSessionHttpClientProxyProperties.Extractor()); + private readonly EasyLoggingStarter _easyLoggingStarter = EasyLoggingStarter.Instance; + private long _startTime = 0; internal string connStr = null; @@ -129,8 +133,18 @@ internal Uri BuildLoginUrl() /// Constructor /// /// A string in the form of "key1=value1;key2=value2" - internal SFSession(String connectionString, SecureString password) + internal SFSession( + String connectionString, + SecureString password) : this(connectionString, password, EasyLoggingStarter.Instance) + { + } + + internal SFSession( + String connectionString, + SecureString password, + EasyLoggingStarter easyLoggingStarter) { + _easyLoggingStarter = easyLoggingStarter; connStr = connectionString; properties = SFSessionProperties.parseConnectionString(connectionString, password); _disableQueryContextCache = bool.Parse(properties[SFSessionProperty.DISABLEQUERYCONTEXTCACHE]); @@ -145,6 +159,8 @@ internal SFSession(String connectionString, SecureString password) restRequester = new RestRequester(_HttpClient); extractedProperties.WarnOnTimeout(); connectionTimeout = extractedProperties.TimeoutDuration(); + properties.TryGetValue(SFSessionProperty.CLIENT_CONFIG_FILE, out var easyLoggingConfigFile); + _easyLoggingStarter.Init(easyLoggingConfigFile); } catch (Exception e) { @@ -155,7 +171,7 @@ internal SFSession(String connectionString, SecureString password) "Unable to connect"); } } - + private void ValidateApplicationName(SFSessionProperties properties) { // If there is an "application" setting, verify that it matches the expect pattern diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index 993a9279b..ade4d516c 100755 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -86,6 +86,8 @@ internal enum SFSessionProperty INCLUDERETRYREASON, [SFSessionPropertyAttr(required = false, defaultValue = "false")] DISABLEQUERYCONTEXTCACHE, + [SFSessionPropertyAttr(required = false)] + CLIENT_CONFIG_FILE } class SFSessionPropertyAttr : Attribute diff --git a/Snowflake.Data/Core/Tools/DirectoryOperations.cs b/Snowflake.Data/Core/Tools/DirectoryOperations.cs new file mode 100644 index 000000000..2d5d0424b --- /dev/null +++ b/Snowflake.Data/Core/Tools/DirectoryOperations.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System.IO; + +namespace Snowflake.Data.Core.Tools +{ + internal class DirectoryOperations + { + public static readonly DirectoryOperations Instance = new DirectoryOperations(); + + public virtual bool Exists(string path) => Directory.Exists(path); + + public virtual DirectoryInfo CreateDirectory(string path) => Directory.CreateDirectory(path); + } +} diff --git a/Snowflake.Data/Configuration/EnvironmentOperations.cs b/Snowflake.Data/Core/Tools/EnvironmentOperations.cs similarity index 64% rename from Snowflake.Data/Configuration/EnvironmentOperations.cs rename to Snowflake.Data/Core/Tools/EnvironmentOperations.cs index e6e30454f..a295d73a5 100644 --- a/Snowflake.Data/Configuration/EnvironmentOperations.cs +++ b/Snowflake.Data/Core/Tools/EnvironmentOperations.cs @@ -1,9 +1,15 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + using System; -namespace Snowflake.Data.Configuration +namespace Snowflake.Data.Core.Tools { internal class EnvironmentOperations { + public static readonly EnvironmentOperations Instance = new EnvironmentOperations(); + public virtual string GetEnvironmentVariable(string variable) { return Environment.GetEnvironmentVariable(variable); diff --git a/Snowflake.Data/Core/Tools/FileOperations.cs b/Snowflake.Data/Core/Tools/FileOperations.cs new file mode 100644 index 000000000..9efe481bd --- /dev/null +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System.IO; + +namespace Snowflake.Data.Core.Tools +{ + internal class FileOperations + { + public static readonly FileOperations Instance = new FileOperations(); + + public virtual bool Exists(string path) + { + return File.Exists(path); + } + } +} diff --git a/Snowflake.Data/Logger/EasyLoggerManager.cs b/Snowflake.Data/Logger/EasyLoggerManager.cs new file mode 100644 index 000000000..da1748912 --- /dev/null +++ b/Snowflake.Data/Logger/EasyLoggerManager.cs @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using log4net; +using log4net.Appender; +using log4net.Layout; +using Snowflake.Data.Configuration; + +namespace Snowflake.Data.Log +{ + internal class EasyLoggerManager + { + public static readonly EasyLoggerManager Instance = new EasyLoggerManager(); + + private readonly object _lockForExclusiveConfigure = new object(); + + private const string AppenderPrefix = "SFEasyLogging"; + + private readonly EasyLoggingLevelMapper _levelMapper = EasyLoggingLevelMapper.Instance; + + public virtual void ReconfigureEasyLogging(EasyLoggingLogLevel easyLoggingLogLevel, string logsPath) + { + var log4netLevel = _levelMapper.ToLog4NetLevel(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 appender = string.Equals(logsPath, "STDOUT", StringComparison.OrdinalIgnoreCase) + ? AddConsoleAppender(rootLogger) + : AddRollingFileAppender(rootLogger, logsPath); + RemoveOtherEasyLoggingAppenders(rootLogger, appender); + repository.RaiseConfigurationChanged(EventArgs.Empty); + } + } + + private static void RemoveOtherEasyLoggingAppenders(log4net.Repository.Hierarchy.Logger logger, IAppender appender) + { + var existingAppenders = logger.Appenders.ToArray(); + foreach (var existingAppender in existingAppenders) + { + if (IsEasyLoggingAppender(existingAppender) && existingAppender != appender) + { + logger.RemoveAppender(existingAppender); + } + } + } + + private static IAppender AddRollingFileAppender(log4net.Repository.Hierarchy.Logger 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 + { + 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() + }; + appender.ActivateOptions(); + logger.AddAppender(appender); + return appender; + } + + private static bool IsEasyLoggingAppender(IAppender appender) + { + if (appender.GetType() == typeof(ConsoleAppender)) + { + var consoleAppender = (ConsoleAppender)appender; + return consoleAppender.Name != null && consoleAppender.Name.StartsWith(AppenderPrefix); + } + + if (appender.GetType() == typeof(RollingFileAppender)) + { + var rollingFileAppender = (RollingFileAppender)appender; + return rollingFileAppender.Name != null && rollingFileAppender.Name.StartsWith(AppenderPrefix); + } + + return false; + } + + private static IAppender AddConsoleAppender(log4net.Repository.Hierarchy.Logger logger) + { + var patternLayout = PatternLayout(); + var appender = new ConsoleAppender() + { + Layout = patternLayout, + Name = $"{AppenderPrefix}ConsoleAppender" + }; + appender.ActivateOptions(); + logger.AddAppender(appender); + return appender; + } + + private static PatternLayout PatternLayout() + { + var patternLayout = new PatternLayout + { + 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 new file mode 100644 index 000000000..609265e30 --- /dev/null +++ b/Snowflake.Data/Logger/EasyLoggingLevelMapper.cs @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using log4net.Core; +using Snowflake.Data.Configuration; + +namespace Snowflake.Data.Log +{ + internal class EasyLoggingLevelMapper + { + public static readonly EasyLoggingLevelMapper Instance = new EasyLoggingLevelMapper(); + + public Level ToLog4NetLevel(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; + default: throw new Exception("Unknown log level"); + } + } + } +} \ No newline at end of file