feat: add file-based logging with hourly rotation and configurable retention period

Add LOG_FILE and LOG_RETENTION_HOURS environment variables to .env.example with defaults /data/media-max.log and 6 hours. Install TimedRotatingFileHandler with hourly rotation, 1 backup, UTF-8 encoding, and UTC timestamps. Add OSError exception handling with warning log when file logging unavailable. Add warn_on_nonzero parameter to _read_command() with default True and disable for smartctl -H calls to suppress expected
This commit is contained in:
2026-08-16 12:22:06 +02:00
parent 02a5db61be
commit 24b2a3a12f
2 changed files with 15 additions and 4 deletions
+2
View File
@@ -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
+13 -4
View File
@@ -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)