Files
duck-preview/app/logging_setup.py

119 lines
3.7 KiB
Python

"""Logging initialisation — file + console handlers with session isolation."""
from __future__ import annotations
import logging
import platform
import sys
from pathlib import Path
from app.config import APP_NAME, APP_VERSION, MAX_LOG_FILES
_LOG_FORMAT = "%(asctime)s.%(msecs)03d [%(levelname)-7s] %(name)s: %(message)s"
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
_console_handler: logging.StreamHandler | None = None
def setup_logging(log_dir: Path, session_id: str) -> Path:
"""
Configure the root logger for a new session.
Creates:
<log_dir>/duck-preview_<session_id>.log — DEBUG level, all messages
console StreamHandler — WARNING by default
Prunes oldest log files when count exceeds MAX_LOG_FILES.
Args:
log_dir: Directory where log files are stored.
session_id: Timestamp string used as filename suffix (e.g. "2026-05-12_14-30-00").
Returns:
Path to the created log file.
"""
global _console_handler
log_dir.mkdir(parents=True, exist_ok=True)
log_path = log_dir / f"duck-preview_{session_id}.log"
root = logging.getLogger()
root.setLevel(logging.DEBUG) # handlers filter individually
formatter = logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT)
# --- File handler — always DEBUG ---
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
root.addHandler(file_handler)
# --- Console handler — WARNING by default, toggled by Debug menu ---
_console_handler = logging.StreamHandler(sys.stdout)
_console_handler.setLevel(logging.WARNING)
_console_handler.setFormatter(formatter)
root.addHandler(_console_handler)
# Write session header to file
_write_session_header(log_path, session_id)
# Prune old log files
_prune_old_logs(log_dir, log_path)
return log_path
def set_console_level(debug: bool) -> None:
"""Toggle console handler between DEBUG and WARNING (called from Debug menu)."""
if _console_handler is not None:
_console_handler.setLevel(logging.DEBUG if debug else logging.WARNING)
def _write_session_header(log_path: Path, session_id: str) -> None:
"""Write a human-readable header block at the top of the log file."""
try:
import PySide6
pyside_version = PySide6.__version__
except Exception:
pyside_version = "unknown"
try:
import psutil
mem = psutil.virtual_memory()
cpu_count = psutil.cpu_count(logical=True)
mem_total_gb = mem.total / (1024 ** 3)
hw_info = f"{cpu_count} logical CPUs, {mem_total_gb:.1f} GB RAM"
except Exception:
hw_info = "unknown"
lines = [
"=" * 72,
f" {APP_NAME} {APP_VERSION}",
f" Session: {session_id}",
"=" * 72,
f" Platform : {platform.platform()}",
f" Python : {sys.version.split()[0]}",
f" PySide6 : {pyside_version}",
f" Hardware : {hw_info}",
f" Log file : {log_path.resolve()}",
"=" * 72,
"",
]
with log_path.open("w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
def _prune_old_logs(log_dir: Path, current: Path) -> None:
"""Delete oldest .log files if total count exceeds MAX_LOG_FILES."""
log_files = sorted(
[p for p in log_dir.glob("duck-preview_*.log") if p != current],
key=lambda p: p.stat().st_mtime,
)
excess = len(log_files) - (MAX_LOG_FILES - 1)
for path in log_files[:excess]:
try:
path.unlink()
logging.getLogger(__name__).debug("Pruned old log: %s", path.name)
except OSError:
pass