from __future__ import annotations
"""Logger Configuration Module.
Provides centralized logging configuration for the Sec Interp plugin.
"""
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any
from qgis.core import Qgis, QgsMessageLog
[docs]
class QgsLogHandler(logging.Handler):
"""Custom logging handler that writes to QGIS message log."""
[docs]
def __init__(self, tag: str = "SecInterp") -> None:
"""Initialize handler with a specific tag for QGIS message log."""
super().__init__()
self.tag = tag
[docs]
def emit(self, record: logging.LogRecord) -> None:
"""Emit a log record to QGIS message log safely."""
try:
from qgis.PyQt.QtCore import QCoreApplication, QThread
msg = self.format(record)
# Map Python logging levels to QGIS levels
if record.levelno >= logging.ERROR:
level = Qgis.Critical
elif record.levelno >= logging.WARNING:
level = Qgis.Warning
elif record.levelno >= logging.INFO:
level = Qgis.Info
else:
level = Qgis.Info
# Critical: UI updates from background threads cause segfaults in QGIS
instance = QCoreApplication.instance()
if instance and QThread.currentThread() == instance.thread():
QgsMessageLog.logMessage(msg, self.tag, level)
else:
# Fallback to standard error for background threads to avoid potential UI hangs
# sys.stderr is safer and doesn't trigger "print" deviations
sys.stderr.write(f"[{self.tag}] (BG) {msg}\n")
except Exception:
self.handleError(record)
ROOT_LOGGER_NAME = "SecInterp"
[docs]
def setup_logging(level: int = logging.DEBUG) -> logging.Logger:
"""Configure the root logger for the plugin.
This should be called once at plugin initialization. It sets up
handlers for QGIS message log, file logging with rotation, and stderr.
Args:
level: Logging level for the root logger. Defaults to DEBUG.
Returns:
logging.Logger: The configured root logger.
"""
root_logger = logging.getLogger(ROOT_LOGGER_NAME)
# Only configure if not already configured
if not root_logger.handlers:
root_logger.setLevel(level)
# 1. Create QGIS message log handler (for UI)
qgis_handler = QgsLogHandler(tag=ROOT_LOGGER_NAME)
qgis_handler.setLevel(logging.INFO)
qgis_formatter = logging.Formatter("%(levelname)s - %(message)s")
qgis_handler.setFormatter(qgis_formatter)
root_logger.addHandler(qgis_handler)
# 2. Create file handler for detailed crash analysis
try:
# Determine log directory (in repository root)
# Find the root of the project (where logger_config.py is)
root_dir = Path(__file__).parent
log_dir = root_dir / "logs"
log_dir.mkdir(exist_ok=True)
log_file = log_dir / "sec_interp_debug.log"
# Use ImmediateFlushFileHandler for crash analysis
file_handler = ImmediateFlushFileHandler(
log_file,
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)
# Detailed formatter for file logs
file_formatter = logging.Formatter(
"%(asctime)s | %(levelname)-8s | %(name)s | "
"%(funcName)s:%(lineno)d | Thread-%(thread)d | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler.setFormatter(file_formatter)
root_logger.addHandler(file_handler)
# 3. Add stderr handler as backup for crash scenarios
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(logging.WARNING) # Only warnings and errors to stderr
stderr_handler.setFormatter(file_formatter)
root_logger.addHandler(stderr_handler)
# Log the initialization
root_logger.debug(f"Logging system initialized. Root Level: {level}")
root_logger.debug(f"File logging path: {log_file}")
except Exception as e:
# If file logging fails, continue with QGIS logging only
QgsMessageLog.logMessage(
f"Warning: Could not initialize file logging: {e}",
ROOT_LOGGER_NAME,
Qgis.Warning,
)
# Ensure propagation is enabled for children
root_logger.propagate = True
return root_logger
[docs]
def get_logger(name: str | None = None) -> logging.Logger:
"""Get a configured logger for a plugin module.
Args:
name: Name of the logger (typically __name__ from calling module).
If None, returns the root plugin logger.
Returns:
logging.Logger: Configured logger instance.
"""
if name is None or name == ROOT_LOGGER_NAME:
return logging.getLogger(ROOT_LOGGER_NAME)
# Ensure we use the hierarchy (SecInterp.something.else)
full_name = name if name.startswith(ROOT_LOGGER_NAME + ".") else f"{ROOT_LOGGER_NAME}.{name}"
logger = logging.getLogger(full_name)
# Auto-initialize if root has no handlers (for tests or standalone usage)
root = logging.getLogger(ROOT_LOGGER_NAME)
if not root.handlers and name != ROOT_LOGGER_NAME:
setup_logging()
return logger
[docs]
def log_critical_operation(logger: logging.Logger, operation_name: str, **context: Any) -> None:
"""Log a critical operation with maximum persistence.
Use this before operations that might crash QGIS (e.g., canvas operations,
rubber band manipulation, tool activation).
Args:
logger: Logger instance.
operation_name: Name of the operation.
**context: Additional context to log.
"""
import datetime
msg = f"CRITICAL_OP: {operation_name}"
if context:
ctx_str = ", ".join(f"{k}={v}" for k, v in context.items())
msg += f" | {ctx_str}"
# Log through normal channels
logger.debug(msg)
# Also write directly to stderr with timestamp (bypasses all buffering)
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
sys.stderr.write(f"[{timestamp}] {msg}\n")
sys.stderr.flush()
# Try to force OS sync on stderr too
try:
if hasattr(sys.stderr, "fileno"):
os.fsync(sys.stderr.fileno())
except (OSError, AttributeError):
pass