"""Logging that goes to a file as well as to stdout. WHY BOTH stdout is what `docker compose logs` reads and what Docker's own log driver captures, so it has to stay. The file is so that every component in this stack has ONE findable place - ./logs, bind-mounted at /logs - rather than leaving somebody to work out which compose invocation shows the thing they are looking for. Falling back to stdout alone is deliberate. A container run without the mount, or with the directory not writable, should keep working and say why once; refusing to start because a log file could not be opened would be the tail wagging the dog. """ from __future__ import annotations import logging import os import sys FORMAT = "%(asctime)s %(levelname)-7s %(name)s | %(message)s" def configure(component: str, level: str = "INFO") -> logging.Logger: """stdout always; ./logs/.log when the directory is there.""" handlers: list[logging.Handler] = [logging.StreamHandler(sys.stdout)] problem = "" log_dir = os.environ.get("OWUITOOLS_LOG_DIR", "/logs") if log_dir: try: os.makedirs(log_dir, exist_ok=True) handlers.append(logging.FileHandler( os.path.join(log_dir, f"{component}.log"), encoding="utf-8")) except OSError as exc: problem = f"{log_dir} is not writable ({exc}); logging to stdout only" logging.basicConfig(level=getattr(logging, level.upper(), logging.INFO), format=FORMAT, handlers=handlers, force=True) logger = logging.getLogger(component) if problem: logger.warning(problem) return logger