diff --git a/.env.example b/.env.example index c4f8eaa..01a4abc 100644 --- a/.env.example +++ b/.env.example @@ -38,4 +38,6 @@ STORAGE_CACHE_SECONDS=900 # Optional: SMART-Abfragen aktivieren (benötigt Zugriff auf Host-Devices) SMART_ENABLED=false HOST_SYS_PATH=/host-sys +LOG_FILE=/data/media-max.log +LOG_RETENTION_HOURS=6 LOG_LEVEL=INFO diff --git a/app.py b/app.py index 0a8d0b7..e2f8cfd 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ import json import logging +from logging.handlers import TimedRotatingFileHandler import os import sqlite3 import subprocess @@ -44,9 +45,17 @@ HOST_DEVICE_PATH = os.environ.get("HOST_DEVICE_PATH", "/host-dev") HOST_SYS_PATH = os.environ.get("HOST_SYS_PATH", "/host-sys") STORAGE_CACHE_SECONDS = int(os.environ.get("STORAGE_CACHE_SECONDS", "900")) SMART_ENABLED = os.environ.get("SMART_ENABLED", "false").lower() in {"1", "true", "yes", "on"} +LOG_FILE = os.environ.get("LOG_FILE", "/data/media-max.log") +LOG_RETENTION_HOURS = max(1, int(os.environ.get("LOG_RETENTION_HOURS", "6"))) logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper(), format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("media-max") +try: + file_handler = TimedRotatingFileHandler(LOG_FILE, when="H", interval=LOG_RETENTION_HOURS, backupCount=1, encoding="utf-8", delay=True, utc=True) + file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) + log.addHandler(file_handler) +except OSError as exc: + log.warning("Datei-Logging nicht verfügbar (%s): %s", LOG_FILE, exc) app = Flask(__name__) executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "2"))) scan_executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "4"))) @@ -233,12 +242,12 @@ def scan_datastore_storage(): "free": format_bytes(usage.free), "percent": round(usage.used / usage.total * 100, 1) if usage.total else 0, "categories": values, "scanned_at": time.time()} -def _read_command(command, timeout=4): +def _read_command(command, timeout=4, warn_on_nonzero=True): command_text = " ".join(str(part) for part in command) log.debug("Systembefehl gestartet: %s", command_text) try: result = subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=False) - if result.returncode != 0: + if result.returncode != 0 and warn_on_nonzero: log.warning("Systembefehl beendet mit Code %s: %s%s", result.returncode, command_text, f" · {result.stderr.strip()[:180]}" if result.stderr.strip() else "") return result.stdout, result.stderr except (OSError, subprocess.SubprocessError) as exc: @@ -386,9 +395,9 @@ def raid_status(): hardware_log_once.add(serial_log_key) if SMART_ENABLED and shutil.which("smartctl"): smart_device = disk["device"].replace("/dev/", f"{HOST_DEVICE_PATH.rstrip('/')}/", 1) - smart_output, smart_error = _read_command(["smartctl", "-H", smart_device], timeout=6) + smart_output, smart_error = _read_command(["smartctl", "-H", smart_device], timeout=6, warn_on_nonzero=False) if not re.search(r"(?:overall-health|Health Status|SMART support is)", smart_output, re.IGNORECASE): - sat_output, sat_error = _read_command(["smartctl", "-H", "-d", "sat", smart_device], timeout=6) + sat_output, sat_error = _read_command(["smartctl", "-H", "-d", "sat", smart_device], timeout=6, warn_on_nonzero=False) if sat_output: smart_output, smart_error = sat_output, sat_error health = re.search(r"(?:SMART overall-health self-assessment test result|SMART Health Status):\s*(.+)", smart_output, re.IGNORECASE)