127 lines
4.1 KiB
Python
127 lines
4.1 KiB
Python
"""
|
|
logging - Centralized logging configuration for Vacuum Wall.
|
|
|
|
Call :func:`setup_logging` once at application startup. All other
|
|
modules obtain a logger via ``logging.getLogger(__name__)``.
|
|
|
|
Output:
|
|
* **stderr** (StreamHandler) - captured by systemd journald
|
|
* **data/logs/vacuum-wall.log** (RotatingFileHandler) - persisted for
|
|
viewing via the WebUI ``/logs`` page.
|
|
"""
|
|
|
|
import contextlib
|
|
import logging
|
|
import os
|
|
import sys
|
|
from logging.handlers import RotatingFileHandler
|
|
from pathlib import Path
|
|
|
|
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
|
_LOG_DIR = PROJECT_DIR / "data" / "logs"
|
|
_LOG_FILE = _LOG_DIR / "vacuum-wall.log"
|
|
|
|
_MAX_BYTES = 5 * 1024 * 1024 # 5 MB
|
|
_BACKUP_COUNT = 3
|
|
|
|
_LOG_FMT = "[%(asctime)s] %(levelname)-8s %(name)s %(message)s"
|
|
_DATE_FMT = "%Y-%m-%d %H:%M:%S"
|
|
|
|
_initialized = False
|
|
|
|
|
|
def setup_logging(level: str | None = None) -> None:
|
|
"""Configure and enable root-level logging for the application.
|
|
|
|
Safe to call multiple times; subsequent calls are no-ops.
|
|
|
|
Args:
|
|
level: Override log level string (e.g. ``"DEBUG"``). If ``None``,
|
|
reads ``VACUUM_WALL_LOG_LEVEL`` from the environment, defaulting
|
|
to ``"INFO"``.
|
|
"""
|
|
global _initialized
|
|
if _initialized:
|
|
return
|
|
_initialized = True
|
|
|
|
if level is None:
|
|
level = os.environ.get("VACUUM_WALL_LOG_LEVEL", "INFO").upper()
|
|
|
|
valid_levels = {
|
|
"DEBUG": logging.DEBUG,
|
|
"INFO": logging.INFO,
|
|
"WARNING": logging.WARNING,
|
|
"ERROR": logging.ERROR,
|
|
"CRITICAL": logging.CRITICAL,
|
|
}
|
|
numeric = valid_levels.get(level, logging.INFO)
|
|
|
|
root = logging.getLogger()
|
|
root.setLevel(numeric)
|
|
|
|
fmt = logging.Formatter(_LOG_FMT, datefmt=_DATE_FMT)
|
|
|
|
# stderr handler — feeds systemd journal
|
|
sh = logging.StreamHandler(sys.stderr)
|
|
sh.setFormatter(fmt)
|
|
root.addHandler(sh)
|
|
|
|
class GroupWriteHandler(RotatingFileHandler):
|
|
"""RotatingFileHandler that always opens files with group-write mode.
|
|
|
|
Ensures the log file is group-writable so both the WebUI process
|
|
(vacuum-wall user) and daemon process (vacuum-walld user) can write
|
|
to it when they share a group.
|
|
"""
|
|
|
|
def _open(self):
|
|
# Ensure group-write on an existing stale file (e.g. left by the
|
|
# other process with a stricter umask at creation time).
|
|
with contextlib.suppress(OSError):
|
|
os.chmod(self.baseFilename, 0o664)
|
|
# Temporarily clear group-write umask bits so os.open's mode is
|
|
# not masked away.
|
|
old = os.umask(0o002)
|
|
try:
|
|
fd = os.open(
|
|
self.baseFilename, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o664
|
|
)
|
|
finally:
|
|
os.umask(old)
|
|
return os.fdopen(fd, "a", errors="backslashreplace")
|
|
|
|
def doRollover(self):
|
|
"""Override to enforce group-write on rotated files."""
|
|
super().doRollover()
|
|
# Set group-write on all log files (current + backups)
|
|
base = Path(self.baseFilename)
|
|
for suffix in ("", ".1", ".2", ".3"):
|
|
fp = str(base.parent / base.name + suffix)
|
|
with contextlib.suppress(OSError):
|
|
os.chmod(fp, 0o664)
|
|
|
|
# rotating file handler with group-write permissions
|
|
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
fh = GroupWriteHandler(
|
|
str(_LOG_FILE),
|
|
maxBytes=_MAX_BYTES,
|
|
backupCount=_BACKUP_COUNT,
|
|
)
|
|
fh.setFormatter(fmt)
|
|
root.addHandler(fh)
|
|
except PermissionError:
|
|
# Log file exists but is not writable (e.g. stale file from the other
|
|
# process created with a stricter umask). Fall back to stderr-only.
|
|
print(
|
|
f"WARNING: cannot open log file {_LOG_FILE}, "
|
|
"logging to stderr only. Fix with: chmod g+w "
|
|
f"{_LOG_FILE}",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
# Silence noisy third-party loggers in production
|
|
for name in ("werkzeug", "urllib3"):
|
|
logging.getLogger(name).setLevel(logging.WARNING)
|