Skip to content

Commit

Permalink
Enable runtime config log level
Browse files Browse the repository at this point in the history
  • Loading branch information
Junchao-Mellanox committed Jun 19, 2024
1 parent 0ffdf4e commit 835a84e
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 3 deletions.
5 changes: 3 additions & 2 deletions src/sonic-py-common/sonic_py_common/daemon_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@ 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)
else:
self.logger_instance = Logger(
log_identifier=log_identifier,
log_facility=Logger.LOG_FACILITY_DAEMON,
log_option=(Logger.LOG_OPTION_NDELAY | Logger.LOG_OPTION_PID)
log_option=(Logger.LOG_OPTION_NDELAY | Logger.LOG_OPTION_PID),
enable_runtime_config=enable_runtime_log_config
)

# Register our default signal handlers, unless the signal already has a
Expand Down
118 changes: 117 additions & 1 deletion src/sonic-py-common/sonic_py_common/syslogger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -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 refresh_config(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):
"""
Expand Down
83 changes: 83 additions & 0 deletions src/sonic-py-common/tests/test_syslogger.py
Original file line number Diff line number Diff line change
@@ -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.refresh_config()
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.refresh_config()
assert not ret
assert msg

0 comments on commit 835a84e

Please sign in to comment.