]> git.giorgioravera.it Git - network-manager.git/commitdiff
Extended log module to support access log & fixed issue with uvicorn logs.
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Fri, 23 Jan 2026 22:22:38 +0000 (23:22 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Fri, 23 Jan 2026 22:22:38 +0000 (23:22 +0100)
backend/main.py
log/log.py
settings/default.py
settings/settings.py

index 7303938393f066236ef497b54c2688b70ecdf59c..8bde8df681ecf45a0b7fd6d5d0176f08e10dc3e6 100644 (file)
@@ -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,
index 155f5cb7adda9c6d74855f2b8f1782cff7948a5d..fa0b77a6913d9510c56ffd47c522cc77414a94f0 100644 (file)
@@ -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")
index 9126c0f13bfde1e2dd5e539678bb126b8192533e..1013cad25e1e825fa2faee2be657afd939c4e2a3 100644 (file)
@@ -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
index 4da9a99517ec739978ecf8684e9a2059cf206a4a..76e6a93663d0f7228ddfb094978e30109da6ddc1 100644 (file)
@@ -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))