import json import logging from datetime import UTC, datetime try: # For module mode: `uvicorn app.main:app` from app.settings import settings except ModuleNotFoundError: # For script mode: `python app/main.py` or VS Code "Run Python File" from settings import settings _CONFIGURED = False class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: payload: dict[str, object] = { "timestamp": datetime.now(UTC).isoformat(), "level": record.levelname, "logger": record.name, "message": record.getMessage(), } for key in ( "event", "request_id", "method", "path", "status_code", "duration_ms", "client_ip", "user_id", "conversation_id", "message_id", "seq", "client_msg_id", "idempotent", "count", "has_more", "cursor_seq", "limit", "reason", "sql_ms", "statement", "username", ): if hasattr(record, key): payload[key] = getattr(record, key) if record.exc_info: payload["exception"] = self.formatException(record.exc_info) return json.dumps(payload, ensure_ascii=False) def configure_logging() -> None: global _CONFIGURED if _CONFIGURED: return log_level_name = settings.log_level.upper().strip() log_level = getattr(logging, log_level_name, logging.INFO) handler = logging.StreamHandler() if settings.log_json: handler.setFormatter(JsonFormatter()) else: handler.setFormatter( logging.Formatter( fmt="%(asctime)s | %(levelname)s | %(name)s | %(message)s", ) ) root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.setLevel(log_level) root_logger.addHandler(handler) # Keep uvicorn logs consistent with app logs. for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"): uvicorn_logger = logging.getLogger(logger_name) uvicorn_logger.handlers.clear() uvicorn_logger.propagate = True logging.getLogger("httpx").setLevel(logging.WARNING) _CONFIGURED = True