Улучшение логгирования

This commit is contained in:
2025-12-29 15:20:43 +03:00
parent 5a814d99e6
commit 83957ff548
6 changed files with 183 additions and 64 deletions
Vendored
+1
View File
@@ -1,4 +1,5 @@
.env
*.log
# Byte-compiled / optimized / DLL files
__pycache__/
+1 -1
View File
@@ -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")
+93 -8
View File
@@ -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,
)
+75 -54
View File
@@ -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)
Generated
+12 -1
View File
@@ -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"
+1
View File
@@ -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"