From 491d351eb1caea544fd192d97444e7e9c1df761a Mon Sep 17 00:00:00 2001 From: zurdi Date: Fri, 23 May 2025 00:15:33 +0000 Subject: [PATCH] feat: implement custom logging middleware and configure logging settings --- backend/logger/log_middleware.py | 35 +++++++++++++++++++++++ backend/logger/logger.py | 33 ++++++++++++++++++++++ backend/main.py | 8 +++++- poetry.lock | 48 +++++++++++++++++++++++++++++++- pyproject.toml | 2 ++ 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 backend/logger/log_middleware.py diff --git a/backend/logger/log_middleware.py b/backend/logger/log_middleware.py new file mode 100644 index 000000000..37f96494a --- /dev/null +++ b/backend/logger/log_middleware.py @@ -0,0 +1,35 @@ +import logging +import time + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from user_agents import parse as parse_user_agent + +log = logging.getLogger("uvicorn.access") + + +class CustomLoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + + client_addr = request.client.host if request.client else "-" + method = request.method + path = request.url.path + status_code = response.status_code + length = response.headers.get("content-length", "-") + ua_string = request.headers.get("user-agent", "-") + ua = parse_user_agent(ua_string) + + browser = ua.browser.family + os = ua.os.family + + log_msg = ( + f"{client_addr} | " + f"{method} {path} {status_code} | {length} | " + f"{browser} {os} | {process_time:.3f}" + ) + + log.info(log_msg) + return response diff --git a/backend/logger/logger.py b/backend/logger/logger.py index 2e7f26b58..6abd3675f 100644 --- a/backend/logger/logger.py +++ b/backend/logger/logger.py @@ -16,6 +16,39 @@ log.addHandler(stdout_handler) # Hush passlib warnings logging.getLogger("passlib").setLevel(logging.ERROR) +LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "romm": { + "()": "logger.formatter.Formatter", + } + }, + "handlers": { + "default": { + "formatter": "romm", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + } + }, + "root": { + "handlers": ["default"], + "level": "DEBUG", + }, + "loggers": { + "uvicorn": { + "level": "INFO", + "handlers": ["default"], + "propagate": False, + }, + "uvicorn.error": { + "level": "INFO", + "handlers": ["default"], + "propagate": False, + }, + }, +} + def unify_logger(logger: str) -> None: """ diff --git a/backend/main.py b/backend/main.py index b84b0dfd7..77d62edda 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,3 +1,4 @@ +import logging.config import re from collections.abc import AsyncGenerator from contextlib import asynccontextmanager @@ -39,10 +40,14 @@ from handler.auth.constants import ALGORITHM from handler.auth.hybrid_auth import HybridAuthBackend from handler.auth.middleware import CustomCSRFMiddleware, SessionMiddleware from handler.socket_handler import socket_handler +from logger.log_middleware import CustomLoggingMiddleware +from logger.logger import LOGGING_CONFIG from starlette.middleware.authentication import AuthenticationMiddleware from utils import get_version from utils.context import ctx_httpx_client, initialize_context, set_context_middleware +logging.config.dictConfig(LOGGING_CONFIG) + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: @@ -127,4 +132,5 @@ if __name__ == "__main__": alembic.config.main(argv=["upgrade", "head"]) # Run application - uvicorn.run("main:app", host=DEV_HOST, port=DEV_PORT, reload=True) + app.add_middleware(CustomLoggingMiddleware) + uvicorn.run("main:app", host=DEV_HOST, port=DEV_PORT, reload=True, access_log=False) diff --git a/poetry.lock b/poetry.lock index d56d905af..9176d4a06 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3648,6 +3648,37 @@ files = [ {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, ] +[[package]] +name = "ua-parser" +version = "1.0.1" +description = "Python port of Browserscope's user agent parser" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ua_parser-1.0.1-py3-none-any.whl", hash = "sha256:b059f2cb0935addea7e551251cbbf42e9a8872f86134163bc1a4f79e0945ffea"}, + {file = "ua_parser-1.0.1.tar.gz", hash = "sha256:f9d92bf19d4329019cef91707aecc23c6d65143ad7e29a233f0580fb0d15547d"}, +] + +[package.dependencies] +ua-parser-builtins = "*" + +[package.extras] +re2 = ["google-re2"] +regex = ["ua-parser-rs"] +yaml = ["PyYaml"] + +[[package]] +name = "ua-parser-builtins" +version = "0.18.0.post1" +description = "Precompiled rules for User Agent Parser" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ua_parser_builtins-0.18.0.post1-py3-none-any.whl", hash = "sha256:eb4f93504040c3a990a6b0742a2afd540d87d7f9f05fd66e94c101db1564674d"}, +] + [[package]] name = "uc-micro-py" version = "1.0.3" @@ -3694,6 +3725,21 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "user-agents" +version = "2.2.0" +description = "A library to identify devices (phones, tablets) and their capabilities by parsing browser user agent strings." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "user-agents-2.2.0.tar.gz", hash = "sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26"}, + {file = "user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7"}, +] + +[package.dependencies] +ua-parser = ">=0.10.0" + [[package]] name = "uvicorn" version = "0.29.0" @@ -4131,4 +4177,4 @@ test = ["fakeredis", "pytest", "pytest-asyncio", "pytest-env", "pytest-mock", "p [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "7e116a77af549750e6a4b4cc066b34d87ed6242ef52e0848954e7b77ee03078f" +content-hash = "5e5aac10f3ab3173592b281814a38a126edd59a0bf41982dc49b8eca43b672c2" diff --git a/pyproject.toml b/pyproject.toml index 175484f58..28b6f650a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,8 @@ dependencies = [ "yarl ~= 1.14", "zipfile-deflate64 ~= 0.2", "fastapi-pagination (>=0.12.34,<0.13.0)", + "ua-parser (>=1.0.1,<2.0.0)", + "user-agents (>=2.2.0,<3.0.0)", ] [project.optional-dependencies]