diff --git a/src/logging/session_file_handler.py b/src/logging/session_file_handler.py new file mode 100644 index 0000000..5562735 --- /dev/null +++ b/src/logging/session_file_handler.py @@ -0,0 +1,70 @@ +import lzma +from io import StringIO +from logging import StreamHandler +from lzma import FORMAT_XZ, PRESET_DEFAULT +from os import PathLike +from pathlib import Path + + +class SessionFileHandler(StreamHandler): + """ + A logging handler that writes to a new file on every app restart. + The files are compressed and older sessions logs are kept up to a small limit. + """ + + backup_count: int + filename: Path + log_file: StringIO = None + + def create_dir(self) -> None: + """Create the log dir if needed""" + self.filename.parent.mkdir(exist_ok=True) + + def rotate_file(self, file: Path): + """Rotate a file's number suffix and remove it if it's too old""" + + # Skip non interesting dir entries + if not (file.is_file() and file.name.startswith(self.filename.name)): + return + + # Compute the new number suffix + suffixes = file.suffixes + has_number = len(suffixes) != len(self.filename.suffixes) + current_number = 0 if not has_number else int(suffixes[-1][1:]) + new_number = current_number + 1 + + # Rename with new number suffix + if has_number: + suffixes.pop() + suffixes.append(f".{new_number}") + stem = file.name.split(".", maxsplit=1)[0] + new_name = stem + "".join(suffixes) + print(f"Log file renamed: {file.name} -> {new_name}") + file = file.rename(file.with_name(new_name)) + + # Remove older files + if new_number > self.backup_count: + print(f"Log file deleted: {file.name}") + file.unlink() + return + + def rotate(self) -> None: + """Rotate the numbered suffix on the log files and remove old ones""" + files = list(self.filename.parent.iterdir()) + files.sort(key=lambda file: file.name, reverse=True) + for file in files: + self.rotate_file(file) + + def __init__(self, filename: PathLike, backup_count: int = 2) -> None: + self.filename = Path(filename) + self.backup_count = backup_count + self.create_dir() + self.rotate() + self.log_file = lzma.open( + self.filename, "at", format=FORMAT_XZ, preset=PRESET_DEFAULT + ) + super().__init__(self.log_file) + + def close(self) -> None: + self.log_file.close() + super().close() diff --git a/src/logging/setup.py b/src/logging/setup.py index c8184ff..696d574 100644 --- a/src/logging/setup.py +++ b/src/logging/setup.py @@ -8,18 +8,14 @@ from src import shared def setup_logging(): """Intitate the app's logging""" - # Prepare the log file - log_dir = shared.data_dir / "cartridges" / "logs" - log_dir.mkdir(exist_ok=True) - log_file_path = log_dir / "cartridges.log" - log_file_max_size_bytes = 8 * 10**6 # 8 MB - - # Define log levels - profile_app_log_level = "DEBUG" if shared.PROFILE == "development" else "INFO" - profile_lib_log_level = "INFO" if shared.PROFILE == "development" else "WARNING" + is_dev = shared.PROFILE == "development" + profile_app_log_level = "DEBUG" if is_dev else "INFO" + profile_lib_log_level = "INFO" if is_dev else "WARNING" app_log_level = os.environ.get("LOGLEVEL", profile_app_log_level).upper() lib_log_level = os.environ.get("LIBLOGLEVEL", profile_lib_log_level).upper() + log_filename = shared.data_dir / "cartridges" / "logs" / "cartridges.log.xz" + config = { "version": 1, "formatters": { @@ -33,12 +29,11 @@ def setup_logging(): }, "handlers": { "file_handler": { - "class": "logging.handlers.RotatingFileHandler", + "class": "src.logging.session_file_handler.SessionFileHandler", "formatter": "file_formatter", "level": "DEBUG", - "filename": log_file_path, - "maxBytes": log_file_max_size_bytes, - "backupCount": 1, + "filename": log_filename, + "backup_count": 2, }, "app_console_handler": { "class": "logging.StreamHandler",