Skip to content

Commit bd2e62b

Browse files
SNOW-1292905:Implement configuration log parser (#1909)
1 parent 86ba28a commit bd2e62b

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

src/snowflake/connector/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
ProgrammingError,
4545
_Warning,
4646
)
47+
from .log_configuration import EasyLoggingConfigPython
4748
from .version import VERSION
4849

4950
logging.getLogger(__name__).addHandler(NullHandler())
@@ -92,4 +93,5 @@ def Connect(**kwargs) -> SnowflakeConnection:
9293
"DATETIME",
9394
"ROWID",
9495
# Extended data type (experimental)
96+
"EasyLoggingConfigPython",
9597
]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
6+
from __future__ import annotations
7+
8+
import logging
9+
import os
10+
from logging.handlers import TimedRotatingFileHandler
11+
12+
from snowflake.connector.config_manager import CONFIG_MANAGER
13+
from snowflake.connector.constants import DIRS
14+
from snowflake.connector.secret_detector import SecretDetector
15+
16+
LOG_FILE_NAME = "python-connector.log"
17+
18+
19+
class EasyLoggingConfigPython:
20+
def __init__(self):
21+
self.path: str | None = None
22+
self.level: str | None = None
23+
self.save_logs: bool = False
24+
self.parse_config_file()
25+
26+
def parse_config_file(self):
27+
CONFIG_MANAGER.read_config()
28+
data = CONFIG_MANAGER.conf_file_cache
29+
if log := data.get("log"):
30+
self.save_logs = log.get("save_logs", False)
31+
self.level = log.get("level", "INFO")
32+
self.path = log.get("path", os.path.join(DIRS.user_config_path, "logs"))
33+
34+
if not os.path.isabs(self.path):
35+
raise FileNotFoundError(
36+
f"Log path must be an absolute file path: {self.path}"
37+
)
38+
# if log path does not exist, create it, else check accessibility
39+
if not os.path.exists(self.path):
40+
os.makedirs(self.path, exist_ok=True)
41+
elif not os.access(self.path, os.R_OK | os.W_OK):
42+
raise PermissionError(
43+
f"log path: {self.path} is not accessible, please verify your config file"
44+
)
45+
46+
# create_log() is called outside __init__() so that it can be easily turned off
47+
def create_log(self):
48+
if self.save_logs:
49+
logging.basicConfig(
50+
filename=os.path.join(self.path, LOG_FILE_NAME),
51+
level=logging.getLevelName(self.level),
52+
)
53+
for logger_name in ["snowflake.connector", "botocore", "boto3"]:
54+
logger = logging.getLogger(logger_name)
55+
logger.setLevel(logging.getLevelName(self.level))
56+
ch = TimedRotatingFileHandler(
57+
os.path.join(self.path, LOG_FILE_NAME), when="midnight"
58+
)
59+
ch.setLevel(logging.getLevelName(self.level))
60+
ch.setFormatter(
61+
SecretDetector(
62+
"%(asctime)s - %(threadName)s %(filename)s:%(lineno)d - %(funcName)s() - %(levelname)s - %(message)s"
63+
)
64+
)
65+
logger.addHandler(ch)

