Files
Tha.Les 995e402220 feat(logging): rotating file log + level control; stop leaking exceptions into the UI (#295)
Attach a RotatingFileHandler (LOGS_DIR/stemdeck.log, 5 MB x 3, timestamped)
to the stemdeck logger so server and Docker deployments keep an on-disk
trail -- until now LOGS_DIR existed but nothing ever wrote to it, and
stdout scrollback was the only record. Best-effort: a read-only FS
degrades to stdout-only logging instead of failing startup.

Level is now controllable: STEMDECK_LOG_LEVEL=DEBUG|INFO|WARNING, with
STEMDECK_DEBUG=1 as shorthand. This also un-deadens the analyze
diagnostics ("chroma:", "key candidates:") -- they are logger.debug
calls that could never emit under the previous hardcoded INFO level,
despite the comment claiming otherwise.

Also stop interpolating raw exception reprs into the user-visible
"Analysis skipped" stage message; the traceback is already in the log.

Closes #291
Closes #292
Closes #283

Co-authored-by: Thales <>
2026-07-16 23:45:29 +01:00

85 lines
2.8 KiB
Python

"""File logging for the stemdeck logger tree (#291).
Until now the app logged to stdout only (via uvicorn's root handler): server
and Docker deployments kept no log file at all, and LOGS_DIR existed but was
never written to. This module attaches a rotating file handler to the
"stemdeck" logger so every deployment keeps a bounded on-disk trail:
LOGS_DIR/stemdeck.log (5 MB x 3 backups, UTF-8, timestamped)
Level control:
- STEMDECK_LOG_LEVEL=DEBUG|INFO|WARNING (default INFO)
- STEMDECK_DEBUG=1 (shorthand for DEBUG; enables the
per-job analyze diagnostics: "chroma:", "key candidates:")
Everything here is best-effort: a read-only filesystem (locked-down Docker)
must never prevent startup, so failures degrade to stdout-only logging.
"""
from __future__ import annotations
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from app.core.config import LOGS_DIR
# Module-level so tests can shrink them to exercise rotation.
_MAX_BYTES = 5 * 1024 * 1024
_BACKUP_COUNT = 3
# Marker attribute so repeat calls (uvicorn --reload re-imports app.main)
# don't stack duplicate handlers.
_HANDLER_MARK = "_stemdeck_file_handler"
_LEVELS = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING}
def _resolve_level() -> int:
if os.environ.get("STEMDECK_DEBUG", "").strip() == "1":
return logging.DEBUG
name = os.environ.get("STEMDECK_LOG_LEVEL", "").strip().upper()
return _LEVELS.get(name, logging.INFO)
def configure_logging() -> None:
"""Set the stemdeck logger level and attach the rotating file handler.
Propagation stays on, so records continue to flow to uvicorn's stdout
handler exactly as before -- the file is additive.
"""
root = logging.getLogger("stemdeck")
root.setLevel(_resolve_level())
if any(getattr(h, _HANDLER_MARK, False) for h in root.handlers):
return # already configured (reload / repeated import)
try:
LOGS_DIR.mkdir(parents=True, exist_ok=True)
# delay=True: don't open the file until the first record, so a
# read-only FS fails at emit time (swallowed by logging's internal
# error handling) instead of at startup.
handler = RotatingFileHandler(
LOGS_DIR / "stemdeck.log",
maxBytes=_MAX_BYTES,
backupCount=_BACKUP_COUNT,
encoding="utf-8",
delay=True,
)
except OSError:
print(
f"stemdeck: file logging disabled (cannot use logs dir {LOGS_DIR})",
file=sys.stderr,
)
return
handler.setFormatter(
logging.Formatter(
"%(asctime)s %(levelname).1s %(name)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
)
setattr(handler, _HANDLER_MARK, True)
root.addHandler(handler)