diff --git a/common_lib/middlewares/error_handler.py b/common_lib/middlewares/error_handler.py index 9c64065..f3279e8 100644 --- a/common_lib/middlewares/error_handler.py +++ b/common_lib/middlewares/error_handler.py @@ -1,6 +1,8 @@ import os import uuid import logging +import asyncio +import traceback from fastapi import FastAPI, Request, HTTPException from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError @@ -16,11 +18,101 @@ from starlette.status import ( ) from datetime import datetime from config import settings +from common_lib.utils.http_client import client # Настраиваем логирование logging.basicConfig(level=logging.ERROR) logger = logging.getLogger(__name__) +FEEDBACK_SERVICE_URL = "http://yobble-feedback-service:8000" +ERROR_LOG_ENVIRONMENT = "beta" +ERROR_LOG_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE"} + + +def _get_service_name() -> str: + return ( + getattr(settings, "SERVICE_NAME", None) + or os.getenv("SERVICE_NAME") + or "unknown" + ) + + +def _parse_uuid(value: str | None) -> str | None: + if not value: + return None + try: + return str(uuid.UUID(str(value))) + except ValueError: + return None + + +async def _read_request_body(request: Request): + try: + body = await request.body() + except Exception: + return None + + if not body: + return None + + content_type = request.headers.get("content-type", "") + if "application/json" in content_type: + try: + return await request.json() + except Exception: + pass + + return { + "raw": body.decode("utf-8", errors="replace") + } + + +async def _send_internal_error_log(request: Request, exc: Exception, error_id: str) -> None: + service_name = _get_service_name() + if service_name == "feedback_service": + return + + method = request.method.upper() + if method not in ERROR_LOG_METHODS: + method = "GET" + + current_user = getattr(request.state, "current_user", None) + user_id = getattr(current_user, "user_id", None) + session_id = getattr(current_user, "session_id", None) + + payload = { + "user_id": str(user_id) if user_id else None, + "service_name": service_name, + "source": "api", + "endpoint": request.url.path, + "method": method, + "http_status": 500, + "request_headers": dict(request.headers), + "request_body": await _read_request_body(request), + "request_query_params": dict(request.query_params), + "response_body": { + "status": "error", + "errors": [{"field": "server", "message": "Internal Server Error"}], + }, + "ip_address": request.client.host if request.client else None, + "user_agent": request.headers.get("user-agent"), + "session_id": str(session_id) if session_id else _parse_uuid(request.headers.get("x-session-id")), + "error_message": str(exc), + "error_stack": "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), + "error_code": error_id, + "severity": "high", + "environment": ERROR_LOG_ENVIRONMENT, + "correlation_id": _parse_uuid(request.headers.get("x-request-id")), + } + + try: + await client.post( + f"{FEEDBACK_SERVICE_URL}/internal/error-log", + json=payload, + ) + except Exception as log_exc: + print("error auto log failed:", log_exc) + def register_error_handlers(app: FastAPI): @@ -156,6 +248,7 @@ def register_error_handlers(app: FastAPI): ) print("500 critical error:", full_log_message) + asyncio.create_task(_send_internal_error_log(request, exc, error_id)) # with open(log_filename, "w", encoding="utf-8") as log_file: # log_file.write(full_log_message) diff --git a/common_lib/utils/auth.py b/common_lib/utils/auth.py index bbbf3d8..fe0feee 100644 --- a/common_lib/utils/auth.py +++ b/common_lib/utils/auth.py @@ -183,7 +183,9 @@ async def get_current_user( detail="Not authenticated" ) - return await _fetch_current_user(request, token, require_permissions=False, is_web=is_web) + current_user = await _fetch_current_user(request, token, require_permissions=False, is_web=is_web) + request.state.current_user = current_user + return current_user async def get_current_user_with_permissions( @@ -205,7 +207,9 @@ async def get_current_user_with_permissions( detail="Not authenticated" ) - return await _fetch_current_user(request, token, require_permissions=True, is_web=is_web) + current_user = await _fetch_current_user(request, token, require_permissions=True, is_web=is_web) + request.state.current_user = current_user + return current_user async def get_current_user_or_bot( @@ -229,7 +233,9 @@ async def get_current_user_or_bot( ) is_bot_flag = str(is_bot).lower() in ("true", "1", "yes") if is_bot else False - return await _fetch_current_user(request, token, require_permissions=False, is_bot=is_bot_flag, is_web=is_web) + current_user = await _fetch_current_user(request, token, require_permissions=False, is_bot=is_bot_flag, is_web=is_web) + request.state.current_user = current_user + return current_user def validate_username(value: str, diff --git a/pyproject.toml b/pyproject.toml index a212326..1ea4b41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "common-lib" -version = "0.0.57" +version = "0.0.58" description = "Библиотека общих компонентов для микросервисов yobble" authors = [{ name = "cheykrym", email = "you@example.com" }] license = "MIT"