From 83957ff548297846c13d800e743db56adaf4122a Mon Sep 17 00:00:00 2001 From: wowlikon Date: Mon, 29 Dec 2025 15:20:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + library_service/auth.py | 2 +- library_service/main.py | 101 +++++++++++++++++++++++++--- library_service/settings.py | 129 +++++++++++++++++++++--------------- poetry.lock | 13 +++- pyproject.toml | 1 + 6 files changed, 183 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index c3bd4a8..3eda569 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +*.log # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/library_service/auth.py b/library_service/auth.py index 72e48a1..f82860f 100644 --- a/library_service/auth.py +++ b/library_service/auth.py @@ -22,7 +22,7 @@ REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "7")) # Получение логгера -logger = get_logger("uvicorn") +logger = get_logger() # OAuth2 схема oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token") diff --git a/library_service/main.py b/library_service/main.py index 692b6fd..4e3b5e5 100644 --- a/library_service/main.py +++ b/library_service/main.py @@ -1,22 +1,32 @@ """Основной модуль""" from contextlib import asynccontextmanager +from datetime import datetime from pathlib import Path +from time import perf_counter +from uuid import uuid4 from alembic import command from alembic.config import Config -from fastapi import FastAPI +from fastapi import Request, Response from fastapi.staticfiles import StaticFiles from sqlmodel import Session -from .auth import run_seeds -from .routers import api_router -from .settings import engine, get_app, get_logger +from library_service.auth import run_seeds +from library_service.routers import api_router +from library_service.settings import ( + LOGGING_CONFIG, + engine, + get_app, + get_logger, +) + +SKIP_LOGGING_PATHS = frozenset({"/health", "/favicon.ico"}) @asynccontextmanager -async def lifespan(app: FastAPI): +async def lifespan(_): """Жизненный цикл сервиса""" - logger = get_logger("uvicorn") + logger = get_logger() logger.info("[+] Initializing database...") try: @@ -45,7 +55,82 @@ async def lifespan(app: FastAPI): app = get_app(lifespan) +# Улучшеное логгирование +@app.middleware("http") +async def log_requests(request: Request, call_next): + """Middleware для логирования HTTP-запросов""" + path = request.url.path + if path.startswith("/static") or path in SKIP_LOGGING_PATHS: + return await call_next(request) + + logger = get_logger() + request_id = uuid4().hex[:8] + timestamp = datetime.now().isoformat() + method = request.method + url = str(request.url) + user_agent = request.headers.get("user-agent", "Unknown") + client_ip = request.client.host if request.client else None + + start_time = perf_counter() + + try: + logger.debug( + f"[{request_id}] Starting: {method} {url}", + extra={"request_id": request_id, "user_agent": user_agent}, + ) + + response: Response = await call_next(request) + process_time = perf_counter() - start_time + + logger.info( + f"[{request_id}] {method} {url} - {response.status_code} - {process_time:.4f}s", + extra={ + "request_id": request_id, + "timestamp": timestamp, + "method": method, + "url": url, + "status": response.status_code, + "process_time": process_time, + "client_ip": client_ip, + "user_agent": user_agent, + }, + ) + return response + + except Exception as e: + process_time = perf_counter() - start_time + logger.error( + f"[{request_id}] {method} {url} - Error: {e} - {process_time:.4f}s", + extra={ + "request_id": request_id, + "timestamp": timestamp, + "method": method, + "url": url, + "error": str(e), + "process_time": process_time, + "client_ip": client_ip, + "user_agent": user_agent, + }, + exc_info=True, + ) + return Response(status_code=500, content="Internal Server Error") + + # Подключение маршрутов app.include_router(api_router) -static_path = Path(__file__).parent / "static" -app.mount("/static", StaticFiles(directory=static_path), name="static") +app.mount( + "/static", + StaticFiles(directory=Path(__file__).parent / "static"), + name="static", +) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "library_service.main:app", + host="0.0.0.0", + port=8000, + log_config=LOGGING_CONFIG, + access_log=False, + ) diff --git a/library_service/settings.py b/library_service/settings.py index 19be3ec..2a8643a 100644 --- a/library_service/settings.py +++ b/library_service/settings.py @@ -1,5 +1,6 @@ """Модуль настроек проекта""" import os, logging +from pathlib import Path from dotenv import load_dotenv from fastapi import FastAPI @@ -8,63 +9,75 @@ from toml import load load_dotenv() -with open("pyproject.toml", 'r', encoding='utf-8') as f: - config = load(f) +with open("pyproject.toml", "r", encoding="utf-8") as f: + _pyproject = load(f) + +_APP_NAME = "library_service" + +LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": True, + "formatters": { + "json": { + "class": "json_log_formatter.JSONFormatter", + "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d", + }, + }, + "handlers": { + "console": { + "()": "rich.logging.RichHandler", + "level": "INFO", + "show_time": True, + "show_path": True, + "rich_tracebacks": True, + }, + "file": { + "class": "logging.FileHandler", + "filename": Path(__file__).parent / "app.log", + "formatter": "json", + "level": "INFO", + }, + }, + "loggers": { + "uvicorn": { + "handlers": [], + "level": "INFO", + "propagate": False, + }, + _APP_NAME: { + "handlers": ["console", "file"], + "level": "INFO", + "propagate": False, + }, + }, +} + +OPENAPI_TAGS = [ + {"name": "authentication", "description": "Авторизация пользователя."}, + {"name": "authors", "description": "Действия с авторами."}, + {"name": "books", "description": "Действия с книгами."}, + {"name": "genres", "description": "Действия с жанрами."}, + {"name": "loans", "description": "Действия с выдачами."}, + {"name": "relations", "description": "Действия со связями."}, + {"name": "misc", "description": "Прочие."}, +] def get_app(lifespan=None, /) -> FastAPI: """Возвращает экземпляр FastAPI приложения""" - if not hasattr(get_app, 'instance'): - get_app.instance = FastAPI( - title=config["tool"]["poetry"]["name"], - description=config["tool"]["poetry"]["description"] + " | [Вернутьсяна главную](/)", - version=config["tool"]["poetry"]["version"], - lifespan=lifespan, - openapi_tags=[ - { - "name": "authentication", - "description": "Авторизация пользователя." - }, - { - "name": "authors", - "description": "Действия с авторами.", - }, - { - "name": "books", - "description": "Действия с книгами.", - }, - { - "name": "genres", - "description": "Действия с жанрами.", - }, - { - "name": "loans", - "description": "Действия с выдачами.", - }, - { - "name": "relations", - "description": "Действия с связями.", - }, - { - "name": "misc", - "description": "Прочие.", - }, - ], - ) - return get_app.instance + poetry_cfg = _pyproject["tool"]["poetry"] + return FastAPI( + title=poetry_cfg["name"], + description=f"{poetry_cfg['description']} | [Вернуться на главную](/)", + version=poetry_cfg["version"], + lifespan=lifespan, + openapi_tags=OPENAPI_TAGS, + ) -HOST = os.getenv("POSTGRES_HOST") -PORT = os.getenv("POSTGRES_PORT") -USER = os.getenv("POSTGRES_USER") -PASSWORD = os.getenv("POSTGRES_PASSWORD") -DATABASE = os.getenv("POSTGRES_DB") - -if not USER or not PASSWORD or not DATABASE or not HOST: - raise ValueError("Missing environment variables") - -POSTGRES_DATABASE_URL = f"postgresql://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}" -engine = create_engine(POSTGRES_DATABASE_URL, echo=False, future=True) +def get_logger(name: str = _APP_NAME) -> logging.Logger: + """Возвращает логгер с указанным именем""" + return logging.getLogger(name) def get_session(): @@ -73,6 +86,14 @@ def get_session(): yield session -def get_logger(name: str = "uvicorn"): - """Возвращает логгер с указанным именем""" - return logging.getLogger(name) +HOST = os.getenv("POSTGRES_HOST") +PORT = os.getenv("POSTGRES_PORT") +USER = os.getenv("POSTGRES_USER") +PASSWORD = os.getenv("POSTGRES_PASSWORD") +DATABASE = os.getenv("POSTGRES_DB") + +if not all([HOST, PORT, USER, PASSWORD, DATABASE]): + raise ValueError("Missing required POSTGRES environment variables") + +POSTGRES_DATABASE_URL = f"postgresql://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}" +engine = create_engine(POSTGRES_DATABASE_URL, echo=False, future=True) diff --git a/poetry.lock b/poetry.lock index 97f431e..4d06ec4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -776,6 +776,17 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "json-log-formatter" +version = "1.1.1" +description = "JSON log formatter" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "json_log_formatter-1.1.1.tar.gz", hash = "sha256:0815e3b4469e5c79cf3f6dc8a0613ba6601f4a7464f85ba03655cfa6e3e17d10"}, +] + [[package]] name = "mako" version = "1.3.10" @@ -2247,4 +2258,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "6048c7b120fbeb4533a1e858a87eb09aec68e5da569ca821b9e41ecabf220a9e" +content-hash = "2b11386d46acf1ce961f4fd14df46e5bedb6d5a894bf448708b3aaf5b02a401a" diff --git a/pyproject.toml b/pyproject.toml index 88a56b7..2c47b1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ python-jose = {extras = ["cryptography"], version = "^3.5.0"} passlib = {extras = ["argon2"], version = "^1.7.4"} aiofiles = "^25.1.0" pydantic = {extras = ["email"], version = "^2.12.5"} +json-log-formatter = "^1.1.1" [tool.poetry.group.dev.dependencies] black = "^25.1.0"