#!/usr/bin/env python3 """Encrypted log writer for SystemMonitor. Named bb_logging to avoid shadowing stdlib logging.""" import logging import os import time from pathlib import Path from typing import Optional LOG_FORMAT = "[{timestamp}] [{module}] [{level}] {message}" MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB default rotation size LOG_DIR = "storage/logs" class EncryptedLogHandler(logging.Handler): def __init__( self, module_name: str, log_dir: str = LOG_DIR, max_bytes: int = MAX_LOG_SIZE, encrypt: bool = False, crypto_engine=None, ): super().__init__() self.module_name = module_name self.log_dir = Path(log_dir) self.log_dir.mkdir(parents=True, exist_ok=True) self.max_bytes = max_bytes self.encrypt = encrypt self.crypto_engine = crypto_engine self._log_path = self.log_dir / f"{module_name}.log" self._file = None self._current_size = 0 self._open_file() def _open_file(self): if self._file: self._file.close() mode = "ab" if self.encrypt else "a" self._file = open(self._log_path, mode) self._current_size = self._log_path.stat().st_size if self._log_path.exists() else 0 def _rotate(self): if self._file: self._file.close() rotated = self._log_path.with_suffix(f".log.{int(time.time())}") if self._log_path.exists(): self._log_path.rename(rotated) self._open_file() def emit(self, record): try: ts = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(record.created)) line = LOG_FORMAT.format( timestamp=ts, module=self.module_name, level=record.levelname, message=record.getMessage(), ) if self.encrypt and self.crypto_engine: data = self.crypto_engine.encrypt(line.encode("utf-8")) length_prefix = len(data).to_bytes(4, "big") self._file.write(length_prefix) self._file.write(data) self._current_size += 4 + len(data) else: line += "\n" self._file.write(line) self._current_size += len(line.encode("utf-8")) self._file.flush() if self._current_size >= self.max_bytes: self._rotate() except Exception: self.handleError(record) def close(self): if self._file: self._file.close() self._file = None super().close() class BBLogger: def __init__( self, module_name: str, log_dir: str = LOG_DIR, max_bytes: int = MAX_LOG_SIZE, level: int = logging.INFO, encrypt: bool = False, crypto_engine=None, suppress_stdout: bool = True, ): self.module_name = module_name self.logger = logging.getLogger(f"sensor.{module_name}") self.logger.setLevel(level) self.logger.propagate = False # Clear existing handlers to prevent duplicates self.logger.handlers.clear() self._file_handler = EncryptedLogHandler( module_name=module_name, log_dir=log_dir, max_bytes=max_bytes, encrypt=encrypt, crypto_engine=crypto_engine, ) self._file_handler.setLevel(level) self.logger.addHandler(self._file_handler) if not suppress_stdout: console = logging.StreamHandler() console.setLevel(level) fmt = logging.Formatter( "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) console.setFormatter(fmt) self.logger.addHandler(console) def debug(self, msg: str, *args, **kwargs): self.logger.debug(msg, *args, **kwargs) def info(self, msg: str, *args, **kwargs): self.logger.info(msg, *args, **kwargs) def warning(self, msg: str, *args, **kwargs): self.logger.warning(msg, *args, **kwargs) def error(self, msg: str, *args, **kwargs): self.logger.error(msg, *args, **kwargs) def critical(self, msg: str, *args, **kwargs): self.logger.critical(msg, *args, **kwargs) def set_level(self, level: int): self.logger.setLevel(level) self._file_handler.setLevel(level) def close(self): self._file_handler.close() self.logger.removeHandler(self._file_handler)