test/unit/test_easy_logging.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#
4+
import os.path
5+
import platform
6+
from logging import getLogger
7+
from pathlib import Path
8+
9+
import pytest
10+
import tomlkit
11+
12+
import snowflake.connector
13+
from snowflake.connector import EasyLoggingConfigPython
14+
from snowflake.connector.config_manager import CONFIG_MANAGER
15+
from snowflake.connector.constants import CONFIG_FILE
16+
17+
logger = getLogger("snowflake.connector")
18+
19+
20+
@pytest.fixture(scope="function")
21+
def temp_config_file(tmp_path_factory):
22+
return tmp_path_factory.mktemp("config_file_path") / "config.toml"
23+
24+
25+
@pytest.fixture(scope="function")
26+
def log_directory(tmp_path_factory):
27+
return tmp_path_factory.mktemp("log")
28+
29+
30+
@pytest.fixture(scope="module")
31+
def nonexist_file(tmp_path_factory):
32+
return tmp_path_factory.mktemp("log_path") / "nonexist_file"
33+
34+
35+
@pytest.fixture(scope="module")
36+
def inaccessible_file(tmp_path_factory):
37+
return tmp_path_factory.mktemp("inaccessible_file")
38+
39+
40+
@pytest.fixture(scope="module")
41+
def inabsolute_file(tmp_path_factory):
42+
directory = tmp_path_factory.mktemp("inabsolute_file")
43+
return os.path.basename(directory)
44+
45+
46+
def fake_connector(**kwargs) -> snowflake.connector.SnowflakeConnection:
47+
return snowflake.connector.connect(
48+
user="user",
49+
account="account",
50+
password="testpassword",
51+
database="TESTDB",
52+
warehouse="TESTWH",
53+
**kwargs,
54+
)
55+
56+
57+
@pytest.fixture(scope="function")
58+
def config_file_setup(
59+
request,
60+
temp_config_file,
61+
nonexist_file,
62+
inaccessible_file,
63+
inabsolute_file,
64+
log_directory,
65+
):
66+
param = request.param
67+
# making different config file dir for each test to avoid race condition on modifying config.toml
68+
CONFIG_MANAGER.file_path = Path(temp_config_file)
69+
configs = {
70+
"nonexist_path": {"log": {"save_logs": False, "path": str(nonexist_file)}},
71+
"inabsolute_path": {"log": {"save_logs": False, "path": str(inabsolute_file)}},
72+
"inaccessible_path": {
73+
"log": {"save_logs": False, "path": str(inaccessible_file)}
74+
},
75+
"save_logs": {"log": {"save_logs": True, "path": str(log_directory)}},
76+
"no_save_logs": {"log": {"save_logs": False, "path": str(log_directory)}},
77+
}
78+
# create inaccessible path and make it inaccessible
79+
os.chmod(inaccessible_file, os.stat(inaccessible_file).st_mode & ~0o222)
80+
try:
81+
# create temp config file
82+
with open(temp_config_file, "w") as f:
83+
f.write(tomlkit.dumps(configs[param]))
84+
yield
85+
finally:
86+
# remove created dir and file, including log paths and config file paths
87+
CONFIG_MANAGER.file_path = CONFIG_FILE
88+
89+
90+
@pytest.mark.parametrize("config_file_setup", ["nonexist_path"], indirect=True)
91+
@pytest.mark.skipolddriver
92+
def test_config_file_nonexist_path(config_file_setup, nonexist_file):
93+
assert not os.path.exists(nonexist_file)
94+
EasyLoggingConfigPython()
95+
assert os.path.exists(nonexist_file)
96+
97+
98+
@pytest.mark.parametrize("config_file_setup", ["inabsolute_path"], indirect=True)
99+
@pytest.mark.skipolddriver
100+
def test_config_file_inabsolute_path(config_file_setup, inabsolute_file):
101+
with pytest.raises(FileNotFoundError) as e:
102+
EasyLoggingConfigPython()
103+
assert f"Log path must be an absolute file path: {str(inabsolute_file)}" in str(e)
104+
105+
106+
@pytest.mark.parametrize("config_file_setup", ["inaccessible_path"], indirect=True)
107+
@pytest.mark.skipolddriver
108+
@pytest.mark.skipif(
109+
platform.system() == "Windows", reason="Test only applicable to Windows"
110+
)
111+
def test_config_file_inaccessible_path(config_file_setup, inaccessible_file):
112+
with pytest.raises(PermissionError) as e:
113+
EasyLoggingConfigPython()
114+
assert (
115+
f"log path: {str(inaccessible_file)} is not accessible, please verify your config file"
116+
in str(e)
117+
)
118+
119+
120+
@pytest.mark.parametrize("config_file_setup", ["save_logs"], indirect=True)
121+
@pytest.mark.skipolddriver
122+
def test_save_logs(config_file_setup, log_directory):
123+
easy_logging = EasyLoggingConfigPython()
124+
easy_logging.create_log()
125+
logger.info("this is a test logger")
126+
assert os.path.exists(os.path.join(log_directory, "python-connector.log"))
127+
with open(os.path.join(log_directory, "python-connector.log")) as f:
128+
data = f.read()
129+
assert "this is a test logger" in data
130+
# reset log level
131+
getLogger("snowflake.connector").setLevel(10)
132+
getLogger("botocore").setLevel(0)
133+
getLogger("boto3").setLevel(0)
134+
135+
136+
@pytest.mark.parametrize("config_file_setup", ["no_save_logs"], indirect=True)
137+
@pytest.mark.skipolddriver
138+
def test_no_save_logs(config_file_setup, log_directory):
139+
easy_logging = EasyLoggingConfigPython()
140+
easy_logging.create_log()
141+
logger.info("this is a test logger")
142+
143+
assert not os.path.exists(os.path.join(log_directory, "python-connector.log"))

0 commit comments

Comments
 (0)