diff --git a/src/sonic-py-common/sonic_py_common/daemon_base.py b/src/sonic-py-common/sonic_py_common/daemon_base.py index 8bfd09cd881d..fd98a12b28e2 100644 --- a/src/sonic-py-common/sonic_py_common/daemon_base.py +++ b/src/sonic-py-common/sonic_py_common/daemon_base.py @@ -35,10 +35,10 @@ def db_connect(db_name, namespace=EMPTY_NAMESPACE): class DaemonBase(Logger): - def __init__(self, log_identifier, use_syslogger=True): + def __init__(self, log_identifier, use_syslogger=True, enable_runtime_log_config=False): super().__init__() if use_syslogger: - self.logger_instance = SysLogger(log_identifier) + self.logger_instance = SysLogger(log_identifier, enable_runtime_config=enable_runtime_log_config) else: self.logger_instance = Logger( log_identifier=log_identifier, diff --git a/src/sonic-py-common/sonic_py_common/syslogger.py b/src/sonic-py-common/sonic_py_common/syslogger.py index b3c8f726502c..9839f202625d 100644 --- a/src/sonic-py-common/sonic_py_common/syslogger.py +++ b/src/sonic-py-common/sonic_py_common/syslogger.py @@ -3,16 +3,25 @@ import os import socket import sys +import threading + +CONFIG_DB = 'CONFIG_DB' +FIELD_LOG_LEVEL = 'LOGLEVEL' +FIELD_REQUIRE_REFRESH = 'require_manual_refresh' + class SysLogger: """ SysLogger class for Python applications using SysLogHandler """ + + log_registry = {} + lock = threading.Lock() DEFAULT_LOG_FACILITY = SysLogHandler.LOG_USER DEFAULT_LOG_LEVEL = SysLogHandler.LOG_NOTICE - def __init__(self, log_identifier=None, log_facility=DEFAULT_LOG_FACILITY, log_level=DEFAULT_LOG_LEVEL): + def __init__(self, log_identifier=None, log_facility=DEFAULT_LOG_FACILITY, log_level=DEFAULT_LOG_LEVEL, enable_runtime_config=False): if log_identifier is None: log_identifier = os.path.basename(sys.argv[0]) @@ -29,6 +38,113 @@ def __init__(self, log_identifier=None, log_facility=DEFAULT_LOG_FACILITY, log_l self.logger.addHandler(handler) self.set_min_log_priority(log_level) + + if enable_runtime_config: + self.register_for_runtime_config(log_identifier) + + def register_for_runtime_config(self, log_identifier): + """Register current log instance to log registry. If DB entry exists, + update log level according to DB configuration; otherwise, save current + log level to DB. + + Args: + log_identifier (str): key of LOGGER table + """ + with SysLogger.lock: + if log_identifier not in SysLogger.log_registry: + SysLogger.log_registry[log_identifier] = [self] + else: + SysLogger.log_registry[log_identifier].append(self) + + from swsscommon import swsscommon + try: + config_db = swsscommon.SonicV2Connector(use_unix_socket_path=True) + config_db.connect(CONFIG_DB) + log_level_in_db = config_db.get(CONFIG_DB, f'{swsscommon.CFG_LOGGER_TABLE_NAME}|{log_identifier}', FIELD_LOG_LEVEL) + if log_level_in_db: + self.set_min_log_priority(self.log_priority_from_str(log_level_in_db)) + else: + data = { + FIELD_LOG_LEVEL: self.log_priority_to_str(self._min_log_level), + FIELD_REQUIRE_REFRESH: 'true' + } + config_db.hmset(CONFIG_DB, f'{swsscommon.CFG_LOGGER_TABLE_NAME}|{log_identifier}', data) + except Exception as e: + self.log_notice(f'DB is not available when initialize logger instance - {e}') + + @classmethod + def update_log_level(cls): + """Refresh log level for each log instances registered to cls.log_registry. + + Returns: + tuple: (refresh result, fail reason) + """ + with cls.lock: + if not cls.log_registry: + return True, '' + + from swsscommon import swsscommon + try: + config_db = swsscommon.SonicV2Connector(use_unix_socket_path=True) + config_db.connect(CONFIG_DB) + for log_identifier, log_instances in cls.log_registry.items(): + log_level_in_db = config_db.get(CONFIG_DB, f'{swsscommon.CFG_LOGGER_TABLE_NAME}|{log_identifier}', FIELD_LOG_LEVEL) + if log_level_in_db: + for log_instance in log_instances: + log_instance.set_min_log_priority(log_instance.log_priority_from_str(log_level_in_db)) + else: + for log_instance in log_instances: + data = { + FIELD_LOG_LEVEL: log_instance.log_priority_to_str(log_instance._min_log_level), + FIELD_REQUIRE_REFRESH: 'true' + } + config_db.hmset(CONFIG_DB, f'{swsscommon.CFG_LOGGER_TABLE_NAME}|{log_identifier}', data) + break + return True, '' + except Exception as e: + return False, f'Failed to refresh log configuration - {e}' + + def log_priority_to_str(self, priority): + """Convert log priority to string. + Args: + priority (int): log priority. + Returns: + str: log priority in string. + """ + if priority == logging.INFO: + return 'INFO' + elif priority == logging.NOTICE: + return 'NOTICE' + elif priority == logging.DEBUG: + return 'DEBUG' + elif priority == logging.WARNING: + return 'WARN' + elif priority == logging.ERROR: + return 'ERROR' + else: + self.log_error(f'Invalid log priority: {priority}') + return 'NOTICE' + + def log_priority_from_str(self, priority_in_str): + """Convert log priority from string. + Args: + priority_in_str (str): log priority in string. + Returns: + _type_: log priority. + """ + if priority_in_str == 'DEBUG': + return logging.DEBUG + elif priority_in_str == 'INFO': + return logging.INFO + elif priority_in_str == 'NOTICE': + return logging.NOTICE + elif priority_in_str == 'WARN': + return logging.WARNING + elif priority_in_str == 'ERROR': + return logging.ERROR + else: + self.log_error(f'Invalid log priority string: {priority_in_str}') + return logging.NOTICE def set_min_log_priority(self, priority): """ diff --git a/src/sonic-py-common/tests/test_syslogger.py b/src/sonic-py-common/tests/test_syslogger.py new file mode 100644 index 000000000000..d845af2c139c --- /dev/null +++ b/src/sonic-py-common/tests/test_syslogger.py @@ -0,0 +1,83 @@ +import logging +import os +import sys + +if sys.version_info.major == 3: + from unittest import mock +else: + import mock + +modules_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(modules_path, 'sonic_py_common')) +from sonic_py_common import syslogger + +logging.NOTICE = logging.INFO + 1 + + +class TestSysLogger: + def test_basic(self): + log = syslogger.SysLogger() + log.logger.log = mock.MagicMock() + log.log_error('error message') + log.log_warning('warning message') + log.log_notice('notice message') + log.log_info('info message') + log.log_debug('debug message') + log.log(logging.ERROR, 'error msg', also_print_to_console=True) + + def test_log_priority(self): + log = syslogger.SysLogger() + log.set_min_log_priority(logging.ERROR) + assert log.logger.level == logging.ERROR + + def test_log_priority_from_str(self): + log = syslogger.SysLogger() + assert log.log_priority_from_str('ERROR') == logging.ERROR + assert log.log_priority_from_str('INFO') == logging.INFO + assert log.log_priority_from_str('NOTICE') == logging.NOTICE + assert log.log_priority_from_str('WARN') == logging.WARN + assert log.log_priority_from_str('DEBUG') == logging.DEBUG + assert log.log_priority_from_str('invalid') == logging.NOTICE + + def test_log_priority_to_str(self): + log = syslogger.SysLogger() + assert log.log_priority_to_str(logging.NOTICE) == 'NOTICE' + assert log.log_priority_to_str(logging.INFO) == 'INFO' + assert log.log_priority_to_str(logging.DEBUG) == 'DEBUG' + assert log.log_priority_to_str(logging.WARN) == 'WARN' + assert log.log_priority_to_str(logging.ERROR) == 'ERROR' + assert log.log_priority_to_str(-1) == 'NOTICE' + + @mock.patch('swsscommon.swsscommon.SonicV2Connector') + def test_runtime_config(self, mock_connector): + mock_db = mock.MagicMock() + mock_db.get = mock.MagicMock(return_value='DEBUG') + mock_connector.return_value = mock_db + log1 = syslogger.SysLogger(log_identifier='log1', enable_runtime_config=True, log_level=logging.INFO) + assert 'log1' in syslogger.SysLogger.log_registry + assert log1.logger.level == logging.DEBUG + + mock_db.get.return_value = None + mock_db.hmset = mock.MagicMock() + log2 = syslogger.SysLogger(log_identifier='log2', enable_runtime_config=True, log_level=logging.INFO) + assert 'log2' in syslogger.SysLogger.log_registry + mock_db.hmset.assert_called_once() + + mock_db.get.return_value = 'ERROR' + ret, msg = syslogger.SysLogger.update_log_level() + assert ret + assert not msg + assert log1.logger.level == logging.ERROR + assert log2.logger.level == logging.ERROR + + @mock.patch('swsscommon.swsscommon.SonicV2Connector') + def test_runtime_config_negative(self, mock_connector): + mock_db = mock.MagicMock() + mock_db.get = mock.MagicMock(side_effect=Exception('')) + mock_connector.return_value = mock_db + syslogger.SysLogger(log_identifier='log', enable_runtime_config=True) + assert 'log' in syslogger.SysLogger.log_registry + + ret, msg = syslogger.SysLogger.update_log_level() + assert not ret + assert msg