From 23be1f51532eb10bd8f84b7ef7dede440c881aba Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Fri, 23 Jan 2026 23:22:38 +0100 Subject: [PATCH] Extended log module to support access log & fixed issue with uvicorn logs. --- backend/main.py | 5 ++- log/log.py | 78 ++++++++++++++++++++++++++++++++------------ settings/default.py | 1 + settings/settings.py | 1 + 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/backend/main.py b/backend/main.py index 7303938..8bde8df 100644 --- a/backend/main.py +++ b/backend/main.py @@ -21,8 +21,8 @@ from log.log import setup_logging, get_logger # ------------------------------------------------------------------------------ # Logging setup # ------------------------------------------------------------------------------ -setup_logging(settings.LOG_LEVEL, settings.LOG_TO_FILE, settings.LOG_FILE) -logger = get_logger(__name__) +setup_logging(level=settings.LOG_LEVEL, to_file=settings.LOG_TO_FILE, log_file=settings.LOG_FILE, log_access_file=settings.LOG_ACCESS_FILE) +logger = get_logger("backend.main") # ------------------------------------------------------------------------------ # App init @@ -79,7 +79,6 @@ cors_origins = [ f"http://localhost:{settings.HTTP_PORT}", f"http://127.0.0.1:{settings.HTTP_PORT}", ] - app.add_middleware( CORSMiddleware, allow_origins=cors_origins, diff --git a/log/log.py b/log/log.py index 155f5cb..fa0b77a 100644 --- a/log/log.py +++ b/log/log.py @@ -1,9 +1,11 @@ # log/log.py +from __future__ import annotations + import logging import logging.config import os -import sys +from typing import Optional # Module-level flag to prevent re-initialization _INITIALIZED = False @@ -11,7 +13,7 @@ _INITIALIZED = False # --------------------------------------------------------- # Build a full dictConfig for logging # --------------------------------------------------------- -def build_log_config(level: str = "INFO", to_file: bool = False, file: str | None = None) -> dict: +def build_log_config(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None,) -> dict: """ Returns a complete dictConfig for the logging system, including formatters, console/file handlers, and specific loggers (uvicorn, fastapi, etc). @@ -19,51 +21,81 @@ def build_log_config(level: str = "INFO", to_file: bool = False, file: str | Non level = (level or "INFO").upper() formatters = { + # Generic Formatter "detailed": { "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s", "datefmt": "%Y-%m-%dT%H:%M:%S%z", }, + # Access log Formatter "access": { - "format": '%(asctime)s %(levelname)s [%(name)s] ' - '%(client_addr)s - "%(request_line)s" %(status_code)s', + "()": "uvicorn.logging.AccessFormatter", + "fmt": '%(asctime)s %(levelname)s [%(name)s] %(client_addr)s - "%(request_line)s" %(status_code)s', "datefmt": "%Y-%m-%dT%H:%M:%S%z", }, } handlers = { + # Generic Console (root/uvicorn/uvicorn.error/fastapi/app) "console": { "class": "logging.StreamHandler", "level": level, "formatter": "detailed", "stream": "ext://sys.stdout", - } + }, + # Access log Console + "access_console": { + "class": "logging.StreamHandler", + "level": level, + "formatter": "access", + "stream": "ext://sys.stdout", + }, } # Select active handler based on console active_handlers = ["console"] + access_handlers = ["access_console"] if to_file: - if file is not None: + if log_file is not None: # Ensure the log directory exists and add a rotating file handler - log_dir = os.path.dirname(file) or "." + log_dir = os.path.dirname(log_file) or "." os.makedirs(log_dir, exist_ok=True) + # handler for generic log handlers["file"] = { "class": "logging.handlers.RotatingFileHandler", "level": level, "formatter": "detailed", - "filename": file, + "filename": log_file, "maxBytes": 5 * 1024 * 1024, "backupCount": 5, "encoding": "utf-8", } - # Add active handler based on file + # Add active handler for generic log file active_handlers.append("file") + if log_access_file is not None: + # Ensure the log directory exists and add a rotating file handler + log_dir = os.path.dirname(log_access_file) or "." + os.makedirs(log_dir, exist_ok=True) + # handler for access log + handlers["access_file"] = { + "class": "logging.handlers.RotatingFileHandler", + "level": level, + "formatter": "access", + "filename": log_access_file, + "maxBytes": 5 * 1024 * 1024, + "backupCount": 5, + "encoding": "utf-8", + } + # Add active handler for access log file + access_handlers.append("access_file") return { "version": 1, "disable_existing_loggers": False, "formatters": formatters, "handlers": handlers, + + # Root logger "root": { "level": level, "handlers": active_handlers, @@ -82,10 +114,10 @@ def build_log_config(level: str = "INFO", to_file: bool = False, file: str | Non "handlers": active_handlers, "propagate": False, }, - # Uvicorn HTTP access + # Uvicorn access log "uvicorn.access": { "level": level, - "handlers": active_handlers, + "handlers": access_handlers, "propagate": False, }, # FastAPI @@ -94,28 +126,34 @@ def build_log_config(level: str = "INFO", to_file: bool = False, file: str | Non "handlers": active_handlers, "propagate": False, }, + # Logger applicativo di comodo (puoi usarlo come logging.getLogger("main")) + "main": { + "level": level, + "handlers": active_handlers, + "propagate": False, + }, }, } # --------------------------------------------------------- # Initialize logging once (singleton guard) # --------------------------------------------------------- -def setup_logging(level: str = "INFO", to_file: bool = False, file: str | None = None) -> None: +def setup_logging(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None, *, force: bool = False) -> None: """ Initializes the logging system only once. Subsequent calls are no-ops. Useful to prevent duplicated handlers or reconfiguration side effects. """ global _INITIALIZED - if _INITIALIZED: + if _INITIALIZED and not force: return - config = build_log_config(level=level, to_file=to_file, file=file) + config = build_log_config(level=level, to_file=to_file, log_file=log_file, log_access_file=log_access_file) logging.config.dictConfig(config) - logging.getLogger(__name__).info( - "Logging configured (level=%s, to_file=%s, file=%s)", - level.upper(), to_file, file + logging.getLogger("main").info( + "Logging configured (level=%s, to_file=%s, log_file=%s, log_access_file=%s)", + level.upper(), to_file, log_file, log_access_file ) _INITIALIZED = True @@ -123,11 +161,9 @@ def setup_logging(level: str = "INFO", to_file: bool = False, file: str | None = # --------------------------------------------------------- # Get a configured logger for the given module/name # --------------------------------------------------------- -def get_logger(name: str = None) -> logging.Logger: +def get_logger(name: str | None = None) -> logging.Logger: """ Returns a logger instance configured via the module setup. If setup was not called yet, it falls back to the standard logging defaults. """ - if not name: - name = __name__ - return logging.getLogger(name) + return logging.getLogger(name or "main") diff --git a/settings/default.py b/settings/default.py index 9126c0f..1013cad 100644 --- a/settings/default.py +++ b/settings/default.py @@ -17,6 +17,7 @@ DB_RESET = False LOG_LEVEL = "INFO" LOG_TO_FILE = False LOG_FILE = "/data/app.log" +LOG_ACCESS_FILE = "/data/access.log" # --------------------------------------------------------- # Host diff --git a/settings/settings.py b/settings/settings.py index 4da9a99..76e6a93 100644 --- a/settings/settings.py +++ b/settings/settings.py @@ -62,6 +62,7 @@ class Settings(BaseModel): LOG_LEVEL: str = Field(default_factory=lambda: os.getenv("LOG_LEVEL", default.LOG_LEVEL)) LOG_TO_FILE: bool = Field(default_factory=lambda: _to_bool(os.getenv("LOG_TO_FILE", default.LOG_TO_FILE))) LOG_FILE: str = Field(default_factory=lambda: os.getenv("LOG_FILE", default.LOG_FILE)) + LOG_ACCESS_FILE: str = Field(default_factory=lambda: os.getenv("LOG_ACCESS_FILE", default.LOG_ACCESS_FILE)) # Hosts DOMAIN: str = Field(default_factory=lambda: os.getenv("DOMAIN", default.DOMAIN)) -- 2.47.3