diff --git a/src/exceptions.py b/src/exceptions.py index 6db15c9..bd74c4a 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -15,6 +15,9 @@ from sqlalchemy.exc import (DataError, DBAPIError, IntegrityError, from src.enums import ResponseStatus +from starlette.exceptions import HTTPException as StarletteHTTPException + +log = logging.getLogger(__name__) class ErrorDetail(BaseModel): field: Optional[str] = None @@ -57,7 +60,7 @@ def get_request_context(request: Request): return { "endpoint": request.url.path, - "url": request.url, + "url": str(request.url), "method": request.method, "remote_addr": get_client_ip(), } @@ -71,7 +74,6 @@ def handle_sqlalchemy_error(error: SQLAlchemyError): original_error = error.orig except AttributeError: original_error = None - print(original_error) if isinstance(error, IntegrityError): if "unique constraint" in str(error).lower(): @@ -95,7 +97,7 @@ def handle_sqlalchemy_error(error: SQLAlchemyError): return "Database error.", 500 else: # Log the full error for debugging purposes - logging.error(f"Unexpected database error: {str(error)}") + log.error(f"Unexpected database error: {str(error)}") return "An unexpected database error occurred.", 500 @@ -106,28 +108,62 @@ def handle_exception(request: Request, exc: Exception): request_info = get_request_context(request) if isinstance(exc, RateLimitExceeded): - _rate_limit_exceeded_handler(request, exc) - if isinstance(exc, HTTPException): - logging.error( - f"HTTP exception | Code: {exc.status_code} | Error: {exc.detail} | Request: {request_info}", - extra={"error_category": "http"}, + return _rate_limit_exceeded_handler(request, exc) + + if isinstance(exc, RequestValidationError): + log.error( + "Validation error occurred", + extra={ + "error_category": "validation", + "errors": exc.errors(), + "request": request_info, + }, + ) + return JSONResponse( + status_code=422, + content={ + "data": None, + "message": "Validation Error", + "status": ResponseStatus.ERROR, + "errors": exc.errors(), + }, + ) + + if isinstance(exc, (HTTPException, StarletteHTTPException)): + log.error( + "HTTP exception occurred", + extra={ + "error_category": "http", + "status_code": exc.status_code, + "detail": exc.detail if hasattr(exc, "detail") else str(exc), + "request": request_info, + }, ) return JSONResponse( status_code=exc.status_code, content={ "data": None, - "message": str(exc.detail), + "message": str(exc.detail) if hasattr(exc, "detail") else str(exc), "status": ResponseStatus.ERROR, - "errors": [ErrorDetail(message=str(exc.detail)).model_dump()], + "errors": [ + ErrorDetail( + message=str(exc.detail) if hasattr(exc, "detail") else str(exc) + ).model_dump() + ], }, ) if isinstance(exc, SQLAlchemyError): error_message, status_code = handle_sqlalchemy_error(exc) - logging.error( - f"Database Error | Error: {str(error_message)} | Request: {request_info}", - extra={"error_category": "database"}, + log.error( + "Database error occurred", + extra={ + "error_category": "database", + "error_message": error_message, + "request": request_info, + "exception": str(exc), + }, ) return JSONResponse( @@ -141,9 +177,14 @@ def handle_exception(request: Request, exc: Exception): ) # Log unexpected errors - logging.error( - f"Unexpected Error | Error: {str(exc)} | Request: {request_info}", - extra={"error_category": "unexpected"}, + log.error( + "Unexpected error occurred", + extra={ + "error_category": "unexpected", + "error_message": str(exc), + "request": request_info, + }, + exc_info=True, ) return JSONResponse( diff --git a/src/logging.py b/src/logging.py index bdf3731..32d9316 100644 --- a/src/logging.py +++ b/src/logging.py @@ -123,5 +123,8 @@ def configure_logging(): logger.handlers = [] logger.propagate = True + # Silence the chatty uvicorn access logs as we have custom middleware logging + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) + # sometimes the slack client can be too verbose logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 9eaabcb..e1225a7 100644 --- a/src/main.py +++ b/src/main.py @@ -30,24 +30,30 @@ from src.exceptions import handle_exception from src.logging import configure_logging from src.middleware import RequestValidationMiddleware from src.rate_limiter import limiter +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException + log = logging.getLogger(__name__) # we configure the logging level and format configure_logging() -# we define the exception handlers -exception_handlers = {Exception: handle_exception} - # we create the ASGI for the app app = FastAPI( - exception_handlers=exception_handlers, openapi_url="", title="LCCA API", description="Welcome to LCCA's API documentation!", version="0.1.0", ) -app.state.limiter = limiter + +# we define the exception handlers +app.add_exception_handler(Exception, handle_exception) +app.add_exception_handler(HTTPException, handle_exception) +app.add_exception_handler(StarletteHTTPException, handle_exception) +app.add_exception_handler(RequestValidationError, handle_exception) app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +app.state.limiter = limiter app.add_middleware(GZipMiddleware, minimum_size=1000) # credentials: "include", @@ -142,10 +148,24 @@ async def db_session_middleware(request: Request, call_next): process_time = (time.time() - start_time) * 1000 log.info( - f"Request: {request.method} {request.url.path} Status: {response.status_code} Duration: {process_time:.2f}ms" + "Request finished", + extra={ + "method": request.method, + "path": request.url.path, + "status_code": response.status_code, + "duration_ms": round(process_time, 2), + }, ) except Exception as e: - log.error(f"Request failed: {request.method} {request.url.path} Error: {str(e)}") + log.error( + "Request failed", + extra={ + "method": request.method, + "path": request.url.path, + "error": str(e), + }, + exc_info=True, + ) raise e from None finally: await request.state.db.close()