refactor: improve exception handling, introduce structured logging, and refine uvicorn access logging.
digital-twin/RBD-multibranch/pipeline/head There was a failure building this commit Details

main
Cizz22 4 weeks ago
parent 3ba2ce1b02
commit 05e03db903

@ -3,4 +3,4 @@ import uvicorn
from src.config import HOST, PORT from src.config import HOST, PORT
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("src.main:app", host=HOST, port=PORT, reload=True) uvicorn.run("src.main:app", host=HOST, port=PORT, reload=True,)

@ -21,13 +21,6 @@ class ErrorResponse(BaseModel):
api_router = APIRouter( api_router = APIRouter(
default_response_class=JSONResponse, default_response_class=JSONResponse,
responses={
400: {"model": ErrorResponse},
401: {"model": ErrorResponse},
403: {"model": ErrorResponse},
404: {"model": ErrorResponse},
500: {"model": ErrorResponse},
},
) )

@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
from asyncpg.exceptions import DataError as AsyncPGDataError from asyncpg.exceptions import DataError as AsyncPGDataError
from asyncpg.exceptions import PostgresError from asyncpg.exceptions import PostgresError
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -14,6 +15,7 @@ from sqlalchemy.exc import DataError, DBAPIError, IntegrityError, SQLAlchemyErro
from src.enums import ResponseStatus from src.enums import ResponseStatus
log = logging.getLogger(__name__)
class ErrorDetail(BaseModel): class ErrorDetail(BaseModel):
field: Optional[str] = Field(None, max_length=100) field: Optional[str] = Field(None, max_length=100)
@ -56,7 +58,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(),
} }
@ -103,29 +105,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): if isinstance(exc, RequestValidationError):
logging.error( log.error(
f"HTTP exception | Code: {exc.status_code} | Error: {exc.detail} | Request: {request_info}", "Validation error occurred",
extra={"error_category": "http"}, 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(
@ -139,9 +174,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(

@ -117,13 +117,17 @@ def configure_logging():
handler.setFormatter(formatter) handler.setFormatter(formatter)
root_logger.addHandler(handler) root_logger.addHandler(handler)
# Reconfigure uvicorn loggers to use our JSON formatter for logger_name in ["uvicorn", "uvicorn.error", "fastapi"]:
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]:
logger = logging.getLogger(logger_name) logger = logging.getLogger(logger_name)
logger.handlers = [] logger.handlers = []
logger.propagate = True logger.propagate = True
# sometimes the slack client can be too verbose for logger_name in ["uvicorn.access"]:
logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL) logger = logging.getLogger(logger_name)
logger.handlers = []
logger.propagate = False
# # sometimes the slack client can be too verbose
# logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL)

@ -7,6 +7,7 @@ from uuid import uuid1
from typing import Optional, Final from typing import Optional, Final
from fastapi import FastAPI, HTTPException, status, Path from fastapi import FastAPI, HTTPException, status, Path
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import ValidationError from pydantic import ValidationError
@ -32,21 +33,29 @@ from src.database.core import engine, async_session, async_aeros_session
from src.exceptions import handle_exception from src.exceptions import handle_exception
from src.middleware import RequestValidationMiddleware from src.middleware import RequestValidationMiddleware
from src.context import set_request_id, reset_request_id, get_request_id from src.context import set_request_id, reset_request_id, get_request_id
from sqlalchemy.exc import SQLAlchemyError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from starlette.exceptions import HTTPException as StarletteHTTPException
# 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(exception_handlers=exception_handlers, openapi_url="", title="LCCA API", app = FastAPI(openapi_url="", title="LCCA API",
description="Welcome to RBD's API documentation!", description="Welcome to RBD'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.add_exception_handler(SQLAlchemyError, handle_exception)
app.state.limiter = limiter
app.add_middleware(GZipMiddleware, minimum_size=2000) app.add_middleware(GZipMiddleware, minimum_size=2000)
app.add_middleware(RequestValidationMiddleware) app.add_middleware(RequestValidationMiddleware)
@ -72,11 +81,25 @@ 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