main.logger
1import sys 2from datetime import datetime 3import logging 4import os 5import gzip 6import shutil 7from logging.handlers import BaseRotatingHandler 8from pathlib import Path 9 10from utils import get_app_dir 11 12 13class CompressedRotatingFileHandler(BaseRotatingHandler): 14 """ 15 A rotating log file handler that compresses rotated files. 16 17 This handler behaves like a standard rotating file handler, but when a 18 rollover occurs, the old log file is compressed into a `.gz` archive. 19 It also limits the number of backups retained. 20 21 Attributes 22 ---------- 23 maxBytes : int 24 Maximum size in bytes before a rollover is triggered. 25 backupCount : int 26 Maximum number of compressed backup files to retain. 27 log_dir : Path 28 Directory where log files are stored. 29 """ 30 31 def __init__(self, filename, mode="a", maxBytes=10 * 1024 * 1024, 32 backupCount=5, encoding=None, delay=False): 33 """ 34 Initialize the handler and ensure the log directory exists. 35 36 Parameters 37 ---------- 38 filename : str or Path 39 Path to the main log file. 40 mode : str, optional 41 File mode (default is "a"). 42 maxBytes : int, optional 43 Maximum size in bytes before triggering a rollover. 44 backupCount : int, optional 45 Number of compressed backups to retain. 46 encoding : str, optional 47 Encoding for the log file. 48 delay : bool, optional 49 If True, file opening is deferred until the first write. 50 """ 51 self.maxBytes = maxBytes 52 self.backupCount = backupCount 53 self.log_dir = Path(filename).parent 54 55 # Ensure log directory exists 56 self.log_dir.mkdir(parents=True, exist_ok=True) 57 58 super().__init__(filename, mode, encoding=encoding, delay=delay) 59 60 def shouldRollover(self, record): 61 """ 62 Determine whether the log file should be rotated. 63 64 Checks if the size of the log file plus the next log message 65 would exceed the `maxBytes` limit. 66 67 Parameters 68 ---------- 69 record : logging.LogRecord 70 The log record about to be written. 71 72 Returns 73 ------- 74 bool 75 True if rollover should occur, False otherwise. 76 """ 77 if self.stream is None: 78 self.stream = self._open() 79 80 if self.maxBytes > 0: 81 msg = f"{self.format(record)}\n" 82 self.stream.seek(0, os.SEEK_END) 83 if self.stream.tell() + len(msg.encode(self.encoding or "utf-8")) >= self.maxBytes: 84 return True 85 return False 86 87 def doRollover(self): 88 """ 89 Perform the rollover, compress the old file, and maintain only recent backups. 90 91 The current log file is closed, compressed into `.gz`, and deleted. 92 Then only the most recent `backupCount` compressed files are kept. 93 """ 94 if self.stream: 95 self.stream.close() 96 self.stream = None 97 98 # Create timestamped filename 99 ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 100 rotated_file = self.log_dir / f"{Path(self.baseFilename).name}.{ts}.gz" 101 102 # Compress current file 103 if os.path.exists(self.baseFilename): 104 with open(self.baseFilename, "rb") as f_in, gzip.open(rotated_file, "wb") as f_out: 105 shutil.copyfileobj(f_in, f_out) 106 os.remove(self.baseFilename) 107 108 # Keep only the latest N compressed backups 109 files = sorted( 110 (f for f in self.log_dir.glob("*.gz") if f.is_file()), 111 key=lambda f: f.stat().st_mtime 112 ) 113 if len(files) > self.backupCount: 114 for old_file in files[:-self.backupCount]: 115 old_file.unlink() 116 117 # Reopen new empty log file 118 if not self.delay: 119 self.stream = self._open() 120 121 122def setup_logger(console, 123 logger_name="GliAAns-UI", 124 logfile=None, 125 level=logging.ERROR, 126 maxBytes=10 * 1024 * 1024, 127 backupCount=5): 128 """ 129 Configure and return a logger with optional console output. 130 131 Parameters 132 ---------- 133 console : bool 134 If True, also log messages to the console (stdout). 135 logger_name : str, optional 136 Name of the logger (default: "GliAAns-UI"). 137 logfile : str or Path, optional 138 Path to the log file. If None, defaults to `~/.log/log.txt` inside 139 the app directory. 140 level : int, optional 141 Logging level (default: logging.ERROR). 142 maxBytes : int, optional 143 Maximum log file size before rotation. 144 backupCount : int, optional 145 Number of compressed backups to retain. 146 147 Returns 148 ------- 149 logging.Logger 150 The configured logger instance. 151 """ 152 # Determine log directory under the application path 153 app_dir = get_app_dir() 154 log_dir = app_dir / ".log" 155 log_dir.mkdir(parents=True, exist_ok=True) 156 157 if logfile is None: 158 logfile = log_dir / "log.txt" 159 160 logger = logging.getLogger(logger_name) 161 logger.setLevel(level) 162 163 # Remove existing handlers to prevent duplicates 164 if logger.hasHandlers(): 165 logger.handlers.clear() 166 167 # File handler with compression and rotation 168 handler = CompressedRotatingFileHandler( 169 logfile, 170 maxBytes=maxBytes, 171 backupCount=backupCount 172 ) 173 file_formatter = logging.Formatter( 174 "%(asctime)s\t%(levelname)s\t%(filename)s:%(lineno)d\t%(funcName)s\t%(message)s" 175 ) 176 handler.setFormatter(file_formatter) 177 logger.addHandler(handler) 178 179 # Optional console handler for interactive debugging 180 if console: 181 console_handler = logging.StreamHandler(sys.stdout) 182 console_formatter = logging.Formatter( 183 "%(levelname)s\t%(filename)s:%(lineno)d\t%(funcName)s\t%(message)s" 184 ) 185 console_handler.setLevel(logging.DEBUG) 186 console_handler.setFormatter(console_formatter) 187 logger.addHandler(console_handler) 188 189 return logger 190 191 192def get_logger(logger_name="GliAAns-UI"): 193 """ 194 Retrieve an existing logger instance by name. 195 196 Parameters 197 ---------- 198 logger_name : str, optional 199 The logger name (default: "GliAAns-UI"). 200 201 Returns 202 ------- 203 logging.Logger 204 The requested logger instance. 205 """ 206 return logging.getLogger(logger_name) 207 208 209def set_log_level(level, logger_name="GliAAns-UI"): 210 """ 211 Set the log level for an existing logger. 212 213 Parameters 214 ---------- 215 level : int 216 Logging level (e.g., logging.DEBUG, logging.INFO). 217 logger_name : str, optional 218 Name of the logger (default: "GliAAns-UI"). 219 """ 220 logging.getLogger(logger_name).setLevel(level)
14class CompressedRotatingFileHandler(BaseRotatingHandler): 15 """ 16 A rotating log file handler that compresses rotated files. 17 18 This handler behaves like a standard rotating file handler, but when a 19 rollover occurs, the old log file is compressed into a `.gz` archive. 20 It also limits the number of backups retained. 21 22 Attributes 23 ---------- 24 maxBytes : int 25 Maximum size in bytes before a rollover is triggered. 26 backupCount : int 27 Maximum number of compressed backup files to retain. 28 log_dir : Path 29 Directory where log files are stored. 30 """ 31 32 def __init__(self, filename, mode="a", maxBytes=10 * 1024 * 1024, 33 backupCount=5, encoding=None, delay=False): 34 """ 35 Initialize the handler and ensure the log directory exists. 36 37 Parameters 38 ---------- 39 filename : str or Path 40 Path to the main log file. 41 mode : str, optional 42 File mode (default is "a"). 43 maxBytes : int, optional 44 Maximum size in bytes before triggering a rollover. 45 backupCount : int, optional 46 Number of compressed backups to retain. 47 encoding : str, optional 48 Encoding for the log file. 49 delay : bool, optional 50 If True, file opening is deferred until the first write. 51 """ 52 self.maxBytes = maxBytes 53 self.backupCount = backupCount 54 self.log_dir = Path(filename).parent 55 56 # Ensure log directory exists 57 self.log_dir.mkdir(parents=True, exist_ok=True) 58 59 super().__init__(filename, mode, encoding=encoding, delay=delay) 60 61 def shouldRollover(self, record): 62 """ 63 Determine whether the log file should be rotated. 64 65 Checks if the size of the log file plus the next log message 66 would exceed the `maxBytes` limit. 67 68 Parameters 69 ---------- 70 record : logging.LogRecord 71 The log record about to be written. 72 73 Returns 74 ------- 75 bool 76 True if rollover should occur, False otherwise. 77 """ 78 if self.stream is None: 79 self.stream = self._open() 80 81 if self.maxBytes > 0: 82 msg = f"{self.format(record)}\n" 83 self.stream.seek(0, os.SEEK_END) 84 if self.stream.tell() + len(msg.encode(self.encoding or "utf-8")) >= self.maxBytes: 85 return True 86 return False 87 88 def doRollover(self): 89 """ 90 Perform the rollover, compress the old file, and maintain only recent backups. 91 92 The current log file is closed, compressed into `.gz`, and deleted. 93 Then only the most recent `backupCount` compressed files are kept. 94 """ 95 if self.stream: 96 self.stream.close() 97 self.stream = None 98 99 # Create timestamped filename 100 ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 101 rotated_file = self.log_dir / f"{Path(self.baseFilename).name}.{ts}.gz" 102 103 # Compress current file 104 if os.path.exists(self.baseFilename): 105 with open(self.baseFilename, "rb") as f_in, gzip.open(rotated_file, "wb") as f_out: 106 shutil.copyfileobj(f_in, f_out) 107 os.remove(self.baseFilename) 108 109 # Keep only the latest N compressed backups 110 files = sorted( 111 (f for f in self.log_dir.glob("*.gz") if f.is_file()), 112 key=lambda f: f.stat().st_mtime 113 ) 114 if len(files) > self.backupCount: 115 for old_file in files[:-self.backupCount]: 116 old_file.unlink() 117 118 # Reopen new empty log file 119 if not self.delay: 120 self.stream = self._open()
A rotating log file handler that compresses rotated files.
This handler behaves like a standard rotating file handler, but when a
rollover occurs, the old log file is compressed into a .gz archive.
It also limits the number of backups retained.
Attributes
maxBytes : int Maximum size in bytes before a rollover is triggered. backupCount : int Maximum number of compressed backup files to retain. log_dir : Path Directory where log files are stored.
32 def __init__(self, filename, mode="a", maxBytes=10 * 1024 * 1024, 33 backupCount=5, encoding=None, delay=False): 34 """ 35 Initialize the handler and ensure the log directory exists. 36 37 Parameters 38 ---------- 39 filename : str or Path 40 Path to the main log file. 41 mode : str, optional 42 File mode (default is "a"). 43 maxBytes : int, optional 44 Maximum size in bytes before triggering a rollover. 45 backupCount : int, optional 46 Number of compressed backups to retain. 47 encoding : str, optional 48 Encoding for the log file. 49 delay : bool, optional 50 If True, file opening is deferred until the first write. 51 """ 52 self.maxBytes = maxBytes 53 self.backupCount = backupCount 54 self.log_dir = Path(filename).parent 55 56 # Ensure log directory exists 57 self.log_dir.mkdir(parents=True, exist_ok=True) 58 59 super().__init__(filename, mode, encoding=encoding, delay=delay)
Initialize the handler and ensure the log directory exists.
Parameters
filename : str or Path Path to the main log file. mode : str, optional File mode (default is "a"). maxBytes : int, optional Maximum size in bytes before triggering a rollover. backupCount : int, optional Number of compressed backups to retain. encoding : str, optional Encoding for the log file. delay : bool, optional If True, file opening is deferred until the first write.
61 def shouldRollover(self, record): 62 """ 63 Determine whether the log file should be rotated. 64 65 Checks if the size of the log file plus the next log message 66 would exceed the `maxBytes` limit. 67 68 Parameters 69 ---------- 70 record : logging.LogRecord 71 The log record about to be written. 72 73 Returns 74 ------- 75 bool 76 True if rollover should occur, False otherwise. 77 """ 78 if self.stream is None: 79 self.stream = self._open() 80 81 if self.maxBytes > 0: 82 msg = f"{self.format(record)}\n" 83 self.stream.seek(0, os.SEEK_END) 84 if self.stream.tell() + len(msg.encode(self.encoding or "utf-8")) >= self.maxBytes: 85 return True 86 return False
Determine whether the log file should be rotated.
Checks if the size of the log file plus the next log message
would exceed the maxBytes limit.
Parameters
record : logging.LogRecord The log record about to be written.
Returns
bool True if rollover should occur, False otherwise.
88 def doRollover(self): 89 """ 90 Perform the rollover, compress the old file, and maintain only recent backups. 91 92 The current log file is closed, compressed into `.gz`, and deleted. 93 Then only the most recent `backupCount` compressed files are kept. 94 """ 95 if self.stream: 96 self.stream.close() 97 self.stream = None 98 99 # Create timestamped filename 100 ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 101 rotated_file = self.log_dir / f"{Path(self.baseFilename).name}.{ts}.gz" 102 103 # Compress current file 104 if os.path.exists(self.baseFilename): 105 with open(self.baseFilename, "rb") as f_in, gzip.open(rotated_file, "wb") as f_out: 106 shutil.copyfileobj(f_in, f_out) 107 os.remove(self.baseFilename) 108 109 # Keep only the latest N compressed backups 110 files = sorted( 111 (f for f in self.log_dir.glob("*.gz") if f.is_file()), 112 key=lambda f: f.stat().st_mtime 113 ) 114 if len(files) > self.backupCount: 115 for old_file in files[:-self.backupCount]: 116 old_file.unlink() 117 118 # Reopen new empty log file 119 if not self.delay: 120 self.stream = self._open()
Perform the rollover, compress the old file, and maintain only recent backups.
The current log file is closed, compressed into .gz, and deleted.
Then only the most recent backupCount compressed files are kept.
123def setup_logger(console, 124 logger_name="GliAAns-UI", 125 logfile=None, 126 level=logging.ERROR, 127 maxBytes=10 * 1024 * 1024, 128 backupCount=5): 129 """ 130 Configure and return a logger with optional console output. 131 132 Parameters 133 ---------- 134 console : bool 135 If True, also log messages to the console (stdout). 136 logger_name : str, optional 137 Name of the logger (default: "GliAAns-UI"). 138 logfile : str or Path, optional 139 Path to the log file. If None, defaults to `~/.log/log.txt` inside 140 the app directory. 141 level : int, optional 142 Logging level (default: logging.ERROR). 143 maxBytes : int, optional 144 Maximum log file size before rotation. 145 backupCount : int, optional 146 Number of compressed backups to retain. 147 148 Returns 149 ------- 150 logging.Logger 151 The configured logger instance. 152 """ 153 # Determine log directory under the application path 154 app_dir = get_app_dir() 155 log_dir = app_dir / ".log" 156 log_dir.mkdir(parents=True, exist_ok=True) 157 158 if logfile is None: 159 logfile = log_dir / "log.txt" 160 161 logger = logging.getLogger(logger_name) 162 logger.setLevel(level) 163 164 # Remove existing handlers to prevent duplicates 165 if logger.hasHandlers(): 166 logger.handlers.clear() 167 168 # File handler with compression and rotation 169 handler = CompressedRotatingFileHandler( 170 logfile, 171 maxBytes=maxBytes, 172 backupCount=backupCount 173 ) 174 file_formatter = logging.Formatter( 175 "%(asctime)s\t%(levelname)s\t%(filename)s:%(lineno)d\t%(funcName)s\t%(message)s" 176 ) 177 handler.setFormatter(file_formatter) 178 logger.addHandler(handler) 179 180 # Optional console handler for interactive debugging 181 if console: 182 console_handler = logging.StreamHandler(sys.stdout) 183 console_formatter = logging.Formatter( 184 "%(levelname)s\t%(filename)s:%(lineno)d\t%(funcName)s\t%(message)s" 185 ) 186 console_handler.setLevel(logging.DEBUG) 187 console_handler.setFormatter(console_formatter) 188 logger.addHandler(console_handler) 189 190 return logger
Configure and return a logger with optional console output.
Parameters
console : bool
If True, also log messages to the console (stdout).
logger_name : str, optional
Name of the logger (default: "GliAAns-UI").
logfile : str or Path, optional
Path to the log file. If None, defaults to ~/.log/log.txt inside
the app directory.
level : int, optional
Logging level (default: logging.ERROR).
maxBytes : int, optional
Maximum log file size before rotation.
backupCount : int, optional
Number of compressed backups to retain.
Returns
logging.Logger The configured logger instance.
193def get_logger(logger_name="GliAAns-UI"): 194 """ 195 Retrieve an existing logger instance by name. 196 197 Parameters 198 ---------- 199 logger_name : str, optional 200 The logger name (default: "GliAAns-UI"). 201 202 Returns 203 ------- 204 logging.Logger 205 The requested logger instance. 206 """ 207 return logging.getLogger(logger_name)
Retrieve an existing logger instance by name.
Parameters
logger_name : str, optional The logger name (default: "GliAAns-UI").
Returns
logging.Logger The requested logger instance.
210def set_log_level(level, logger_name="GliAAns-UI"): 211 """ 212 Set the log level for an existing logger. 213 214 Parameters 215 ---------- 216 level : int 217 Logging level (e.g., logging.DEBUG, logging.INFO). 218 logger_name : str, optional 219 Name of the logger (default: "GliAAns-UI"). 220 """ 221 logging.getLogger(logger_name).setLevel(level)
Set the log level for an existing logger.
Parameters
level : int Logging level (e.g., logging.DEBUG, logging.INFO). logger_name : str, optional Name of the logger (default: "GliAAns-UI").