refactor: Improve exception handling with structured logging, add specific validation error handling, and silence uvicorn access logs.

main
Cizz22 4 weeks ago
parent 24ecc2f5f4
commit da3f9433ca

@ -15,6 +15,9 @@ from sqlalchemy.exc import (DataError, DBAPIError, IntegrityError,
from src.enums import ResponseStatus from src.enums import ResponseStatus
from starlette.exceptions import HTTPException as StarletteHTTPException
log = logging.getLogger(__name__)
class ErrorDetail(BaseModel): class ErrorDetail(BaseModel):
field: Optional[str] = None field: Optional[str] = None
@ -57,7 +60,7 @@ def get_request_context(request: Request):
return { return {
"endpoint": request.url.path, "endpoint": request.url.path,
"url": request.url, "url": str(request.url),
"method": request.method, "method": request.method,
"remote_addr": get_client_ip(), "remote_addr": get_client_ip(),
} }
@ -71,7 +74,6 @@ def handle_sqlalchemy_error(error: SQLAlchemyError):
original_error = error.orig original_error = error.orig
except AttributeError: except AttributeError:
original_error = None original_error = None
print(original_error)
if isinstance(error, IntegrityError): if isinstance(error, IntegrityError):
if "unique constraint" in str(error).lower(): if "unique constraint" in str(error).lower():
@ -95,7 +97,7 @@ def handle_sqlalchemy_error(error: SQLAlchemyError):
return "Database error.", 500 return "Database error.", 500
else: else:
# Log the full error for debugging purposes # 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 return "An unexpected database error occurred.", 500
@ -106,28 +108,62 @@ def handle_exception(request: Request, exc: Exception):
request_info = get_request_context(request) request_info = get_request_context(request)
if isinstance(exc, RateLimitExceeded): if isinstance(exc, RateLimitExceeded):
_rate_limit_exceeded_handler(request, exc) return _rate_limit_exceeded_handler(request, exc)
if isinstance(exc, HTTPException):
logging.error( if isinstance(exc, RequestValidationError):
f"HTTP exception | Code: {exc.status_code} | Error: {exc.detail} | Request: {request_info}", log.error(
extra={"error_category": "http"}, "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( return JSONResponse(
status_code=exc.status_code, status_code=exc.status_code,
content={ content={
"data": None, "data": None,
"message": str(exc.detail), "message": str(exc.detail) if hasattr(exc, "detail") else str(exc),
"status": ResponseStatus.ERROR, "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): if isinstance(exc, SQLAlchemyError):
error_message, status_code = handle_sqlalchemy_error(exc) error_message, status_code = handle_sqlalchemy_error(exc)
logging.error( log.error(
f"Database Error | Error: {str(error_message)} | Request: {request_info}", "Database error occurred",
extra={"error_category": "database"}, extra={
"error_category": "database",
"error_message": error_message,
"request": request_info,
"exception": str(exc),
},
) )
return JSONResponse( return JSONResponse(
@ -141,9 +177,14 @@ def handle_exception(request: Request, exc: Exception):
) )
# Log unexpected errors # Log unexpected errors
logging.error( log.error(
f"Unexpected Error | Error: {str(exc)} | Request: {request_info}", "Unexpected error occurred",
extra={"error_category": "unexpected"}, extra={
"error_category": "unexpected",
"error_message": str(exc),
"request": request_info,
},
exc_info=True,
) )
return JSONResponse( return JSONResponse(

@ -123,5 +123,8 @@ def configure_logging():
logger.handlers = [] logger.handlers = []
logger.propagate = True 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 # sometimes the slack client can be too verbose
logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL) logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL)

@ -30,24 +30,30 @@ from src.exceptions import handle_exception
from src.logging import configure_logging from src.logging import configure_logging
from src.middleware import RequestValidationMiddleware from src.middleware import RequestValidationMiddleware
from src.rate_limiter import limiter from src.rate_limiter import limiter
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# we configure the logging level and format # we configure the logging level and format
configure_logging() configure_logging()
# we define the exception handlers
exception_handlers = {Exception: handle_exception}
# we create the ASGI for the app # we create the ASGI for the app
app = FastAPI( app = FastAPI(
exception_handlers=exception_handlers,
openapi_url="", openapi_url="",
title="LCCA API", title="LCCA API",
description="Welcome to LCCA's API documentation!", description="Welcome to LCCA's API documentation!",
version="0.1.0", 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.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.state.limiter = limiter
app.add_middleware(GZipMiddleware, minimum_size=1000) app.add_middleware(GZipMiddleware, minimum_size=1000)
# credentials: "include", # credentials: "include",
@ -142,10 +148,24 @@ async def db_session_middleware(request: Request, call_next):
process_time = (time.time() - start_time) * 1000 process_time = (time.time() - start_time) * 1000
log.info( 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: 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 raise e from None
finally: finally:
await request.state.db.close() await request.state.db.close()

Loading…
Cancel
Save