From 21822224b7685a72a05d291837bcdec58211ef9b Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Fri, 24 Jul 2026 22:01:41 +0200 Subject: [PATCH] Update AzLogs to 2.0.1 and centralize rotated log filename handling --- __init__.py | 18 +++------ core/log_system/__init__.py | 7 ++-- core/log_system/config.py | 2 +- core/log_system/log_funcs.py | 2 +- core/log_system/logger.py | 23 ++++++++++- tests/test_log_rotation.py | 77 ++++++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 tests/test_log_rotation.py diff --git a/__init__.py b/__init__.py index 609258c..f280bbf 100644 --- a/__init__.py +++ b/__init__.py @@ -35,6 +35,7 @@ from .core.file_manager import ( from .core.log_system import LogLevel, create_module_logger from .core.log_system import logger as backend_log_controller from .core.log_system.config import LOG_LEVEL as BACKEND_DEFAULT_LOG_LEVEL +from .core.log_system.logger import parse_rotated_log_filename from .core.path_utils import get_filename_from_path, get_metadata_sidecar_path, get_safe_metadata_sidecar_path # Web directory for JavaScript interface @@ -5039,17 +5040,10 @@ class ModelResolverExtension: def _backend_log_sort_key(path): name = os.path.basename(path) - rotation = 0 - if ".log." in name: - base_name, rotation_text = name.rsplit(".log.", 1) - try: - rotation = int(rotation_text) - except ValueError: - rotation = 999 - elif name.endswith(".log"): - base_name = name[:-4] - else: - base_name = name + parsed_name = parse_rotated_log_filename(name) + if parsed_name is None: + return (name.lower(), 999) + base_name, rotation = parsed_name return (base_name.lower(), rotation) def _collect_backend_log_files(log_dir): @@ -5061,7 +5055,7 @@ class ModelResolverExtension: name = str(entry or "") if not name.startswith("azlogs_"): continue - if not (name.endswith(".log") or ".log." in name): + if parse_rotated_log_filename(name) is None: continue path = os.path.abspath(os.path.join(log_dir, name)) diff --git a/core/log_system/__init__.py b/core/log_system/__init__.py index eeac67a..d2412e6 100644 --- a/core/log_system/__init__.py +++ b/core/log_system/__init__.py @@ -1,7 +1,8 @@ """ -AzLogs - Central logging system - -Provides colored console logging, file rotation, and per-module log levels. +@author: Azornes +@title: AzLogs +@version: 2.0.1 +@description: Logging Initializer """ from .log_funcs import create_module_logger diff --git a/core/log_system/config.py b/core/log_system/config.py index 477d42f..121ca44 100644 --- a/core/log_system/config.py +++ b/core/log_system/config.py @@ -1,7 +1,7 @@ """ @author: Azornes @title: AzLogs -@version: 2.0.0 +@version: 2.0.1 @description: Logging Configuration """ # Log level for development. diff --git a/core/log_system/log_funcs.py b/core/log_system/log_funcs.py index 1dc63aa..767dc3e 100644 --- a/core/log_system/log_funcs.py +++ b/core/log_system/log_funcs.py @@ -1,7 +1,7 @@ """ @author: Azornes @title: AzLogs -@version: 2.0.0 +@version: 2.0.1 @description: Logging Initializer """ import logging diff --git a/core/log_system/logger.py b/core/log_system/logger.py index 5b32249..778a0ff 100644 --- a/core/log_system/logger.py +++ b/core/log_system/logger.py @@ -1,7 +1,7 @@ """ @author: Azornes @title: AzLogs -@version: 2.0.0 +@version: 2.0.1 @description: Logging Setup - Central logging system Features: @@ -73,6 +73,26 @@ DEFAULT_CONFIG = { _WINDOWS_VT_PROCESSING_ENABLED = False +def rotated_log_filename(default_name): + """Keep the .log extension at the end of rotated log filenames.""" + match = re.fullmatch(r"(.+)\.log\.(\d+)", str(default_name or "")) + if not match: + return default_name + return f"{match.group(1)}.{match.group(2)}.log" + + +def parse_rotated_log_filename(filename): + """Return the log family and rotation number for the current filename style.""" + name = os.path.basename(str(filename or "")) + match = re.fullmatch(r"(.+)\.(\d+)\.log", name) + if match: + return match.group(1), int(match.group(2)) + + if name.endswith(".log"): + return name[:-4], 0 + return None + + def _enable_windows_virtual_terminal_processing(): """Enable ANSI escape sequence handling for classic Windows consoles.""" global _WINDOWS_VT_PROCESSING_ENABLED @@ -127,6 +147,7 @@ class SafeRotatingFileHandler(RotatingFileHandler): def __init__(self, *args, **kwargs): self._disabled_by_stream_error = False super().__init__(*args, **kwargs) + self.namer = rotated_log_filename def emit(self, record): if self._disabled_by_stream_error: diff --git a/tests/test_log_rotation.py b/tests/test_log_rotation.py new file mode 100644 index 0000000..283bfe5 --- /dev/null +++ b/tests/test_log_rotation.py @@ -0,0 +1,77 @@ +import logging +import os +import tempfile +import unittest + +from core.log_system.logger import ( + SafeRotatingFileHandler, + parse_rotated_log_filename, + rotated_log_filename, +) + + +class RotatedLogFilenameTests(unittest.TestCase): + def test_places_rotation_before_log_extension(self): + path = os.path.join("logs", "azlogs_MResolver.log.3") + self.assertEqual( + rotated_log_filename(path), + os.path.join("logs", "azlogs_MResolver.3.log"), + ) + + def test_leaves_unrelated_filename_unchanged(self): + path = os.path.join("logs", "azlogs_MResolver.log") + self.assertEqual(rotated_log_filename(path), path) + + def test_parser_accepts_current_style(self): + expected = ("azlogs_MResolver", 2) + self.assertEqual( + parse_rotated_log_filename("azlogs_MResolver.2.log"), + expected, + ) + self.assertEqual( + parse_rotated_log_filename("azlogs_MResolver.log"), + ("azlogs_MResolver", 0), + ) + + def test_parser_rejects_non_log_and_legacy_files(self): + self.assertIsNone(parse_rotated_log_filename("other.txt")) + self.assertIsNone(parse_rotated_log_filename("azlogs_MResolver.log.2")) + self.assertIsNone(parse_rotated_log_filename("azlogs_MResolver.log2")) + + +class SafeRotatingFileHandlerTests(unittest.TestCase): + def test_rollover_keeps_log_as_the_final_extension(self): + with tempfile.TemporaryDirectory() as temp_dir: + log_path = os.path.join(temp_dir, "azlogs_MResolver.log") + logger = logging.getLogger(f"test-log-rotation-{id(self)}") + logger.handlers.clear() + logger.propagate = False + logger.setLevel(logging.INFO) + handler = SafeRotatingFileHandler( + log_path, + maxBytes=32, + backupCount=2, + encoding="utf-8", + ) + handler.setFormatter(logging.Formatter("%(message)s")) + logger.addHandler(handler) + + try: + logger.info("first message large enough to rotate") + logger.info("second message large enough to rotate") + logger.info("third message large enough to rotate") + handler.flush() + finally: + logger.removeHandler(handler) + handler.close() + + files = set(os.listdir(temp_dir)) + self.assertIn("azlogs_MResolver.log", files) + self.assertIn("azlogs_MResolver.1.log", files) + self.assertIn("azlogs_MResolver.2.log", files) + self.assertNotIn("azlogs_MResolver.3.log", files) + self.assertNotIn("azlogs_MResolver.log.1", files) + + +if __name__ == "__main__": + unittest.main()