From d29f827fc979a0e6f4c528f5b7a699030e2ac735 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 19 Nov 2025 15:17:25 +0900 Subject: [PATCH] Add a function to Log system to update the console formatter dynamically. --- src/corelibs/logging_handling/log.py | 122 +++++++++++---- test-run/logging_handling/log.py | 15 +- .../test_log_3_custom_console_formatter.py | 143 ++++++++++++++++++ 3 files changed, 247 insertions(+), 33 deletions(-) diff --git a/src/corelibs/logging_handling/log.py b/src/corelibs/logging_handling/log.py index f774d03..b2f20cb 100644 --- a/src/corelibs/logging_handling/log.py +++ b/src/corelibs/logging_handling/log.py @@ -445,6 +445,9 @@ class Log(LogParent): logger setup """ + CONSOLE_HANDLER: str = 'stream_handler' + FILE_HANDLER: str = 'file_handler' + # spacer lenght characters and the character SPACER_CHAR: str = '=' SPACER_LENGTH: int = 32 @@ -510,13 +513,13 @@ class Log(LogParent): # in the file writer too, for the ones where color is set BEFORE the format # Any is logging.StreamHandler, logging.FileHandler and all logging.handlers.* self.handlers: dict[str, Any] = {} - self.add_handler('file_handler', self.__create_file_handler( - 'file_handler', self.log_settings['log_level_file'], log_path) + self.add_handler(self.FILE_HANDLER, self.__create_file_handler( + self.FILE_HANDLER, self.log_settings['log_level_file'], log_path) ) if self.log_settings['console_enabled']: # console - self.add_handler('stream_handler', self.__create_console_handler( - 'stream_handler', + self.add_handler(self.CONSOLE_HANDLER, self.__create_console_handler( + self.CONSOLE_HANDLER, self.log_settings['log_level_console'], console_format_type=self.log_settings['console_format_type'], )) @@ -613,19 +616,17 @@ class Log(LogParent): self.handlers[handler_name] = handler return True - # MARK: console handler - def __create_console_handler( - self, handler_name: str, - log_level_console: LoggingLevel = LoggingLevel.WARNING, - filter_exceptions: bool = True, - console_format_type: ConsoleFormat = ConsoleFormatSettings.ALL, - ) -> logging.StreamHandler[TextIO]: - # console logger - if not self.validate_log_level(log_level_console): - log_level_console = self.DEFAULT_LOG_LEVEL_CONSOLE - console_handler = logging.StreamHandler() - print(f"Console format type: {console_format_type}") - # build the format string based on what flags are set + # MARK: console logger format + def __build_console_format_from_string(self, console_format_type: ConsoleFormat) -> str: + """ + Build console format string from the given console format type + + Arguments: + console_format_type {ConsoleFormat} -- _description_ + + Returns: + str -- _description_ + """ format_string = '' # time part if any of the times are requested if ( @@ -656,15 +657,18 @@ class Log(LogParent): format_string += '] ' # always level + message format_string += '<%(levelname)s> %(message)s' - # basic date, but this will be overridden to ISO in formatTime - # format_date = "%Y-%m-%d %H:%M:%S" - # color or not - if self.log_settings['console_color_output_enabled']: - # formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date) - formatter_console = CustomConsoleFormatter(format_string) - else: - # formatter_console = logging.Formatter(format_string, datefmt=format_date) - formatter_console = logging.Formatter(format_string) + return format_string + + def __set_time_format_for_console_formatter( + self, formatter_console: CustomConsoleFormatter | logging.Formatter, console_format_type: ConsoleFormat + ) -> None: + """ + Format time for a given format handler, this is for console format only + + Arguments: + formatter_console {CustomConsoleFormatter | logging.Formatter} -- _description_ + console_format_type {ConsoleFormat} -- _description_ + """ # default for TIME is milliseconds # if we have multiple set, the smallest precision wins if ConsoleFormat.TIME_MICROSECONDS in console_format_type: @@ -701,11 +705,75 @@ class Log(LogParent): .fromtimestamp(record.created) .isoformat(sep=" ", timespec=iso_precision) ) + + def __set_console_formatter(self, console_format_type: ConsoleFormat) -> CustomConsoleFormatter | logging.Formatter: + """ + Build the full formatter and return it + + Arguments: + console_format_type {ConsoleFormat} -- _description_ + + Returns: + CustomConsoleFormatter | logging.Formatter -- _description_ + """ + format_string = self.__build_console_format_from_string(console_format_type) + if self.log_settings['console_color_output_enabled']: + # formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date) + formatter_console = CustomConsoleFormatter(format_string) + else: + # formatter_console = logging.Formatter(format_string, datefmt=format_date) + formatter_console = logging.Formatter(format_string) + self.__set_time_format_for_console_formatter(formatter_console, console_format_type) + return formatter_console + + # MARK: console handler update + def update_console_formatter( + self, + console_format_type: ConsoleFormat, + ): + """ + Update the console formatter for format layout and time stamp format + + Arguments: + console_format_type {ConsoleFormat} -- _description_ + """ + if not self.log_settings['console_enabled']: + return + # update the formatter + self.handlers[self.CONSOLE_HANDLER].setFormatter( + self.__set_console_formatter(console_format_type) + ) + + # MARK: console handler + def __create_console_handler( + self, handler_name: str, + log_level_console: LoggingLevel = LoggingLevel.WARNING, + filter_exceptions: bool = True, + console_format_type: ConsoleFormat = ConsoleFormatSettings.ALL, + ) -> logging.StreamHandler[TextIO]: + # console logger + if not self.validate_log_level(log_level_console): + log_level_console = self.DEFAULT_LOG_LEVEL_CONSOLE + console_handler = logging.StreamHandler() + # print(f"Console format type: {console_format_type}") + # build the format string based on what flags are set + # format_string = self.__build_console_format_from_string(console_format_type) + # # basic date, but this will be overridden to ISO in formatTime + # # format_date = "%Y-%m-%d %H:%M:%S" + # # color or not + # if self.log_settings['console_color_output_enabled']: + # # formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date) + # formatter_console = CustomConsoleFormatter(format_string) + # else: + # # formatter_console = logging.Formatter(format_string, datefmt=format_date) + # formatter_console = logging.Formatter(format_string) + # # set the time format + # self.__set_time_format_for_console_formatter(formatter_console, console_format_type) console_handler.set_name(handler_name) console_handler.setLevel(log_level_console.name) # do not show exceptions logs on console console_handler.addFilter(CustomHandlerFilter('console', filter_exceptions)) - console_handler.setFormatter(formatter_console) + console_handler.setFormatter(self.__set_console_formatter(console_format_type)) return console_handler # MARK: file handler diff --git a/test-run/logging_handling/log.py b/test-run/logging_handling/log.py index 769bd74..e1d0dc1 100644 --- a/test-run/logging_handling/log.py +++ b/test-run/logging_handling/log.py @@ -25,12 +25,11 @@ def main(): "log_level_file": 'DEBUG', # "console_color_output_enabled": False, "per_run_log": True, - # Set console log type, must be sent as value for ConsoleFormat or bitwise of ConsoleFormatType # "console_format_type": ConsoleFormatSettings.BARE, # "console_format_type": ConsoleFormatSettings.MINIMAL, - # "console_format_type": ConsoleFormatType.TIME_MICROSECONDS | ConsoleFormatType.NAME, - # "console_format_type": ConsoleFormatType.NAME, - "console_format_type": ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO, + "console_format_type": ConsoleFormat.TIME_MICROSECONDS | ConsoleFormat.NAME, + # "console_format_type": ConsoleFormat.NAME, + # "console_format_type": ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO, } ) logn = Logger(log.get_logger_settings()) @@ -104,10 +103,14 @@ def main(): for key, handler in log.handlers.items(): print(f"Handler (handlers) [{key}] {handler} -> {handler.level} -> {LoggingLevel.from_any(handler.level)}") - log.set_log_level('stream_handler', LoggingLevel.ERROR) + log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.ERROR) log.logger.warning('[NORMAL] Invisible Warning test: %s', log.logger.name) log.logger.error('[NORMAL] Visible Error test: %s', log.logger.name) - # log.handlers['stream_handler'].se + + log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.DEBUG) + log.debug('Current logging format: %s', log.log_settings['console_format_type']) + log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO) + log.info('Does hit show less') if __name__ == "__main__": diff --git a/tests/unit/logging_handling/log_testing/test_log_3_custom_console_formatter.py b/tests/unit/logging_handling/log_testing/test_log_3_custom_console_formatter.py index f78ce73..18b3bca 100644 --- a/tests/unit/logging_handling/log_testing/test_log_3_custom_console_formatter.py +++ b/tests/unit/logging_handling/log_testing/test_log_3_custom_console_formatter.py @@ -140,4 +140,147 @@ class TestCustomConsoleFormatter: assert "Critical message" in result assert "CRITICAL" in result + +# MARK: Test update_console_formatter +class TestUpdateConsoleFormatter: + """Test cases for update_console_formatter method""" + + def test_update_console_formatter_to_minimal(self, log_instance: Log): + """Test updating console formatter to MINIMAL format""" + log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL) + + # Get the console handler's formatter + console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER] + formatter = console_handler.formatter + + # Verify formatter was updated + assert formatter is not None + + def test_update_console_formatter_to_condensed(self, log_instance: Log): + """Test updating console formatter to CONDENSED format""" + log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED) + + # Get the console handler's formatter + console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER] + formatter = console_handler.formatter + + # Verify formatter was updated + assert formatter is not None + + def test_update_console_formatter_to_bare(self, log_instance: Log): + """Test updating console formatter to BARE format""" + log_instance.update_console_formatter(ConsoleFormatSettings.BARE) + + # Get the console handler's formatter + console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER] + formatter = console_handler.formatter + + # Verify formatter was updated + assert formatter is not None + + def test_update_console_formatter_to_all(self, log_instance: Log): + """Test updating console formatter to ALL format""" + log_instance.update_console_formatter(ConsoleFormatSettings.ALL) + + # Get the console handler's formatter + console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER] + formatter = console_handler.formatter + + # Verify formatter was updated + assert formatter is not None + + def test_update_console_formatter_when_disabled( + self, tmp_log_path: Path, basic_log_settings: LogSettings + ): + """Test that update_console_formatter does nothing when console is disabled""" + # Disable console + basic_log_settings['console_enabled'] = False + log = Log( + log_path=tmp_log_path, + log_name="test_log", + log_settings=basic_log_settings + ) + + # This should not raise an error and should return early + log.update_console_formatter(ConsoleFormatSettings.MINIMAL) + + # Verify console handler doesn't exist + assert log.CONSOLE_HANDLER not in log.handlers + + def test_update_console_formatter_with_color_enabled( + self, tmp_log_path: Path, basic_log_settings: LogSettings + ): + """Test updating console formatter with color output enabled""" + basic_log_settings['console_color_output_enabled'] = True + log = Log( + log_path=tmp_log_path, + log_name="test_log", + log_settings=basic_log_settings + ) + + log.update_console_formatter(ConsoleFormatSettings.MINIMAL) + + # Get the console handler's formatter + console_handler = log.handlers[log.CONSOLE_HANDLER] + formatter = console_handler.formatter + + # Verify formatter is CustomConsoleFormatter when colors enabled + assert isinstance(formatter, CustomConsoleFormatter) + + def test_update_console_formatter_without_color(self, log_instance: Log): + """Test updating console formatter without color output""" + log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL) + + # Get the console handler's formatter + console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER] + formatter = console_handler.formatter + + # Verify formatter is standard Formatter when colors disabled + assert isinstance(formatter, logging.Formatter) + # But not the colored version + assert not isinstance(formatter, CustomConsoleFormatter) + + def test_update_console_formatter_multiple_times(self, log_instance: Log): + """Test updating console formatter multiple times""" + # Update to MINIMAL + log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL) + console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER] + formatter1 = console_handler.formatter + + # Update to CONDENSED + log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED) + formatter2 = console_handler.formatter + + # Update to ALL + log_instance.update_console_formatter(ConsoleFormatSettings.ALL) + formatter3 = console_handler.formatter + + # Verify each update created a new formatter + assert formatter1 is not formatter2 + assert formatter2 is not formatter3 + assert formatter1 is not formatter3 + + def test_update_console_formatter_preserves_handler_level(self, log_instance: Log): + """Test that updating formatter preserves the handler's log level""" + original_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level + + log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL) + + new_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level + assert original_level == new_level + + def test_update_console_formatter_format_output( + self, log_instance: Log, caplog: pytest.LogCaptureFixture + ): + """Test that updated formatter actually affects log output""" + # Set to BARE format (message only) + log_instance.update_console_formatter(ConsoleFormatSettings.BARE) + + # Configure caplog to capture at the appropriate level + with caplog.at_level(logging.WARNING): + log_instance.warning("Test warning message") + + # Verify message was logged + assert "Test warning message" in caplog.text + # __END__