# /core/logging.py from __future__ import annotations import json import logging import sys from datetime import datetime from typing import Optional try: # Optional: human-friendly console colors if installed import colorama # type: ignore colorama.init() _HAS_COLOR = True except Exception: # pragma: no cover _HAS_COLOR = False # Very small JSON formatter (avoids extra deps) class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: # type: ignore[override] payload = { "ts": datetime.utcfromtimestamp(record.created).isoformat(timespec="milliseconds") + "Z", "level": record.levelname, "logger": record.name, "msg": record.getMessage(), } if record.exc_info: payload["exc_info"] = self.formatException(record.exc_info) return json.dumps(payload, ensure_ascii=False) class ConsoleFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: # type: ignore[override] ts = datetime.utcfromtimestamp(record.created).strftime("%H:%M:%S") lvl = record.levelname name = record.name msg = record.getMessage() if _HAS_COLOR: COLORS = { "DEBUG": "\033[37m", "INFO": "\033[36m", "WARNING": "\033[33m", "ERROR": "\033[31m", "CRITICAL": "\033[41m", } RESET = "\033[0m" color = COLORS.get(lvl, "") return f"{ts} {color}{lvl:<8}{RESET} {name}: {msg}" return f"{ts} {lvl:<8} {name}: {msg}" _initialized = False def setup_logging(level: str = "INFO", json_logs: bool = False) -> None: """ Initialize root logger once. """ global _initialized if _initialized: return _initialized = True root = logging.getLogger() root.setLevel(level.upper()) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(JsonFormatter() if json_logs else ConsoleFormatter()) root.handlers[:] = [handler] def get_logger(name: Optional[str] = None) -> logging.Logger: """ Get a logger (call setup_logging() first to configure formatting). """ return logging.getLogger(name or "app")