mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 12:31:09 +00:00
Улучшение логгирования
This commit is contained in:
Vendored
+1
@@ -1,4 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
|
*.log
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -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 схема
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||||
|
|||||||
+93
-8
@@ -1,22 +1,32 @@
|
|||||||
"""Основной модуль"""
|
"""Основной модуль"""
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from time import perf_counter
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from alembic import command
|
from alembic import command
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from fastapi import FastAPI
|
from fastapi import Request, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
from .auth import run_seeds
|
from library_service.auth import run_seeds
|
||||||
from .routers import api_router
|
from library_service.routers import api_router
|
||||||
from .settings import engine, get_app, get_logger
|
from library_service.settings import (
|
||||||
|
LOGGING_CONFIG,
|
||||||
|
engine,
|
||||||
|
get_app,
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
SKIP_LOGGING_PATHS = frozenset({"/health", "/favicon.ico"})
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(_):
|
||||||
"""Жизненный цикл сервиса"""
|
"""Жизненный цикл сервиса"""
|
||||||
logger = get_logger("uvicorn")
|
logger = get_logger()
|
||||||
logger.info("[+] Initializing database...")
|
logger.info("[+] Initializing database...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -45,7 +55,82 @@ async def lifespan(app: FastAPI):
|
|||||||
app = get_app(lifespan)
|
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)
|
app.include_router(api_router)
|
||||||
static_path = Path(__file__).parent / "static"
|
app.mount(
|
||||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
"/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
@@ -1,5 +1,6 @@
|
|||||||
"""Модуль настроек проекта"""
|
"""Модуль настроек проекта"""
|
||||||
import os, logging
|
import os, logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
@@ -8,63 +9,75 @@ from toml import load
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
with open("pyproject.toml", 'r', encoding='utf-8') as f:
|
with open("pyproject.toml", "r", encoding="utf-8") as f:
|
||||||
config = load(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:
|
def get_app(lifespan=None, /) -> FastAPI:
|
||||||
"""Возвращает экземпляр FastAPI приложения"""
|
"""Возвращает экземпляр FastAPI приложения"""
|
||||||
if not hasattr(get_app, 'instance'):
|
poetry_cfg = _pyproject["tool"]["poetry"]
|
||||||
get_app.instance = FastAPI(
|
return FastAPI(
|
||||||
title=config["tool"]["poetry"]["name"],
|
title=poetry_cfg["name"],
|
||||||
description=config["tool"]["poetry"]["description"] + " | [Вернутьсяна главную](/)",
|
description=f"{poetry_cfg['description']} | [Вернуться на главную](/)",
|
||||||
version=config["tool"]["poetry"]["version"],
|
version=poetry_cfg["version"],
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
openapi_tags=[
|
openapi_tags=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
|
|
||||||
|
|
||||||
|
|
||||||
HOST = os.getenv("POSTGRES_HOST")
|
def get_logger(name: str = _APP_NAME) -> logging.Logger:
|
||||||
PORT = os.getenv("POSTGRES_PORT")
|
"""Возвращает логгер с указанным именем"""
|
||||||
USER = os.getenv("POSTGRES_USER")
|
return logging.getLogger(name)
|
||||||
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_session():
|
def get_session():
|
||||||
@@ -73,6 +86,14 @@ def get_session():
|
|||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name: str = "uvicorn"):
|
HOST = os.getenv("POSTGRES_HOST")
|
||||||
"""Возвращает логгер с указанным именем"""
|
PORT = os.getenv("POSTGRES_PORT")
|
||||||
return logging.getLogger(name)
|
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
@@ -776,6 +776,17 @@ MarkupSafe = ">=2.0"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
i18n = ["Babel (>=2.7)"]
|
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]]
|
[[package]]
|
||||||
name = "mako"
|
name = "mako"
|
||||||
version = "1.3.10"
|
version = "1.3.10"
|
||||||
@@ -2247,4 +2258,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "6048c7b120fbeb4533a1e858a87eb09aec68e5da569ca821b9e41ecabf220a9e"
|
content-hash = "2b11386d46acf1ce961f4fd14df46e5bedb6d5a894bf448708b3aaf5b02a401a"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ python-jose = {extras = ["cryptography"], version = "^3.5.0"}
|
|||||||
passlib = {extras = ["argon2"], version = "^1.7.4"}
|
passlib = {extras = ["argon2"], version = "^1.7.4"}
|
||||||
aiofiles = "^25.1.0"
|
aiofiles = "^25.1.0"
|
||||||
pydantic = {extras = ["email"], version = "^2.12.5"}
|
pydantic = {extras = ["email"], version = "^2.12.5"}
|
||||||
|
json-log-formatter = "^1.1.1"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^25.1.0"
|
black = "^25.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user