Skip to content

Commit fb4c0d0

Browse files
Junchao-Mellanoxpull[bot]
authored andcommitted
Enable runtime config log level (#19611)
HLD link: sonic-net/SONiC#1522 - Why I did it SONiC provides two Python logger implementations: sonic_py_common.logger.Logger and sonic_py_common.syslogger.SysLogger. Both of them do not provide the ability to change log level at real time. Sometimes, in order to get more debug information, developer has to manually change the log level in code on a running switch and restart the Python daemon. This is not convenient. SONiC also provides a C/C++ logger implementation in sonic-platform-common.common.logger.cpp. This C/C++ logger implementation is also a wrapper of Linux standard syslog which is widely used by swss/syncd. It provides the ability to set log level on fly by starting a thread to listen to CONFIG DB LOGGER table change. SONiC infrastructure also provides the Python wrapper for sonic-platform-common.common.logger.cpp which is swsscommon.Logger. However, this logger implementation also has some drawbacks: swsscommon.Logger assumes redis DB is ready to connect. This is a valid assumption for swss/syncd. But it is not good for a Python logger implementation because some Python script may be called before redis server starting. swsscommon.Logger wraps Linux syslog which only support single log identifier for a daemon. So, swsscommon.Logger is not an option too. This PR is a Python logger enhancement which allows user setting log level at run time. - How I did it swsscommon.Logger depends on a thread to listen to CONFIG DB LOGGER table change. It refreshes log level for each logger instances once the thread detects a DB entry change. A thread is considered heavy in a python script, especially that there are many short and simple python scripts which also use logger. To keep python logger light weight, it uses a different design than swsscommon.Logger: A class level logger registry shall be added to SysLoggerclass Each logger instance shall register itself to logger register if enables runtime configuration Logger configuration shall be refreshed by CLI which send a SIGHUP signal to the daemon - How to verify it Manual test New unit test cases
1 parent 00df4ba commit fb4c0d0

File tree

3 files changed

+135
-6
lines changed

3 files changed

+135
-6
lines changed

src/sonic-py-common/sonic_py_common/daemon_base.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ def db_connect(db_name, namespace=EMPTY_NAMESPACE):
3535

3636

3737
class DaemonBase(Logger):
38-
def __init__(self, log_identifier, use_syslogger=True):
38+
def __init__(self, log_identifier, use_syslogger=True, enable_runtime_log_config=False):
3939
super().__init__()
4040
if use_syslogger:
41-
self.logger_instance = SysLogger(log_identifier)
41+
self.logger_instance = SysLogger(log_identifier, enable_runtime_config=enable_runtime_log_config)
4242
else:
4343
self.logger_instance = Logger(
4444
log_identifier=log_identifier,

src/sonic-py-common/sonic_py_common/syslogger.py

+76-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
import socket
55
import sys
66

7+
CONFIG_DB = 'CONFIG_DB'
8+
FIELD_LOG_LEVEL = 'LOGLEVEL'
9+
FIELD_REQUIRE_REFRESH = 'require_manual_refresh'
10+
11+
712
# customize python logging to support notice logger
813
logging.NOTICE = logging.INFO + 1
914
logging.addLevelName(logging.NOTICE, "NOTICE")
@@ -18,12 +23,11 @@ class SysLogger:
1823
DEFAULT_LOG_FACILITY = SysLogHandler.LOG_USER
1924
DEFAULT_LOG_LEVEL = logging.NOTICE
2025

21-
def __init__(self, log_identifier=None, log_facility=DEFAULT_LOG_FACILITY, log_level=DEFAULT_LOG_LEVEL):
22-
if log_identifier is None:
23-
log_identifier = os.path.basename(sys.argv[0])
26+
def __init__(self, log_identifier=None, log_facility=DEFAULT_LOG_FACILITY, log_level=DEFAULT_LOG_LEVEL, enable_runtime_config=False):
27+
self.log_identifier = log_identifier if log_identifier else os.path.basename(sys.argv[0])
2428

2529
# Initialize SysLogger
26-
self.logger = logging.getLogger(log_identifier)
30+
self.logger = logging.getLogger(self.log_identifier)
2731

2832
# Reset all existing handlers
2933
for handler in self.logger.handlers[:]:
@@ -35,6 +39,74 @@ def __init__(self, log_identifier=None, log_facility=DEFAULT_LOG_FACILITY, log_l
3539
self.logger.addHandler(handler)
3640

3741
self.set_min_log_priority(log_level)
42+
43+
if enable_runtime_config:
44+
self.update_log_level()
45+
46+
def update_log_level(self):
47+
"""Refresh log level.
48+
49+
Returns:
50+
tuple: (refresh result, fail reason)
51+
"""
52+
from swsscommon import swsscommon
53+
try:
54+
config_db = swsscommon.SonicV2Connector(use_unix_socket_path=True)
55+
config_db.connect(CONFIG_DB)
56+
log_level_in_db = config_db.get(CONFIG_DB, f'{swsscommon.CFG_LOGGER_TABLE_NAME}|{self.log_identifier}', FIELD_LOG_LEVEL)
57+
if log_level_in_db:
58+
self.set_min_log_priority(self.log_priority_from_str(log_level_in_db))
59+
else:
60+
data = {
61+
FIELD_LOG_LEVEL: self.log_priority_to_str(self._min_log_level),
62+
FIELD_REQUIRE_REFRESH: 'true'
63+
}
64+
config_db.hmset(CONFIG_DB, f'{swsscommon.CFG_LOGGER_TABLE_NAME}|{self.log_identifier}', data)
65+
return True, ''
66+
except Exception as e:
67+
return False, f'Failed to refresh log configuration - {e}'
68+
69+
def log_priority_to_str(self, priority):
70+
"""Convert log priority to string.
71+
Args:
72+
priority (int): log priority.
73+
Returns:
74+
str: log priority in string.
75+
"""
76+
if priority == logging.INFO:
77+
return 'INFO'
78+
elif priority == logging.NOTICE:
79+
return 'NOTICE'
80+
elif priority == logging.DEBUG:
81+
return 'DEBUG'
82+
elif priority == logging.WARNING:
83+
return 'WARN'
84+
elif priority == logging.ERROR:
85+
return 'ERROR'
86+
else:
87+
self.log_error(f'Invalid log priority: {priority}')
88+
return 'WARN'
89+
90+
def log_priority_from_str(self, priority_in_str):
91+
"""Convert log priority from string.
92+
Args:
93+
priority_in_str (str): log priority in string.
94+
Returns:
95+
_type_: log priority.
96+
"""
97+
if priority_in_str == 'DEBUG':
98+
return logging.DEBUG
99+
elif priority_in_str == 'INFO':
100+
return logging.INFO
101+
elif priority_in_str == 'NOTICE':
102+
return logging.NOTICE
103+
elif priority_in_str == 'WARN':
104+
return logging.WARNING
105+
elif priority_in_str == 'ERROR':
106+
return logging.ERROR
107+
else:
108+
self.log_error(f'Invalid log priority string: {priority_in_str}')
109+
return logging.WARNING
38110

39111
def set_min_log_priority(self, priority):
40112
"""

src/sonic-py-common/tests/test_syslogger.py

+57
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,60 @@ def test_notice_log(self, capsys):
3232
log.log_notice('this is a message')
3333
captured = capsys.readouterr()
3434
assert 'NOTICE' in captured.out
35+
36+
def test_basic(self):
37+
log = syslogger.SysLogger()
38+
log.logger.log = mock.MagicMock()
39+
log.log_error('error message')
40+
log.log_warning('warning message')
41+
log.log_notice('notice message')
42+
log.log_info('info message')
43+
log.log_debug('debug message')
44+
log.log(logging.ERROR, 'error msg', also_print_to_console=True)
45+
46+
def test_log_priority(self):
47+
log = syslogger.SysLogger()
48+
log.set_min_log_priority(logging.ERROR)
49+
assert log.logger.level == logging.ERROR
50+
51+
def test_log_priority_from_str(self):
52+
log = syslogger.SysLogger()
53+
assert log.log_priority_from_str('ERROR') == logging.ERROR
54+
assert log.log_priority_from_str('INFO') == logging.INFO
55+
assert log.log_priority_from_str('NOTICE') == logging.NOTICE
56+
assert log.log_priority_from_str('WARN') == logging.WARN
57+
assert log.log_priority_from_str('DEBUG') == logging.DEBUG
58+
assert log.log_priority_from_str('invalid') == logging.WARN
59+
60+
def test_log_priority_to_str(self):
61+
log = syslogger.SysLogger()
62+
assert log.log_priority_to_str(logging.NOTICE) == 'NOTICE'
63+
assert log.log_priority_to_str(logging.INFO) == 'INFO'
64+
assert log.log_priority_to_str(logging.DEBUG) == 'DEBUG'
65+
assert log.log_priority_to_str(logging.WARN) == 'WARN'
66+
assert log.log_priority_to_str(logging.ERROR) == 'ERROR'
67+
assert log.log_priority_to_str(-1) == 'WARN'
68+
69+
@mock.patch('swsscommon.swsscommon.SonicV2Connector')
70+
def test_runtime_config(self, mock_connector):
71+
mock_db = mock.MagicMock()
72+
mock_db.get = mock.MagicMock(return_value='DEBUG')
73+
mock_connector.return_value = mock_db
74+
log = syslogger.SysLogger(log_identifier='log1', enable_runtime_config=True, log_level=logging.INFO)
75+
assert log.logger.level == logging.DEBUG
76+
77+
mock_db.get.return_value = 'ERROR'
78+
ret, msg = log.update_log_level()
79+
assert ret
80+
assert not msg
81+
82+
@mock.patch('swsscommon.swsscommon.SonicV2Connector')
83+
def test_runtime_config_negative(self, mock_connector):
84+
mock_db = mock.MagicMock()
85+
mock_db.get = mock.MagicMock(side_effect=Exception(''))
86+
mock_connector.return_value = mock_db
87+
log = syslogger.SysLogger(log_identifier='log', enable_runtime_config=True)
88+
89+
ret, msg = log.update_log_level()
90+
assert not ret
91+
assert msg

0 commit comments

Comments
 (0)